agentengine-sdk-python 0.8.1__py3-none-any.whl → 0.8.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/METADATA +25 -12
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/RECORD +421 -232
- ksadk/_build_provenance.json +1 -0
- ksadk/a2a/_space_client_events.py +381 -0
- ksadk/a2a/event_adapter.py +156 -64
- ksadk/a2a/executor.py +179 -63
- ksadk/a2a/space_client.py +6 -333
- ksadk/a2ui/core.py +222 -53
- ksadk/agui/_agent_helpers.py +247 -0
- ksadk/agui/a2ui_projection.py +12 -5
- ksadk/agui/agent.py +443 -259
- ksadk/api/client.py +528 -27
- ksadk/builders/code_builder.py +284 -8
- ksadk/builders/container_builder.py +8 -1
- ksadk/builders/managed_runtime_builder.py +42 -35
- ksadk/cli/__init__.py +23 -0
- ksadk/cli/cmd_dashboard.py +15 -1
- ksadk/cli/cmd_deploy.py +10 -2
- ksadk/cli/cmd_eval.py +66 -9
- ksadk/cli/cmd_evalset.py +284 -0
- ksadk/cli/cmd_files.py +5 -1
- ksadk/cli/cmd_hermes.py +1 -126
- ksadk/cli/cmd_invoke.py +28 -38
- ksadk/cli/cmd_managed_runtime.py +95 -0
- ksadk/cli/cmd_observe.py +85 -0
- ksadk/cli/cmd_openclaw.py +1 -435
- ksadk/cli/cmd_plugin.py +657 -0
- ksadk/cli/cmd_replay.py +28 -4
- ksadk/cli/cmd_studio.py +99 -8
- ksadk/cli/cmd_web.py +10 -2
- ksadk/cli/hermes_env.py +151 -0
- ksadk/cli/invoke_payload.py +49 -0
- ksadk/cli/openclaw_env.py +458 -0
- ksadk/codex/client.py +180 -30
- ksadk/codex/runtime.py +475 -331
- ksadk/configs/env_registry.py +94 -12
- ksadk/configs/env_registry_pcm.py +181 -0
- ksadk/configs/env_var_spec.py +18 -0
- ksadk/configs/global_config.py +5 -0
- ksadk/configs/settings.py +9 -0
- ksadk/context_engine/__init__.py +98 -0
- ksadk/context_engine/assembler.py +176 -0
- ksadk/context_engine/baseline.py +419 -0
- ksadk/context_engine/cache_observability.py +228 -0
- ksadk/context_engine/capabilities.py +443 -0
- ksadk/context_engine/contributors.py +409 -0
- ksadk/context_engine/hosted_pipeline.py +417 -0
- ksadk/context_engine/models.py +117 -0
- ksadk/context_engine/planner.py +698 -0
- ksadk/context_engine/policies.py +319 -0
- ksadk/context_engine/projection.py +31 -0
- ksadk/context_engine/shadow_plan.py +266 -0
- ksadk/context_engine/tokenizer.py +164 -0
- ksadk/conversations/__init__.py +19 -0
- ksadk/conversations/context.py +230 -30
- ksadk/conversations/contracts.py +266 -0
- ksadk/conversations/message_projection.py +308 -26
- ksadk/conversations/model_context.py +49 -3
- ksadk/conversations/projector.py +616 -0
- ksadk/conversations/reducer.py +98 -0
- ksadk/conversations/runtime_compaction.py +207 -5
- ksadk/conversations/runtime_input.py +184 -2
- ksadk/conversations/runtime_invocation.py +81 -3
- ksadk/conversations/runtime_observability.py +141 -0
- ksadk/conversations/runtime_payloads.py +42 -0
- ksadk/conversations/runtime_preparation.py +272 -2
- ksadk/conversations/runtime_resume.py +20 -1
- ksadk/conversations/runtime_stream_events.py +94 -9
- ksadk/conversations/semantic_summary.py +311 -1
- ksadk/conversations/session_lock.py +51 -0
- ksadk/deployment/env_forward.py +72 -0
- ksadk/deployment/managed_runtime.py +5 -0
- ksadk/deployment/providers/serverless.py +143 -69
- ksadk/evaluation/__init__.py +32 -0
- ksadk/evaluation/a2a_adapter.py +9 -1
- ksadk/evaluation/adapters.py +18 -5
- ksadk/evaluation/agent_eval_client.py +395 -0
- ksadk/evaluation/cloud_binding.py +80 -0
- ksadk/evaluation/cloud_converter.py +194 -0
- ksadk/evaluation/cloud_service.py +199 -0
- ksadk/evaluation/contracts.py +73 -4
- ksadk/evaluation/evaluators.py +183 -17
- ksadk/evaluation/evidence.py +258 -0
- ksadk/evaluation/executor.py +31 -6
- ksadk/evaluation/local_adapter.py +550 -0
- ksadk/evaluation/service_env.py +27 -0
- ksadk/evaluation/studio_build_adapter.py +253 -0
- ksadk/evaluation/target.py +15 -6
- ksadk/events/__init__.py +13 -13
- ksadk/events/_v1_compat/__init__.py +1 -0
- ksadk/events/_v1_compat/models.py +299 -0
- ksadk/events/_v1_compat/parser.py +247 -0
- ksadk/events/_v1_compat/projection.py +755 -0
- ksadk/events/adapters/__init__.py +34 -0
- ksadk/events/adapters/_a2a_snapshot.py +622 -0
- ksadk/events/adapters/_a2a_support.py +275 -0
- ksadk/events/adapters/_codex_interactions.py +375 -0
- ksadk/events/adapters/_codex_items.py +801 -0
- ksadk/events/adapters/_codex_validators.py +311 -0
- ksadk/events/adapters/_langgraph_support.py +785 -0
- ksadk/events/adapters/a2a.py +841 -0
- ksadk/events/adapters/adk.py +503 -0
- ksadk/events/adapters/codex.py +907 -0
- ksadk/events/adapters/langgraph.py +745 -0
- ksadk/events/canonical.py +440 -0
- ksadk/events/canonical_replay.py +497 -0
- ksadk/events/canonical_store.py +468 -0
- ksadk/events/cold_recovery.py +257 -0
- ksadk/events/content.py +89 -0
- ksadk/events/identity.py +86 -0
- ksadk/events/pipeline.py +455 -0
- ksadk/events/projections.py +66 -0
- ksadk/events/reducer.py +661 -0
- ksadk/events/replay.py +3 -36
- ksadk/events/runtime_event.py +20 -265
- ksadk/events/session_event.py +324 -0
- ksadk/events/store.py +3 -316
- ksadk/events/v1_compat.py +41 -0
- ksadk/harness/__init__.py +45 -35
- ksadk/harness/runtime.py +220 -58
- ksadk/identity/resolver.py +4 -3
- ksadk/interaction/__init__.py +28 -0
- ksadk/interaction/contracts.py +136 -0
- ksadk/interaction/ledger.py +189 -0
- ksadk/interaction/provider.py +122 -0
- ksadk/interaction/providers/__init__.py +52 -0
- ksadk/interaction/providers/adk.py +40 -0
- ksadk/interaction/providers/codex.py +94 -0
- ksadk/interaction/providers/langgraph.py +58 -0
- ksadk/kernel/__init__.py +117 -0
- ksadk/kernel/authorization.py +258 -0
- ksadk/kernel/bootstrap.py +1074 -0
- ksadk/kernel/contract_fingerprints.py +62 -0
- ksadk/kernel/contracts.py +370 -0
- ksadk/kernel/control.py +220 -0
- ksadk/kernel/errors.py +79 -0
- ksadk/kernel/ingress.py +1123 -0
- ksadk/kernel/mapping.py +111 -0
- ksadk/kernel/memory_store.py +1134 -0
- ksadk/kernel/postgres_store.py +1757 -0
- ksadk/kernel/recovery.py +452 -0
- ksadk/kernel/runtime_identity.py +103 -0
- ksadk/kernel/sql/001_agent_kernel.sql +120 -0
- ksadk/kernel/sqlite_store.py +1411 -0
- ksadk/kernel/state.py +145 -0
- ksadk/kernel/store.py +232 -0
- ksadk/kernel/worker.py +964 -0
- ksadk/knowledge_base/client.py +7 -0
- ksadk/knowledge_base/service.py +32 -5
- ksadk/memory/__init__.py +61 -0
- ksadk/memory/adk/backends/base_ltm_backend.py +96 -1
- ksadk/memory/adk/backends/http_ltm_backend.py +14 -5
- ksadk/memory/adk/backends/inmemory_ltm_backend.py +136 -3
- ksadk/memory/adk/backends/sdk_ltm_backend.py +384 -2
- ksadk/memory/adk/backends/sqlite_ltm_backend.py +95 -0
- ksadk/memory/coordinator.py +461 -0
- ksadk/memory/events.py +202 -0
- ksadk/memory/extraction.py +228 -0
- ksadk/memory/ltm_backend_factory.py +16 -3
- ksadk/memory/models.py +316 -0
- ksadk/memory/policy.py +215 -0
- ksadk/memory/provider.py +47 -0
- ksadk/memory/provider_adapter.py +159 -0
- ksadk/memory/provider_resolver.py +71 -0
- ksadk/memory/providers/__init__.py +5 -0
- ksadk/memory/providers/local_sqlite.py +497 -0
- ksadk/memory/resolved_policy.py +145 -0
- ksadk/memory/service.py +188 -6
- ksadk/model_proxy/bootstrap.py +16 -9
- ksadk/model_proxy/detect.py +202 -9
- ksadk/model_proxy/namespace.py +39 -15
- ksadk/model_proxy/server.py +24 -5
- ksadk/model_proxy/transform.py +60 -2
- ksadk/observability/__init__.py +25 -0
- ksadk/observability/session_log.py +420 -0
- ksadk/observability/trajectory.py +105 -0
- ksadk/plugins/__init__.py +167 -0
- ksadk/plugins/bridges/__init__.py +45 -0
- ksadk/plugins/bridges/codex.py +552 -0
- ksadk/plugins/bridges/dsh.py +1116 -0
- ksadk/plugins/builtins.py +747 -0
- ksadk/plugins/bundle.py +246 -0
- ksadk/plugins/bundle_security.py +167 -0
- ksadk/plugins/composition.py +529 -0
- ksadk/plugins/context_contributor.py +437 -0
- ksadk/plugins/contracts.py +451 -0
- ksadk/plugins/dsh_toolchain.py +935 -0
- ksadk/plugins/ecosystem_bridge.py +1105 -0
- ksadk/plugins/ecosystem_probe.py +191 -0
- ksadk/plugins/host.py +724 -0
- ksadk/plugins/providers/__init__.py +85 -0
- ksadk/plugins/providers/bundles/ksadk-codex/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +18 -0
- ksadk/plugins/providers/bundles/ksadk-codex/package.json +19 -0
- ksadk/plugins/providers/bundles/ksadk-harness/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +19 -0
- ksadk/plugins/providers/bundles/ksadk-harness/package.json +19 -0
- ksadk/plugins/providers/codex.py +722 -0
- ksadk/plugins/providers/codex_dsh.py +83 -0
- ksadk/plugins/providers/dsh.py +951 -0
- ksadk/plugins/providers/dsh_descriptor_host.py +226 -0
- ksadk/plugins/providers/harness.py +627 -0
- ksadk/plugins/providers/harness_dsh.py +91 -0
- ksadk/plugins/providers/legacy.py +215 -0
- ksadk/plugins/providers/legacy_catalog.py +100 -0
- ksadk/plugins/providers/shipped_dsh.py +370 -0
- ksadk/plugins/resolver.py +331 -0
- ksadk/plugins/subagent_providers/__init__.py +12 -0
- ksadk/plugins/subagent_providers/codex.py +443 -0
- ksadk/plugins/subagents.py +333 -0
- ksadk/prompts/__init__.py +71 -0
- ksadk/prompts/compiler.py +215 -0
- ksadk/prompts/models.py +82 -0
- ksadk/prompts/projection.py +89 -0
- ksadk/prompts/resolved.py +137 -0
- ksadk/prompts/sources.py +233 -0
- ksadk/runners/_langgraph_runner_streams.py +822 -0
- ksadk/runners/adk_runner.py +5 -5
- ksadk/runners/base_runner.py +16 -0
- ksadk/runners/factory.py +1 -2
- ksadk/runners/langgraph_runner.py +198 -437
- ksadk/runtime/_runner_adapter/__init__.py +1 -0
- ksadk/runtime/_runner_adapter/stream_mapping.py +797 -0
- ksadk/runtime/adapter.py +119 -2
- ksadk/runtime/conversation_execution.py +316 -182
- ksadk/runtime/executor.py +144 -3
- ksadk/runtime/factory.py +268 -8
- ksadk/runtime/framework_adapters.py +18 -0
- ksadk/runtime/hosted_finalizer.py +268 -0
- ksadk/runtime/launch.py +14 -1
- ksadk/runtime/preprocessing.py +25 -3
- ksadk/runtime/runner_adapter.py +293 -508
- ksadk/runtime/usage.py +33 -0
- ksadk/sandbox/__init__.py +2 -1
- ksadk/sandbox/backends/e2b.py +35 -2
- ksadk/sandbox/factory.py +72 -0
- ksadk/scheduler/__init__.py +18 -0
- ksadk/scheduler/calendar.py +102 -0
- ksadk/scheduler/contracts.py +254 -0
- ksadk/scheduler/dispatcher.py +129 -0
- ksadk/scheduler/engine.py +416 -0
- ksadk/scheduler/sqlite_store.py +563 -0
- ksadk/server/composition.py +8 -0
- ksadk/server/factory.py +69 -6
- ksadk/server/routes/kernel_ingress.py +258 -0
- ksadk/server/routes/openai_compat.py +88 -8
- ksadk/server/routes/projection.py +35 -20
- ksadk/server/routes/run.py +94 -0
- ksadk/server/routes/sessions.py +2 -0
- ksadk/server/routes/streaming.py +5 -2
- ksadk/server/static/assets/{ArtifactsPanel-CakCqxEN.js → ArtifactsPanel-10GPXIJ6.js} +1 -1
- ksadk/server/static/assets/{CodeBlock-CynLknyO.js → CodeBlock-BXk1l-G1.js} +4 -4
- ksadk/server/static/assets/MathMessageMarkdown-BorAY7qD.css +1 -0
- ksadk/server/static/assets/{MathMessageMarkdown-3sQdOysS.js → MathMessageMarkdown-D9uB55Oh.js} +4 -4
- ksadk/server/static/assets/MermaidBlock--OEYoXIJ.js +312 -0
- ksadk/server/static/assets/NativeTerminalPanel-DrEe1syq.js +1 -0
- ksadk/server/static/assets/abnfDiagram-VCTEODGH-eeTFJLvo.js +1 -0
- ksadk/server/static/assets/arc-S3ZNbIAd.js +1 -0
- ksadk/server/static/assets/architecture-7GRP2DOG-Dk9nRQ6-.js +1 -0
- ksadk/server/static/assets/architectureDiagram-5GKGNRK7-CRjgEJDX.js +36 -0
- ksadk/server/static/assets/blockDiagram-NRAW4CY4-C7bsBoNH.js +129 -0
- ksadk/server/static/assets/c4Diagram-UCG6FXSJ-BX3c33Ob.js +38 -0
- ksadk/server/static/assets/channel-s354Yo6o.js +1 -0
- ksadk/server/static/assets/chunk-2Q5K7J3B-CDpPpKR5.js +1 -0
- ksadk/server/static/assets/{chunk-FMBD7UC4-B_fH2YuH.js → chunk-5VM5RSS4-bZ7ws-8m.js} +1 -1
- ksadk/server/static/assets/{chunk-ND2GUHAM-D6rFLaVV.js → chunk-F27PBJKO-D2r0pvhY.js} +1 -1
- ksadk/server/static/assets/chunk-G27WJ6UU-CZwF3oUP.js +231 -0
- ksadk/server/static/assets/{chunk-4BX2VUAB-B9drTgOW.js → chunk-JWPE2WC7-DigFYCML.js} +1 -1
- ksadk/server/static/assets/chunk-LCL6LL3I-CnorGV5q.js +206 -0
- ksadk/server/static/assets/chunk-POPQ4Y6H-CexntQA-.js +1 -0
- ksadk/server/static/assets/chunk-RHFEMEQ7-CHTC7UxA.js +168 -0
- ksadk/server/static/assets/chunk-SVP7TREG-BC1EWt_-.js +88 -0
- ksadk/server/static/assets/chunk-XXDRQBXY-BTG24xN0.js +1 -0
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-Dd0nucl9.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Dd0nucl9.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-S5V4N54A-ChNs_Mdk.js → cose-bilkent-JH36ORCC-CkonRdAS.js} +1 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-BBlT4uMf.js +1 -0
- ksadk/server/static/assets/cynefinDiagram-5FMLGOSQ-DEOCBe15.js +62 -0
- ksadk/server/static/assets/cytoscape.esm-CyCl8rPi.js +321 -0
- ksadk/server/static/assets/dagre-3AP2YEHR-DBThPuLa.js +4 -0
- ksadk/server/static/assets/dagre-ClTPrTht.js +1 -0
- ksadk/server/static/assets/diagram-S7CK7UJ4-DsZHrnpJ.js +30 -0
- ksadk/server/static/assets/diagram-UQ7AKVKN-BJR4nhVr.js +41 -0
- ksadk/server/static/assets/diagram-VSXAHHWV-xzcERX0a.js +3 -0
- ksadk/server/static/assets/diagram-VX7I27RA-tn2xM1xu.js +24 -0
- ksadk/server/static/assets/diagram-Z3DM3KII-BZPGBkw5.js +24 -0
- ksadk/server/static/assets/{dist-gLnrGsoU.js → dist-4zN1_tq5.js} +1 -1
- ksadk/server/static/assets/{dist-C1sz5Eo3.js → dist-8VWI-2bZ.js} +1 -1
- ksadk/server/static/assets/{dist-BjOdlDjj.js → dist-B-46q3Hk.js} +1 -1
- ksadk/server/static/assets/{dist-o2OVmKvt.js → dist-B0gdk4rl.js} +1 -1
- ksadk/server/static/assets/{dist-4N2NMVYg.js → dist-B1oWRmrH.js} +1 -1
- ksadk/server/static/assets/{dist-CmZHzlwZ.js → dist-BI1qQH4n.js} +1 -1
- ksadk/server/static/assets/{dist-X4TnqwOK.js → dist-BJLUwH6W.js} +1 -1
- ksadk/server/static/assets/{dist-B5oS1zcj.js → dist-Bbwqa8TG.js} +1 -1
- ksadk/server/static/assets/{dist-j6Dxmf_j.js → dist-Btyo4vX4.js} +1 -1
- ksadk/server/static/assets/{dist-DQU9EWw-.js → dist-C71CsujO.js} +1 -1
- ksadk/server/static/assets/{dist-BIlKoW0k.js → dist-CXpH_Rdd.js} +1 -1
- ksadk/server/static/assets/{dist-DJN_6Y39.js → dist-Cq4OL6nO.js} +1 -1
- ksadk/server/static/assets/{dist-Dli7qW-B.js → dist-D1V3f1B4.js} +1 -1
- ksadk/server/static/assets/{dist-fhOuWds5.js → dist-DJmCPRVB.js} +1 -1
- ksadk/server/static/assets/{dist-CWV4lDr1.js → dist-DQAB0qn_.js} +2 -2
- ksadk/server/static/assets/{dist-D6MpCZtz.js → dist-DUwKa_l6.js} +1 -1
- ksadk/server/static/assets/dist-DdRBIceA.js +1 -0
- ksadk/server/static/assets/{dist-DWKByxWV.js → dist-DfGFrb0v.js} +1 -1
- ksadk/server/static/assets/{dist-DsHPdk3W.js → dist-DjhwGUgZ.js} +1 -1
- ksadk/server/static/assets/{dist-B-jNR6Vn.js → dist-DyqV4fL4.js} +1 -1
- ksadk/server/static/assets/{dist-BB3-OwTQ.js → dist-M67Hwu0n.js} +1 -1
- ksadk/server/static/assets/{dist-_pHPTyZ-.js → dist-u8tVH5Ak.js} +1 -1
- ksadk/server/static/assets/ebnfDiagram-PWID7BFC-DVfMsLge.js +1 -0
- ksadk/server/static/assets/erDiagram-SSCWMZ5O-CWEZSYaw.js +99 -0
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-Dr5okd8V.js +1 -0
- ksadk/server/static/assets/flowDiagram-A5DVABFB-VEO-cVcO.js +1 -0
- ksadk/server/static/assets/ganttDiagram-EL5Y4UJY-TWIxoyYL.js +292 -0
- ksadk/server/static/assets/gitGraph-4MIJSDKK-CKiKY9Cf.js +1 -0
- ksadk/server/static/assets/gitGraphDiagram-WWUBYQGX-DDh2hd2S.js +106 -0
- ksadk/server/static/assets/index-B2k_urY8.js +241 -0
- ksadk/server/static/assets/index-ILPt6IWF.css +2 -0
- ksadk/server/static/assets/info-A6RAGUB7-ubA4Nm_p.js +1 -0
- ksadk/server/static/assets/infoDiagram-RXCK75RN-CgoyWiDR.js +2 -0
- ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-K0OOfVna.js → ishikawaDiagram-5VMMS53U-CyRIu_om.js} +6 -6
- ksadk/server/static/assets/{journeyDiagram-JHISSGLW-8LeOJaZp.js → journeyDiagram-EYS64GPL-DN5JXHzN.js} +6 -6
- ksadk/server/static/assets/{kanban-definition-UN3LZRKU-C6uCgZrW.js → kanban-definition-3QL26DDD-CNP3ui66.js} +10 -10
- ksadk/server/static/assets/katex-DWEzQy1d.js +257 -0
- ksadk/server/static/assets/{linear-hdBcYzM8.js → linear-C-qHp7d2.js} +1 -1
- ksadk/server/static/assets/mermaid-parser.core-Cl-K943T.js +166 -0
- ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-Bjy_2v4_.js → mindmap-definition-FBJOCRG2-D5DB5mYm.js} +29 -29
- ksadk/server/static/assets/packet-AYTQ26CC-C7R3NUlK.js +1 -0
- ksadk/server/static/assets/pegDiagram-XKGWAZYB-BQVc9ia_.js +1 -0
- ksadk/server/static/assets/pie-WAS4IAKB-BIT6qjr3.js +1 -0
- ksadk/server/static/assets/pieDiagram-E7YTZNPT-B6UCAvtY.js +39 -0
- ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-BmkRbwCg.js → quadrantDiagram-AXDQQJYC-mIr2d3eb.js} +6 -6
- ksadk/server/static/assets/radar-RG4KPBEZ-l5noef9X.js +1 -0
- ksadk/server/static/assets/railroad-74A4TZTK-Baea6ZvM.js +1 -0
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-OKn8zZnU.js +1 -0
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-DdKAKC4l.js +1 -0
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-Bds8udlG.js +1 -0
- ksadk/server/static/assets/railroadDiagram-O6MQD6OU-Dt3FFDmK.js +1 -0
- ksadk/server/static/assets/requirementDiagram-EFPCY7ZU-Cz_vQtgF.js +84 -0
- ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-l6iXmU65.js → sankeyDiagram-P5KCCOFB-wlO1TsH5.js} +7 -7
- ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-Cq6UBeQB.js → sequenceDiagram-WJ2MYXX4-CHRbElYq.js} +10 -10
- ksadk/server/static/assets/sizeCapture-X5ZJPWSS-DXX54iyO.js +1 -0
- ksadk/server/static/assets/stateDiagram-HBIQ2CUA-Dp_lw9II.js +1 -0
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-5b-8o2iS.js +1 -0
- ksadk/server/static/assets/swimlanes-XN3QIQJK-D8O5UlLG.js +1 -0
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-uf6WPRA4.js +8 -0
- ksadk/server/static/assets/{timeline-definition-PNZ67QCA-DEz24vV0.js → timeline-definition-24CTP7MA-8mfKcxvG.js} +7 -7
- ksadk/server/static/assets/treeView-Q6P3EWNA-CK5CSrSO.js +1 -0
- ksadk/server/static/assets/treemap-WGGIJYW6-Ch2BKgAM.js +1 -0
- ksadk/server/static/assets/vennDiagram-4TSXK5OY-EnNl0DE7.js +34 -0
- ksadk/server/static/assets/wardley-WFR3VGLG-YW3Q0ml_.js +1 -0
- ksadk/server/static/assets/wardleyDiagram-VM6X3IG4-DBu5fbYr.js +78 -0
- ksadk/server/static/assets/xychartDiagram-S5SC5T6Z-BSVXs4IY.js +7 -0
- ksadk/server/static/index.html +2 -2
- ksadk/sessions/__init__.py +26 -9
- ksadk/sessions/_local_service_sync.py +688 -0
- ksadk/sessions/_local_tables.py +21 -0
- ksadk/sessions/_postgres_schema.py +305 -0
- ksadk/sessions/_postgres_tables.py +15 -0
- ksadk/sessions/base.py +80 -1
- ksadk/sessions/in_memory.py +53 -2
- ksadk/sessions/local_service.py +63 -604
- ksadk/sessions/postgres_service.py +58 -148
- ksadk/sessions/resilient.py +35 -1
- ksadk/skills/service_client.py +48 -0
- ksadk/studio/agent_lifecycle.py +26 -20
- ksadk/studio/api.py +1063 -69
- ksadk/studio/api_catalog_routes.py +5 -0
- ksadk/studio/api_contracts.py +148 -6
- ksadk/studio/api_memory_routes.py +82 -0
- ksadk/studio/api_plugin_routes.py +480 -0
- ksadk/studio/attachment_store.py +187 -0
- ksadk/studio/authoring.py +259 -28
- ksadk/studio/authoring_coordinator.py +809 -67
- ksadk/studio/builder.py +197 -40
- ksadk/studio/capabilities.py +17 -0
- ksadk/studio/cloud.py +1920 -95
- ksadk/studio/codex_agent_service.py +137 -15
- ksadk/studio/codex_authoring.py +469 -0
- ksadk/studio/codex_builder.py +177 -25
- ksadk/studio/codex_manifest.py +71 -22
- ksadk/studio/codex_run.py +59 -4
- ksadk/studio/compatibility_report.py +445 -0
- ksadk/studio/compiler.py +7 -1
- ksadk/studio/contracts.py +234 -4
- ksadk/studio/dsh_provider_registration.py +768 -0
- ksadk/studio/event_store.py +79 -101
- ksadk/studio/framework_run.py +264 -3
- ksadk/studio/hosted_kernel.py +317 -0
- ksadk/studio/manifest_resolver.py +131 -0
- ksadk/studio/model_client.py +95 -9
- ksadk/studio/model_profile_service.py +1 -1
- ksadk/studio/operations.py +47 -5
- ksadk/studio/otel_trace.py +348 -42
- ksadk/studio/pcm_memory.py +85 -0
- ksadk/studio/plugin_composition.py +301 -0
- ksadk/studio/plugin_kernel_adapter.py +128 -0
- ksadk/studio/plugin_runtime.py +652 -0
- ksadk/studio/repository.py +23 -2
- ksadk/studio/resource_catalog.py +49 -1
- ksadk/studio/run_service.py +1266 -202
- ksadk/studio/runtime_source.py +51 -17
- ksadk/studio/scheduler_runtime.py +374 -0
- ksadk/studio/scheduler_service.py +211 -0
- ksadk/studio/service.py +1815 -117
- ksadk/studio/shared_web.py +199 -50
- ksadk/studio/soul.py +52 -0
- ksadk/studio/static/assets/index-DXs1Eq4M.css +1 -0
- ksadk/studio/static/assets/index-DeDqwTsD.js +283 -0
- ksadk/studio/static/index.html +2 -2
- ksadk/studio/templates.py +100 -8
- ksadk/studio/validator.py +20 -1
- ksadk/studio/workspace.py +31 -32
- ksadk/tracing/setup.py +304 -3
- ksadk/version.py +1 -1
- ksadk_runtime_common/schemas/runtime_event_v1.json +37 -0
- ksadk_runtime_common/schemas/runtime_event_v2.json +271 -0
- ksadk_runtime_common/workspace_files/path_utils.py +41 -0
- ksadk/events/parser.py +0 -191
- ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +0 -1
- ksadk/server/static/assets/MermaidBlock-NERD_ef7.js +0 -303
- ksadk/server/static/assets/NativeTerminalPanel-CiHWf82q.js +0 -1
- ksadk/server/static/assets/arc-DrgjcKhl.js +0 -1
- ksadk/server/static/assets/architecture-7EHR7CIX-CpZLRCcY.js +0 -1
- ksadk/server/static/assets/architectureDiagram-3BPJPVTR-BXoVRbQ6.js +0 -36
- ksadk/server/static/assets/blockDiagram-GPEHLZMM-BnaB3Ywv.js +0 -132
- ksadk/server/static/assets/c4Diagram-AAUBKEIU-Dw4weZcp.js +0 -10
- ksadk/server/static/assets/channel-Dt3rQTFI.js +0 -1
- ksadk/server/static/assets/chunk-2J33WTMH-ChjpLK7j.js +0 -1
- ksadk/server/static/assets/chunk-55IACEB6-CK4ROATy.js +0 -1
- ksadk/server/static/assets/chunk-727SXJPM-B9KtTTgw.js +0 -206
- ksadk/server/static/assets/chunk-AQP2D5EJ-1O1mki3m.js +0 -231
- ksadk/server/static/assets/chunk-QZHKN3VN-iXEZtKQc.js +0 -1
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-DIveV17Q.js +0 -1
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-DIveV17Q.js +0 -1
- ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +0 -321
- ksadk/server/static/assets/dagre-BM42HDAG-D0QXg_MQ.js +0 -4
- ksadk/server/static/assets/dagre-Bx709z4p.js +0 -1
- ksadk/server/static/assets/diagram-2AECGRRQ-DBDDKwBQ.js +0 -43
- ksadk/server/static/assets/diagram-5GNKFQAL-BkwDOxmO.js +0 -10
- ksadk/server/static/assets/diagram-KO2AKTUF-Bv8waYvY.js +0 -3
- ksadk/server/static/assets/diagram-LMA3HP47-e1VECDdu.js +0 -24
- ksadk/server/static/assets/diagram-OG6HWLK6-Ds9adfRl.js +0 -24
- ksadk/server/static/assets/dist-B5J2uhNk.js +0 -1
- ksadk/server/static/assets/erDiagram-TEJ5UH35-BPItFKH6.js +0 -85
- ksadk/server/static/assets/eventmodeling-FCH6USID-DYFSxbqr.js +0 -1
- ksadk/server/static/assets/flowDiagram-I6XJVG4X-BbYB9w5Y.js +0 -162
- ksadk/server/static/assets/ganttDiagram-6RSMTGT7-TQXa0KXv.js +0 -292
- ksadk/server/static/assets/gitGraph-WXDBUCRP-DyNYp6BN.js +0 -1
- ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-bKblCnpa.js +0 -106
- ksadk/server/static/assets/graphlib-B8gBHxth.js +0 -1
- ksadk/server/static/assets/index-R52pJJvg.js +0 -177
- ksadk/server/static/assets/index-jJr3_955.css +0 -2
- ksadk/server/static/assets/info-J43DQDTF-WUx7qTde.js +0 -1
- ksadk/server/static/assets/infoDiagram-5YYISTIA-BGVT7J_i.js +0 -2
- ksadk/server/static/assets/katex-CYWseY5E.js +0 -265
- ksadk/server/static/assets/mermaid-parser.core-BWrDjSZM.js +0 -161
- ksadk/server/static/assets/packet-YPE3B663-BN13y_xl.js +0 -1
- ksadk/server/static/assets/pie-LRSECV5Y-ftSLXhhc.js +0 -1
- ksadk/server/static/assets/pieDiagram-4H26LBE5-GmmqNiVH.js +0 -30
- ksadk/server/static/assets/radar-GUYGQ44K-jaICkIeO.js +0 -1
- ksadk/server/static/assets/requirementDiagram-4Y6WPE33-DSWFqL7c.js +0 -84
- ksadk/server/static/assets/stateDiagram-AJRCARHV-qzTQpg3Y.js +0 -1
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-BYCoYGAN.js +0 -1
- ksadk/server/static/assets/treeView-BLDUP644-Cpib_vlz.js +0 -1
- ksadk/server/static/assets/treemap-LRROVOQU-DQxfmVHK.js +0 -1
- ksadk/server/static/assets/vennDiagram-CIIHVFJN-CMCixki2.js +0 -34
- ksadk/server/static/assets/wardley-L42UT6IY-BQYHnnkU.js +0 -1
- ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-CVzB8F9K.js +0 -78
- ksadk/server/static/assets/xychartDiagram-2RQKCTM6-BFBqBLc1.js +0 -7
- ksadk/studio/evaluation.py +0 -192
- ksadk/studio/static/assets/index-CaEN0eTR.css +0 -1
- ksadk/studio/static/assets/index-DI2eY_NH.js +0 -252
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/WHEEL +0 -0
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/top_level.txt +0 -0
ksadk/studio/service.py
CHANGED
|
@@ -3,11 +3,20 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
6
9
|
import os
|
|
10
|
+
import zipfile
|
|
11
|
+
from collections.abc import Mapping, Sequence
|
|
12
|
+
from dataclasses import dataclass
|
|
7
13
|
from pathlib import Path
|
|
8
14
|
from typing import Any, Callable, Literal, cast
|
|
9
15
|
from urllib.parse import urlparse
|
|
16
|
+
from uuid import uuid4
|
|
10
17
|
|
|
18
|
+
from ksadk.api import AgentEngineClient
|
|
19
|
+
from ksadk.conversations.contracts import ConversationCapability, ConversationSurface
|
|
11
20
|
from ksadk.evaluation import (
|
|
12
21
|
EvaluationConfig as PublicEvaluationConfig,
|
|
13
22
|
)
|
|
@@ -25,15 +34,38 @@ from ksadk.evaluation import (
|
|
|
25
34
|
EvaluationRequest as PublicEvaluationRequest,
|
|
26
35
|
)
|
|
27
36
|
from ksadk.evaluation.evalset import EvalSetParseError
|
|
37
|
+
from ksadk.evaluation.evidence import EvidenceStore
|
|
38
|
+
from ksadk.evaluation.studio_build_adapter import (
|
|
39
|
+
StudioBuildResolution,
|
|
40
|
+
StudioBuildTargetAdapter,
|
|
41
|
+
StudioBuildTargetError,
|
|
42
|
+
)
|
|
43
|
+
from ksadk.events.store import RuntimeEventStore
|
|
44
|
+
from ksadk.observability.session_log import SessionLogError, export_session_log
|
|
45
|
+
from ksadk.observability.trajectory import encode_sse, project_trajectory_event
|
|
46
|
+
from ksadk.plugins.contracts import PluginManifest
|
|
47
|
+
from ksadk.plugins.providers.legacy import LegacyHarnessSource
|
|
48
|
+
from ksadk.plugins.providers.legacy_catalog import (
|
|
49
|
+
KSADK_HARNESS_AGENT_PROVIDER_PLUGIN_ID,
|
|
50
|
+
)
|
|
28
51
|
from ksadk.runtime import RuntimeExecutor, build_default_runtime_registry
|
|
52
|
+
from ksadk.scheduler.contracts import (
|
|
53
|
+
ScheduleCommandTemplate,
|
|
54
|
+
ScheduledTask,
|
|
55
|
+
ScheduledTaskTarget,
|
|
56
|
+
ScheduleSpec,
|
|
57
|
+
)
|
|
58
|
+
from ksadk.sessions.local_service import LocalSessionService
|
|
29
59
|
from ksadk.studio.agent_avatar_assets import AgentAvatarAssetStore
|
|
30
60
|
from ksadk.studio.agent_lifecycle import delete_framework_agent
|
|
61
|
+
from ksadk.studio.attachment_store import ConversationAttachmentStore
|
|
31
62
|
from ksadk.studio.authoring_coordinator import StudioAuthoringCoordinator
|
|
32
63
|
from ksadk.studio.builder import AgentBundleBuilder
|
|
33
64
|
from ksadk.studio.capabilities import builtin_tool_contracts
|
|
34
65
|
from ksadk.studio.cloud import (
|
|
35
66
|
CloudDeploymentGateway,
|
|
36
67
|
CloudDeploymentService,
|
|
68
|
+
DirectAgentEngineCloudDeploymentGateway,
|
|
37
69
|
UnavailableCloudGateway,
|
|
38
70
|
)
|
|
39
71
|
from ksadk.studio.codex_agent_service import CodexAgentService, CodexDraftRepository
|
|
@@ -42,6 +74,9 @@ from ksadk.studio.codex_builder import (
|
|
|
42
74
|
CodexBuildRepository,
|
|
43
75
|
CodexStudioBuilder,
|
|
44
76
|
RuntimeInspector,
|
|
77
|
+
current_proxy_mode,
|
|
78
|
+
normalize_proxy_mode,
|
|
79
|
+
proxy_mode_env_value,
|
|
45
80
|
)
|
|
46
81
|
from ksadk.studio.codex_manifest import (
|
|
47
82
|
CodexAgentManifest,
|
|
@@ -56,26 +91,39 @@ from ksadk.studio.contracts import (
|
|
|
56
91
|
AgentSpec,
|
|
57
92
|
AgentTemplateComposeRequest,
|
|
58
93
|
AgentTemplateComposition,
|
|
94
|
+
BuildStatus,
|
|
59
95
|
DeploymentRequest,
|
|
60
96
|
Operation,
|
|
61
97
|
OperationKind,
|
|
62
98
|
RunEvent,
|
|
63
99
|
RunStatus,
|
|
64
100
|
RuntimeRef,
|
|
101
|
+
ToolContract,
|
|
102
|
+
)
|
|
103
|
+
from ksadk.studio.dsh_provider_registration import (
|
|
104
|
+
StudioDshProviderRegistrationError,
|
|
105
|
+
StudioDshProviderRegistrationManager,
|
|
65
106
|
)
|
|
66
107
|
from ksadk.studio.errors import StudioError
|
|
67
|
-
from ksadk.studio.evaluation import EvaluationRunner
|
|
68
108
|
from ksadk.studio.event_store import RunEventStore
|
|
69
109
|
from ksadk.studio.framework_run import FrameworkRunSpecResolver
|
|
70
110
|
from ksadk.studio.mcp_runtime import MCPRuntimeAdapter
|
|
71
111
|
from ksadk.studio.model_client import CredentialResolver, OpenAICompatibleModelClient
|
|
72
112
|
from ksadk.studio.model_profile_service import test_model_profile_connection
|
|
73
113
|
from ksadk.studio.operations import OperationManager
|
|
114
|
+
from ksadk.studio.plugin_composition import StudioPluginCompositionCompiler
|
|
115
|
+
from ksadk.studio.plugin_runtime import StudioPluginRuntime
|
|
74
116
|
from ksadk.studio.repository import AgentDraftRepository, BuildRepository, load_yaml_file
|
|
75
117
|
from ksadk.studio.resource_catalog import LocalResourceCatalog
|
|
76
|
-
from ksadk.studio.run_service import StudioRunService
|
|
118
|
+
from ksadk.studio.run_service import StudioRunService, StudioRunSpec
|
|
77
119
|
from ksadk.studio.runtime_catalog import inspect_runtime_catalog
|
|
78
120
|
from ksadk.studio.runtime_source import materialize_generated_runtime_source
|
|
121
|
+
from ksadk.studio.scheduler_runtime import (
|
|
122
|
+
StudioScheduledKernelRegistry,
|
|
123
|
+
StudioSchedulerRuntimeError,
|
|
124
|
+
)
|
|
125
|
+
from ksadk.studio.scheduler_service import StudioSchedulerService
|
|
126
|
+
from ksadk.studio.soul import soul_digest
|
|
79
127
|
from ksadk.studio.templates import (
|
|
80
128
|
compose_blank_agent,
|
|
81
129
|
compose_research_agent,
|
|
@@ -85,6 +133,19 @@ from ksadk.studio.templates import (
|
|
|
85
133
|
from ksadk.studio.validator import AgentValidator
|
|
86
134
|
from ksadk.studio.workspace import Workspace
|
|
87
135
|
|
|
136
|
+
_TRAJECTORY_KEEPALIVE_SECONDS = 15.0
|
|
137
|
+
_EVALUATION_TARGET_LABELS = {
|
|
138
|
+
TargetKind.A2A: "A2A Agent",
|
|
139
|
+
TargetKind.LOCAL_SOURCE: "本地源码",
|
|
140
|
+
TargetKind.STUDIO_BUILD: "Studio Build",
|
|
141
|
+
TargetKind.CODEX_WORKTREE: "Codex Worktree",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class _OperationResource:
|
|
147
|
+
id: str
|
|
148
|
+
|
|
88
149
|
|
|
89
150
|
class StudioService:
|
|
90
151
|
def __init__(
|
|
@@ -96,11 +157,34 @@ class StudioService:
|
|
|
96
157
|
cloud_gateway: CloudDeploymentGateway | None = None,
|
|
97
158
|
codex_runtime_inspector: RuntimeInspector | None = None,
|
|
98
159
|
runtime_executor: RuntimeExecutor | None = None,
|
|
160
|
+
harness_reasoner: Any | None = None,
|
|
161
|
+
plugin_provider_manifests: Mapping[str, PluginManifest] | None = None,
|
|
162
|
+
plugin_provider_factories: Mapping[str, Any] | None = None,
|
|
163
|
+
legacy_harness_sources: Sequence[LegacyHarnessSource] = (),
|
|
164
|
+
dsh_provider_registration_manager: StudioDshProviderRegistrationManager | None = None,
|
|
99
165
|
) -> None:
|
|
166
|
+
provider_manifests = dict(plugin_provider_manifests or {})
|
|
167
|
+
provider_factories = dict(plugin_provider_factories or {})
|
|
168
|
+
if provider_manifests.keys() != provider_factories.keys():
|
|
169
|
+
raise ValueError(
|
|
170
|
+
"plugin provider manifests and factories must use the same exact references"
|
|
171
|
+
)
|
|
100
172
|
self.workspace = Workspace(root)
|
|
101
173
|
self.workspace.initialize()
|
|
174
|
+
self._startup_provider_manifests = provider_manifests
|
|
175
|
+
self._startup_provider_factories = provider_factories
|
|
176
|
+
self._active_provider_manifests = dict(provider_manifests)
|
|
177
|
+
self._dsh_provider_registration_manager = (
|
|
178
|
+
dsh_provider_registration_manager
|
|
179
|
+
or StudioDshProviderRegistrationManager.discover_or_create_workspace_default(
|
|
180
|
+
self.workspace.root
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
self._start_lock = asyncio.Lock()
|
|
184
|
+
self._started = False
|
|
102
185
|
self._apply_persisted_settings()
|
|
103
186
|
self.avatar_assets = AgentAvatarAssetStore(self.workspace)
|
|
187
|
+
self.conversation_attachments = ConversationAttachmentStore(self.workspace)
|
|
104
188
|
self.drafts = AgentDraftRepository(self.workspace)
|
|
105
189
|
self.catalog = LocalResourceCatalog(self.workspace)
|
|
106
190
|
self.builds = BuildRepository(self.workspace)
|
|
@@ -114,8 +198,14 @@ class StudioService:
|
|
|
114
198
|
),
|
|
115
199
|
repository=self.builds,
|
|
116
200
|
)
|
|
201
|
+
self.plugin_compositions = StudioPluginCompositionCompiler(
|
|
202
|
+
self.workspace,
|
|
203
|
+
self.catalog,
|
|
204
|
+
provider_manifests=provider_manifests,
|
|
205
|
+
)
|
|
117
206
|
self.event_store = RunEventStore(self.workspace)
|
|
118
|
-
self.
|
|
207
|
+
self.session_service = LocalSessionService(project_dir=str(self.workspace.root))
|
|
208
|
+
self.runtime_events = RuntimeEventStore(self.session_service)
|
|
119
209
|
self.codex_manifests = CodexManifestRepository(self.workspace)
|
|
120
210
|
self.codex_builds = CodexBuildRepository(self.workspace)
|
|
121
211
|
self.codex_drafts = CodexDraftRepository(self.workspace)
|
|
@@ -137,6 +227,8 @@ class StudioService:
|
|
|
137
227
|
self.workspace,
|
|
138
228
|
self.runtime_executor,
|
|
139
229
|
event_store=self.event_store,
|
|
230
|
+
session_service=self.session_service,
|
|
231
|
+
runtime_events=self.runtime_events,
|
|
140
232
|
)
|
|
141
233
|
self.credentials = (
|
|
142
234
|
credential_resolver
|
|
@@ -158,28 +250,565 @@ class StudioService:
|
|
|
158
250
|
credential_resolver=self.credentials
|
|
159
251
|
)
|
|
160
252
|
self.model_client = runtime_model_client
|
|
161
|
-
self.
|
|
162
|
-
self.evaluations = EvaluationRunner(
|
|
253
|
+
self.plugin_runs = StudioPluginRuntime(
|
|
163
254
|
self.workspace,
|
|
164
|
-
run_agent=self.run_build,
|
|
165
|
-
event_store=self.event_store,
|
|
166
255
|
build_repository=self.builds,
|
|
256
|
+
session_service=self.session_service,
|
|
257
|
+
model_client=self.model_client,
|
|
258
|
+
secret_resolver=self.credentials,
|
|
259
|
+
harness_reasoner=harness_reasoner,
|
|
260
|
+
provider_manifests=provider_manifests,
|
|
261
|
+
provider_factories=provider_factories,
|
|
262
|
+
legacy_harness_sources=legacy_harness_sources,
|
|
167
263
|
)
|
|
264
|
+
self.run_service.plugin_runtime = self.plugin_runs
|
|
265
|
+
self.scheduler_runtimes = StudioScheduledKernelRegistry(
|
|
266
|
+
resolve_build=self.resolve_run_spec,
|
|
267
|
+
resolve_adapter_provider=self._scheduler_adapter_provider,
|
|
268
|
+
session_service=self.session_service,
|
|
269
|
+
runtime_executor=self.runtime_executor,
|
|
270
|
+
)
|
|
271
|
+
self.scheduler = StudioSchedulerService(
|
|
272
|
+
self.workspace,
|
|
273
|
+
runtime_registry=self.scheduler_runtimes,
|
|
274
|
+
)
|
|
275
|
+
self.mcp_runtime = MCPRuntimeAdapter(self.workspace, credentials=self.credentials)
|
|
276
|
+
self._cloud_gateway_override = cloud_gateway
|
|
168
277
|
self.cloud = CloudDeploymentService(
|
|
169
278
|
self.workspace,
|
|
170
|
-
gateway=cloud_gateway or
|
|
279
|
+
gateway=cloud_gateway or self._configured_cloud_gateway(),
|
|
171
280
|
build_repository=self.builds,
|
|
172
281
|
)
|
|
173
282
|
self.operations = OperationManager(self.workspace)
|
|
174
|
-
self.evaluation_storage = EvaluationStorage(
|
|
175
|
-
self.workspace.resolve(".agentkit/evaluations")
|
|
176
|
-
)
|
|
283
|
+
self.evaluation_storage = EvaluationStorage(self.workspace.resolve(".agentkit/evaluations"))
|
|
177
284
|
self.authoring = StudioAuthoringCoordinator(self)
|
|
178
285
|
self.codex_agents = CodexAgentService(self)
|
|
179
286
|
|
|
287
|
+
async def start(self) -> None:
|
|
288
|
+
"""Bind ready managed DSH registrations before build or execution."""
|
|
289
|
+
|
|
290
|
+
async with self._start_lock:
|
|
291
|
+
if self._started:
|
|
292
|
+
return
|
|
293
|
+
await self._bootstrap_official_dsh_defaults()
|
|
294
|
+
manifests, factories, manager_refs = await self._provider_snapshot(refresh=False)
|
|
295
|
+
self.plugin_compositions.replace_provider_registrations(manifests)
|
|
296
|
+
self.plugin_runs.replace_provider_registrations(manifests, factories)
|
|
297
|
+
self._active_provider_manifests = manifests
|
|
298
|
+
manager = self._dsh_provider_registration_manager
|
|
299
|
+
if manager is not None and manager_refs is not None:
|
|
300
|
+
manager.mark_bound(manager_refs)
|
|
301
|
+
self._started = True
|
|
302
|
+
|
|
303
|
+
async def refresh_dsh_provider_registrations(self) -> None:
|
|
304
|
+
"""Rebind the exact current DSH Profile and release stale activations."""
|
|
305
|
+
|
|
306
|
+
async with self._start_lock:
|
|
307
|
+
if self._dsh_provider_registration_manager is None:
|
|
308
|
+
self._dsh_provider_registration_manager = (
|
|
309
|
+
StudioDshProviderRegistrationManager.discover_or_create_workspace_default(
|
|
310
|
+
self.workspace.root
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
await self._bootstrap_official_dsh_defaults()
|
|
314
|
+
if self._started:
|
|
315
|
+
await self.plugin_runs.aclose()
|
|
316
|
+
manifests, factories, manager_refs = await self._provider_snapshot(refresh=True)
|
|
317
|
+
self.plugin_compositions.replace_provider_registrations(manifests)
|
|
318
|
+
self.plugin_runs.replace_provider_registrations(manifests, factories)
|
|
319
|
+
self._active_provider_manifests = manifests
|
|
320
|
+
manager = self._dsh_provider_registration_manager
|
|
321
|
+
if manager is not None and manager_refs is not None:
|
|
322
|
+
manager.mark_bound(manager_refs)
|
|
323
|
+
self._started = True
|
|
324
|
+
|
|
325
|
+
async def _bootstrap_official_dsh_defaults(self) -> None:
|
|
326
|
+
manager = self._dsh_provider_registration_manager
|
|
327
|
+
if manager is None:
|
|
328
|
+
return
|
|
329
|
+
try:
|
|
330
|
+
result = await manager.bootstrap_official_codex_provider()
|
|
331
|
+
except Exception as error: # optional DSH must fail closed to legacy paths
|
|
332
|
+
logging.getLogger(__name__).warning(
|
|
333
|
+
"official DSH provider bootstrap skipped: %s", error
|
|
334
|
+
)
|
|
335
|
+
return
|
|
336
|
+
if result in {"installed", "already_enabled"}:
|
|
337
|
+
logging.getLogger(__name__).info("official Codex DSH provider bootstrap: %s", result)
|
|
338
|
+
|
|
339
|
+
async def _provider_snapshot(
|
|
340
|
+
self, *, refresh: bool
|
|
341
|
+
) -> tuple[dict[str, PluginManifest], dict[str, Any], tuple[str, ...] | None]:
|
|
342
|
+
manifests = dict(self._startup_provider_manifests)
|
|
343
|
+
factories = dict(self._startup_provider_factories)
|
|
344
|
+
manager = self._dsh_provider_registration_manager
|
|
345
|
+
if manager is None:
|
|
346
|
+
return manifests, factories, None
|
|
347
|
+
try:
|
|
348
|
+
registrations = await (manager.refresh() if refresh else manager.start())
|
|
349
|
+
except StudioDshProviderRegistrationError as error:
|
|
350
|
+
logging.getLogger(__name__).warning(
|
|
351
|
+
"DSH AgentProvider discovery failed closed: %s", error.code
|
|
352
|
+
)
|
|
353
|
+
return manifests, factories, None
|
|
354
|
+
if registrations.manifests.keys() != registrations.factories.keys():
|
|
355
|
+
logging.getLogger(__name__).warning(
|
|
356
|
+
"DSH AgentProvider discovery returned a partial registration set"
|
|
357
|
+
)
|
|
358
|
+
return manifests, factories, None
|
|
359
|
+
for provider_ref, manifest in registrations.manifests.items():
|
|
360
|
+
if provider_ref in manifests and (
|
|
361
|
+
manifests[provider_ref] != manifest
|
|
362
|
+
or factories[provider_ref] is not registrations.factories[provider_ref]
|
|
363
|
+
):
|
|
364
|
+
logging.getLogger(__name__).warning(
|
|
365
|
+
"DSH AgentProvider registration conflicts with startup provider: %s",
|
|
366
|
+
provider_ref,
|
|
367
|
+
)
|
|
368
|
+
return (
|
|
369
|
+
dict(self._startup_provider_manifests),
|
|
370
|
+
dict(self._startup_provider_factories),
|
|
371
|
+
None,
|
|
372
|
+
)
|
|
373
|
+
manifests[provider_ref] = manifest
|
|
374
|
+
factories[provider_ref] = registrations.factories[provider_ref]
|
|
375
|
+
return manifests, factories, tuple(registrations.manifests)
|
|
376
|
+
|
|
377
|
+
def agent_provider_catalog(self) -> list[dict[str, Any]]:
|
|
378
|
+
"""Return only external providers that reached Studio's bound selector."""
|
|
379
|
+
|
|
380
|
+
reserved = {
|
|
381
|
+
KSADK_HARNESS_AGENT_PROVIDER_PLUGIN_ID,
|
|
382
|
+
}
|
|
383
|
+
package_by_ref: dict[str, str] = {}
|
|
384
|
+
display_name_by_ref: dict[str, str] = {}
|
|
385
|
+
manager = self._dsh_provider_registration_manager
|
|
386
|
+
if manager is not None:
|
|
387
|
+
package_by_ref = {
|
|
388
|
+
item.provider_ref: item.package_name
|
|
389
|
+
for item in manager.inventory.packages
|
|
390
|
+
if item.state == "bound" and item.provider_ref is not None
|
|
391
|
+
}
|
|
392
|
+
display_name_by_ref = {
|
|
393
|
+
item.provider_ref: item.display_name
|
|
394
|
+
for item in manager.inventory.packages
|
|
395
|
+
if item.state == "bound" and item.provider_ref is not None and item.display_name
|
|
396
|
+
}
|
|
397
|
+
items = []
|
|
398
|
+
for provider_ref, manifest in sorted(self._active_provider_manifests.items()):
|
|
399
|
+
if manifest.metadata.id in reserved:
|
|
400
|
+
continue
|
|
401
|
+
items.append(
|
|
402
|
+
{
|
|
403
|
+
"providerRef": provider_ref,
|
|
404
|
+
"pluginId": package_by_ref.get(provider_ref, manifest.metadata.id),
|
|
405
|
+
"resolvedVersion": manifest.metadata.version,
|
|
406
|
+
"displayName": display_name_by_ref.get(provider_ref, manifest.metadata.id),
|
|
407
|
+
"state": "enabled",
|
|
408
|
+
"compatible": True,
|
|
409
|
+
"selectable": True,
|
|
410
|
+
"reason": None,
|
|
411
|
+
"permissions": list(manifest.spec.permissions),
|
|
412
|
+
"isolation": manifest.spec.isolation,
|
|
413
|
+
"configSchemaDeclared": manifest.spec.config_schema is not None,
|
|
414
|
+
"secretFields": list(manifest.spec.secret_fields),
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
return items
|
|
418
|
+
|
|
419
|
+
def dsh_provider_runtime_state(self, package_name: str) -> dict[str, Any] | None:
|
|
420
|
+
"""Project one manager-owned package lifecycle state for plugin APIs."""
|
|
421
|
+
|
|
422
|
+
manager = self._dsh_provider_registration_manager
|
|
423
|
+
if manager is None:
|
|
424
|
+
return None
|
|
425
|
+
status = next(
|
|
426
|
+
(item for item in manager.inventory.packages if item.package_name == package_name),
|
|
427
|
+
None,
|
|
428
|
+
)
|
|
429
|
+
if status is None:
|
|
430
|
+
return None
|
|
431
|
+
return {
|
|
432
|
+
"state": status.state,
|
|
433
|
+
"providerRef": status.provider_ref,
|
|
434
|
+
"errorCode": status.error_code,
|
|
435
|
+
}
|
|
436
|
+
|
|
180
437
|
def runtime_catalog(self) -> list[dict]:
|
|
181
438
|
return inspect_runtime_catalog(self.runtime_executor)
|
|
182
439
|
|
|
440
|
+
def _scheduler_adapter_provider(self, spec: StudioRunSpec): # type: ignore[no-untyped-def]
|
|
441
|
+
if spec.plugin_bundle_root is not None:
|
|
442
|
+
return self.plugin_runs.kernel_adapter_provider(spec)
|
|
443
|
+
return lambda: self.runtime_executor.create_adapter(spec.launch_context)
|
|
444
|
+
|
|
445
|
+
def resolve_run_spec(
|
|
446
|
+
self,
|
|
447
|
+
build_id: str,
|
|
448
|
+
*,
|
|
449
|
+
model: str | None = None,
|
|
450
|
+
sandbox: str | None = None,
|
|
451
|
+
approval_mode: str | None = None,
|
|
452
|
+
) -> StudioRunSpec:
|
|
453
|
+
"""Resolve one immutable Build through its only compatible resolver."""
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
return self.codex_runs.resolve(
|
|
457
|
+
build_id,
|
|
458
|
+
model=model,
|
|
459
|
+
sandbox=sandbox,
|
|
460
|
+
approval_mode=approval_mode,
|
|
461
|
+
)
|
|
462
|
+
except Exception as exc:
|
|
463
|
+
if getattr(exc, "status_code", None) != 404:
|
|
464
|
+
raise
|
|
465
|
+
framework_build = self.builds.get(build_id)
|
|
466
|
+
runtime_type = framework_build.runtime_type.strip().lower()
|
|
467
|
+
if runtime_type in {"harness", "plugin"}:
|
|
468
|
+
return self.plugin_runs.resolve(build_id, model=model)
|
|
469
|
+
return self.framework_runs.resolve(
|
|
470
|
+
build_id,
|
|
471
|
+
model=model,
|
|
472
|
+
approval_mode=approval_mode,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def validate_schedule_build(
|
|
476
|
+
self,
|
|
477
|
+
build_ref: str | None,
|
|
478
|
+
*,
|
|
479
|
+
agent_id: str | None = None,
|
|
480
|
+
) -> None:
|
|
481
|
+
"""Reject a ScheduledTask target build that is not a real immutable Build.
|
|
482
|
+
|
|
483
|
+
The agent-scoped authoring path resolves the Build server-side via
|
|
484
|
+
:meth:`ensure_current_build`, so this guard is primarily for the raw
|
|
485
|
+
``/api/v1/schedules`` surface that accepts a caller-supplied
|
|
486
|
+
``agent_version_ref``. A version name or tag (for example "v1") is
|
|
487
|
+
not an immutable Build id and must fail at admission with an actionable
|
|
488
|
+
error instead of collapsing into a later opaque DISPATCH_FAILED.
|
|
489
|
+
"""
|
|
490
|
+
ref = (build_ref or "").strip()
|
|
491
|
+
if not ref:
|
|
492
|
+
raise StudioError(
|
|
493
|
+
"SCHEDULE_BUILD_REQUIRED",
|
|
494
|
+
"定时任务必须绑定一个已成功构建的 Build id(不能为空或使用版本名)",
|
|
495
|
+
status_code=422,
|
|
496
|
+
field="target.agentVersionRef",
|
|
497
|
+
)
|
|
498
|
+
try:
|
|
499
|
+
spec = self.resolve_run_spec(ref)
|
|
500
|
+
except StudioError as error:
|
|
501
|
+
if error.status_code == 404:
|
|
502
|
+
raise StudioError(
|
|
503
|
+
"SCHEDULE_BUILD_UNAVAILABLE",
|
|
504
|
+
f"定时任务绑定的 Build {ref!r} 不可用:请使用已成功构建的 Build id,而非版本名",
|
|
505
|
+
status_code=422,
|
|
506
|
+
field="target.agentVersionRef",
|
|
507
|
+
details={"buildId": ref},
|
|
508
|
+
) from error
|
|
509
|
+
raise
|
|
510
|
+
except Exception as error:
|
|
511
|
+
raise StudioError(
|
|
512
|
+
"SCHEDULE_BUILD_UNAVAILABLE",
|
|
513
|
+
f"定时任务绑定的 Build {ref!r} 不可用:{error}",
|
|
514
|
+
status_code=422,
|
|
515
|
+
field="target.agentVersionRef",
|
|
516
|
+
details={"buildId": ref},
|
|
517
|
+
) from error
|
|
518
|
+
if agent_id and spec.agent_id != agent_id:
|
|
519
|
+
raise StudioError(
|
|
520
|
+
"SCHEDULE_AGENT_MISMATCH",
|
|
521
|
+
"定时任务绑定的 Build 与所选 Agent 不一致",
|
|
522
|
+
status_code=422,
|
|
523
|
+
field="target.agentVersionRef",
|
|
524
|
+
details={"buildId": ref, "agentId": agent_id},
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
def conversation_surface(
|
|
528
|
+
self,
|
|
529
|
+
build_id: str,
|
|
530
|
+
*,
|
|
531
|
+
session_id: str,
|
|
532
|
+
) -> ConversationSurface:
|
|
533
|
+
"""Describe the actual local composer contract for one Build/session.
|
|
534
|
+
|
|
535
|
+
This is intentionally conservative. It derives the declaration from
|
|
536
|
+
the resolved Build, rather than guessing support from a model name or
|
|
537
|
+
sending provider-specific fields to every Runtime.
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
spec = self.resolve_run_spec(build_id)
|
|
541
|
+
runtime_type = spec.launch_context.runtime_type.strip().lower()
|
|
542
|
+
runtime_mode: Literal["native", "translated"] = (
|
|
543
|
+
"native" if runtime_type == "codex" else "translated"
|
|
544
|
+
)
|
|
545
|
+
inputs = [ConversationCapability(name="text", mode="native")]
|
|
546
|
+
if spec.model:
|
|
547
|
+
inputs.append(ConversationCapability(name="model.select", mode=runtime_mode))
|
|
548
|
+
inputs.append(ConversationCapability(name="approval", mode=runtime_mode))
|
|
549
|
+
if runtime_type == "codex":
|
|
550
|
+
inputs.extend(
|
|
551
|
+
(
|
|
552
|
+
# Studio projects image data URLs to native Codex ImageInput.
|
|
553
|
+
ConversationCapability(name="attachment.image", mode="native"),
|
|
554
|
+
# Bounded UTF-8/code attachments are made model-visible as
|
|
555
|
+
# deterministic text parts by the Responses compatibility
|
|
556
|
+
# adapter. Binary files remain unavailable in Phase 2.
|
|
557
|
+
ConversationCapability(name="attachment.file", mode="translated"),
|
|
558
|
+
ConversationCapability(name="reasoning.effort", mode="native"),
|
|
559
|
+
ConversationCapability(name="goal", mode="native"),
|
|
560
|
+
ConversationCapability(name="plan", mode="native"),
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
outputs = [
|
|
565
|
+
ConversationCapability(name="text", mode="native"),
|
|
566
|
+
ConversationCapability(name="streaming", mode=runtime_mode),
|
|
567
|
+
]
|
|
568
|
+
if runtime_type == "codex":
|
|
569
|
+
outputs.extend(
|
|
570
|
+
(
|
|
571
|
+
ConversationCapability(name="reasoning", mode="native"),
|
|
572
|
+
ConversationCapability(name="tool.inspect", mode="native"),
|
|
573
|
+
ConversationCapability(name="approval", mode="native"),
|
|
574
|
+
ConversationCapability(name="goal", mode="native"),
|
|
575
|
+
ConversationCapability(name="plan", mode="native"),
|
|
576
|
+
ConversationCapability(name="cancel", mode="native"),
|
|
577
|
+
)
|
|
578
|
+
)
|
|
579
|
+
return ConversationSurface(
|
|
580
|
+
surface_id=f"studio.build.{spec.build_id}",
|
|
581
|
+
session_id=session_id,
|
|
582
|
+
provider_ref=f"studio.runtime.{runtime_type}",
|
|
583
|
+
inputs=tuple(inputs),
|
|
584
|
+
outputs=tuple(outputs),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
async def ensure_current_build(self, agent_id: str):
|
|
588
|
+
"""Return the Build that is eligible for a local Studio turn.
|
|
589
|
+
|
|
590
|
+
This is deliberately one shared resolver for Web chat and Scheduler
|
|
591
|
+
authoring. A Scheduler must never select a mutable Draft or a stale
|
|
592
|
+
Codex manifest merely because a UI happened to show it as an Agent.
|
|
593
|
+
"""
|
|
594
|
+
|
|
595
|
+
await self.start()
|
|
596
|
+
if self.is_codex_agent(agent_id):
|
|
597
|
+
self.codex_agent_detail(agent_id)
|
|
598
|
+
builds = [
|
|
599
|
+
record
|
|
600
|
+
for record in self.codex_builds.list()
|
|
601
|
+
if record.agent_name == agent_id and self.codex_builder.is_current(record)
|
|
602
|
+
]
|
|
603
|
+
if builds:
|
|
604
|
+
return builds[0]
|
|
605
|
+
return await asyncio.to_thread(self.codex_builder.build, agent_id)
|
|
606
|
+
draft = self.drafts.get(agent_id)
|
|
607
|
+
composition_required = self.plugin_compositions.required_for(draft)
|
|
608
|
+
for record in self.builds.list_for_agent(agent_id):
|
|
609
|
+
if record.status == BuildStatus.SUCCEEDED:
|
|
610
|
+
if composition_required and (
|
|
611
|
+
record.source_revision != draft.metadata.revision
|
|
612
|
+
or not self._build_has_composition(record)
|
|
613
|
+
):
|
|
614
|
+
continue
|
|
615
|
+
if composition_required:
|
|
616
|
+
composition = self.plugin_compositions.compile_if_required(draft)
|
|
617
|
+
if composition is None: # pragma: no cover - guarded by required_for
|
|
618
|
+
raise StudioError(
|
|
619
|
+
"PLUGIN_COMPOSITION_REQUIRED",
|
|
620
|
+
"当前 Agent 需要插件组合,但无法生成 Composition",
|
|
621
|
+
status_code=409,
|
|
622
|
+
)
|
|
623
|
+
self.plugin_compositions.bind_build(
|
|
624
|
+
composition,
|
|
625
|
+
agent_id=draft.metadata.id,
|
|
626
|
+
build_id=record.id,
|
|
627
|
+
)
|
|
628
|
+
return record
|
|
629
|
+
if draft.spec.model is None and not draft.spec.bindings.model_profile_id:
|
|
630
|
+
raise StudioError(
|
|
631
|
+
"AGENT_MODEL_REQUIRED",
|
|
632
|
+
"当前 Agent 未绑定 Model Profile,请先在 Agent 配置中选择模型;"
|
|
633
|
+
"API Key 只提供访问凭证,不会自动绑定模型。",
|
|
634
|
+
status_code=422,
|
|
635
|
+
field="spec.bindings.modelProfileId",
|
|
636
|
+
)
|
|
637
|
+
return await asyncio.to_thread(self._build_agent_bundle, draft)
|
|
638
|
+
|
|
639
|
+
def _build_agent_bundle(self, draft: AgentDraft):
|
|
640
|
+
composition = self.plugin_compositions.compile_if_required(draft)
|
|
641
|
+
record = self.builder.build(draft, composition=composition)
|
|
642
|
+
if composition is not None:
|
|
643
|
+
self.plugin_compositions.bind_build(
|
|
644
|
+
composition,
|
|
645
|
+
agent_id=draft.metadata.id,
|
|
646
|
+
build_id=record.id,
|
|
647
|
+
)
|
|
648
|
+
return record
|
|
649
|
+
|
|
650
|
+
def _build_has_composition(self, record: Any) -> bool:
|
|
651
|
+
"""Reject stale pre-Phase-2 builds for a composed runtime.
|
|
652
|
+
|
|
653
|
+
This check is intentionally scoped to Harness/plugin runtimes. Legacy
|
|
654
|
+
ADK/LangGraph build selection retains its existing behavior.
|
|
655
|
+
"""
|
|
656
|
+
|
|
657
|
+
if not record.artifact_path:
|
|
658
|
+
return False
|
|
659
|
+
try:
|
|
660
|
+
archive = self.workspace.resolve(record.artifact_path, must_exist=True)
|
|
661
|
+
with zipfile.ZipFile(archive) as bundle:
|
|
662
|
+
manifest = json.loads(bundle.read("manifest.json"))
|
|
663
|
+
names = frozenset(bundle.namelist())
|
|
664
|
+
return bool(
|
|
665
|
+
isinstance(manifest, dict)
|
|
666
|
+
and manifest.get("compositionProfileDigest")
|
|
667
|
+
and "composition-profile.json" in names
|
|
668
|
+
and "plugin-lock.json" in names
|
|
669
|
+
)
|
|
670
|
+
except (KeyError, OSError, UnicodeError, ValueError, zipfile.BadZipFile):
|
|
671
|
+
return False
|
|
672
|
+
|
|
673
|
+
async def create_agent_schedule(
|
|
674
|
+
self,
|
|
675
|
+
agent_id: str,
|
|
676
|
+
*,
|
|
677
|
+
display_name: str,
|
|
678
|
+
prompt: str,
|
|
679
|
+
schedule: ScheduleSpec,
|
|
680
|
+
enabled: bool = True,
|
|
681
|
+
continuity: Literal["new_session", "continue_session"] = "new_session",
|
|
682
|
+
session_id: str | None = None,
|
|
683
|
+
) -> ScheduledTask:
|
|
684
|
+
"""Create a local-only schedule for the Kernel Runtime that owns it.
|
|
685
|
+
|
|
686
|
+
The browser provides only human intent (prompt/calendar/continuity).
|
|
687
|
+
The trusted Studio process resolves the immutable Build and concrete
|
|
688
|
+
Kernel instance. Thus a task cannot be pointed at another Agent
|
|
689
|
+
Instance, smuggle a permit reference, or claim cloud scheduling.
|
|
690
|
+
"""
|
|
691
|
+
|
|
692
|
+
await self.start()
|
|
693
|
+
build = await self.ensure_current_build(agent_id)
|
|
694
|
+
spec = self.resolve_run_spec(build.id)
|
|
695
|
+
await self.scheduler_runtimes.start()
|
|
696
|
+
try:
|
|
697
|
+
target = await self.scheduler_runtimes.ensure_build(
|
|
698
|
+
build.id,
|
|
699
|
+
expected_agent_id=agent_id,
|
|
700
|
+
)
|
|
701
|
+
runtime = self.scheduler_runtimes.runtime_for_build(build.id)
|
|
702
|
+
except StudioSchedulerRuntimeError as error:
|
|
703
|
+
raise StudioError(
|
|
704
|
+
error.code,
|
|
705
|
+
str(error),
|
|
706
|
+
status_code=409,
|
|
707
|
+
details={"agentId": agent_id, "buildId": build.id},
|
|
708
|
+
) from error
|
|
709
|
+
if continuity == "continue_session" and not session_id:
|
|
710
|
+
raise StudioError(
|
|
711
|
+
"SCHEDULE_SESSION_REQUIRED",
|
|
712
|
+
"继续会话的定时任务必须选择一个已有会话。",
|
|
713
|
+
status_code=422,
|
|
714
|
+
field="sessionId",
|
|
715
|
+
)
|
|
716
|
+
if continuity == "continue_session":
|
|
717
|
+
self._require_schedule_continuation(runtime, spec.launch_context.runtime_type)
|
|
718
|
+
task = ScheduledTask(
|
|
719
|
+
task_id=f"sched-{uuid4().hex[:20]}",
|
|
720
|
+
display_name=display_name,
|
|
721
|
+
target=ScheduledTaskTarget(
|
|
722
|
+
agent_id=agent_id,
|
|
723
|
+
tenant_id=target.tenant_id,
|
|
724
|
+
agent_instance_id=target.agent_instance_id,
|
|
725
|
+
agent_version_ref=str(build.id),
|
|
726
|
+
session_id=session_id,
|
|
727
|
+
# This is an opaque local authority marker, never a browser
|
|
728
|
+
# supplied credential. The dispatcher obtains a short-lived
|
|
729
|
+
# in-process permit at the AgentControl ingress boundary.
|
|
730
|
+
authorization_ref="runtime://local-kernel-ingress",
|
|
731
|
+
),
|
|
732
|
+
schedule=schedule,
|
|
733
|
+
command=ScheduleCommandTemplate(payload={"content": prompt}),
|
|
734
|
+
enabled=enabled,
|
|
735
|
+
continuity=continuity,
|
|
736
|
+
)
|
|
737
|
+
return self.scheduler.create_task(task)
|
|
738
|
+
|
|
739
|
+
@staticmethod
|
|
740
|
+
def _require_schedule_continuation(runtime: Any, runtime_type: str) -> None:
|
|
741
|
+
"""Fail closed unless the bound Provider can preserve one conversation.
|
|
742
|
+
|
|
743
|
+
Harness owns process-local Session history without exposing the generic
|
|
744
|
+
checkpoint ``resume`` verb. Codex and external adapters must declare
|
|
745
|
+
the typed resume capability. An older Runtime that lacks either proof
|
|
746
|
+
remains usable for normal conversations and ``new_session`` schedules;
|
|
747
|
+
only the optional follow-up mode is rejected.
|
|
748
|
+
"""
|
|
749
|
+
|
|
750
|
+
if runtime_type.strip().lower() == "harness":
|
|
751
|
+
return
|
|
752
|
+
kernel = getattr(runtime, "kernel", None)
|
|
753
|
+
describe = getattr(kernel, "capabilities", None)
|
|
754
|
+
matrix = describe() if callable(describe) else None
|
|
755
|
+
resume = getattr(matrix, "resume", None)
|
|
756
|
+
if not bool(getattr(resume, "supported", False)):
|
|
757
|
+
raise StudioError(
|
|
758
|
+
"SCHEDULE_CONTINUATION_UNAVAILABLE",
|
|
759
|
+
"当前 Runtime 未声明可恢复的会话续接能力;可改用新会话定时任务。",
|
|
760
|
+
status_code=409,
|
|
761
|
+
field="continuity",
|
|
762
|
+
details={"runtimeType": runtime_type},
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
def list_agent_schedules(self, agent_id: str) -> list[ScheduledTask]:
|
|
766
|
+
return [task for task in self.scheduler.list_tasks() if task.target.agent_id == agent_id]
|
|
767
|
+
|
|
768
|
+
def get_agent_schedule(self, agent_id: str, task_id: str) -> ScheduledTask:
|
|
769
|
+
task = self.scheduler.get_task(task_id)
|
|
770
|
+
if task.target.agent_id != agent_id:
|
|
771
|
+
from ksadk.studio.errors import not_found
|
|
772
|
+
|
|
773
|
+
raise not_found("schedule", task_id)
|
|
774
|
+
return task
|
|
775
|
+
|
|
776
|
+
def update_agent_schedule(
|
|
777
|
+
self,
|
|
778
|
+
agent_id: str,
|
|
779
|
+
task_id: str,
|
|
780
|
+
*,
|
|
781
|
+
display_name: str,
|
|
782
|
+
prompt: str,
|
|
783
|
+
schedule: ScheduleSpec,
|
|
784
|
+
enabled: bool,
|
|
785
|
+
continuity: Literal["new_session", "continue_session"],
|
|
786
|
+
session_id: str | None,
|
|
787
|
+
) -> ScheduledTask:
|
|
788
|
+
existing = self.get_agent_schedule(agent_id, task_id)
|
|
789
|
+
if continuity == "continue_session" and not session_id:
|
|
790
|
+
raise StudioError(
|
|
791
|
+
"SCHEDULE_SESSION_REQUIRED",
|
|
792
|
+
"继续会话的定时任务必须选择一个已有会话。",
|
|
793
|
+
status_code=422,
|
|
794
|
+
field="sessionId",
|
|
795
|
+
)
|
|
796
|
+
task = existing.model_copy(
|
|
797
|
+
update={
|
|
798
|
+
"display_name": display_name,
|
|
799
|
+
"schedule": schedule,
|
|
800
|
+
"command": ScheduleCommandTemplate(payload={"content": prompt}),
|
|
801
|
+
"enabled": enabled,
|
|
802
|
+
"continuity": continuity,
|
|
803
|
+
"target": existing.target.model_copy(update={"session_id": session_id}),
|
|
804
|
+
}
|
|
805
|
+
)
|
|
806
|
+
return self.scheduler.update_task(task_id, task)
|
|
807
|
+
|
|
808
|
+
async def run_agent_schedule_now(self, agent_id: str, task_id: str):
|
|
809
|
+
self.get_agent_schedule(agent_id, task_id)
|
|
810
|
+
return await self.scheduler.run_now(task_id)
|
|
811
|
+
|
|
183
812
|
def codex_manifest_state(self, agent_id: str | None = None) -> dict:
|
|
184
813
|
return self.codex_agents.manifest_state(agent_id)
|
|
185
814
|
|
|
@@ -210,11 +839,13 @@ class StudioService:
|
|
|
210
839
|
spec: AgentSpec,
|
|
211
840
|
*,
|
|
212
841
|
expected_revision: int,
|
|
842
|
+
name: str | None = None,
|
|
213
843
|
) -> AgentDraft:
|
|
214
844
|
return self.codex_agents.update(
|
|
215
845
|
agent_id,
|
|
216
846
|
spec,
|
|
217
847
|
expected_revision=expected_revision,
|
|
848
|
+
name=name,
|
|
218
849
|
)
|
|
219
850
|
|
|
220
851
|
def delete_codex_agent(self, agent_id: str, *, purge: bool = False) -> None:
|
|
@@ -249,6 +880,7 @@ class StudioService:
|
|
|
249
880
|
approval_mode: str | None = None,
|
|
250
881
|
collaboration_mode: str | None = None,
|
|
251
882
|
goal_objective: str | None = None,
|
|
883
|
+
reasoning_effort: str | None = None,
|
|
252
884
|
runtime_input: Any = None,
|
|
253
885
|
idempotency_key: str,
|
|
254
886
|
on_event: Callable[[RunEvent], None] | None = None,
|
|
@@ -262,12 +894,13 @@ class StudioService:
|
|
|
262
894
|
approval_mode=approval_mode,
|
|
263
895
|
collaboration_mode=collaboration_mode,
|
|
264
896
|
goal_objective=goal_objective,
|
|
897
|
+
reasoning_effort=reasoning_effort,
|
|
265
898
|
runtime_input=runtime_input,
|
|
266
899
|
idempotency_key=idempotency_key,
|
|
267
900
|
on_event=on_event,
|
|
268
901
|
)
|
|
269
902
|
|
|
270
|
-
def delete_session(self, session_id: str) -> None:
|
|
903
|
+
async def delete_session(self, session_id: str) -> None:
|
|
271
904
|
from ksadk.studio.errors import not_found
|
|
272
905
|
|
|
273
906
|
runs = self.event_store.list_runs(session_id=session_id)
|
|
@@ -280,8 +913,132 @@ class StudioService:
|
|
|
280
913
|
status_code=409,
|
|
281
914
|
details={"sessionId": session_id},
|
|
282
915
|
)
|
|
916
|
+
await self.plugin_runs.close_session(session_id)
|
|
917
|
+
await self.session_service.delete_session(session_id)
|
|
283
918
|
self.event_store.delete_session(session_id)
|
|
284
919
|
|
|
920
|
+
async def aclose(self) -> None:
|
|
921
|
+
"""Release local provider activations and supervised plugin processes."""
|
|
922
|
+
|
|
923
|
+
await self.plugin_runs.aclose()
|
|
924
|
+
if self._dsh_provider_registration_manager is not None:
|
|
925
|
+
await self._dsh_provider_registration_manager.aclose()
|
|
926
|
+
|
|
927
|
+
async def _require_runtime_session(self, session_id: str) -> None:
|
|
928
|
+
if await self.session_service.get_session_metadata(session_id) is None:
|
|
929
|
+
raise StudioError(
|
|
930
|
+
"SESSION_NOT_FOUND",
|
|
931
|
+
"session 不存在",
|
|
932
|
+
status_code=404,
|
|
933
|
+
details={"id": session_id},
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
async def trajectory_page(
|
|
937
|
+
self,
|
|
938
|
+
session_id: str,
|
|
939
|
+
*,
|
|
940
|
+
before_seq_id: int | None = None,
|
|
941
|
+
invocation_id: str | None = None,
|
|
942
|
+
limit: int = 100,
|
|
943
|
+
) -> dict[str, Any]:
|
|
944
|
+
"""Return the bounded, canonical RuntimeEvent trajectory for a session."""
|
|
945
|
+
|
|
946
|
+
await self._require_runtime_session(session_id)
|
|
947
|
+
events = await self.runtime_events.list(
|
|
948
|
+
session_id,
|
|
949
|
+
before_seq=before_seq_id,
|
|
950
|
+
run_id=invocation_id,
|
|
951
|
+
limit=limit,
|
|
952
|
+
)
|
|
953
|
+
oldest_seq_id = events[0].seq if events else None
|
|
954
|
+
latest_seq_id = events[-1].seq if events else None
|
|
955
|
+
older = (
|
|
956
|
+
await self.runtime_events.list(
|
|
957
|
+
session_id,
|
|
958
|
+
before_seq=oldest_seq_id,
|
|
959
|
+
run_id=invocation_id,
|
|
960
|
+
limit=1,
|
|
961
|
+
)
|
|
962
|
+
if oldest_seq_id is not None
|
|
963
|
+
else []
|
|
964
|
+
)
|
|
965
|
+
return {
|
|
966
|
+
"items": [project_trajectory_event(event) for event in events],
|
|
967
|
+
"page": {
|
|
968
|
+
"oldestSeqId": oldest_seq_id,
|
|
969
|
+
"latestSeqId": latest_seq_id,
|
|
970
|
+
"hasMore": bool(older),
|
|
971
|
+
},
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async def stream_trajectory(
|
|
975
|
+
self,
|
|
976
|
+
session_id: str,
|
|
977
|
+
after_seq_id: int = 0,
|
|
978
|
+
*,
|
|
979
|
+
invocation_id: str | None = None,
|
|
980
|
+
):
|
|
981
|
+
await self._require_runtime_session(session_id)
|
|
982
|
+
cursor = after_seq_id
|
|
983
|
+
while True:
|
|
984
|
+
async for event in self.runtime_events.subscribe_session(
|
|
985
|
+
session_id,
|
|
986
|
+
after_seq=cursor,
|
|
987
|
+
poll_interval=min(0.25, _TRAJECTORY_KEEPALIVE_SECONDS),
|
|
988
|
+
timeout=_TRAJECTORY_KEEPALIVE_SECONDS,
|
|
989
|
+
):
|
|
990
|
+
cursor = event.seq
|
|
991
|
+
if invocation_id is not None and event.run_id != invocation_id:
|
|
992
|
+
continue
|
|
993
|
+
yield encode_sse(project_trajectory_event(event), event_id=cursor)
|
|
994
|
+
yield ": keepalive\n\n"
|
|
995
|
+
|
|
996
|
+
async def export_runtime_session(
|
|
997
|
+
self,
|
|
998
|
+
session_id: str,
|
|
999
|
+
*,
|
|
1000
|
+
filename: str,
|
|
1001
|
+
invocation_id: str | None = None,
|
|
1002
|
+
) -> dict[str, Any]:
|
|
1003
|
+
await self._require_runtime_session(session_id)
|
|
1004
|
+
relative = Path(filename)
|
|
1005
|
+
if (
|
|
1006
|
+
not filename
|
|
1007
|
+
or relative.is_absolute()
|
|
1008
|
+
or len(relative.parts) != 1
|
|
1009
|
+
or relative.name in {".", ".."}
|
|
1010
|
+
):
|
|
1011
|
+
raise StudioError(
|
|
1012
|
+
"SESSION_EXPORT_FILENAME_INVALID",
|
|
1013
|
+
"导出文件名必须是普通文件名",
|
|
1014
|
+
status_code=422,
|
|
1015
|
+
field="filename",
|
|
1016
|
+
)
|
|
1017
|
+
export_dir = self.workspace.resolve(".agentkit/exports")
|
|
1018
|
+
export_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
1019
|
+
export_dir.chmod(0o700)
|
|
1020
|
+
try:
|
|
1021
|
+
result = await export_session_log(
|
|
1022
|
+
self.session_service,
|
|
1023
|
+
session_id,
|
|
1024
|
+
export_dir / relative.name,
|
|
1025
|
+
invocation_id=invocation_id,
|
|
1026
|
+
)
|
|
1027
|
+
except SessionLogError as exc:
|
|
1028
|
+
code = str(exc).partition(":")[0]
|
|
1029
|
+
raise StudioError(
|
|
1030
|
+
code,
|
|
1031
|
+
str(exc),
|
|
1032
|
+
status_code=409 if code == "SESSION_LOG_TARGET_EXISTS" else 422,
|
|
1033
|
+
) from exc
|
|
1034
|
+
return {
|
|
1035
|
+
"path": result.path.relative_to(self.workspace.root).as_posix(),
|
|
1036
|
+
"eventCount": result.event_count,
|
|
1037
|
+
"firstSeqId": result.first_seq_id,
|
|
1038
|
+
"lastSeqId": result.last_seq_id,
|
|
1039
|
+
"exportedThroughSeqId": result.exported_through_seq_id,
|
|
1040
|
+
}
|
|
1041
|
+
|
|
285
1042
|
async def test_model_profile(self, resource_id: str) -> dict:
|
|
286
1043
|
return await test_model_profile_connection(
|
|
287
1044
|
catalog=self.catalog,
|
|
@@ -313,6 +1070,10 @@ class StudioService:
|
|
|
313
1070
|
description=description,
|
|
314
1071
|
)
|
|
315
1072
|
self._validate_bindings(resolved_spec.bindings)
|
|
1073
|
+
self._validate_framework_binding_support(
|
|
1074
|
+
resolved_spec.runtime,
|
|
1075
|
+
resolved_spec.bindings,
|
|
1076
|
+
)
|
|
316
1077
|
draft = self.drafts.create(
|
|
317
1078
|
agent_id=agent_id,
|
|
318
1079
|
name=name,
|
|
@@ -321,7 +1082,11 @@ class StudioService:
|
|
|
321
1082
|
spec=resolved_spec,
|
|
322
1083
|
labels=labels,
|
|
323
1084
|
)
|
|
324
|
-
materialize_generated_runtime_source(
|
|
1085
|
+
materialize_generated_runtime_source(
|
|
1086
|
+
self.workspace,
|
|
1087
|
+
draft,
|
|
1088
|
+
catalog=self.catalog,
|
|
1089
|
+
)
|
|
325
1090
|
return draft
|
|
326
1091
|
|
|
327
1092
|
def create_authored_agent(
|
|
@@ -382,12 +1147,31 @@ class StudioService:
|
|
|
382
1147
|
*,
|
|
383
1148
|
messages: list[dict[str, str]],
|
|
384
1149
|
model_profile_id: str,
|
|
1150
|
+
runtime_type: str = "codex",
|
|
1151
|
+
agent_model_profile_ids: list[str] | None = None,
|
|
1152
|
+
agent_default_model_profile_id: str | None = None,
|
|
1153
|
+
tool_resource_ids: list[str] | None = None,
|
|
1154
|
+
mcp_resource_ids: list[str] | None = None,
|
|
1155
|
+
skill_resource_ids: list[str] | None = None,
|
|
1156
|
+
request_id: str | None = None,
|
|
385
1157
|
) -> dict:
|
|
386
1158
|
return await self.authoring.compose_conversation(
|
|
387
1159
|
messages=messages,
|
|
388
1160
|
model_profile_id=model_profile_id,
|
|
1161
|
+
runtime_type=runtime_type,
|
|
1162
|
+
agent_model_profile_ids=agent_model_profile_ids,
|
|
1163
|
+
agent_default_model_profile_id=agent_default_model_profile_id,
|
|
1164
|
+
tool_resource_ids=tool_resource_ids,
|
|
1165
|
+
mcp_resource_ids=mcp_resource_ids,
|
|
1166
|
+
skill_resource_ids=skill_resource_ids,
|
|
1167
|
+
request_id=request_id,
|
|
389
1168
|
)
|
|
390
1169
|
|
|
1170
|
+
def conversation_authoring_status(self, request_id: str) -> dict | None:
|
|
1171
|
+
"""Return the in-memory stage snapshot for one authoring request."""
|
|
1172
|
+
|
|
1173
|
+
return self.authoring.conversation_status(request_id)
|
|
1174
|
+
|
|
391
1175
|
def is_codex_agent(self, agent_id: str) -> bool:
|
|
392
1176
|
"""Return whether one Agent is backed by the Codex YAML contract."""
|
|
393
1177
|
|
|
@@ -406,6 +1190,241 @@ class StudioService:
|
|
|
406
1190
|
return framework.strip().lower() or "adk"
|
|
407
1191
|
return runtime.type
|
|
408
1192
|
|
|
1193
|
+
def _agent_prompt_sources(self, agent_id: str) -> tuple[str, str, str]:
|
|
1194
|
+
"""Resolve the canonical prompt inputs used by preview and execution."""
|
|
1195
|
+
|
|
1196
|
+
if self.is_codex_agent(agent_id):
|
|
1197
|
+
snapshot = self.codex_manifests.load(agent_id)
|
|
1198
|
+
return (
|
|
1199
|
+
snapshot.manifest.prompt or "",
|
|
1200
|
+
snapshot.manifest.task_prompt or "",
|
|
1201
|
+
"codex",
|
|
1202
|
+
)
|
|
1203
|
+
draft = self.drafts.get(agent_id)
|
|
1204
|
+
instructions = draft.spec.instructions
|
|
1205
|
+
return (
|
|
1206
|
+
str(instructions.system or ""),
|
|
1207
|
+
str(instructions.task or ""),
|
|
1208
|
+
self.agent_runtime_type(agent_id),
|
|
1209
|
+
)
|
|
1210
|
+
|
|
1211
|
+
def compile_prompt_preview(
|
|
1212
|
+
self,
|
|
1213
|
+
agent_id: str,
|
|
1214
|
+
*,
|
|
1215
|
+
request_instructions: str = "",
|
|
1216
|
+
include_content: bool = False,
|
|
1217
|
+
) -> dict:
|
|
1218
|
+
"""Compile the real prompt without creating a Session, Trace, or Build."""
|
|
1219
|
+
|
|
1220
|
+
from ksadk.prompts.resolved import (
|
|
1221
|
+
ResolvedPromptSources,
|
|
1222
|
+
compile_resolved_prompt_dict,
|
|
1223
|
+
get_default_platform_policy_source,
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
agent_system, agent_task, runtime_type = self._agent_prompt_sources(agent_id)
|
|
1227
|
+
compiled = compile_resolved_prompt_dict(
|
|
1228
|
+
ResolvedPromptSources(
|
|
1229
|
+
agent_system=agent_system,
|
|
1230
|
+
agent_task=agent_task,
|
|
1231
|
+
request_instructions=str(request_instructions or "").strip(),
|
|
1232
|
+
platform_policy_source=get_default_platform_policy_source(),
|
|
1233
|
+
)
|
|
1234
|
+
)
|
|
1235
|
+
if compiled is None:
|
|
1236
|
+
return {
|
|
1237
|
+
"promptVersion": "v1",
|
|
1238
|
+
"contentHash": "",
|
|
1239
|
+
"stablePrefixHash": "",
|
|
1240
|
+
"sections": [],
|
|
1241
|
+
"runtimeType": runtime_type,
|
|
1242
|
+
"warnings": ["no prompt content to compile"],
|
|
1243
|
+
}
|
|
1244
|
+
result: dict[str, Any] = {
|
|
1245
|
+
"promptVersion": compiled["prompt_compiler_version"],
|
|
1246
|
+
"contentHash": compiled["prompt_content_hash"],
|
|
1247
|
+
"stablePrefixHash": compiled["prompt_stable_prefix_hash"],
|
|
1248
|
+
"sectionHashes": compiled["prompt_section_hashes"],
|
|
1249
|
+
"tokensBySection": compiled["prompt_tokens_by_section"],
|
|
1250
|
+
"estimatedTokens": compiled["prompt_estimated_tokens"],
|
|
1251
|
+
"sectionCount": compiled["prompt_section_count"],
|
|
1252
|
+
"runtimeType": runtime_type,
|
|
1253
|
+
"platformPolicyVersion": compiled.get("prompt_platform_policy_version"),
|
|
1254
|
+
"warnings": [],
|
|
1255
|
+
}
|
|
1256
|
+
if include_content:
|
|
1257
|
+
result["content"] = compiled["prompt_content"]
|
|
1258
|
+
return result
|
|
1259
|
+
|
|
1260
|
+
def reveal_run_prompt(self, run_id: str) -> dict:
|
|
1261
|
+
"""Rebuild immutable Run prompt sections and reveal only on hash match."""
|
|
1262
|
+
|
|
1263
|
+
from ksadk.prompts.resolved import (
|
|
1264
|
+
ResolvedPromptSources,
|
|
1265
|
+
compile_resolved_prompt_dict,
|
|
1266
|
+
get_default_platform_policy_source,
|
|
1267
|
+
sections_from_resolved_sources,
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
record = self.event_store.get(run_id)
|
|
1271
|
+
resolver = self.codex_runs if record.runtime_type == "codex" else self.framework_runs
|
|
1272
|
+
spec = resolver.resolve(record.build_id, model=record.model or None)
|
|
1273
|
+
config = dict(spec.request_config or {})
|
|
1274
|
+
sources = ResolvedPromptSources(
|
|
1275
|
+
agent_system=str(config.get("agent_system") or ""),
|
|
1276
|
+
agent_task=str(config.get("agent_task") or ""),
|
|
1277
|
+
request_instructions=str(config.get("instructions") or ""),
|
|
1278
|
+
platform_policy_source=get_default_platform_policy_source(),
|
|
1279
|
+
)
|
|
1280
|
+
compiled = compile_resolved_prompt_dict(sources)
|
|
1281
|
+
expected_hash = str((record.prompt_evidence or {}).get("contentHash") or "")
|
|
1282
|
+
actual_hash = str((compiled or {}).get("prompt_content_hash") or "")
|
|
1283
|
+
if not compiled or not expected_hash or actual_hash != expected_hash:
|
|
1284
|
+
return {
|
|
1285
|
+
"available": False,
|
|
1286
|
+
"reason": "无法证明重建内容与本次运行一致,已拒绝展示正文。",
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
"available": True,
|
|
1290
|
+
"contentHash": actual_hash,
|
|
1291
|
+
"sections": [
|
|
1292
|
+
{
|
|
1293
|
+
"id": section.section_id,
|
|
1294
|
+
"source": section.source,
|
|
1295
|
+
"content": section.content,
|
|
1296
|
+
}
|
|
1297
|
+
for section in sections_from_resolved_sources(sources)
|
|
1298
|
+
],
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
async def preview_context(
|
|
1302
|
+
self,
|
|
1303
|
+
agent_id: str,
|
|
1304
|
+
*,
|
|
1305
|
+
user_input: str = "",
|
|
1306
|
+
request_instructions: str = "",
|
|
1307
|
+
simulated_history: list[dict[str, str]] | None = None,
|
|
1308
|
+
include_content: bool = False,
|
|
1309
|
+
) -> dict:
|
|
1310
|
+
"""Run the production context planner without model or persistence side effects."""
|
|
1311
|
+
|
|
1312
|
+
from ksadk.context_engine.capabilities import capabilities_for_runtime_type
|
|
1313
|
+
from ksadk.context_engine.hosted_pipeline import run_hosted_pipeline
|
|
1314
|
+
from ksadk.prompts.resolved import (
|
|
1315
|
+
ResolvedPromptSources,
|
|
1316
|
+
compile_resolved_prompt_dict,
|
|
1317
|
+
get_default_platform_policy_source,
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
agent_system, agent_task, runtime_type = self._agent_prompt_sources(agent_id)
|
|
1321
|
+
capabilities = capabilities_for_runtime_type(runtime_type)
|
|
1322
|
+
compiled_prompt = compile_resolved_prompt_dict(
|
|
1323
|
+
ResolvedPromptSources(
|
|
1324
|
+
agent_system=agent_system,
|
|
1325
|
+
agent_task=agent_task,
|
|
1326
|
+
request_instructions=str(request_instructions or "").strip(),
|
|
1327
|
+
platform_policy_source=get_default_platform_policy_source(),
|
|
1328
|
+
)
|
|
1329
|
+
)
|
|
1330
|
+
history = [
|
|
1331
|
+
{
|
|
1332
|
+
"role": item.get("role", "user"),
|
|
1333
|
+
"content": item.get("content", ""),
|
|
1334
|
+
}
|
|
1335
|
+
for item in (simulated_history or [])
|
|
1336
|
+
]
|
|
1337
|
+
pipeline = await run_hosted_pipeline(
|
|
1338
|
+
compiled_prompt=compiled_prompt,
|
|
1339
|
+
user_input=str(user_input or "").strip(),
|
|
1340
|
+
history=history,
|
|
1341
|
+
working_state=None,
|
|
1342
|
+
model_metadata={
|
|
1343
|
+
"context_window_tokens": 200_000,
|
|
1344
|
+
"max_output_tokens": 32_000,
|
|
1345
|
+
},
|
|
1346
|
+
contributors=None,
|
|
1347
|
+
integration_mode=capabilities.integration_mode,
|
|
1348
|
+
accounting_accuracy="estimated",
|
|
1349
|
+
session_id=f"preview-{agent_id}",
|
|
1350
|
+
invocation_id=f"preview-{agent_id}",
|
|
1351
|
+
)
|
|
1352
|
+
if pipeline is None:
|
|
1353
|
+
return {
|
|
1354
|
+
"accuracy": "estimated",
|
|
1355
|
+
"warnings": ["no content to plan"],
|
|
1356
|
+
"items": [],
|
|
1357
|
+
}
|
|
1358
|
+
plan = pipeline.plan
|
|
1359
|
+
assembled = pipeline.assembled
|
|
1360
|
+
return {
|
|
1361
|
+
"accuracy": plan.get("accounting_accuracy", "estimated"),
|
|
1362
|
+
"policyVersion": plan.get("policy_version"),
|
|
1363
|
+
"planId": plan.get("plan_id"),
|
|
1364
|
+
"budget": {
|
|
1365
|
+
"maxInputTokens": (plan.get("budget") or {}).get("max_input_tokens"),
|
|
1366
|
+
"softLimitTokens": (plan.get("budget") or {}).get("soft_limit_tokens"),
|
|
1367
|
+
"hardLimitTokens": (plan.get("budget") or {}).get("hard_limit_tokens"),
|
|
1368
|
+
},
|
|
1369
|
+
"items": plan.get("selected", []),
|
|
1370
|
+
"decisions": plan.get("decisions", []),
|
|
1371
|
+
"totalsByKind": plan.get("tokens_by_kind", {}),
|
|
1372
|
+
"plannedInputTokens": plan.get("planned_input_tokens"),
|
|
1373
|
+
"warnings": list(assembled.warnings),
|
|
1374
|
+
"projection": {
|
|
1375
|
+
"runtimeType": runtime_type,
|
|
1376
|
+
"integrationMode": capabilities.integration_mode,
|
|
1377
|
+
"promptOwner": capabilities.prompt_owner,
|
|
1378
|
+
},
|
|
1379
|
+
**({"system": assembled.system} if include_content else {}),
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
def detect_importable_project(self) -> dict | None:
|
|
1383
|
+
"""Expose a root framework project for explicit Studio import only."""
|
|
1384
|
+
from ksadk.studio.manifest_resolver import detect_manifest_kind
|
|
1385
|
+
|
|
1386
|
+
result = detect_manifest_kind(self.workspace.root)
|
|
1387
|
+
if result.kind != "framework":
|
|
1388
|
+
return None
|
|
1389
|
+
import yaml
|
|
1390
|
+
|
|
1391
|
+
try:
|
|
1392
|
+
payload = yaml.safe_load(result.path.read_text(encoding="utf-8-sig")) or {}
|
|
1393
|
+
except Exception: # noqa: BLE001
|
|
1394
|
+
return None
|
|
1395
|
+
return {
|
|
1396
|
+
"kind": "framework",
|
|
1397
|
+
"runtimeType": result.framework or result.runtime_type,
|
|
1398
|
+
"name": str(payload.get("name") or self.workspace.root.name or "imported-agent"),
|
|
1399
|
+
"model": str(payload.get("model") or ""),
|
|
1400
|
+
"prompt": str(payload.get("prompt") or payload.get("instruction") or ""),
|
|
1401
|
+
"task": str(payload.get("task") or ""),
|
|
1402
|
+
"manifestPath": "agentengine.yaml",
|
|
1403
|
+
"requiresConfirmation": True,
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
def import_root_project(
|
|
1407
|
+
self,
|
|
1408
|
+
*,
|
|
1409
|
+
name: str | None = None,
|
|
1410
|
+
slug: str | None = None,
|
|
1411
|
+
) -> AgentDraft:
|
|
1412
|
+
"""Import an explicitly detected root Framework project as a Studio draft."""
|
|
1413
|
+
|
|
1414
|
+
importable = self.detect_importable_project()
|
|
1415
|
+
if importable is None:
|
|
1416
|
+
raise StudioError(
|
|
1417
|
+
"PROJECT_NOT_IMPORTABLE",
|
|
1418
|
+
"当前工作区根没有可导入的 Framework 项目",
|
|
1419
|
+
status_code=422,
|
|
1420
|
+
)
|
|
1421
|
+
inspection = self.inspect_agent_project(".")
|
|
1422
|
+
return self.commit_agent_project(
|
|
1423
|
+
inspection["inspectionToken"],
|
|
1424
|
+
name=name or importable.get("name"),
|
|
1425
|
+
slug=slug or importable.get("name"),
|
|
1426
|
+
)
|
|
1427
|
+
|
|
409
1428
|
def list_agents(self, *, query: str = "", limit: int = 50) -> list[AgentDraft]:
|
|
410
1429
|
"""List all local Agents from one registry view across runtime types."""
|
|
411
1430
|
|
|
@@ -429,12 +1448,36 @@ class StudioService:
|
|
|
429
1448
|
|
|
430
1449
|
def agent_detail(self, agent_id: str) -> dict:
|
|
431
1450
|
if self.is_codex_agent(agent_id):
|
|
432
|
-
|
|
1451
|
+
detail = self.codex_agent_detail(agent_id)
|
|
1452
|
+
detail["soulProjection"] = self._soul_projection(detail["draft"])
|
|
1453
|
+
return detail
|
|
433
1454
|
draft = self.drafts.get(agent_id)
|
|
434
1455
|
return {
|
|
435
1456
|
"draft": draft,
|
|
436
1457
|
"builds": self.builds.list_for_agent(agent_id)[:10],
|
|
437
1458
|
"validation": self.validator.validate(draft),
|
|
1459
|
+
"soulProjection": self._soul_projection(draft),
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
@staticmethod
|
|
1463
|
+
def _soul_projection(draft: AgentDraft) -> dict[str, object]:
|
|
1464
|
+
"""Describe the reviewed Soul source without claiming runtime learning."""
|
|
1465
|
+
|
|
1466
|
+
soul = draft.spec.soul
|
|
1467
|
+
runtime_type = draft.spec.runtime.type if draft.spec.runtime is not None else ""
|
|
1468
|
+
compile_target = {
|
|
1469
|
+
"codex": "managed-runtime.base_instructions",
|
|
1470
|
+
"plugin": "instructions/soul.md",
|
|
1471
|
+
}.get(runtime_type, "resolved-agent-spec.instructions.system")
|
|
1472
|
+
return {
|
|
1473
|
+
"present": soul is not None,
|
|
1474
|
+
"source": "AgentSpec.soul",
|
|
1475
|
+
"sourceRevision": draft.metadata.revision,
|
|
1476
|
+
"schemaVersion": soul.schema_version if soul is not None else "agentkit.soul/v1",
|
|
1477
|
+
"digest": soul_digest(soul) if soul is not None else None,
|
|
1478
|
+
"digestAlgorithm": "sha256-canonical-json",
|
|
1479
|
+
"compileTarget": compile_target,
|
|
1480
|
+
"compileOrder": "before-instructions.system",
|
|
438
1481
|
}
|
|
439
1482
|
|
|
440
1483
|
def create_studio_agent(
|
|
@@ -483,6 +1526,7 @@ class StudioService:
|
|
|
483
1526
|
spec: AgentSpec,
|
|
484
1527
|
*,
|
|
485
1528
|
expected_revision: int,
|
|
1529
|
+
name: str | None = None,
|
|
486
1530
|
) -> AgentDraft:
|
|
487
1531
|
if self.is_codex_agent(agent_id):
|
|
488
1532
|
spec.runtime = self.agent_detail(agent_id)["draft"].spec.runtime
|
|
@@ -492,6 +1536,7 @@ class StudioService:
|
|
|
492
1536
|
agent_id,
|
|
493
1537
|
spec,
|
|
494
1538
|
expected_revision=expected_revision,
|
|
1539
|
+
name=name,
|
|
495
1540
|
),
|
|
496
1541
|
)
|
|
497
1542
|
current = self.drafts.get(agent_id)
|
|
@@ -508,7 +1553,12 @@ class StudioService:
|
|
|
508
1553
|
)
|
|
509
1554
|
return cast(
|
|
510
1555
|
AgentDraft,
|
|
511
|
-
self.update_agent(
|
|
1556
|
+
self.update_agent(
|
|
1557
|
+
agent_id,
|
|
1558
|
+
spec,
|
|
1559
|
+
expected_revision=expected_revision,
|
|
1560
|
+
name=name,
|
|
1561
|
+
),
|
|
512
1562
|
)
|
|
513
1563
|
|
|
514
1564
|
def update_studio_agent_bindings(
|
|
@@ -580,32 +1630,11 @@ class StudioService:
|
|
|
580
1630
|
revision: int,
|
|
581
1631
|
idempotency_key: str,
|
|
582
1632
|
) -> Operation:
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if runtime is None:
|
|
589
|
-
from ksadk.studio.errors import StudioError
|
|
590
|
-
|
|
591
|
-
raise StudioError(
|
|
592
|
-
"RUNTIME_NOT_REGISTERED",
|
|
593
|
-
"Agent 引用的 RuntimeAdapter 未注册",
|
|
594
|
-
status_code=422,
|
|
595
|
-
details={"runtimeType": runtime_type},
|
|
596
|
-
)
|
|
597
|
-
if runtime["status"] != "ready":
|
|
598
|
-
from ksadk.studio.errors import StudioError
|
|
599
|
-
|
|
600
|
-
raise StudioError(
|
|
601
|
-
"RUNTIME_DEPENDENCY_MISSING",
|
|
602
|
-
f"{runtime['displayName']} Runtime 依赖未安装",
|
|
603
|
-
status_code=422,
|
|
604
|
-
details={
|
|
605
|
-
"runtimeType": runtime_type,
|
|
606
|
-
"installCommand": runtime["installCommand"],
|
|
607
|
-
},
|
|
608
|
-
)
|
|
1633
|
+
# Codex build admission already runs the injected runtime inspector and
|
|
1634
|
+
# pins its result into the immutable build. Checking package metadata
|
|
1635
|
+
# first would reject an explicitly supplied RuntimeExecutor/inspector
|
|
1636
|
+
# (including an out-of-process Codex runtime) merely because the local
|
|
1637
|
+
# Studio interpreter does not have the optional wheel installed.
|
|
609
1638
|
if self.is_codex_agent(agent_id):
|
|
610
1639
|
detail = self.agent_detail(agent_id)
|
|
611
1640
|
if revision != detail["draft"].metadata.revision:
|
|
@@ -620,6 +1649,12 @@ class StudioService:
|
|
|
620
1649
|
idempotency_key=idempotency_key,
|
|
621
1650
|
agent_id=agent_id,
|
|
622
1651
|
)
|
|
1652
|
+
|
|
1653
|
+
# A framework Build compiles and seals source/configuration only. Its
|
|
1654
|
+
# optional Runtime dependency belongs to run preflight, and may live in
|
|
1655
|
+
# a different process or deployment image. Rejecting the Build from
|
|
1656
|
+
# this Studio interpreter's package metadata would make portable
|
|
1657
|
+
# bundles impossible and incorrectly couple authoring to execution.
|
|
623
1658
|
return self.submit_build(
|
|
624
1659
|
agent_id,
|
|
625
1660
|
revision=revision,
|
|
@@ -646,6 +1681,7 @@ class StudioService:
|
|
|
646
1681
|
approval_mode: str | None = None,
|
|
647
1682
|
collaboration_mode: str | None = None,
|
|
648
1683
|
goal_objective: str | None = None,
|
|
1684
|
+
reasoning_effort: str | None = None,
|
|
649
1685
|
runtime_input: Any = None,
|
|
650
1686
|
on_event: Callable[[RunEvent], None] | None = None,
|
|
651
1687
|
) -> Operation:
|
|
@@ -664,6 +1700,7 @@ class StudioService:
|
|
|
664
1700
|
approval_mode=approval_mode,
|
|
665
1701
|
collaboration_mode=collaboration_mode,
|
|
666
1702
|
goal_objective=goal_objective,
|
|
1703
|
+
reasoning_effort=reasoning_effort,
|
|
667
1704
|
runtime_input=runtime_input,
|
|
668
1705
|
idempotency_key=idempotency_key,
|
|
669
1706
|
on_event=on_event,
|
|
@@ -677,6 +1714,7 @@ class StudioService:
|
|
|
677
1714
|
approval_mode=approval_mode,
|
|
678
1715
|
collaboration_mode=collaboration_mode,
|
|
679
1716
|
goal_objective=goal_objective,
|
|
1717
|
+
reasoning_effort=reasoning_effort,
|
|
680
1718
|
runtime_input=runtime_input,
|
|
681
1719
|
idempotency_key=idempotency_key,
|
|
682
1720
|
on_event=on_event,
|
|
@@ -725,15 +1763,50 @@ class StudioService:
|
|
|
725
1763
|
spec: AgentSpec,
|
|
726
1764
|
*,
|
|
727
1765
|
expected_revision: int,
|
|
1766
|
+
name: str | None = None,
|
|
728
1767
|
):
|
|
729
|
-
self.
|
|
730
|
-
|
|
1768
|
+
current = self.drafts.get(agent_id)
|
|
1769
|
+
if current.metadata.revision != expected_revision:
|
|
1770
|
+
raise StudioError(
|
|
1771
|
+
"AGENT_REVISION_CONFLICT",
|
|
1772
|
+
"Agent 已被其他操作更新",
|
|
1773
|
+
status_code=409,
|
|
1774
|
+
field="metadata.revision",
|
|
1775
|
+
details={
|
|
1776
|
+
"expected": expected_revision,
|
|
1777
|
+
"actual": current.metadata.revision,
|
|
1778
|
+
},
|
|
1779
|
+
)
|
|
1780
|
+
materializable_bindings = self._materializable_update_bindings(
|
|
1781
|
+
current.spec.bindings,
|
|
1782
|
+
spec.bindings,
|
|
1783
|
+
runtime=spec.runtime or current.spec.runtime,
|
|
1784
|
+
)
|
|
1785
|
+
self._validate_bindings(materializable_bindings)
|
|
1786
|
+
self._validate_framework_binding_support(
|
|
1787
|
+
spec.runtime or current.spec.runtime,
|
|
1788
|
+
materializable_bindings,
|
|
1789
|
+
)
|
|
1790
|
+
preview = current.model_copy(deep=True)
|
|
1791
|
+
preview.metadata.revision += 1
|
|
1792
|
+
if name is not None:
|
|
1793
|
+
preview.metadata.name = name
|
|
1794
|
+
preview.spec = spec.model_copy(deep=True)
|
|
1795
|
+
preview.spec.bindings = materializable_bindings
|
|
1796
|
+
# Generated source is a materialized view of the candidate Revision.
|
|
1797
|
+
# Complete it before the authoritative draft write so any resolver,
|
|
1798
|
+
# digest or runtime-capability failure leaves Revision/content intact.
|
|
1799
|
+
materialize_generated_runtime_source(
|
|
1800
|
+
self.workspace,
|
|
1801
|
+
preview,
|
|
1802
|
+
catalog=self.catalog,
|
|
1803
|
+
)
|
|
1804
|
+
return self.drafts.update(
|
|
731
1805
|
agent_id,
|
|
732
1806
|
spec,
|
|
733
1807
|
expected_revision=expected_revision,
|
|
1808
|
+
name=name,
|
|
734
1809
|
)
|
|
735
|
-
materialize_generated_runtime_source(self.workspace, updated)
|
|
736
|
-
return updated
|
|
737
1810
|
|
|
738
1811
|
def update_agent_bindings(
|
|
739
1812
|
self,
|
|
@@ -742,11 +1815,10 @@ class StudioService:
|
|
|
742
1815
|
*,
|
|
743
1816
|
expected_revision: int,
|
|
744
1817
|
):
|
|
745
|
-
self._validate_bindings(bindings)
|
|
746
1818
|
draft = self.drafts.get(agent_id)
|
|
747
1819
|
spec = draft.spec.model_copy(deep=True)
|
|
748
1820
|
spec.bindings = bindings
|
|
749
|
-
return self.
|
|
1821
|
+
return self.update_agent(
|
|
750
1822
|
agent_id,
|
|
751
1823
|
spec,
|
|
752
1824
|
expected_revision=expected_revision,
|
|
@@ -760,6 +1832,116 @@ class StudioService:
|
|
|
760
1832
|
self.catalog.resolve_mcp_tools(bindings)
|
|
761
1833
|
self.catalog.resolve_skills(bindings)
|
|
762
1834
|
|
|
1835
|
+
def _materializable_update_bindings(
|
|
1836
|
+
self,
|
|
1837
|
+
current: AgentBindings,
|
|
1838
|
+
candidate: AgentBindings,
|
|
1839
|
+
*,
|
|
1840
|
+
runtime: RuntimeRef | None,
|
|
1841
|
+
) -> AgentBindings:
|
|
1842
|
+
"""Return bindings that can be safely resolved into this Revision.
|
|
1843
|
+
|
|
1844
|
+
Unknown bindings written by older Studio versions remain authoritative
|
|
1845
|
+
draft data, but are dormant until they re-enter the resource catalog.
|
|
1846
|
+
A new or modified unknown binding is still rejected by the normal
|
|
1847
|
+
resolver. Framework MCP bindings are likewise read-only until generated
|
|
1848
|
+
runtime source has a real injection path.
|
|
1849
|
+
"""
|
|
1850
|
+
|
|
1851
|
+
if (
|
|
1852
|
+
runtime is not None
|
|
1853
|
+
and runtime.type in {"adk", "langgraph"}
|
|
1854
|
+
and candidate.mcp_servers != current.mcp_servers
|
|
1855
|
+
):
|
|
1856
|
+
raise StudioError(
|
|
1857
|
+
"MCP_RUNTIME_INCOMPATIBLE",
|
|
1858
|
+
"当前 Runtime 尚未支持把 MCP 绑定注入生成源码;历史绑定仅可保留",
|
|
1859
|
+
status_code=422,
|
|
1860
|
+
field="spec.bindings.mcpServers",
|
|
1861
|
+
details={"runtimeType": runtime.type},
|
|
1862
|
+
)
|
|
1863
|
+
|
|
1864
|
+
known = {item.resource_id for item in self.catalog.list(limit=10_000)}
|
|
1865
|
+
|
|
1866
|
+
def selected_for_materialization(
|
|
1867
|
+
selected: list,
|
|
1868
|
+
historical: list,
|
|
1869
|
+
) -> list:
|
|
1870
|
+
return [
|
|
1871
|
+
binding
|
|
1872
|
+
for binding in selected
|
|
1873
|
+
if binding.resource_id in known or binding not in historical
|
|
1874
|
+
]
|
|
1875
|
+
|
|
1876
|
+
resolved = candidate.model_copy(deep=True)
|
|
1877
|
+
if candidate.model_profile_id not in known:
|
|
1878
|
+
unchanged_model = (
|
|
1879
|
+
candidate.model_profile_id == current.model_profile_id
|
|
1880
|
+
and candidate.model_profile_ids == current.model_profile_ids
|
|
1881
|
+
)
|
|
1882
|
+
if unchanged_model:
|
|
1883
|
+
resolved.model_profile_id = None
|
|
1884
|
+
resolved.model_profile_ids = []
|
|
1885
|
+
if resolved.model_profile_id is not None:
|
|
1886
|
+
resolved.model_profile_ids = [
|
|
1887
|
+
resource_id
|
|
1888
|
+
for resource_id in resolved.model_profile_ids
|
|
1889
|
+
if resource_id in known or resource_id not in current.model_profile_ids
|
|
1890
|
+
]
|
|
1891
|
+
resolved.skills = selected_for_materialization(
|
|
1892
|
+
candidate.skills,
|
|
1893
|
+
current.skills,
|
|
1894
|
+
)
|
|
1895
|
+
resolved.tools = selected_for_materialization(
|
|
1896
|
+
candidate.tools,
|
|
1897
|
+
current.tools,
|
|
1898
|
+
)
|
|
1899
|
+
# No framework generated source consumes MCP today. Exact historical
|
|
1900
|
+
# values remain in `candidate`, while this preview omits them honestly.
|
|
1901
|
+
if runtime is not None and runtime.type in {"adk", "langgraph"}:
|
|
1902
|
+
resolved.mcp_servers = []
|
|
1903
|
+
else:
|
|
1904
|
+
resolved.mcp_servers = selected_for_materialization(
|
|
1905
|
+
candidate.mcp_servers,
|
|
1906
|
+
current.mcp_servers,
|
|
1907
|
+
)
|
|
1908
|
+
return resolved
|
|
1909
|
+
|
|
1910
|
+
def _validate_framework_binding_support(
|
|
1911
|
+
self,
|
|
1912
|
+
runtime: RuntimeRef | None,
|
|
1913
|
+
bindings: AgentBindings,
|
|
1914
|
+
) -> None:
|
|
1915
|
+
if runtime is None or runtime.type not in {"adk", "langgraph"}:
|
|
1916
|
+
return
|
|
1917
|
+
if bindings.mcp_servers:
|
|
1918
|
+
raise StudioError(
|
|
1919
|
+
"MCP_RUNTIME_INCOMPATIBLE",
|
|
1920
|
+
"当前 Runtime 尚未支持把 MCP 绑定注入生成源码",
|
|
1921
|
+
status_code=422,
|
|
1922
|
+
field="spec.bindings.mcpServers",
|
|
1923
|
+
details={"runtimeType": runtime.type},
|
|
1924
|
+
)
|
|
1925
|
+
unsupported: list[str] = []
|
|
1926
|
+
for binding in bindings.tools:
|
|
1927
|
+
if not binding.enabled:
|
|
1928
|
+
continue
|
|
1929
|
+
descriptor = self.catalog.get(binding.resource_id)
|
|
1930
|
+
contract = ToolContract.model_validate(descriptor.contract)
|
|
1931
|
+
if contract.executor not in {"builtin", "python"}:
|
|
1932
|
+
unsupported.append(binding.resource_id)
|
|
1933
|
+
if unsupported:
|
|
1934
|
+
raise StudioError(
|
|
1935
|
+
"TOOL_RUNTIME_INCOMPATIBLE",
|
|
1936
|
+
"当前 Runtime 仅支持 builtin/python Tool",
|
|
1937
|
+
status_code=422,
|
|
1938
|
+
field="spec.bindings.tools",
|
|
1939
|
+
details={
|
|
1940
|
+
"runtimeType": runtime.type,
|
|
1941
|
+
"resourceIds": unsupported,
|
|
1942
|
+
},
|
|
1943
|
+
)
|
|
1944
|
+
|
|
763
1945
|
def validate_agent(
|
|
764
1946
|
self,
|
|
765
1947
|
agent_id: str,
|
|
@@ -787,8 +1969,8 @@ class StudioService:
|
|
|
787
1969
|
)
|
|
788
1970
|
snapshot = draft.model_copy(deep=True)
|
|
789
1971
|
|
|
790
|
-
async def runner():
|
|
791
|
-
return await asyncio.to_thread(self.
|
|
1972
|
+
async def runner(_operation_id: str):
|
|
1973
|
+
return await asyncio.to_thread(self._build_agent_bundle, snapshot)
|
|
792
1974
|
|
|
793
1975
|
return self.operations.submit(
|
|
794
1976
|
kind=OperationKind.BUILD,
|
|
@@ -808,11 +1990,12 @@ class StudioService:
|
|
|
808
1990
|
approval_mode: str | None = None,
|
|
809
1991
|
collaboration_mode: str | None = None,
|
|
810
1992
|
goal_objective: str | None = None,
|
|
1993
|
+
reasoning_effort: str | None = None,
|
|
811
1994
|
runtime_input: Any = None,
|
|
812
1995
|
idempotency_key: str,
|
|
813
1996
|
on_event: Callable[[RunEvent], None] | None = None,
|
|
814
1997
|
) -> Operation:
|
|
815
|
-
async def runner():
|
|
1998
|
+
async def runner(_operation_id: str):
|
|
816
1999
|
return await self.run_build(
|
|
817
2000
|
build_id,
|
|
818
2001
|
user_input,
|
|
@@ -822,6 +2005,7 @@ class StudioService:
|
|
|
822
2005
|
approval_mode=approval_mode,
|
|
823
2006
|
collaboration_mode=collaboration_mode,
|
|
824
2007
|
goal_objective=goal_objective,
|
|
2008
|
+
reasoning_effort=reasoning_effort,
|
|
825
2009
|
runtime_input=runtime_input,
|
|
826
2010
|
on_event=on_event,
|
|
827
2011
|
)
|
|
@@ -844,27 +2028,20 @@ class StudioService:
|
|
|
844
2028
|
approval_mode: str | None = None,
|
|
845
2029
|
collaboration_mode: str | None = None,
|
|
846
2030
|
goal_objective: str | None = None,
|
|
2031
|
+
reasoning_effort: str | None = None,
|
|
847
2032
|
runtime_input: Any = None,
|
|
848
2033
|
on_event: Callable[[RunEvent], None] | None = None,
|
|
849
2034
|
):
|
|
850
2035
|
"""Execute any immutable Studio Build through the canonical executor."""
|
|
851
2036
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if getattr(exc, "status_code", None) != 404:
|
|
861
|
-
raise
|
|
862
|
-
spec = self.framework_runs.resolve(
|
|
863
|
-
build_id,
|
|
864
|
-
model=model,
|
|
865
|
-
approval_mode=approval_mode,
|
|
866
|
-
)
|
|
867
|
-
if collaboration_mode or goal_objective:
|
|
2037
|
+
await self.start()
|
|
2038
|
+
spec = self.resolve_run_spec(
|
|
2039
|
+
build_id,
|
|
2040
|
+
model=model,
|
|
2041
|
+
sandbox=sandbox,
|
|
2042
|
+
approval_mode=approval_mode,
|
|
2043
|
+
)
|
|
2044
|
+
if collaboration_mode or goal_objective or reasoning_effort:
|
|
868
2045
|
from dataclasses import replace
|
|
869
2046
|
|
|
870
2047
|
request_config = dict(spec.request_config)
|
|
@@ -873,6 +2050,8 @@ class StudioService:
|
|
|
873
2050
|
if goal_objective:
|
|
874
2051
|
request_config["goal_objective"] = goal_objective
|
|
875
2052
|
request_config["ephemeral"] = False
|
|
2053
|
+
if reasoning_effort:
|
|
2054
|
+
request_config["effort"] = reasoning_effort
|
|
876
2055
|
spec = replace(spec, request_config=request_config)
|
|
877
2056
|
return await self.run_service.run(
|
|
878
2057
|
spec,
|
|
@@ -882,28 +2061,6 @@ class StudioService:
|
|
|
882
2061
|
on_event=on_event,
|
|
883
2062
|
)
|
|
884
2063
|
|
|
885
|
-
def submit_evaluation(
|
|
886
|
-
self,
|
|
887
|
-
build_id: str,
|
|
888
|
-
suite_refs: list[str],
|
|
889
|
-
*,
|
|
890
|
-
fail_fast: bool,
|
|
891
|
-
idempotency_key: str,
|
|
892
|
-
) -> Operation:
|
|
893
|
-
async def runner():
|
|
894
|
-
return await self.evaluations.run(
|
|
895
|
-
build_id,
|
|
896
|
-
suite_refs,
|
|
897
|
-
fail_fast=fail_fast,
|
|
898
|
-
)
|
|
899
|
-
|
|
900
|
-
return self.operations.submit(
|
|
901
|
-
kind=OperationKind.EVALUATION,
|
|
902
|
-
resource_id=build_id,
|
|
903
|
-
idempotency_key=idempotency_key,
|
|
904
|
-
runner=runner,
|
|
905
|
-
)
|
|
906
|
-
|
|
907
2064
|
def submit_public_evaluation(
|
|
908
2065
|
self,
|
|
909
2066
|
evalset_file: str,
|
|
@@ -941,10 +2098,20 @@ class StudioService:
|
|
|
941
2098
|
config=config,
|
|
942
2099
|
report_dir=str(self.evaluation_storage.root),
|
|
943
2100
|
)
|
|
2101
|
+
evaluation_id = f"eval_{uuid4().hex}"
|
|
944
2102
|
|
|
945
|
-
async def runner():
|
|
2103
|
+
async def runner(operation_id: str):
|
|
946
2104
|
try:
|
|
947
|
-
report = await execute_evaluation(
|
|
2105
|
+
report = await execute_evaluation(
|
|
2106
|
+
request,
|
|
2107
|
+
adapter=self._public_evaluation_adapter(request),
|
|
2108
|
+
run_id=evaluation_id,
|
|
2109
|
+
on_case_started=lambda case_id, index, total: self.operations.append(
|
|
2110
|
+
operation_id,
|
|
2111
|
+
"evaluation.case.started",
|
|
2112
|
+
{"caseId": case_id, "index": index, "total": total},
|
|
2113
|
+
),
|
|
2114
|
+
)
|
|
948
2115
|
except EvaluationNotImplementedError as exc:
|
|
949
2116
|
raise StudioError(
|
|
950
2117
|
"EVALUATION_EXECUTOR_UNAVAILABLE",
|
|
@@ -957,26 +2124,191 @@ class StudioService:
|
|
|
957
2124
|
str(exc),
|
|
958
2125
|
status_code=502,
|
|
959
2126
|
) from exc
|
|
960
|
-
|
|
961
|
-
self.evaluation_storage.write_report(report)
|
|
962
|
-
except EvaluationStorageError as exc:
|
|
963
|
-
raise StudioError(
|
|
964
|
-
"EVALUATION_REPORT_WRITE_FAILED",
|
|
965
|
-
"评测报告写入失败",
|
|
966
|
-
status_code=500,
|
|
967
|
-
) from exc
|
|
968
|
-
return report
|
|
2127
|
+
return _OperationResource(id=report.spec.id)
|
|
969
2128
|
|
|
970
2129
|
return self.operations.submit(
|
|
971
2130
|
kind=OperationKind.EVALUATION,
|
|
972
|
-
resource_id=
|
|
2131
|
+
resource_id=evaluation_id,
|
|
973
2132
|
idempotency_key=idempotency_key,
|
|
2133
|
+
metadata={
|
|
2134
|
+
"evalset": {"name": evalset.name, "caseCount": len(evalset.cases)},
|
|
2135
|
+
"target": {
|
|
2136
|
+
"kind": target.kind.value,
|
|
2137
|
+
"label": self._evaluation_target_label(target.kind, build_id=target.locator),
|
|
2138
|
+
},
|
|
2139
|
+
"evaluators": list(config.evaluators),
|
|
2140
|
+
},
|
|
974
2141
|
runner=runner,
|
|
975
2142
|
)
|
|
976
2143
|
|
|
977
2144
|
def list_public_evaluations(self):
|
|
978
2145
|
return self.evaluation_storage.list_reports()
|
|
979
2146
|
|
|
2147
|
+
def list_public_evaluation_runs(self) -> list[dict[str, Any]]:
|
|
2148
|
+
return [
|
|
2149
|
+
self._public_evaluation_run(operation)
|
|
2150
|
+
for operation in self.operations.list(kind=OperationKind.EVALUATION)
|
|
2151
|
+
]
|
|
2152
|
+
|
|
2153
|
+
def get_public_evaluation_run(self, evaluation_id: str) -> dict[str, Any]:
|
|
2154
|
+
operation = next(
|
|
2155
|
+
(
|
|
2156
|
+
item
|
|
2157
|
+
for item in self.operations.list(kind=OperationKind.EVALUATION)
|
|
2158
|
+
if item.resource_id == evaluation_id
|
|
2159
|
+
),
|
|
2160
|
+
None,
|
|
2161
|
+
)
|
|
2162
|
+
if operation is None:
|
|
2163
|
+
raise StudioError(
|
|
2164
|
+
"EVALUATION_RUN_NOT_FOUND",
|
|
2165
|
+
"Evaluation Run 不存在",
|
|
2166
|
+
status_code=404,
|
|
2167
|
+
details={"id": evaluation_id},
|
|
2168
|
+
)
|
|
2169
|
+
return self._public_evaluation_run(operation, include_report=True)
|
|
2170
|
+
|
|
2171
|
+
def _public_evaluation_run(
|
|
2172
|
+
self, operation: Operation, *, include_report: bool = False
|
|
2173
|
+
) -> dict[str, Any]:
|
|
2174
|
+
try:
|
|
2175
|
+
report = self.evaluation_storage.read_report(operation.resource_id)
|
|
2176
|
+
except EvaluationStorageError:
|
|
2177
|
+
report = None
|
|
2178
|
+
progress = None
|
|
2179
|
+
for event in self.operations.events(operation.id):
|
|
2180
|
+
if event.type == "evaluation.case.started":
|
|
2181
|
+
progress = {
|
|
2182
|
+
"current": event.data.get("index", 0),
|
|
2183
|
+
"total": event.data.get("total", 0),
|
|
2184
|
+
"caseId": event.data.get("caseId"),
|
|
2185
|
+
}
|
|
2186
|
+
metadata = operation.metadata or {}
|
|
2187
|
+
evalset = metadata.get("evalset")
|
|
2188
|
+
target = metadata.get("target")
|
|
2189
|
+
evaluators = metadata.get("evaluators")
|
|
2190
|
+
if report is not None:
|
|
2191
|
+
if not evalset:
|
|
2192
|
+
evalset = {
|
|
2193
|
+
"name": report.spec.evalset.name,
|
|
2194
|
+
"caseCount": len(report.spec.evalset.cases),
|
|
2195
|
+
}
|
|
2196
|
+
if not target:
|
|
2197
|
+
target = {
|
|
2198
|
+
"kind": report.spec.target.kind.value,
|
|
2199
|
+
"label": self._evaluation_target_label(
|
|
2200
|
+
report.spec.target.kind,
|
|
2201
|
+
metadata=report.spec.target.metadata,
|
|
2202
|
+
),
|
|
2203
|
+
}
|
|
2204
|
+
if not evaluators:
|
|
2205
|
+
evaluators = list(report.spec.config.evaluators)
|
|
2206
|
+
payload: dict[str, Any] = {
|
|
2207
|
+
"id": operation.resource_id,
|
|
2208
|
+
"operationId": operation.id,
|
|
2209
|
+
"status": report.status.value if report else operation.status,
|
|
2210
|
+
"createdAt": operation.created_at,
|
|
2211
|
+
"completedAt": operation.completed_at,
|
|
2212
|
+
"evalset": evalset or {},
|
|
2213
|
+
"target": target or {},
|
|
2214
|
+
"evaluators": evaluators or [],
|
|
2215
|
+
"progress": progress,
|
|
2216
|
+
"summary": report.summary if report else None,
|
|
2217
|
+
"hasReport": report is not None,
|
|
2218
|
+
"error": operation.error,
|
|
2219
|
+
}
|
|
2220
|
+
if include_report:
|
|
2221
|
+
payload["report"] = report
|
|
2222
|
+
return payload
|
|
2223
|
+
|
|
2224
|
+
def _evaluation_target_label(
|
|
2225
|
+
self,
|
|
2226
|
+
kind: TargetKind,
|
|
2227
|
+
*,
|
|
2228
|
+
build_id: str | None = None,
|
|
2229
|
+
metadata: dict[str, Any] | None = None,
|
|
2230
|
+
) -> str:
|
|
2231
|
+
if kind is not TargetKind.STUDIO_BUILD:
|
|
2232
|
+
return _EVALUATION_TARGET_LABELS[kind]
|
|
2233
|
+
agent_id = str((metadata or {}).get("agentId") or "")
|
|
2234
|
+
if not agent_id and build_id:
|
|
2235
|
+
agent_id = self.builds.get(build_id).agent_id
|
|
2236
|
+
if not agent_id:
|
|
2237
|
+
return _EVALUATION_TARGET_LABELS[kind]
|
|
2238
|
+
try:
|
|
2239
|
+
return self.drafts.get(agent_id).metadata.name
|
|
2240
|
+
except StudioError as exc:
|
|
2241
|
+
if exc.status_code != 404:
|
|
2242
|
+
raise
|
|
2243
|
+
return agent_id
|
|
2244
|
+
|
|
2245
|
+
def evaluation_catalog(self) -> dict[str, list[dict]]:
|
|
2246
|
+
builds = [
|
|
2247
|
+
{
|
|
2248
|
+
"id": record.id,
|
|
2249
|
+
"agentId": record.agent_id,
|
|
2250
|
+
"runtime": record.runtime_type,
|
|
2251
|
+
"digest": _sha256_digest(record.bundle_digest),
|
|
2252
|
+
"createdAt": record.created_at.isoformat().replace("+00:00", "Z"),
|
|
2253
|
+
}
|
|
2254
|
+
for record in self.builds.list()
|
|
2255
|
+
if record.status == BuildStatus.SUCCEEDED and record.artifact_path
|
|
2256
|
+
]
|
|
2257
|
+
builds.sort(key=lambda item: item["createdAt"], reverse=True)
|
|
2258
|
+
evalsets: list[dict] = []
|
|
2259
|
+
candidates: set[Path] = set()
|
|
2260
|
+
for pattern in ("*.yaml", "*.yml", "*.json"):
|
|
2261
|
+
candidates.update(self.workspace.root.glob(pattern))
|
|
2262
|
+
candidates.update(self.workspace.root.glob(f"evaluations/**/{pattern}"))
|
|
2263
|
+
candidates.update(self.workspace.root.glob(f"agents/*/evaluations/**/{pattern}"))
|
|
2264
|
+
for path in sorted(candidates):
|
|
2265
|
+
try:
|
|
2266
|
+
evalset = load_evalset(path)
|
|
2267
|
+
except (EvalSetParseError, OSError):
|
|
2268
|
+
continue
|
|
2269
|
+
evalsets.append(
|
|
2270
|
+
{
|
|
2271
|
+
"path": path.relative_to(self.workspace.root).as_posix(),
|
|
2272
|
+
"name": evalset.name,
|
|
2273
|
+
"caseCount": len(evalset.cases),
|
|
2274
|
+
"contentDigest": evalset.content_digest,
|
|
2275
|
+
}
|
|
2276
|
+
)
|
|
2277
|
+
evalsets.sort(key=lambda item: item["path"])
|
|
2278
|
+
return {"builds": builds, "evalsets": evalsets}
|
|
2279
|
+
|
|
2280
|
+
def import_evaluation_file(self, content: bytes, *, filename: str) -> dict:
|
|
2281
|
+
if len(content) > 2 * 1024 * 1024:
|
|
2282
|
+
raise StudioError(
|
|
2283
|
+
"EVALSET_FILE_TOO_LARGE",
|
|
2284
|
+
"EvalSet 文件不能超过 2 MiB",
|
|
2285
|
+
status_code=413,
|
|
2286
|
+
field="file",
|
|
2287
|
+
)
|
|
2288
|
+
safe_name = Path(filename.replace("\\", "/")).name or "evalset.yaml"
|
|
2289
|
+
if Path(safe_name).suffix.lower() not in {".yaml", ".yml", ".json"}:
|
|
2290
|
+
raise StudioError(
|
|
2291
|
+
"EVALSET_FILE_TYPE_INVALID",
|
|
2292
|
+
"EvalSet 只支持 YAML 或 JSON 文件",
|
|
2293
|
+
status_code=422,
|
|
2294
|
+
field="file",
|
|
2295
|
+
)
|
|
2296
|
+
target = self.workspace.resolve(
|
|
2297
|
+
Path("evaluations/uploads") / f"{uuid4().hex[:12]}-{safe_name}"
|
|
2298
|
+
)
|
|
2299
|
+
self.workspace.atomic_write_bytes(target, content)
|
|
2300
|
+
try:
|
|
2301
|
+
evalset = load_evalset(target)
|
|
2302
|
+
except EvalSetParseError as exc:
|
|
2303
|
+
target.unlink(missing_ok=True)
|
|
2304
|
+
raise StudioError(exc.code, str(exc), status_code=422, field="file") from exc
|
|
2305
|
+
return {
|
|
2306
|
+
"path": self.workspace.relative(target),
|
|
2307
|
+
"name": evalset.name,
|
|
2308
|
+
"caseCount": len(evalset.cases),
|
|
2309
|
+
"contentDigest": evalset.content_digest,
|
|
2310
|
+
}
|
|
2311
|
+
|
|
980
2312
|
def get_public_evaluation(self, evaluation_id: str):
|
|
981
2313
|
try:
|
|
982
2314
|
return self.evaluation_storage.read_report(evaluation_id)
|
|
@@ -999,6 +2331,9 @@ class StudioService:
|
|
|
999
2331
|
field="target.locator",
|
|
1000
2332
|
)
|
|
1001
2333
|
return target
|
|
2334
|
+
if target.kind == TargetKind.STUDIO_BUILD:
|
|
2335
|
+
self._validate_evaluation_build(target.locator)
|
|
2336
|
+
return target
|
|
1002
2337
|
path = self.workspace.resolve(target.locator, must_exist=True)
|
|
1003
2338
|
if not path.is_dir():
|
|
1004
2339
|
raise StudioError(
|
|
@@ -1009,6 +2344,78 @@ class StudioService:
|
|
|
1009
2344
|
)
|
|
1010
2345
|
return target.model_copy(update={"locator": str(path)})
|
|
1011
2346
|
|
|
2347
|
+
def _public_evaluation_adapter(self, request: PublicEvaluationRequest):
|
|
2348
|
+
if request.target.kind != TargetKind.STUDIO_BUILD:
|
|
2349
|
+
return None
|
|
2350
|
+
evidence_store = EvidenceStore(request.report_dir) if request.report_dir else None
|
|
2351
|
+
return StudioBuildTargetAdapter(
|
|
2352
|
+
timeout_seconds=request.config.timeout_seconds,
|
|
2353
|
+
resolve_build=self._resolve_evaluation_build,
|
|
2354
|
+
run_service=self.run_service,
|
|
2355
|
+
evidence_store=evidence_store,
|
|
2356
|
+
)
|
|
2357
|
+
|
|
2358
|
+
def _resolve_evaluation_build(self, build_id: str) -> StudioBuildResolution:
|
|
2359
|
+
try:
|
|
2360
|
+
self.codex_builds.get(build_id)
|
|
2361
|
+
except Exception as exc:
|
|
2362
|
+
if getattr(exc, "status_code", None) != 404:
|
|
2363
|
+
raise
|
|
2364
|
+
else:
|
|
2365
|
+
raise StudioError(
|
|
2366
|
+
"CODEX_BUILD_NOT_IMMUTABLE",
|
|
2367
|
+
"Codex Build 尚未冻结工作区源码,不能作为不可变评测 Target",
|
|
2368
|
+
status_code=422,
|
|
2369
|
+
field="target.locator",
|
|
2370
|
+
)
|
|
2371
|
+
build = self.builds.get(build_id)
|
|
2372
|
+
if build.status != BuildStatus.SUCCEEDED or not build.artifact_path:
|
|
2373
|
+
raise StudioBuildTargetError(
|
|
2374
|
+
"STUDIO_BUILD_NOT_READY",
|
|
2375
|
+
"Studio Build must be SUCCEEDED before evaluation",
|
|
2376
|
+
)
|
|
2377
|
+
run_spec = self.framework_runs.resolve(build_id)
|
|
2378
|
+
revision_digest = str(build.bundle_digest or build.resolved_digest).strip()
|
|
2379
|
+
if not revision_digest:
|
|
2380
|
+
raise StudioBuildTargetError(
|
|
2381
|
+
"STUDIO_BUILD_INVALID",
|
|
2382
|
+
"Studio Build is missing an immutable digest",
|
|
2383
|
+
)
|
|
2384
|
+
return StudioBuildResolution(
|
|
2385
|
+
build_id=build.id,
|
|
2386
|
+
agent_id=build.agent_id,
|
|
2387
|
+
revision_digest=revision_digest,
|
|
2388
|
+
runtime=build.runtime_type,
|
|
2389
|
+
model=run_spec.model,
|
|
2390
|
+
run_spec=run_spec,
|
|
2391
|
+
metadata={
|
|
2392
|
+
"bundleDigest": build.bundle_digest,
|
|
2393
|
+
"resolvedDigest": build.resolved_digest,
|
|
2394
|
+
"sourceDigest": build.source_digest,
|
|
2395
|
+
"sourceRevision": build.source_revision,
|
|
2396
|
+
},
|
|
2397
|
+
)
|
|
2398
|
+
|
|
2399
|
+
def _validate_evaluation_build(self, build_id: str) -> None:
|
|
2400
|
+
try:
|
|
2401
|
+
self.codex_builds.get(build_id)
|
|
2402
|
+
except Exception as exc:
|
|
2403
|
+
if getattr(exc, "status_code", None) != 404:
|
|
2404
|
+
raise
|
|
2405
|
+
else:
|
|
2406
|
+
raise StudioError(
|
|
2407
|
+
"CODEX_BUILD_NOT_IMMUTABLE",
|
|
2408
|
+
"Codex Build 尚未冻结工作区源码,不能作为不可变评测 Target",
|
|
2409
|
+
status_code=422,
|
|
2410
|
+
field="target.locator",
|
|
2411
|
+
)
|
|
2412
|
+
build = self.builds.get(build_id)
|
|
2413
|
+
if build.status != BuildStatus.SUCCEEDED or not build.artifact_path:
|
|
2414
|
+
raise StudioBuildTargetError(
|
|
2415
|
+
"STUDIO_BUILD_NOT_READY",
|
|
2416
|
+
"Studio Build must be SUCCEEDED before evaluation",
|
|
2417
|
+
)
|
|
2418
|
+
|
|
1012
2419
|
def submit_deployment(
|
|
1013
2420
|
self,
|
|
1014
2421
|
build_id: str,
|
|
@@ -1016,7 +2423,84 @@ class StudioService:
|
|
|
1016
2423
|
*,
|
|
1017
2424
|
idempotency_key: str,
|
|
1018
2425
|
) -> Operation:
|
|
1019
|
-
|
|
2426
|
+
logging.getLogger(__name__).info(
|
|
2427
|
+
"deployment submitted: build=%s idempotencyKey=%s environment=%s",
|
|
2428
|
+
build_id,
|
|
2429
|
+
idempotency_key,
|
|
2430
|
+
getattr(request, "environment", "-"),
|
|
2431
|
+
)
|
|
2432
|
+
try:
|
|
2433
|
+
codex_build = self.codex_builds.get(build_id)
|
|
2434
|
+
except StudioError as exc:
|
|
2435
|
+
if exc.status_code != 404:
|
|
2436
|
+
raise
|
|
2437
|
+
else:
|
|
2438
|
+
snapshot = self.codex_manifests.load(codex_build.agent_name)
|
|
2439
|
+
if not self.codex_builder.is_current(codex_build):
|
|
2440
|
+
raise StudioError(
|
|
2441
|
+
"BUILD_NOT_CURRENT",
|
|
2442
|
+
"Codex YAML 已变更,请重新 Build 后再部署",
|
|
2443
|
+
status_code=409,
|
|
2444
|
+
details={"buildId": build_id},
|
|
2445
|
+
)
|
|
2446
|
+
# The deployed YAML records only the selected model identity. Its
|
|
2447
|
+
# credential stays in the local resolver and is materialized for
|
|
2448
|
+
# this outbound control-plane request only, never in the Build or
|
|
2449
|
+
# deployment receipt.
|
|
2450
|
+
launch = self.codex_runs.resolve(build_id)
|
|
2451
|
+
runtime_environment = dict((launch.launch_context.config or {}).get("env") or {})
|
|
2452
|
+
# Retrying a YAML deployment must update the Agent that the prior
|
|
2453
|
+
# receipt already created. CreateAgent can have succeeded before
|
|
2454
|
+
# a downstream Runtime-Service start failed; creating again would
|
|
2455
|
+
# leave duplicate cloud Agents and make the retry path non-idempotent.
|
|
2456
|
+
# A YAML edit necessarily produces a new immutable Build. Match
|
|
2457
|
+
# the prior receipt through that Build's immutable agent_name,
|
|
2458
|
+
# rather than by build_id, otherwise every edit attempts a second
|
|
2459
|
+
# CreateAgent and Server correctly rejects the duplicate name.
|
|
2460
|
+
replacement_candidates = []
|
|
2461
|
+
for deployment in self.cloud.list():
|
|
2462
|
+
if deployment.artifact_id != "managed-runtime" or not deployment.agent_id:
|
|
2463
|
+
continue
|
|
2464
|
+
try:
|
|
2465
|
+
deployed_build = self.codex_builds.get(deployment.build_id)
|
|
2466
|
+
except StudioError:
|
|
2467
|
+
# A malformed/legacy local receipt cannot establish
|
|
2468
|
+
# ownership of this Agent and must not block a new deploy.
|
|
2469
|
+
continue
|
|
2470
|
+
if deployed_build.agent_name == codex_build.agent_name:
|
|
2471
|
+
replacement_candidates.append((deployed_build, deployment))
|
|
2472
|
+
replacing = (
|
|
2473
|
+
max(
|
|
2474
|
+
replacement_candidates,
|
|
2475
|
+
key=lambda item: (item[0].source_revision, item[0].created_at),
|
|
2476
|
+
)[1]
|
|
2477
|
+
if replacement_candidates
|
|
2478
|
+
else None
|
|
2479
|
+
)
|
|
2480
|
+
|
|
2481
|
+
async def managed_runtime_runner(_operation_id: str):
|
|
2482
|
+
return await self.cloud.deploy_managed_runtime(
|
|
2483
|
+
build_id=build_id,
|
|
2484
|
+
agent_name=codex_build.agent_name,
|
|
2485
|
+
manifest=snapshot.source_bytes.decode("utf-8"),
|
|
2486
|
+
runtime_name=codex_build.runtime_name,
|
|
2487
|
+
runtime_version=codex_build.runtime_version,
|
|
2488
|
+
# Server canonicalizes and records its own digest. This
|
|
2489
|
+
# source digest remains the immutable Studio Build receipt.
|
|
2490
|
+
manifest_digest=codex_build.manifest_sha256,
|
|
2491
|
+
request=request,
|
|
2492
|
+
runtime_environment=runtime_environment,
|
|
2493
|
+
replacing=replacing,
|
|
2494
|
+
)
|
|
2495
|
+
|
|
2496
|
+
return self.operations.submit(
|
|
2497
|
+
kind=OperationKind.DEPLOYMENT,
|
|
2498
|
+
resource_id=build_id,
|
|
2499
|
+
idempotency_key=idempotency_key,
|
|
2500
|
+
runner=managed_runtime_runner,
|
|
2501
|
+
)
|
|
2502
|
+
|
|
2503
|
+
async def runner(_operation_id: str):
|
|
1020
2504
|
return await self.cloud.deploy(build_id, request)
|
|
1021
2505
|
|
|
1022
2506
|
return self.operations.submit(
|
|
@@ -1033,7 +2517,56 @@ class StudioService:
|
|
|
1033
2517
|
target_build_id: str,
|
|
1034
2518
|
idempotency_key: str,
|
|
1035
2519
|
) -> Operation:
|
|
1036
|
-
|
|
2520
|
+
deployment = self.cloud.get(deployment_id)
|
|
2521
|
+
if deployment.artifact_id == "managed-runtime":
|
|
2522
|
+
deployed_build = self.codex_builds.get(deployment.build_id)
|
|
2523
|
+
target_build = self.codex_builds.get(target_build_id)
|
|
2524
|
+
if target_build.agent_name != deployed_build.agent_name:
|
|
2525
|
+
raise StudioError(
|
|
2526
|
+
"MANAGED_RUNTIME_ROLLBACK_AGENT_MISMATCH",
|
|
2527
|
+
"声明式 Agent 只能回滚到同一 Agent 的 Build",
|
|
2528
|
+
status_code=409,
|
|
2529
|
+
details={
|
|
2530
|
+
"deploymentId": deployment_id,
|
|
2531
|
+
"targetBuildId": target_build_id,
|
|
2532
|
+
},
|
|
2533
|
+
)
|
|
2534
|
+
manifest = self.codex_builds.manifest_text(target_build)
|
|
2535
|
+
request = self.cloud.request_for(deployment_id)
|
|
2536
|
+
|
|
2537
|
+
async def managed_runtime_runner(_operation_id: str):
|
|
2538
|
+
return await self.cloud.deploy_managed_runtime(
|
|
2539
|
+
build_id=target_build.id,
|
|
2540
|
+
agent_name=target_build.agent_name,
|
|
2541
|
+
manifest=manifest,
|
|
2542
|
+
runtime_name=target_build.runtime_name,
|
|
2543
|
+
runtime_version=target_build.runtime_version,
|
|
2544
|
+
manifest_digest=target_build.manifest_sha256,
|
|
2545
|
+
request=request,
|
|
2546
|
+
replacing=deployment,
|
|
2547
|
+
)
|
|
2548
|
+
|
|
2549
|
+
return self.operations.submit(
|
|
2550
|
+
kind=OperationKind.DEPLOYMENT,
|
|
2551
|
+
resource_id=deployment_id,
|
|
2552
|
+
idempotency_key=idempotency_key,
|
|
2553
|
+
runner=managed_runtime_runner,
|
|
2554
|
+
)
|
|
2555
|
+
|
|
2556
|
+
deployed_build = self.builds.get(deployment.build_id)
|
|
2557
|
+
target_build = self.builds.get(target_build_id)
|
|
2558
|
+
if target_build.agent_id != deployed_build.agent_id:
|
|
2559
|
+
raise StudioError(
|
|
2560
|
+
"DEPLOYMENT_ROLLBACK_AGENT_MISMATCH",
|
|
2561
|
+
"高代码 Agent 只能回滚到同一 Agent 的 Build",
|
|
2562
|
+
status_code=409,
|
|
2563
|
+
details={
|
|
2564
|
+
"deploymentId": deployment_id,
|
|
2565
|
+
"targetBuildId": target_build_id,
|
|
2566
|
+
},
|
|
2567
|
+
)
|
|
2568
|
+
|
|
2569
|
+
async def runner(_operation_id: str):
|
|
1037
2570
|
return await self.cloud.rollback(
|
|
1038
2571
|
deployment_id,
|
|
1039
2572
|
target_build_id=target_build_id,
|
|
@@ -1046,6 +2579,73 @@ class StudioService:
|
|
|
1046
2579
|
runner=runner,
|
|
1047
2580
|
)
|
|
1048
2581
|
|
|
2582
|
+
def submit_account_agent_version_rollback(
|
|
2583
|
+
self,
|
|
2584
|
+
agent_id: str,
|
|
2585
|
+
*,
|
|
2586
|
+
version_id: str,
|
|
2587
|
+
idempotency_key: str,
|
|
2588
|
+
) -> Operation:
|
|
2589
|
+
normalized_agent_id = str(agent_id or "").strip()
|
|
2590
|
+
normalized_version_id = str(version_id or "").strip()
|
|
2591
|
+
if not normalized_agent_id:
|
|
2592
|
+
raise StudioError(
|
|
2593
|
+
"CLOUD_AGENT_NOT_FOUND",
|
|
2594
|
+
"云端 Agent 标识不能为空",
|
|
2595
|
+
status_code=404,
|
|
2596
|
+
)
|
|
2597
|
+
if not normalized_version_id:
|
|
2598
|
+
raise StudioError(
|
|
2599
|
+
"CLOUD_AGENT_VERSION_REQUIRED",
|
|
2600
|
+
"请选择要回滚的云端版本",
|
|
2601
|
+
status_code=422,
|
|
2602
|
+
field="versionId",
|
|
2603
|
+
)
|
|
2604
|
+
resource_id = f"{normalized_agent_id}:{normalized_version_id}"
|
|
2605
|
+
|
|
2606
|
+
async def runner(_operation_id: str):
|
|
2607
|
+
await self.cloud.rollback_account_agent_version(
|
|
2608
|
+
normalized_agent_id,
|
|
2609
|
+
version_id=normalized_version_id,
|
|
2610
|
+
)
|
|
2611
|
+
return _OperationResource(id=resource_id)
|
|
2612
|
+
|
|
2613
|
+
return self.operations.submit(
|
|
2614
|
+
kind=OperationKind.DEPLOYMENT,
|
|
2615
|
+
resource_id=resource_id,
|
|
2616
|
+
idempotency_key=idempotency_key,
|
|
2617
|
+
metadata={
|
|
2618
|
+
"agentId": normalized_agent_id,
|
|
2619
|
+
"targetVersionId": normalized_version_id,
|
|
2620
|
+
"source": "server-version",
|
|
2621
|
+
},
|
|
2622
|
+
runner=runner,
|
|
2623
|
+
)
|
|
2624
|
+
|
|
2625
|
+
async def deployment_dashboard_access(self, deployment_id: str) -> dict[str, str | None]:
|
|
2626
|
+
"""Return a receipt-bound private Hosted UI link on explicit user request."""
|
|
2627
|
+
|
|
2628
|
+
return await self.cloud.dashboard_access(deployment_id)
|
|
2629
|
+
|
|
2630
|
+
def deployment_operation_scope(self) -> dict[str, str]:
|
|
2631
|
+
"""Return opaque browser storage scopes without exposing cloud credentials."""
|
|
2632
|
+
|
|
2633
|
+
workspace_identity = str(self.workspace.root.resolve())
|
|
2634
|
+
region = (
|
|
2635
|
+
os.environ.get("AGENTENGINE_REGION") or os.environ.get("KSYUN_REGION") or "cn-beijing-6"
|
|
2636
|
+
).strip()
|
|
2637
|
+
access_key = (
|
|
2638
|
+
os.environ.get("KSYUN_ACCESS_KEY") or os.environ.get("KS3_ACCESS_KEY") or "unsigned"
|
|
2639
|
+
).strip()
|
|
2640
|
+
|
|
2641
|
+
def opaque(value: str) -> str:
|
|
2642
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
2643
|
+
|
|
2644
|
+
return {
|
|
2645
|
+
"workspace": opaque(workspace_identity),
|
|
2646
|
+
"cloudCredential": opaque(f"{region}\0{access_key}"),
|
|
2647
|
+
}
|
|
2648
|
+
|
|
1049
2649
|
def get_settings(self) -> dict[str, Any]:
|
|
1050
2650
|
path = self.workspace.resolve(".agentkit/settings.yaml")
|
|
1051
2651
|
data: dict[str, Any] = {}
|
|
@@ -1057,13 +2657,30 @@ class StudioService:
|
|
|
1057
2657
|
defaults = {
|
|
1058
2658
|
"sandbox": os.environ.get("KSADK_CODEX_SANDBOX", "read_only"),
|
|
1059
2659
|
"buildAfterCreate": True,
|
|
1060
|
-
"codexProxy":
|
|
1061
|
-
"
|
|
1062
|
-
|
|
1063
|
-
|
|
2660
|
+
"codexProxy": current_proxy_mode(),
|
|
2661
|
+
"cloudRegion": os.environ.get(
|
|
2662
|
+
"AGENTENGINE_REGION", os.environ.get("KSYUN_REGION", "cn-beijing-6")
|
|
2663
|
+
),
|
|
2664
|
+
"cloudBucket": os.environ.get("KS3_BUCKET", ""),
|
|
2665
|
+
"cloudAccountId": (os.environ.get("KSYUN_ACCOUNT_ID", "").strip()),
|
|
2666
|
+
# AK/SK 不回显(避免本地 UI 回传 secret);只暴露配置状态。
|
|
2667
|
+
# 已配置 = 启动环境或已持久化 settings 里 AK+SK 齐全。
|
|
2668
|
+
"cloudAccountConfigured": bool(
|
|
2669
|
+
(os.environ.get("KSYUN_ACCESS_KEY") or os.environ.get("KS3_ACCESS_KEY", "")).strip()
|
|
2670
|
+
and (
|
|
2671
|
+
os.environ.get("KSYUN_SECRET_KEY") or os.environ.get("KS3_SECRET_KEY", "")
|
|
2672
|
+
).strip()
|
|
2673
|
+
),
|
|
2674
|
+
"cloudSignedAccountConfigured": bool(
|
|
2675
|
+
(os.environ.get("KSYUN_ACCESS_KEY") or os.environ.get("KS3_ACCESS_KEY", "")).strip()
|
|
2676
|
+
and (
|
|
2677
|
+
os.environ.get("KSYUN_SECRET_KEY") or os.environ.get("KS3_SECRET_KEY", "")
|
|
2678
|
+
).strip()
|
|
2679
|
+
),
|
|
1064
2680
|
"traceContent": os.environ.get("KSADK_STUDIO_TRACE_CONTENT", "1") != "0",
|
|
1065
2681
|
}
|
|
1066
|
-
defaults.update({k: v for k, v in data.items() if v is not None})
|
|
2682
|
+
defaults.update({k: v for k, v in data.items() if k in defaults and v is not None})
|
|
2683
|
+
defaults["codexProxy"] = normalize_proxy_mode(defaults.get("codexProxy"))
|
|
1067
2684
|
return defaults
|
|
1068
2685
|
|
|
1069
2686
|
def update_settings(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1071,12 +2688,23 @@ class StudioService:
|
|
|
1071
2688
|
"sandbox",
|
|
1072
2689
|
"buildAfterCreate",
|
|
1073
2690
|
"codexProxy",
|
|
1074
|
-
"cloudAccessKey",
|
|
1075
|
-
"cloudSecretKey",
|
|
1076
2691
|
"cloudRegion",
|
|
2692
|
+
"cloudBucket",
|
|
1077
2693
|
"traceContent",
|
|
2694
|
+
# 云账号:AK/SK 留空 = 不修改(保留已存值);AccountID 可单独更新。
|
|
2695
|
+
"cloudAccessKey",
|
|
2696
|
+
"cloudSecretKey",
|
|
2697
|
+
"cloudAccountId",
|
|
1078
2698
|
}
|
|
1079
2699
|
data = {k: payload[k] for k in allowed if k in payload}
|
|
2700
|
+
if "cloudAccessKey" in data and not str(data["cloudAccessKey"]).strip():
|
|
2701
|
+
data.pop("cloudAccessKey")
|
|
2702
|
+
if "cloudSecretKey" in data and not str(data["cloudSecretKey"]).strip():
|
|
2703
|
+
data.pop("cloudSecretKey")
|
|
2704
|
+
if "cloudAccountId" in data:
|
|
2705
|
+
data["cloudAccountId"] = str(data["cloudAccountId"]).strip()
|
|
2706
|
+
if not data["cloudAccountId"]:
|
|
2707
|
+
data.pop("cloudAccountId")
|
|
1080
2708
|
if data.get("sandbox") and data["sandbox"] not in {
|
|
1081
2709
|
"read-only",
|
|
1082
2710
|
"workspace-write",
|
|
@@ -1088,9 +2716,19 @@ class StudioService:
|
|
|
1088
2716
|
"full_access",
|
|
1089
2717
|
}:
|
|
1090
2718
|
raise StudioError("SETTINGS_INVALID", "sandbox 取值非法", status_code=422)
|
|
2719
|
+
if "codexProxy" in data and data["codexProxy"] not in {"auto", "forced", "direct"}:
|
|
2720
|
+
raise StudioError("SETTINGS_INVALID", "codexProxy 取值非法", status_code=422)
|
|
1091
2721
|
path = self.workspace.resolve(".agentkit/settings.yaml")
|
|
1092
2722
|
self.workspace.atomic_write_yaml(path, data)
|
|
1093
|
-
|
|
2723
|
+
# env 桥接用落盘后的全量数据:局部更新(只改 AccountID)时,
|
|
2724
|
+
# 已持久化的 AK/SK 也要继续桥接,否则签名凭证丢失。
|
|
2725
|
+
try:
|
|
2726
|
+
persisted = load_yaml_file(path) or {}
|
|
2727
|
+
except Exception:
|
|
2728
|
+
persisted = data
|
|
2729
|
+
self._apply_settings_to_env(persisted if isinstance(persisted, dict) else data)
|
|
2730
|
+
if self._cloud_gateway_override is None:
|
|
2731
|
+
self.cloud.gateway = self._configured_cloud_gateway()
|
|
1094
2732
|
return self.get_settings()
|
|
1095
2733
|
|
|
1096
2734
|
def _apply_persisted_settings(self) -> None:
|
|
@@ -1114,16 +2752,71 @@ class StudioService:
|
|
|
1114
2752
|
def _apply_settings_to_env(data: dict[str, Any]) -> None:
|
|
1115
2753
|
if data.get("sandbox"):
|
|
1116
2754
|
os.environ["KSADK_CODEX_SANDBOX"] = data["sandbox"]
|
|
1117
|
-
if
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
2755
|
+
if "codexProxy" in data:
|
|
2756
|
+
proxy_env = proxy_mode_env_value(data["codexProxy"])
|
|
2757
|
+
if proxy_env is None:
|
|
2758
|
+
os.environ.pop("KSADK_CODEX_USE_PROXY", None)
|
|
2759
|
+
else:
|
|
2760
|
+
os.environ["KSADK_CODEX_USE_PROXY"] = proxy_env
|
|
1123
2761
|
if data.get("cloudRegion"):
|
|
1124
|
-
os.environ["
|
|
2762
|
+
os.environ["AGENTENGINE_REGION"] = data["cloudRegion"]
|
|
2763
|
+
if data.get("cloudBucket"):
|
|
2764
|
+
os.environ["KS3_BUCKET"] = data["cloudBucket"]
|
|
1125
2765
|
if "traceContent" in data:
|
|
1126
2766
|
os.environ["KSADK_STUDIO_TRACE_CONTENT"] = "1" if data["traceContent"] else "0"
|
|
2767
|
+
# 云账号凭证:桥接到 KSYUN_* env(AgentEngineClient 的 V4 签名与
|
|
2768
|
+
# X-Ksc-Account-Id 注入都从这里取值)。
|
|
2769
|
+
if data.get("cloudAccessKey"):
|
|
2770
|
+
os.environ["KSYUN_ACCESS_KEY"] = str(data["cloudAccessKey"])
|
|
2771
|
+
if data.get("cloudSecretKey"):
|
|
2772
|
+
os.environ["KSYUN_SECRET_KEY"] = str(data["cloudSecretKey"])
|
|
2773
|
+
if data.get("cloudAccountId"):
|
|
2774
|
+
os.environ["KSYUN_ACCOUNT_ID"] = str(data["cloudAccountId"])
|
|
2775
|
+
|
|
2776
|
+
@staticmethod
|
|
2777
|
+
def _configured_cloud_gateway() -> CloudDeploymentGateway:
|
|
2778
|
+
"""Compose the existing signed Code deployment path from process-only credentials."""
|
|
2779
|
+
|
|
2780
|
+
access_key = (
|
|
2781
|
+
os.environ.get("KSYUN_ACCESS_KEY") or os.environ.get("KS3_ACCESS_KEY", "")
|
|
2782
|
+
).strip()
|
|
2783
|
+
secret_key = (
|
|
2784
|
+
os.environ.get("KSYUN_SECRET_KEY") or os.environ.get("KS3_SECRET_KEY", "")
|
|
2785
|
+
).strip()
|
|
2786
|
+
region = os.environ.get("AGENTENGINE_REGION", os.environ.get("KSYUN_REGION", "")).strip()
|
|
2787
|
+
if not all((access_key, secret_key, region)):
|
|
2788
|
+
return UnavailableCloudGateway()
|
|
2789
|
+
control_client = AgentEngineClient(
|
|
2790
|
+
region=region,
|
|
2791
|
+
access_key=access_key,
|
|
2792
|
+
secret_key=secret_key,
|
|
2793
|
+
)
|
|
2794
|
+
stream_base_url = os.environ.get("AGENTENGINE_STREAM_SERVER_URL", "").strip()
|
|
2795
|
+
if not stream_base_url and region.lower() == "pre-online":
|
|
2796
|
+
# The pre-online KOP response path currently buffers SSE until
|
|
2797
|
+
# EOF. Its internal Server ingress validates the same V4
|
|
2798
|
+
# signature and preserves the RunAgent streaming response.
|
|
2799
|
+
stream_base_url = "http://agent-api-pre.kspmas-internal.ksyun.com"
|
|
2800
|
+
stream_client = (
|
|
2801
|
+
AgentEngineClient(
|
|
2802
|
+
base_url=stream_base_url,
|
|
2803
|
+
region=region,
|
|
2804
|
+
access_key=access_key,
|
|
2805
|
+
secret_key=secret_key,
|
|
2806
|
+
)
|
|
2807
|
+
if stream_base_url
|
|
2808
|
+
else control_client
|
|
2809
|
+
)
|
|
2810
|
+
return DirectAgentEngineCloudDeploymentGateway(
|
|
2811
|
+
region=region,
|
|
2812
|
+
client=control_client,
|
|
2813
|
+
stream_client=stream_client,
|
|
2814
|
+
bucket=os.environ.get("KS3_BUCKET", "").strip() or None,
|
|
2815
|
+
ks3_credentials={
|
|
2816
|
+
"access_key": access_key,
|
|
2817
|
+
"secret_key": secret_key,
|
|
2818
|
+
},
|
|
2819
|
+
)
|
|
1127
2820
|
|
|
1128
2821
|
@staticmethod
|
|
1129
2822
|
def builtin_capabilities():
|
|
@@ -1139,3 +2832,8 @@ class StudioService:
|
|
|
1139
2832
|
item.model_dump(by_alias=True, exclude_none=True, mode="json")
|
|
1140
2833
|
for item in self.catalog.list(kind=kind, query=query, limit=200)
|
|
1141
2834
|
]
|
|
2835
|
+
|
|
2836
|
+
|
|
2837
|
+
def _sha256_digest(value: str) -> str:
|
|
2838
|
+
digest = str(value or "").strip()
|
|
2839
|
+
return digest if digest.startswith("sha256:") else f"sha256:{digest}"
|