agentengine-sdk-python 0.7.0__py3-none-any.whl → 0.8.1__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.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/METADATA +52 -20
- agentengine_sdk_python-0.8.1.dist-info/RECORD +644 -0
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/WHEEL +1 -1
- ksadk/a2a/__init__.py +180 -11
- ksadk/a2a/bootstrap.py +332 -0
- ksadk/a2a/card.py +15 -0
- ksadk/a2a/context_store.py +203 -0
- ksadk/a2a/control_plane.py +1056 -0
- ksadk/a2a/credential.py +99 -0
- ksadk/a2a/event_adapter.py +149 -0
- ksadk/a2a/executor.py +361 -137
- ksadk/a2a/external_transport.py +516 -0
- ksadk/a2a/identity.py +304 -0
- ksadk/a2a/ids.py +22 -0
- ksadk/a2a/langgraph.py +285 -0
- ksadk/a2a/resume_store.py +201 -0
- ksadk/a2a/routes.py +112 -0
- ksadk/a2a/server.py +102 -62
- ksadk/a2a/service_env.py +33 -0
- ksadk/a2a/space_client.py +1122 -0
- ksadk/a2a/task_adapter.py +423 -0
- ksadk/a2a/task_event_dispatcher.py +167 -0
- ksadk/a2a/task_event_outbox.py +326 -0
- ksadk/a2a/task_store.py +206 -0
- ksadk/a2a_card.py +101 -0
- ksadk/a2ui/__init__.py +27 -0
- ksadk/a2ui/core.py +186 -0
- ksadk/a2ui/models.py +166 -0
- ksadk/a2ui/renderer.py +198 -0
- ksadk/agui/__init__.py +9 -0
- ksadk/agui/a2ui_projection.py +92 -0
- ksadk/agui/agent.py +872 -0
- ksadk/agui/config.py +92 -0
- ksadk/agui/routes.py +47 -0
- ksadk/api/client.py +238 -204
- ksadk/api/environment.py +27 -0
- ksadk/builders/__init__.py +2 -0
- ksadk/builders/base.py +16 -14
- ksadk/builders/code_builder.py +375 -270
- ksadk/builders/container_builder.py +240 -200
- ksadk/builders/framework_requirements.py +31 -12
- ksadk/builders/ks3_uploader.py +60 -29
- ksadk/builders/managed_runtime_builder.py +173 -0
- ksadk/builders/mcp_builder.py +91 -92
- ksadk/builders/requirements_utils.py +1 -5
- ksadk/cli/__init__.py +97 -171
- ksadk/cli/cmd_a2a.py +255 -84
- ksadk/cli/cmd_agent.py +19 -5
- ksadk/cli/cmd_build.py +158 -29
- ksadk/cli/cmd_completion.py +47 -35
- ksadk/cli/cmd_config.py +267 -198
- ksadk/cli/cmd_create.py +507 -740
- ksadk/cli/cmd_dashboard.py +120 -45
- ksadk/cli/cmd_deploy.py +135 -59
- ksadk/cli/cmd_destroy.py +44 -14
- ksadk/cli/cmd_eval.py +386 -0
- ksadk/cli/cmd_files.py +63 -24
- ksadk/cli/cmd_hermes.py +313 -135
- ksadk/cli/cmd_invoke.py +184 -122
- ksadk/cli/cmd_launch.py +76 -28
- ksadk/cli/cmd_mcp.py +98 -64
- ksadk/cli/cmd_model.py +14 -5
- ksadk/cli/cmd_openclaw.py +564 -268
- ksadk/cli/cmd_replay.py +71 -0
- ksadk/cli/cmd_run.py +58 -73
- ksadk/cli/cmd_status.py +80 -31
- ksadk/cli/cmd_studio.py +110 -0
- ksadk/cli/cmd_version.py +20 -13
- ksadk/cli/cmd_web.py +95 -18
- ksadk/cli/deploy_utils.py +20 -20
- ksadk/cli/dry_run.py +7 -4
- ksadk/cli/env_options.py +83 -0
- ksadk/cli/error_utils.py +37 -12
- ksadk/cli/global_options.py +4 -3
- ksadk/cli/local_runtime.py +1 -2
- ksadk/cli/model_catalog.py +6 -2
- ksadk/cli/network_options.py +41 -19
- ksadk/cli/resource_common.py +6 -2
- ksadk/cli/runtime_bootstrap.py +51 -0
- ksadk/cli/storage.py +0 -1
- ksadk/cli/ui.py +11 -3
- ksadk/cli/workflow_common.py +52 -18
- ksadk/codex/__init__.py +12 -0
- ksadk/codex/client.py +1042 -0
- ksadk/codex/phase.py +143 -0
- ksadk/codex/runtime.py +940 -0
- ksadk/common/aicp_env.py +0 -1
- ksadk/common/auth.py +2 -2
- ksadk/common/constants.py +1 -3
- ksadk/common/kop_client.py +232 -0
- ksadk/common/llm_utils.py +17 -13
- ksadk/compat/__init__.py +5 -0
- ksadk/compat/adk_compat.py +200 -0
- ksadk/compat/copilotkit_a2ui.py +330 -0
- ksadk/configs/__init__.py +12 -14
- ksadk/configs/env_registry.py +661 -116
- ksadk/configs/global_config.py +3 -2
- ksadk/configs/settings.py +133 -209
- ksadk/conversations/__init__.py +85 -92
- ksadk/conversations/attachment_storage.py +16 -11
- ksadk/conversations/attachments.py +44 -36
- ksadk/conversations/compaction_pipeline.py +12 -12
- ksadk/conversations/compaction_prompt.py +9 -5
- ksadk/conversations/context.py +221 -6
- ksadk/conversations/message_projection.py +438 -51
- ksadk/conversations/model_context.py +12 -6
- ksadk/conversations/model_options.py +0 -1
- ksadk/conversations/normalize.py +20 -16
- ksadk/conversations/reasoning_markup.py +1 -1
- ksadk/conversations/runtime.py +339 -5255
- ksadk/conversations/runtime_compaction.py +333 -0
- ksadk/conversations/runtime_constants.py +27 -0
- ksadk/conversations/runtime_governance.py +185 -0
- ksadk/conversations/runtime_input.py +660 -0
- ksadk/conversations/runtime_invocation.py +395 -0
- ksadk/conversations/runtime_metadata.py +475 -0
- ksadk/conversations/runtime_observability.py +367 -0
- ksadk/conversations/runtime_payloads.py +272 -0
- ksadk/conversations/runtime_persistence.py +440 -0
- ksadk/conversations/runtime_preparation.py +426 -0
- ksadk/conversations/runtime_resume.py +836 -0
- ksadk/conversations/runtime_stream_events.py +902 -0
- ksadk/conversations/runtime_streaming.py +700 -0
- ksadk/conversations/semantic_summary.py +17 -5
- ksadk/conversations/session_title.py +6 -3
- ksadk/deployment/__init__.py +15 -16
- ksadk/deployment/agent_access.py +12 -23
- ksadk/deployment/base.py +51 -38
- ksadk/deployment/managed_runtime.py +33 -0
- ksadk/deployment/manager.py +85 -71
- ksadk/deployment/providers/serverless.py +379 -210
- ksadk/deployment/registry.py +38 -32
- ksadk/deployment/state.py +11 -11
- ksadk/deployment/ui_config.py +1 -1
- ksadk/detection/__init__.py +1 -1
- ksadk/detection/detector.py +146 -83
- ksadk/detection/mcp_detector.py +9 -6
- ksadk/evaluation/__init__.py +84 -0
- ksadk/evaluation/a2a_adapter.py +444 -0
- ksadk/evaluation/adapters.py +54 -0
- ksadk/evaluation/contracts.py +409 -0
- ksadk/evaluation/evalset.py +351 -0
- ksadk/evaluation/evaluators.py +461 -0
- ksadk/evaluation/executor.py +118 -0
- ksadk/evaluation/storage.py +68 -0
- ksadk/evaluation/target.py +73 -0
- ksadk/events/__init__.py +25 -0
- ksadk/events/parser.py +191 -0
- ksadk/events/replay.py +37 -0
- ksadk/events/runtime_event.py +273 -0
- ksadk/events/store.py +317 -0
- ksadk/harness/__init__.py +35 -0
- ksadk/harness/app.py +269 -0
- ksadk/harness/config.py +181 -0
- ksadk/harness/reasoner.py +105 -0
- ksadk/harness/runtime.py +364 -0
- ksadk/harness/sandbox.py +141 -0
- ksadk/harness/tools.py +222 -0
- ksadk/hermes_terminal.py +28 -12
- ksadk/identity/resolver.py +15 -9
- ksadk/ids.py +32 -0
- ksadk/knowledge_base/adk_tool.py +1 -1
- ksadk/knowledge_base/client.py +38 -33
- ksadk/managed_a2a_card.py +160 -0
- ksadk/managed_runtime.py +195 -0
- ksadk/markdown.py +5 -2
- ksadk/mcp_runtime/__init__.py +9 -6
- ksadk/memory/__init__.py +1 -1
- ksadk/memory/adk/__init__.py +2 -0
- ksadk/memory/adk/backends/base_ltm_backend.py +2 -6
- ksadk/memory/adk/backends/http_ltm_backend.py +13 -21
- ksadk/memory/adk/backends/inmemory_ltm_backend.py +7 -13
- ksadk/memory/adk/backends/sdk_ltm_backend.py +36 -36
- ksadk/memory/adk/long_term_memory.py +34 -37
- ksadk/memory/adk/resilient_session_service.py +4 -5
- ksadk/memory/adk/short_term_memory.py +12 -21
- ksadk/memory/adk_tool.py +3 -2
- ksadk/memory/backends/base.py +19 -19
- ksadk/memory/backends/memory.py +24 -25
- ksadk/memory/backends/redis.py +31 -30
- ksadk/memory/langchain_tool.py +2 -1
- ksadk/memory/manager.py +52 -47
- ksadk/memory/service.py +16 -6
- ksadk/memory/tool.py +4 -0
- ksadk/model_policy.py +15 -7
- ksadk/model_proxy/__init__.py +51 -0
- ksadk/model_proxy/bootstrap.py +80 -0
- ksadk/model_proxy/cache.py +165 -0
- ksadk/model_proxy/config.py +66 -0
- ksadk/model_proxy/demo_webui.py +115 -0
- ksadk/model_proxy/detect.py +145 -0
- ksadk/model_proxy/gate.py +68 -0
- ksadk/model_proxy/namespace.py +201 -0
- ksadk/model_proxy/protocol_proxy.py +22 -0
- ksadk/model_proxy/server.py +443 -0
- ksadk/model_proxy/transform.py +735 -0
- ksadk/openclaw_gateway.py +29 -14
- ksadk/runners/adk_runner.py +354 -146
- ksadk/runners/base_runner.py +71 -29
- ksadk/runners/deepagents_runner.py +0 -1
- ksadk/runners/factory.py +18 -5
- ksadk/runners/langchain_runner.py +29 -742
- ksadk/runners/langgraph_runner.py +417 -97
- ksadk/runners/patch_langchain.py +16 -16
- ksadk/runners/remote_runner.py +134 -33
- ksadk/runners/usage_accumulator.py +1 -0
- ksadk/runners/utils/__init__.py +0 -10
- ksadk/runners/utils/loader.py +23 -7
- ksadk/runtime/__init__.py +67 -0
- ksadk/runtime/adapter.py +412 -0
- ksadk/runtime/conversation_execution.py +553 -0
- ksadk/runtime/executor.py +251 -0
- ksadk/runtime/factory.py +156 -0
- ksadk/runtime/framework_adapters.py +118 -0
- ksadk/runtime/launch.py +48 -0
- ksadk/runtime/preprocessing.py +131 -0
- ksadk/runtime/runner_adapter.py +1032 -0
- ksadk/runtime/runner_loading.py +36 -0
- ksadk/runtime_context.py +18 -3
- ksadk/runtime_state.py +0 -1
- ksadk/sandbox/__init__.py +4 -1
- ksadk/sandbox/backends/__init__.py +4 -1
- ksadk/sandbox/backends/e2b.py +13 -5
- ksadk/sandbox/backends/local_process.py +11 -3
- ksadk/sandbox/base.py +10 -15
- ksadk/sandbox/factory.py +14 -4
- ksadk/sandbox/registry.py +57 -6
- ksadk/server/__init__.py +39 -4
- ksadk/server/api_models.py +15 -5
- ksadk/server/app.py +24 -3928
- ksadk/server/composition.py +47 -0
- ksadk/server/factory.py +622 -0
- ksadk/server/routes/__init__.py +1 -0
- ksadk/server/routes/checkpoint_resolution.py +116 -0
- ksadk/server/routes/common.py +682 -0
- ksadk/server/routes/control.py +331 -0
- ksadk/server/routes/dependencies.py +98 -0
- ksadk/server/routes/misc.py +425 -0
- ksadk/server/routes/models.py +451 -0
- ksadk/server/routes/openai_compat.py +229 -0
- ksadk/server/routes/projection.py +848 -0
- ksadk/server/routes/routers.py +52 -0
- ksadk/server/routes/run.py +519 -0
- ksadk/server/routes/sessions.py +644 -0
- ksadk/server/routes/streaming.py +265 -0
- ksadk/server/routes/workspace.py +414 -0
- ksadk/server/static/assets/{ArtifactsPanel-D-SSn2ee.js → ArtifactsPanel-CakCqxEN.js} +1 -1
- ksadk/server/static/assets/CodeBlock-CynLknyO.js +9 -0
- ksadk/server/static/assets/MathMessageMarkdown-3sQdOysS.js +8 -0
- ksadk/server/static/assets/MermaidBlock-NERD_ef7.js +303 -0
- ksadk/server/static/assets/NativeTerminalPanel-CiHWf82q.js +1 -0
- ksadk/server/static/assets/{arc-BA13sHc9.js → arc-DrgjcKhl.js} +1 -1
- ksadk/server/static/assets/architecture-7EHR7CIX-CpZLRCcY.js +1 -0
- ksadk/server/static/assets/{architectureDiagram-3BPJPVTR-DzJzgVoL.js → architectureDiagram-3BPJPVTR-BXoVRbQ6.js} +1 -1
- ksadk/server/static/assets/{blockDiagram-GPEHLZMM-UTdVcZhn.js → blockDiagram-GPEHLZMM-BnaB3Ywv.js} +1 -1
- ksadk/server/static/assets/{c4Diagram-AAUBKEIU-Dix0GD4m.js → c4Diagram-AAUBKEIU-Dw4weZcp.js} +1 -1
- ksadk/server/static/assets/channel-Dt3rQTFI.js +1 -0
- ksadk/server/static/assets/{chunk-2J33WTMH-C2JxuZ42.js → chunk-2J33WTMH-ChjpLK7j.js} +1 -1
- ksadk/server/static/assets/{chunk-4BX2VUAB-Be57QPgD.js → chunk-4BX2VUAB-B9drTgOW.js} +1 -1
- ksadk/server/static/assets/{chunk-55IACEB6-WjSuy4nv.js → chunk-55IACEB6-CK4ROATy.js} +1 -1
- ksadk/server/static/assets/{chunk-727SXJPM-BOovP-b5.js → chunk-727SXJPM-B9KtTTgw.js} +1 -1
- ksadk/server/static/assets/{chunk-AQP2D5EJ-DOmZNmWj.js → chunk-AQP2D5EJ-1O1mki3m.js} +1 -1
- ksadk/server/static/assets/{chunk-FMBD7UC4-DzwlodkU.js → chunk-FMBD7UC4-B_fH2YuH.js} +1 -1
- ksadk/server/static/assets/{chunk-ND2GUHAM-DMqAikEU.js → chunk-ND2GUHAM-D6rFLaVV.js} +1 -1
- ksadk/server/static/assets/chunk-QZHKN3VN-iXEZtKQc.js +1 -0
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-DIveV17Q.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-DIveV17Q.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-S5V4N54A-Zhy2GBnP.js → cose-bilkent-S5V4N54A-ChNs_Mdk.js} +1 -1
- ksadk/server/static/assets/{dagre-BM42HDAG-CbKrtIUz.js → dagre-BM42HDAG-D0QXg_MQ.js} +1 -1
- ksadk/server/static/assets/{diagram-2AECGRRQ-BRApgQRK.js → diagram-2AECGRRQ-DBDDKwBQ.js} +1 -1
- ksadk/server/static/assets/{diagram-5GNKFQAL-D0xbnDiK.js → diagram-5GNKFQAL-BkwDOxmO.js} +1 -1
- ksadk/server/static/assets/{diagram-KO2AKTUF-CVugVI9L.js → diagram-KO2AKTUF-Bv8waYvY.js} +1 -1
- ksadk/server/static/assets/{diagram-LMA3HP47-DozUGgee.js → diagram-LMA3HP47-e1VECDdu.js} +1 -1
- ksadk/server/static/assets/{diagram-OG6HWLK6-ZOYsKlru.js → diagram-OG6HWLK6-Ds9adfRl.js} +1 -1
- ksadk/server/static/assets/{dist-DpKtt4Bo.js → dist-4N2NMVYg.js} +1 -1
- ksadk/server/static/assets/{dist-CkEKJFtk.js → dist-B-jNR6Vn.js} +1 -1
- ksadk/server/static/assets/{dist-D57wnGWd.js → dist-B5J2uhNk.js} +1 -1
- ksadk/server/static/assets/{dist-8_SfK1IP.js → dist-B5oS1zcj.js} +1 -1
- ksadk/server/static/assets/{dist-DMezvBGJ.js → dist-BB3-OwTQ.js} +1 -1
- ksadk/server/static/assets/{dist-2hInLS_J.js → dist-BIlKoW0k.js} +1 -1
- ksadk/server/static/assets/{dist-BrCcQ47T.js → dist-BjOdlDjj.js} +1 -1
- ksadk/server/static/assets/{dist-BmlMPsCc.js → dist-C1sz5Eo3.js} +1 -1
- ksadk/server/static/assets/{dist-B0NP_Z0z.js → dist-CWV4lDr1.js} +1 -1
- ksadk/server/static/assets/{dist-D-xOD048.js → dist-CmZHzlwZ.js} +1 -1
- ksadk/server/static/assets/{dist-DL_NKc_z.js → dist-D6MpCZtz.js} +1 -1
- ksadk/server/static/assets/{dist-DyOkZmze.js → dist-DJN_6Y39.js} +1 -1
- ksadk/server/static/assets/{dist-DmFaq_Iy.js → dist-DQU9EWw-.js} +1 -1
- ksadk/server/static/assets/{dist-gX6GOLKq.js → dist-DWKByxWV.js} +1 -1
- ksadk/server/static/assets/{dist-DyWsb78Q.js → dist-Dli7qW-B.js} +1 -1
- ksadk/server/static/assets/{dist-BIJVDMuB.js → dist-DsHPdk3W.js} +1 -1
- ksadk/server/static/assets/{dist-Dp0k7bWI.js → dist-X4TnqwOK.js} +1 -1
- ksadk/server/static/assets/{dist-8EEyGgDx.js → dist-_pHPTyZ-.js} +1 -1
- ksadk/server/static/assets/{dist-CPBtqRTW.js → dist-fhOuWds5.js} +1 -1
- ksadk/server/static/assets/{dist-D-JMdWPQ.js → dist-gLnrGsoU.js} +1 -1
- ksadk/server/static/assets/{dist-BssVmjIo.js → dist-j6Dxmf_j.js} +1 -1
- ksadk/server/static/assets/{dist-C3HIIaN5.js → dist-o2OVmKvt.js} +1 -1
- ksadk/server/static/assets/{erDiagram-TEJ5UH35-Bk-9bgE5.js → erDiagram-TEJ5UH35-BPItFKH6.js} +1 -1
- ksadk/server/static/assets/eventmodeling-FCH6USID-DYFSxbqr.js +1 -0
- ksadk/server/static/assets/{flowDiagram-I6XJVG4X-Dn9kZVuZ.js → flowDiagram-I6XJVG4X-BbYB9w5Y.js} +1 -1
- ksadk/server/static/assets/{ganttDiagram-6RSMTGT7-BGxXlJqm.js → ganttDiagram-6RSMTGT7-TQXa0KXv.js} +1 -1
- ksadk/server/static/assets/gitGraph-WXDBUCRP-DyNYp6BN.js +1 -0
- ksadk/server/static/assets/{gitGraphDiagram-PVQCEYII-C7Q0X-wi.js → gitGraphDiagram-PVQCEYII-bKblCnpa.js} +1 -1
- ksadk/server/static/assets/index-R52pJJvg.js +177 -0
- ksadk/server/static/assets/index-jJr3_955.css +2 -0
- ksadk/server/static/assets/info-J43DQDTF-WUx7qTde.js +1 -0
- ksadk/server/static/assets/{infoDiagram-5YYISTIA-BO_CSqq5.js → infoDiagram-5YYISTIA-BGVT7J_i.js} +1 -1
- ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-eYLPLd46.js → ishikawaDiagram-YF4QCWOH-K0OOfVna.js} +1 -1
- ksadk/server/static/assets/{journeyDiagram-JHISSGLW-De9geSb0.js → journeyDiagram-JHISSGLW-8LeOJaZp.js} +1 -1
- ksadk/server/static/assets/{kanban-definition-UN3LZRKU-Cs80kQto.js → kanban-definition-UN3LZRKU-C6uCgZrW.js} +1 -1
- ksadk/server/static/assets/{katex-DSrCFD8n.js → katex-CYWseY5E.js} +1 -1
- ksadk/server/static/assets/{linear-CN9sbKTF.js → linear-hdBcYzM8.js} +1 -1
- ksadk/server/static/assets/{mermaid-parser.core-DlgSBU8l.js → mermaid-parser.core-BWrDjSZM.js} +2 -2
- ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-9afz7Oug.js → mindmap-definition-RKZ34NQL-Bjy_2v4_.js} +1 -1
- ksadk/server/static/assets/packet-YPE3B663-BN13y_xl.js +1 -0
- ksadk/server/static/assets/pie-LRSECV5Y-ftSLXhhc.js +1 -0
- ksadk/server/static/assets/{pieDiagram-4H26LBE5-DPv_VstN.js → pieDiagram-4H26LBE5-GmmqNiVH.js} +1 -1
- ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-o77wZ1XK.js → quadrantDiagram-W4KKPZXB-BmkRbwCg.js} +1 -1
- ksadk/server/static/assets/radar-GUYGQ44K-jaICkIeO.js +1 -0
- ksadk/server/static/assets/{requirementDiagram-4Y6WPE33-BYVw81VC.js → requirementDiagram-4Y6WPE33-DSWFqL7c.js} +1 -1
- ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-B7nZFMKT.js → sankeyDiagram-5OEKKPKP-l6iXmU65.js} +1 -1
- ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-BGRRrFw5.js → sequenceDiagram-3UESZ5HK-Cq6UBeQB.js} +1 -1
- ksadk/server/static/assets/{stateDiagram-AJRCARHV-BUfHCoPs.js → stateDiagram-AJRCARHV-qzTQpg3Y.js} +1 -1
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-BYCoYGAN.js +1 -0
- ksadk/server/static/assets/{timeline-definition-PNZ67QCA-FbCTGrf9.js → timeline-definition-PNZ67QCA-DEz24vV0.js} +1 -1
- ksadk/server/static/assets/treeView-BLDUP644-Cpib_vlz.js +1 -0
- ksadk/server/static/assets/treemap-LRROVOQU-DQxfmVHK.js +1 -0
- ksadk/server/static/assets/{vennDiagram-CIIHVFJN-DWgwtPsJ.js → vennDiagram-CIIHVFJN-CMCixki2.js} +1 -1
- ksadk/server/static/assets/wardley-L42UT6IY-BQYHnnkU.js +1 -0
- ksadk/server/static/assets/{wardleyDiagram-YWT4CUSO-2x8G-tTM.js → wardleyDiagram-YWT4CUSO-CVzB8F9K.js} +1 -1
- ksadk/server/static/assets/{xychartDiagram-2RQKCTM6-Drma3AYl.js → xychartDiagram-2RQKCTM6-BFBqBLc1.js} +1 -1
- ksadk/server/static/index.html +2 -2
- ksadk/server/terminal_sessions.py +111 -58
- ksadk/sessions/__init__.py +69 -19
- ksadk/sessions/base.py +31 -11
- ksadk/sessions/continuity.py +2 -3
- ksadk/sessions/in_memory.py +46 -2
- ksadk/sessions/local_service.py +148 -17
- ksadk/sessions/postgres_service.py +143 -25
- ksadk/sessions/resilient.py +36 -8
- ksadk/skills/models.py +17 -5
- ksadk/skills/package_store.py +5 -2
- ksadk/skills/runtime/agent.py +20 -5
- ksadk/skills/runtime/artifacts.py +1 -5
- ksadk/skills/runtime/backends/e2b.py +10 -4
- ksadk/skills/runtime/backends/local.py +15 -3
- ksadk/skills/runtime/base.py +1 -2
- ksadk/skills/runtime/executor.py +10 -3
- ksadk/skills/runtime/factory.py +4 -2
- ksadk/skills/runtime/loader.py +1 -2
- ksadk/skills/runtime/registry.py +15 -10
- ksadk/skills/runtime/request.py +3 -1
- ksadk/skills/service_client.py +47 -16
- ksadk/skills/tool_defs.py +15 -10
- ksadk/studio/__init__.py +5 -0
- ksadk/studio/agent_avatar_assets.py +120 -0
- ksadk/studio/agent_lifecycle.py +51 -0
- ksadk/studio/api.py +1046 -0
- ksadk/studio/api_catalog_routes.py +324 -0
- ksadk/studio/api_contracts.py +177 -0
- ksadk/studio/api_helpers.py +132 -0
- ksadk/studio/authoring.py +524 -0
- ksadk/studio/authoring_coordinator.py +285 -0
- ksadk/studio/builder.py +320 -0
- ksadk/studio/capabilities.py +351 -0
- ksadk/studio/cloud.py +278 -0
- ksadk/studio/codex_agent_service.py +583 -0
- ksadk/studio/codex_builder.py +324 -0
- ksadk/studio/codex_manifest.py +278 -0
- ksadk/studio/codex_run.py +352 -0
- ksadk/studio/compiler.py +189 -0
- ksadk/studio/contracts.py +644 -0
- ksadk/studio/errors.py +47 -0
- ksadk/studio/evaluation.py +192 -0
- ksadk/studio/event_store.py +284 -0
- ksadk/studio/framework_run.py +114 -0
- ksadk/studio/identifiers.py +33 -0
- ksadk/studio/mcp_runtime.py +229 -0
- ksadk/studio/model_client.py +543 -0
- ksadk/studio/model_profile_service.py +214 -0
- ksadk/studio/operations.py +243 -0
- ksadk/studio/otel_trace.py +754 -0
- ksadk/studio/pagination.py +182 -0
- ksadk/studio/python_tool_inspection.py +183 -0
- ksadk/studio/repository.py +306 -0
- ksadk/studio/resource_catalog.py +1217 -0
- ksadk/studio/run_service.py +707 -0
- ksadk/studio/runtime_catalog.py +74 -0
- ksadk/studio/runtime_source.py +339 -0
- ksadk/studio/service.py +1141 -0
- ksadk/studio/shared_web.py +994 -0
- ksadk/studio/skill_discovery.py +628 -0
- ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +3 -0
- ksadk/studio/static/assets/index-CaEN0eTR.css +1 -0
- ksadk/studio/static/assets/index-DI2eY_NH.js +252 -0
- ksadk/studio/static/assets/react-vendor-BNmTiJ-I.js +9 -0
- ksadk/studio/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- ksadk/studio/static/index.html +15 -0
- ksadk/studio/templates.py +630 -0
- ksadk/studio/validator.py +246 -0
- ksadk/studio/workspace.py +137 -0
- ksadk/terminal_exec_policy.py +3 -4
- ksadk/tools/gateway.py +101 -15
- ksadk/tools/result_budget.py +12 -4
- ksadk/toolsets/__init__.py +161 -37
- ksadk/toolsets/a2a.py +426 -0
- ksadk/toolsets/platform.py +21 -5
- ksadk/toolsets/sandbox.py +82 -16
- ksadk/toolsets/skills.py +200 -39
- ksadk/toolsets/web.py +123 -26
- ksadk/toolsets/workspace.py +212 -101
- ksadk/tracing/__init__.py +19 -21
- ksadk/tracing/exporters/inmemory_exporter.py +62 -51
- ksadk/tracing/setup.py +135 -506
- ksadk/tracing/span_utils.py +0 -41
- ksadk/tui/loop.py +78 -30
- ksadk/tui/stream_render.py +1 -0
- ksadk/ui_config.py +5 -3
- ksadk/version.py +1 -1
- ksadk_runtime_common/memory_backend/registry.py +56 -56
- ksadk_runtime_common/schemas/runtime_event_v1.json +95 -0
- ksadk_runtime_common/workspace_files/router.py +8 -2
- agentengine_sdk_python-0.7.0.dist-info/RECORD +0 -482
- ksadk/a2a/card_builder.py +0 -64
- ksadk/a2a/client.py +0 -309
- ksadk/runners/utils/langfuse.py +0 -230
- ksadk/server/static/assets/CodeBlock-P16VOLYi.js +0 -9
- ksadk/server/static/assets/MathMessageMarkdown-CdGiQWHR.js +0 -8
- ksadk/server/static/assets/MermaidBlock-IOcal8cm.js +0 -303
- ksadk/server/static/assets/NativeTerminalPanel-DGSDWsei.js +0 -1
- ksadk/server/static/assets/architecture-7EHR7CIX-BvUlsA6l.js +0 -1
- ksadk/server/static/assets/channel-CTuydARX.js +0 -1
- ksadk/server/static/assets/chunk-QZHKN3VN-BnCydivS.js +0 -1
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-BNHg45Be.js +0 -1
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-BNHg45Be.js +0 -1
- ksadk/server/static/assets/eventmodeling-FCH6USID-DsAgWnlf.js +0 -1
- ksadk/server/static/assets/gitGraph-WXDBUCRP-BIao9Yjg.js +0 -1
- ksadk/server/static/assets/index-C00Lwg3L.js +0 -165
- ksadk/server/static/assets/index-DAURbCsP.css +0 -1
- ksadk/server/static/assets/info-J43DQDTF-CyipqGxU.js +0 -1
- ksadk/server/static/assets/packet-YPE3B663-CGkyRxlg.js +0 -1
- ksadk/server/static/assets/pie-LRSECV5Y-M0uWAf4n.js +0 -1
- ksadk/server/static/assets/radar-GUYGQ44K-Bdbm24-x.js +0 -1
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-8xBN_gLM.js +0 -1
- ksadk/server/static/assets/treeView-BLDUP644-npMo0ak1.js +0 -1
- ksadk/server/static/assets/treemap-LRROVOQU-CHdzrrdo.js +0 -1
- ksadk/server/static/assets/wardley-L42UT6IY-BEQz7wyy.js +0 -1
- ksadk/tracing/exporters/langfuse_exporter.py +0 -445
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
"""Workspace-backed resource catalog for models, tools, MCP servers and Skills."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
import hashlib
|
|
7
|
+
import io
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import stat
|
|
11
|
+
import time
|
|
12
|
+
import zipfile
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path, PurePosixPath
|
|
15
|
+
from typing import Any, Callable, Iterable, Literal, cast
|
|
16
|
+
from uuid import uuid4
|
|
17
|
+
|
|
18
|
+
import yaml # type: ignore[import-untyped]
|
|
19
|
+
from pydantic import ValidationError
|
|
20
|
+
|
|
21
|
+
from ksadk.cli.model_catalog import fetch_provider_model_catalog
|
|
22
|
+
from ksadk.conversations.model_context import normalize_model_metadata
|
|
23
|
+
from ksadk.studio.capabilities import (
|
|
24
|
+
LocalCapabilityResolver,
|
|
25
|
+
canonical_json,
|
|
26
|
+
require_exact_version,
|
|
27
|
+
sha256_digest,
|
|
28
|
+
)
|
|
29
|
+
from ksadk.studio.contracts import (
|
|
30
|
+
AgentBindings,
|
|
31
|
+
CapabilityBinding,
|
|
32
|
+
CapabilityRef,
|
|
33
|
+
MCPServerRef,
|
|
34
|
+
ModelSpec,
|
|
35
|
+
ResourceDescriptor,
|
|
36
|
+
ToolContract,
|
|
37
|
+
)
|
|
38
|
+
from ksadk.studio.errors import StudioError
|
|
39
|
+
from ksadk.studio.pagination import keyset_page
|
|
40
|
+
from ksadk.studio.python_tool_inspection import PythonToolInspector
|
|
41
|
+
from ksadk.studio.repository import load_yaml_file
|
|
42
|
+
from ksadk.studio.skill_discovery import SkillDiscoveryService
|
|
43
|
+
from ksadk.studio.workspace import Workspace
|
|
44
|
+
from ksadk.toolsets import describe_agentengine_tools, get_agentengine_tools
|
|
45
|
+
|
|
46
|
+
_SLUG = re.compile(r"[^a-z0-9]+")
|
|
47
|
+
_MAX_SKILL_ARCHIVE_BYTES = 50 * 1024 * 1024
|
|
48
|
+
_MAX_SKILL_EXPANDED_BYTES = 100 * 1024 * 1024
|
|
49
|
+
_MAX_SKILL_FILES = 1000
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _models_endpoint(api_base: str | None) -> str:
|
|
53
|
+
base = str(api_base or "").strip().rstrip("/")
|
|
54
|
+
if not base:
|
|
55
|
+
return ""
|
|
56
|
+
if base.endswith("/models"):
|
|
57
|
+
return base
|
|
58
|
+
if base.endswith("/v1"):
|
|
59
|
+
return f"{base}/models"
|
|
60
|
+
return f"{base}/v1/models"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _provider_reports_context(raw: dict[str, Any]) -> bool:
|
|
64
|
+
direct = {
|
|
65
|
+
"context_window_tokens",
|
|
66
|
+
"context_length",
|
|
67
|
+
"context_window",
|
|
68
|
+
"input_max_length",
|
|
69
|
+
}
|
|
70
|
+
if direct.intersection(raw):
|
|
71
|
+
return True
|
|
72
|
+
for key in ("metadata", "limits"):
|
|
73
|
+
nested = raw.get(key)
|
|
74
|
+
if isinstance(nested, dict) and direct.intersection(nested):
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _provider_reports_modalities(raw: dict[str, Any]) -> bool:
|
|
80
|
+
architecture = raw.get("architecture")
|
|
81
|
+
if isinstance(architecture, dict) and isinstance(architecture.get("input_modalities"), list):
|
|
82
|
+
return True
|
|
83
|
+
capabilities = raw.get("capabilities")
|
|
84
|
+
return isinstance(capabilities, dict) and any(
|
|
85
|
+
key in capabilities
|
|
86
|
+
for key in (
|
|
87
|
+
"multimodal_input_image",
|
|
88
|
+
"multimodal_input_video",
|
|
89
|
+
"multimodal_input_file",
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _tool_side_effect(side_effects: list[str]) -> str:
|
|
95
|
+
if not side_effects:
|
|
96
|
+
return "none"
|
|
97
|
+
lowered = " ".join(side_effects).lower()
|
|
98
|
+
if any(marker in lowered for marker in ("write", "edit", "delete", "command", "code")):
|
|
99
|
+
return "write"
|
|
100
|
+
if any(marker in lowered for marker in ("network", "external", "http")):
|
|
101
|
+
return "external"
|
|
102
|
+
return "read"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def resource_slug(value: str) -> str:
|
|
106
|
+
slug = _SLUG.sub("-", value.strip().lower()).strip("-")
|
|
107
|
+
if not slug:
|
|
108
|
+
raise StudioError(
|
|
109
|
+
"RESOURCE_NAME_INVALID",
|
|
110
|
+
"资源名称必须至少包含一个字母或数字",
|
|
111
|
+
status_code=422,
|
|
112
|
+
field="name",
|
|
113
|
+
)
|
|
114
|
+
return slug[:80]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def resource_id(kind: str, source: str, name: str, version: str) -> str:
|
|
118
|
+
return f"{kind}:{source}:{resource_slug(name)}:{version}"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class LocalResourceCatalog:
|
|
122
|
+
def __init__(self, workspace: Workspace) -> None:
|
|
123
|
+
self.workspace = workspace
|
|
124
|
+
self.resolver = LocalCapabilityResolver(workspace)
|
|
125
|
+
self.skill_discovery = SkillDiscoveryService(workspace)
|
|
126
|
+
self.python_tool_inspector = PythonToolInspector(workspace)
|
|
127
|
+
self._provider_models: dict[str, ResourceDescriptor] = {}
|
|
128
|
+
# api_base -> (monotonic_ts, descriptors, source),见 discover_provider_models。
|
|
129
|
+
self._provider_catalog_cache: dict[
|
|
130
|
+
str, tuple[float, builtins.list[ResourceDescriptor], str]
|
|
131
|
+
] = {}
|
|
132
|
+
# 内建工具描述构建一次约 40ms(导入 + schema 生成 + digest),
|
|
133
|
+
# catalog.list 调用方很多,用短 TTL 缓存避免页面加载期间重复构建;
|
|
134
|
+
# enabled 依赖 sandbox backend 配置,设置页改完后几秒自动生效。
|
|
135
|
+
self._builtin_tools_cache: tuple[float, builtins.list[ResourceDescriptor]] | None = None
|
|
136
|
+
|
|
137
|
+
def list(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
kind: str | None = None,
|
|
141
|
+
query: str = "",
|
|
142
|
+
source: str | None = None,
|
|
143
|
+
status: str | None = None,
|
|
144
|
+
installed: bool | None = None,
|
|
145
|
+
limit: int = 50,
|
|
146
|
+
) -> list[ResourceDescriptor]:
|
|
147
|
+
filtered = self._filtered_resources(
|
|
148
|
+
kind=kind,
|
|
149
|
+
query=query,
|
|
150
|
+
source=source,
|
|
151
|
+
status=status,
|
|
152
|
+
installed=installed,
|
|
153
|
+
)
|
|
154
|
+
filtered.sort(key=self._default_sort_key)
|
|
155
|
+
return filtered[:limit]
|
|
156
|
+
|
|
157
|
+
def list_page(
|
|
158
|
+
self,
|
|
159
|
+
*,
|
|
160
|
+
kind: str | None = None,
|
|
161
|
+
query: str = "",
|
|
162
|
+
source: str | None = None,
|
|
163
|
+
status: str | None = None,
|
|
164
|
+
installed: bool | None = None,
|
|
165
|
+
limit: int = 50,
|
|
166
|
+
cursor: str | None = None,
|
|
167
|
+
sort: str = "default",
|
|
168
|
+
status_resolver: Callable[[ResourceDescriptor], str] | None = None,
|
|
169
|
+
) -> dict[str, Any]:
|
|
170
|
+
if sort not in {"default", "displayName:asc", "displayName:desc"}:
|
|
171
|
+
raise StudioError(
|
|
172
|
+
"PAGINATION_SORT_INVALID",
|
|
173
|
+
"Resource 排序字段无效",
|
|
174
|
+
status_code=422,
|
|
175
|
+
field="sort",
|
|
176
|
+
)
|
|
177
|
+
filtered = self._filtered_resources(
|
|
178
|
+
kind=kind,
|
|
179
|
+
query=query,
|
|
180
|
+
source=source,
|
|
181
|
+
status=status,
|
|
182
|
+
installed=installed,
|
|
183
|
+
status_resolver=status_resolver,
|
|
184
|
+
)
|
|
185
|
+
if sort == "default":
|
|
186
|
+
sort_key = self._default_sort_key
|
|
187
|
+
reverse = False
|
|
188
|
+
else:
|
|
189
|
+
|
|
190
|
+
def sort_key(item: ResourceDescriptor) -> tuple[str, str, int, str, str]:
|
|
191
|
+
return (
|
|
192
|
+
item.display_name.lower(),
|
|
193
|
+
item.kind,
|
|
194
|
+
0 if item.source in {"builtin", "provider"} else 1,
|
|
195
|
+
item.version,
|
|
196
|
+
item.resource_id,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
reverse = sort.endswith(":desc")
|
|
200
|
+
filters = {
|
|
201
|
+
"kind": kind or "",
|
|
202
|
+
"query": query.strip().lower(),
|
|
203
|
+
"source": source or "",
|
|
204
|
+
"status": status or "",
|
|
205
|
+
"installed": installed,
|
|
206
|
+
}
|
|
207
|
+
return keyset_page(
|
|
208
|
+
filtered,
|
|
209
|
+
key=sort_key,
|
|
210
|
+
reverse=reverse,
|
|
211
|
+
limit=max(1, min(limit, 200)),
|
|
212
|
+
cursor=cursor,
|
|
213
|
+
namespace="catalog-resources",
|
|
214
|
+
sort=sort,
|
|
215
|
+
filters=filters,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _default_sort_key(item: ResourceDescriptor) -> tuple[str, int, str, str, str]:
|
|
220
|
+
return (
|
|
221
|
+
item.kind,
|
|
222
|
+
0 if item.source in {"builtin", "provider"} else 1,
|
|
223
|
+
item.display_name.lower(),
|
|
224
|
+
item.version,
|
|
225
|
+
item.resource_id,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def _filtered_resources(
|
|
229
|
+
self,
|
|
230
|
+
*,
|
|
231
|
+
kind: str | None,
|
|
232
|
+
query: str,
|
|
233
|
+
source: str | None,
|
|
234
|
+
status: str | None,
|
|
235
|
+
installed: bool | None,
|
|
236
|
+
status_resolver: Callable[[ResourceDescriptor], str] | None = None,
|
|
237
|
+
) -> builtins.list[ResourceDescriptor]:
|
|
238
|
+
candidates = [
|
|
239
|
+
*self._provider_models.values(),
|
|
240
|
+
*self._builtin_tools(),
|
|
241
|
+
*self._persisted("models"),
|
|
242
|
+
*self._persisted("mcp"),
|
|
243
|
+
*self._persisted("tools"),
|
|
244
|
+
*self._local_skills(),
|
|
245
|
+
]
|
|
246
|
+
resources = list({item.resource_id: item for item in candidates}.values())
|
|
247
|
+
normalized = query.strip().lower()
|
|
248
|
+
filtered: builtins.list[ResourceDescriptor] = []
|
|
249
|
+
for item in resources:
|
|
250
|
+
effective_status = status_resolver(item) if status_resolver else item.status
|
|
251
|
+
if kind is not None and item.kind != kind:
|
|
252
|
+
continue
|
|
253
|
+
if source is not None and item.source != source:
|
|
254
|
+
continue
|
|
255
|
+
if status is not None and effective_status != status:
|
|
256
|
+
continue
|
|
257
|
+
if installed is not None and item.installed is not installed:
|
|
258
|
+
continue
|
|
259
|
+
if normalized and not any(
|
|
260
|
+
normalized in value.lower()
|
|
261
|
+
for value in (item.name, item.display_name, item.description)
|
|
262
|
+
):
|
|
263
|
+
continue
|
|
264
|
+
filtered.append(
|
|
265
|
+
item
|
|
266
|
+
if effective_status == item.status
|
|
267
|
+
else item.model_copy(update={"status": effective_status})
|
|
268
|
+
)
|
|
269
|
+
return filtered
|
|
270
|
+
|
|
271
|
+
async def discover_provider_models(
|
|
272
|
+
self,
|
|
273
|
+
*,
|
|
274
|
+
api_base: str | None,
|
|
275
|
+
api_key: str | None,
|
|
276
|
+
current_model: str | None,
|
|
277
|
+
timeout: float = 5.0,
|
|
278
|
+
) -> tuple[builtins.list[ResourceDescriptor], str]:
|
|
279
|
+
"""Project the same provider catalog used by ksadk runtimes into Studio.
|
|
280
|
+
|
|
281
|
+
The provider response is authoritative only for fields it actually
|
|
282
|
+
returns. Canonical fallback values remain annotated as ``ksadk-default``
|
|
283
|
+
so the UI never presents a guessed context window or modality as probed.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
# /v1/models 是真实外网往返,catalog/models 等接口每次调用都走这里,
|
|
287
|
+
# 会话/资源页加载会连续触发多次。做 60s 进程内缓存;上游失败且
|
|
288
|
+
# 有缓存时直接回退缓存,避免上游抖动拖垮整个模型目录。
|
|
289
|
+
cache_key = (api_base or "").rstrip("/")
|
|
290
|
+
now = time.monotonic()
|
|
291
|
+
cached = self._provider_catalog_cache.get(cache_key)
|
|
292
|
+
if cached is not None and now - cached[0] < 60.0:
|
|
293
|
+
return cached[1], cached[2]
|
|
294
|
+
|
|
295
|
+
catalog = await fetch_provider_model_catalog(
|
|
296
|
+
api_base=api_base,
|
|
297
|
+
api_key=api_key,
|
|
298
|
+
timeout=timeout,
|
|
299
|
+
)
|
|
300
|
+
source = "provider" if catalog else "fallback"
|
|
301
|
+
if not catalog:
|
|
302
|
+
if cached is not None:
|
|
303
|
+
return cached[1], cached[2]
|
|
304
|
+
catalog = [normalize_model_metadata({"id": current_model or "glm-5.1"})]
|
|
305
|
+
|
|
306
|
+
descriptors: list[ResourceDescriptor] = []
|
|
307
|
+
for item in catalog:
|
|
308
|
+
normalized = dict(item)
|
|
309
|
+
raw = normalized.pop("_provider_raw_model", None)
|
|
310
|
+
raw_mapping = raw if isinstance(raw, dict) else {}
|
|
311
|
+
normalized = normalize_model_metadata(normalized)
|
|
312
|
+
model_id = str(normalized.get("id") or current_model or "unknown-model")
|
|
313
|
+
display_name = str(normalized.get("display_name") or model_id)
|
|
314
|
+
context_source = (
|
|
315
|
+
"provider" if _provider_reports_context(raw_mapping) else "ksadk-default"
|
|
316
|
+
)
|
|
317
|
+
modality_source = (
|
|
318
|
+
"provider" if _provider_reports_modalities(raw_mapping) else "ksadk-default"
|
|
319
|
+
)
|
|
320
|
+
spec = ModelSpec(
|
|
321
|
+
provider="openai-compatible",
|
|
322
|
+
model=model_id,
|
|
323
|
+
base_url=(api_base or "https://api.openai.com/v1").rstrip("/"),
|
|
324
|
+
credential_ref="env://OPENAI_API_KEY",
|
|
325
|
+
metadata=normalized,
|
|
326
|
+
discovery={
|
|
327
|
+
"source": source,
|
|
328
|
+
"endpoint": _models_endpoint(api_base),
|
|
329
|
+
"contextWindow": context_source,
|
|
330
|
+
"inputModalities": modality_source,
|
|
331
|
+
},
|
|
332
|
+
)
|
|
333
|
+
descriptor_source = "provider" if source == "provider" else "builtin"
|
|
334
|
+
descriptor = self._descriptor(
|
|
335
|
+
kind="model",
|
|
336
|
+
source=descriptor_source,
|
|
337
|
+
name=model_id,
|
|
338
|
+
display_name=display_name,
|
|
339
|
+
version=str(
|
|
340
|
+
normalized.get("version") or ("live" if source == "provider" else "1.0.0")
|
|
341
|
+
),
|
|
342
|
+
description=(
|
|
343
|
+
"由模型服务 /v1/models 自动发现"
|
|
344
|
+
if source == "provider"
|
|
345
|
+
else "模型服务未返回目录,使用 ksadk 当前模型配置"
|
|
346
|
+
),
|
|
347
|
+
category="provider-catalog",
|
|
348
|
+
contract=spec.model_dump(
|
|
349
|
+
by_alias=True,
|
|
350
|
+
exclude_none=True,
|
|
351
|
+
mode="json",
|
|
352
|
+
),
|
|
353
|
+
required_secret_refs=[spec.credential_ref],
|
|
354
|
+
)
|
|
355
|
+
descriptors.append(descriptor)
|
|
356
|
+
|
|
357
|
+
self._provider_models = {item.resource_id: item for item in descriptors}
|
|
358
|
+
self._provider_catalog_cache[cache_key] = (now, descriptors, source)
|
|
359
|
+
return descriptors, source
|
|
360
|
+
|
|
361
|
+
def get(self, resource: str) -> ResourceDescriptor:
|
|
362
|
+
found = next(
|
|
363
|
+
(item for item in self.list(limit=10_000) if item.resource_id == resource),
|
|
364
|
+
None,
|
|
365
|
+
)
|
|
366
|
+
if found is None:
|
|
367
|
+
raise StudioError(
|
|
368
|
+
"RESOURCE_NOT_FOUND",
|
|
369
|
+
"Catalog Resource 不存在",
|
|
370
|
+
status_code=404,
|
|
371
|
+
details={"resourceId": resource},
|
|
372
|
+
)
|
|
373
|
+
return found
|
|
374
|
+
|
|
375
|
+
def create_model_profile(
|
|
376
|
+
self,
|
|
377
|
+
*,
|
|
378
|
+
name: str,
|
|
379
|
+
display_name: str,
|
|
380
|
+
version: str,
|
|
381
|
+
description: str,
|
|
382
|
+
spec: ModelSpec,
|
|
383
|
+
) -> ResourceDescriptor:
|
|
384
|
+
require_exact_version(version, field="version")
|
|
385
|
+
return self._persist_descriptor(
|
|
386
|
+
"models",
|
|
387
|
+
self._descriptor(
|
|
388
|
+
kind="model",
|
|
389
|
+
name=name,
|
|
390
|
+
display_name=display_name,
|
|
391
|
+
version=version,
|
|
392
|
+
description=description,
|
|
393
|
+
category="model",
|
|
394
|
+
contract=spec.model_dump(
|
|
395
|
+
by_alias=True,
|
|
396
|
+
exclude_none=True,
|
|
397
|
+
mode="json",
|
|
398
|
+
),
|
|
399
|
+
required_secret_refs=[spec.credential_ref],
|
|
400
|
+
),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def create_mcp_server(
|
|
404
|
+
self,
|
|
405
|
+
*,
|
|
406
|
+
display_name: str,
|
|
407
|
+
description: str,
|
|
408
|
+
server: MCPServerRef,
|
|
409
|
+
) -> ResourceDescriptor:
|
|
410
|
+
require_exact_version(server.version, field="version")
|
|
411
|
+
resolved = self.resolver.resolve_mcp(server)
|
|
412
|
+
descriptor = self._descriptor(
|
|
413
|
+
kind="mcp",
|
|
414
|
+
name=server.name,
|
|
415
|
+
display_name=display_name,
|
|
416
|
+
version=server.version,
|
|
417
|
+
description=description,
|
|
418
|
+
category="mcp",
|
|
419
|
+
contract=resolved,
|
|
420
|
+
required_secret_refs=sorted(set(server.env_refs.values())),
|
|
421
|
+
)
|
|
422
|
+
return self._persist_descriptor("mcp", descriptor)
|
|
423
|
+
|
|
424
|
+
def create_tool(
|
|
425
|
+
self,
|
|
426
|
+
*,
|
|
427
|
+
display_name: str,
|
|
428
|
+
category: str,
|
|
429
|
+
contract: ToolContract,
|
|
430
|
+
) -> ResourceDescriptor:
|
|
431
|
+
require_exact_version(contract.version, field="version")
|
|
432
|
+
if contract.executor == "python":
|
|
433
|
+
contract = self._snapshot_python_tool(contract)
|
|
434
|
+
resolved = self.resolver.resolve_tool(contract)
|
|
435
|
+
descriptor = self._descriptor(
|
|
436
|
+
kind="tool",
|
|
437
|
+
name=contract.name,
|
|
438
|
+
display_name=display_name,
|
|
439
|
+
version=contract.version,
|
|
440
|
+
description=contract.description,
|
|
441
|
+
category=category,
|
|
442
|
+
contract=resolved.model_dump(
|
|
443
|
+
by_alias=True,
|
|
444
|
+
exclude_none=True,
|
|
445
|
+
mode="json",
|
|
446
|
+
),
|
|
447
|
+
)
|
|
448
|
+
return self._persist_descriptor("tools", descriptor)
|
|
449
|
+
|
|
450
|
+
def inspect_python_tool(self, content: bytes, *, filename: str) -> dict[str, Any]:
|
|
451
|
+
return self.python_tool_inspector.inspect(content, filename=filename)
|
|
452
|
+
|
|
453
|
+
def commit_python_tool(
|
|
454
|
+
self,
|
|
455
|
+
inspection_token: str,
|
|
456
|
+
*,
|
|
457
|
+
display_name: str,
|
|
458
|
+
name: str,
|
|
459
|
+
callable_name: str,
|
|
460
|
+
description: str = "",
|
|
461
|
+
) -> ResourceDescriptor:
|
|
462
|
+
inspection = self.python_tool_inspector.load(inspection_token)
|
|
463
|
+
callable_meta = next(
|
|
464
|
+
(
|
|
465
|
+
item
|
|
466
|
+
for item in inspection.get("callables") or []
|
|
467
|
+
if item.get("name") == callable_name
|
|
468
|
+
),
|
|
469
|
+
None,
|
|
470
|
+
)
|
|
471
|
+
if callable_meta is None:
|
|
472
|
+
raise StudioError(
|
|
473
|
+
"PYTHON_TOOL_CALLABLE_INVALID",
|
|
474
|
+
"所选 Callable 不在已检查的源码中",
|
|
475
|
+
status_code=422,
|
|
476
|
+
field="callableName",
|
|
477
|
+
)
|
|
478
|
+
required = list(callable_meta.get("required") or [])
|
|
479
|
+
parameters = [
|
|
480
|
+
value
|
|
481
|
+
for value in callable_meta.get("parameters") or []
|
|
482
|
+
if not str(value).startswith(("*", "**"))
|
|
483
|
+
]
|
|
484
|
+
resolved_description = str(description or callable_meta.get("description") or "").strip()
|
|
485
|
+
descriptor = self.create_tool(
|
|
486
|
+
display_name=display_name,
|
|
487
|
+
category="custom",
|
|
488
|
+
contract=ToolContract(
|
|
489
|
+
name=name,
|
|
490
|
+
version="1.0.0",
|
|
491
|
+
description=resolved_description,
|
|
492
|
+
input_schema={
|
|
493
|
+
"type": "object",
|
|
494
|
+
"properties": {parameter: {} for parameter in parameters},
|
|
495
|
+
**({"required": required} if required else {}),
|
|
496
|
+
},
|
|
497
|
+
output_schema={},
|
|
498
|
+
executor="python",
|
|
499
|
+
source_path=str(inspection["sourcePath"]),
|
|
500
|
+
callable_name=callable_name,
|
|
501
|
+
approval="policy",
|
|
502
|
+
side_effect="none",
|
|
503
|
+
),
|
|
504
|
+
)
|
|
505
|
+
self.python_tool_inspector.consume(inspection_token)
|
|
506
|
+
return descriptor
|
|
507
|
+
|
|
508
|
+
def _snapshot_python_tool(self, contract: ToolContract) -> ToolContract:
|
|
509
|
+
assert contract.source_path is not None
|
|
510
|
+
try:
|
|
511
|
+
source = self.workspace.resolve(contract.source_path, must_exist=True)
|
|
512
|
+
except FileNotFoundError as exc:
|
|
513
|
+
raise StudioError(
|
|
514
|
+
"TOOL_SOURCE_NOT_FOUND",
|
|
515
|
+
f"Python Tool 源码文件不存在:{contract.source_path}(相对于当前工作区)",
|
|
516
|
+
status_code=422,
|
|
517
|
+
field="sourcePath",
|
|
518
|
+
) from exc
|
|
519
|
+
if source.is_symlink() or not source.is_file() or source.suffix != ".py":
|
|
520
|
+
raise StudioError(
|
|
521
|
+
"TOOL_SOURCE_INVALID",
|
|
522
|
+
"Python Tool sourcePath 必须是工作区内的普通 .py 文件",
|
|
523
|
+
status_code=422,
|
|
524
|
+
field="sourcePath",
|
|
525
|
+
)
|
|
526
|
+
content = source.read_bytes()
|
|
527
|
+
if len(content) > 1024 * 1024:
|
|
528
|
+
raise StudioError(
|
|
529
|
+
"TOOL_SOURCE_TOO_LARGE",
|
|
530
|
+
"Python Tool 源码不能超过 1 MiB",
|
|
531
|
+
status_code=422,
|
|
532
|
+
field="sourcePath",
|
|
533
|
+
)
|
|
534
|
+
try:
|
|
535
|
+
text = content.decode("utf-8")
|
|
536
|
+
except UnicodeDecodeError as exc:
|
|
537
|
+
raise StudioError(
|
|
538
|
+
"TOOL_SOURCE_INVALID",
|
|
539
|
+
"Python Tool 源码必须使用 UTF-8 编码",
|
|
540
|
+
status_code=422,
|
|
541
|
+
field="sourcePath",
|
|
542
|
+
) from exc
|
|
543
|
+
target = self.workspace.resolve(
|
|
544
|
+
Path(".agentkit/catalog/tool-sources")
|
|
545
|
+
/ f"{resource_slug(contract.name)}-{resource_slug(contract.version)}"
|
|
546
|
+
/ "tool.py"
|
|
547
|
+
)
|
|
548
|
+
self.workspace.atomic_write_text(target, text)
|
|
549
|
+
return cast(
|
|
550
|
+
ToolContract,
|
|
551
|
+
contract.model_copy(
|
|
552
|
+
update={
|
|
553
|
+
"source_path": self.workspace.relative(target),
|
|
554
|
+
"source_sha256": f"sha256:{hashlib.sha256(content).hexdigest()}",
|
|
555
|
+
}
|
|
556
|
+
),
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def save_probe(
|
|
560
|
+
self,
|
|
561
|
+
resource: str,
|
|
562
|
+
*,
|
|
563
|
+
result: dict[str, Any],
|
|
564
|
+
) -> ResourceDescriptor:
|
|
565
|
+
descriptor = self.get(resource)
|
|
566
|
+
if descriptor.kind != "mcp" or descriptor.source != "local":
|
|
567
|
+
raise StudioError(
|
|
568
|
+
"RESOURCE_KIND_INVALID",
|
|
569
|
+
"只有本地 MCP Resource 可以保存探测结果",
|
|
570
|
+
status_code=422,
|
|
571
|
+
details={"resourceId": resource},
|
|
572
|
+
)
|
|
573
|
+
updated = descriptor.model_copy(deep=True)
|
|
574
|
+
updated.status = "ready"
|
|
575
|
+
updated.health = {
|
|
576
|
+
"status": "ready",
|
|
577
|
+
"toolCount": len(result.get("tools") or []),
|
|
578
|
+
"serverInfo": result.get("serverInfo") or {},
|
|
579
|
+
"probedAt": datetime.now(timezone.utc).isoformat(),
|
|
580
|
+
}
|
|
581
|
+
updated.contract["discoveredTools"] = result.get("tools") or []
|
|
582
|
+
updated.updated_at = datetime.now(timezone.utc)
|
|
583
|
+
return self._persist_descriptor("mcp", updated, overwrite=True)
|
|
584
|
+
|
|
585
|
+
def mark_probe_failed(
|
|
586
|
+
self,
|
|
587
|
+
resource: str,
|
|
588
|
+
*,
|
|
589
|
+
code: str,
|
|
590
|
+
detail: str | None = None,
|
|
591
|
+
) -> ResourceDescriptor:
|
|
592
|
+
descriptor = self.get(resource)
|
|
593
|
+
updated = descriptor.model_copy(deep=True)
|
|
594
|
+
updated.status = "unhealthy"
|
|
595
|
+
updated.health = {
|
|
596
|
+
"status": "unhealthy",
|
|
597
|
+
"code": code,
|
|
598
|
+
"probedAt": datetime.now(timezone.utc).isoformat(),
|
|
599
|
+
}
|
|
600
|
+
if detail:
|
|
601
|
+
updated.health["message"] = detail[:500]
|
|
602
|
+
updated.updated_at = datetime.now(timezone.utc)
|
|
603
|
+
return self._persist_descriptor("mcp", updated, overwrite=True)
|
|
604
|
+
|
|
605
|
+
def policy_preview(
|
|
606
|
+
self,
|
|
607
|
+
bindings: AgentBindings,
|
|
608
|
+
) -> tuple[builtins.list[ToolContract], builtins.list[str]]:
|
|
609
|
+
tools: builtins.list[ToolContract] = []
|
|
610
|
+
permissions: set[str] = set()
|
|
611
|
+
for binding in bindings.tools:
|
|
612
|
+
if not binding.enabled:
|
|
613
|
+
continue
|
|
614
|
+
descriptor = self.get(binding.resource_id)
|
|
615
|
+
if descriptor.kind != "tool":
|
|
616
|
+
raise StudioError(
|
|
617
|
+
"RESOURCE_KIND_INVALID",
|
|
618
|
+
"Tool binding 必须引用 Tool Resource",
|
|
619
|
+
status_code=422,
|
|
620
|
+
details={"resourceId": binding.resource_id},
|
|
621
|
+
)
|
|
622
|
+
if descriptor.status != "ready":
|
|
623
|
+
raise StudioError(
|
|
624
|
+
"RESOURCE_NOT_READY",
|
|
625
|
+
"不可用的 Tool Resource 不能绑定",
|
|
626
|
+
status_code=409,
|
|
627
|
+
details={
|
|
628
|
+
"resourceId": descriptor.resource_id,
|
|
629
|
+
"status": descriptor.status,
|
|
630
|
+
},
|
|
631
|
+
)
|
|
632
|
+
tool = ToolContract.model_validate(descriptor.contract)
|
|
633
|
+
approval = self._effective_approval(
|
|
634
|
+
tool,
|
|
635
|
+
bindings.policy_template,
|
|
636
|
+
binding,
|
|
637
|
+
)
|
|
638
|
+
tool = cast(
|
|
639
|
+
ToolContract,
|
|
640
|
+
tool.model_copy(update={"approval": approval}),
|
|
641
|
+
)
|
|
642
|
+
tools.append(tool)
|
|
643
|
+
permissions.update(tool.permissions)
|
|
644
|
+
tools.sort(key=lambda item: (item.name, item.version))
|
|
645
|
+
return tools, sorted(permissions)
|
|
646
|
+
|
|
647
|
+
def resolve_model(self, bindings: AgentBindings) -> ModelSpec | None:
|
|
648
|
+
if not bindings.model_profile_id:
|
|
649
|
+
return None
|
|
650
|
+
descriptor = self.get(bindings.model_profile_id)
|
|
651
|
+
if descriptor.kind != "model":
|
|
652
|
+
raise StudioError(
|
|
653
|
+
"RESOURCE_KIND_INVALID",
|
|
654
|
+
"modelProfileId 必须引用 Model Resource",
|
|
655
|
+
status_code=422,
|
|
656
|
+
)
|
|
657
|
+
if descriptor.status not in {"ready", "missing-secret"}:
|
|
658
|
+
raise StudioError(
|
|
659
|
+
"RESOURCE_NOT_READY",
|
|
660
|
+
"模型资源当前不可用",
|
|
661
|
+
status_code=409,
|
|
662
|
+
details={"resourceId": descriptor.resource_id, "status": descriptor.status},
|
|
663
|
+
)
|
|
664
|
+
resolved = cast(ModelSpec, ModelSpec.model_validate(descriptor.contract))
|
|
665
|
+
if bindings.model_parameters is not None:
|
|
666
|
+
resolved.parameters = bindings.model_parameters
|
|
667
|
+
return resolved
|
|
668
|
+
|
|
669
|
+
def resolve_models(self, bindings: AgentBindings) -> builtins.list[ModelSpec]:
|
|
670
|
+
resource_ids = builtins.list(bindings.model_profile_ids)
|
|
671
|
+
if not resource_ids and bindings.model_profile_id:
|
|
672
|
+
resource_ids = [bindings.model_profile_id]
|
|
673
|
+
resolved: builtins.list[ModelSpec] = []
|
|
674
|
+
for resource_id in resource_ids:
|
|
675
|
+
selected = bindings.model_copy(
|
|
676
|
+
update={"model_profile_id": resource_id, "model_profile_ids": []}
|
|
677
|
+
)
|
|
678
|
+
model = self.resolve_model(selected)
|
|
679
|
+
if model is not None:
|
|
680
|
+
resolved.append(model)
|
|
681
|
+
return resolved
|
|
682
|
+
|
|
683
|
+
def resolve_skills(self, bindings: AgentBindings) -> builtins.list[CapabilityRef]:
|
|
684
|
+
refs: builtins.list[CapabilityRef] = []
|
|
685
|
+
for binding in bindings.skills:
|
|
686
|
+
if not binding.enabled:
|
|
687
|
+
continue
|
|
688
|
+
descriptor = self._ready_binding(binding, expected_kind="skill")
|
|
689
|
+
refs.append(
|
|
690
|
+
CapabilityRef(
|
|
691
|
+
name=descriptor.name,
|
|
692
|
+
version=descriptor.version,
|
|
693
|
+
digest=descriptor.digest,
|
|
694
|
+
)
|
|
695
|
+
)
|
|
696
|
+
return refs
|
|
697
|
+
|
|
698
|
+
def resolve_mcp_servers(
|
|
699
|
+
self,
|
|
700
|
+
bindings: AgentBindings,
|
|
701
|
+
) -> builtins.list[MCPServerRef]:
|
|
702
|
+
refs: builtins.list[MCPServerRef] = []
|
|
703
|
+
for binding in bindings.mcp_servers:
|
|
704
|
+
if not binding.enabled:
|
|
705
|
+
continue
|
|
706
|
+
descriptor = self._ready_binding(binding, expected_kind="mcp")
|
|
707
|
+
payload = {
|
|
708
|
+
key: value for key, value in descriptor.contract.items() if key != "discoveredTools"
|
|
709
|
+
}
|
|
710
|
+
refs.append(MCPServerRef.model_validate(payload))
|
|
711
|
+
return refs
|
|
712
|
+
|
|
713
|
+
def resolve_mcp_tools(
|
|
714
|
+
self,
|
|
715
|
+
bindings: AgentBindings,
|
|
716
|
+
) -> builtins.list[ToolContract]:
|
|
717
|
+
tools: builtins.list[ToolContract] = []
|
|
718
|
+
for binding in bindings.mcp_servers:
|
|
719
|
+
if not binding.enabled:
|
|
720
|
+
continue
|
|
721
|
+
descriptor = self._ready_binding(binding, expected_kind="mcp")
|
|
722
|
+
for payload in descriptor.contract.get("discoveredTools") or []:
|
|
723
|
+
tool = ToolContract.model_validate(payload)
|
|
724
|
+
approval = self._effective_approval(
|
|
725
|
+
tool,
|
|
726
|
+
bindings.policy_template,
|
|
727
|
+
binding,
|
|
728
|
+
)
|
|
729
|
+
tools.append(
|
|
730
|
+
cast(
|
|
731
|
+
ToolContract,
|
|
732
|
+
tool.model_copy(update={"approval": approval}),
|
|
733
|
+
)
|
|
734
|
+
)
|
|
735
|
+
return tools
|
|
736
|
+
|
|
737
|
+
def import_skill_zip(self, content: bytes, *, filename: str) -> ResourceDescriptor:
|
|
738
|
+
if not filename.lower().endswith(".zip"):
|
|
739
|
+
raise StudioError(
|
|
740
|
+
"SKILL_ARCHIVE_INVALID",
|
|
741
|
+
"Skill 包必须是 .zip 文件",
|
|
742
|
+
status_code=422,
|
|
743
|
+
)
|
|
744
|
+
if len(content) > _MAX_SKILL_ARCHIVE_BYTES:
|
|
745
|
+
raise StudioError(
|
|
746
|
+
"SKILL_ARCHIVE_TOO_LARGE",
|
|
747
|
+
"Skill 包不能超过 50MB",
|
|
748
|
+
status_code=413,
|
|
749
|
+
)
|
|
750
|
+
try:
|
|
751
|
+
archive = zipfile.ZipFile(io.BytesIO(content))
|
|
752
|
+
except zipfile.BadZipFile as exc:
|
|
753
|
+
raise StudioError(
|
|
754
|
+
"SKILL_ARCHIVE_INVALID",
|
|
755
|
+
"Skill ZIP 无法解析",
|
|
756
|
+
status_code=422,
|
|
757
|
+
) from exc
|
|
758
|
+
with archive:
|
|
759
|
+
files = [info for info in archive.infolist() if not info.is_dir()]
|
|
760
|
+
if not files or len(files) > _MAX_SKILL_FILES:
|
|
761
|
+
raise StudioError(
|
|
762
|
+
"SKILL_ARCHIVE_INVALID",
|
|
763
|
+
"Skill ZIP 文件数量不合法",
|
|
764
|
+
status_code=422,
|
|
765
|
+
)
|
|
766
|
+
expanded = sum(info.file_size for info in files)
|
|
767
|
+
if expanded > _MAX_SKILL_EXPANDED_BYTES:
|
|
768
|
+
raise StudioError(
|
|
769
|
+
"SKILL_ARCHIVE_TOO_LARGE",
|
|
770
|
+
"Skill ZIP 解压后不能超过 100MB",
|
|
771
|
+
status_code=413,
|
|
772
|
+
)
|
|
773
|
+
for info in files:
|
|
774
|
+
path = PurePosixPath(info.filename)
|
|
775
|
+
mode = (info.external_attr >> 16) & 0o170000
|
|
776
|
+
if (
|
|
777
|
+
path.is_absolute()
|
|
778
|
+
or ".." in path.parts
|
|
779
|
+
or mode == stat.S_IFLNK
|
|
780
|
+
or (info.compress_size == 0 and info.file_size > 0)
|
|
781
|
+
or (info.compress_size > 0 and info.file_size / info.compress_size > 200)
|
|
782
|
+
):
|
|
783
|
+
raise StudioError(
|
|
784
|
+
"SKILL_ARCHIVE_UNSAFE",
|
|
785
|
+
"Skill ZIP 包含不安全路径、链接或压缩条目",
|
|
786
|
+
status_code=422,
|
|
787
|
+
details={"entry": info.filename},
|
|
788
|
+
)
|
|
789
|
+
skill_entry, prefix = self._skill_entry(files)
|
|
790
|
+
skill_text = archive.read(skill_entry).decode("utf-8")
|
|
791
|
+
metadata = self._skill_frontmatter(skill_text)
|
|
792
|
+
name = str(metadata["name"])
|
|
793
|
+
description = str(metadata["description"])
|
|
794
|
+
version = str(metadata.get("version") or "1.0.0")
|
|
795
|
+
require_exact_version(version, field="SKILL.md.version")
|
|
796
|
+
slug = resource_slug(name)
|
|
797
|
+
destination = self.workspace.resolve(Path("capabilities/skills") / slug)
|
|
798
|
+
if destination.exists():
|
|
799
|
+
raise StudioError(
|
|
800
|
+
"RESOURCE_ALREADY_EXISTS",
|
|
801
|
+
"同名 Skill 已安装",
|
|
802
|
+
status_code=409,
|
|
803
|
+
details={"name": slug},
|
|
804
|
+
)
|
|
805
|
+
staging = self.workspace.resolve(
|
|
806
|
+
Path(".agentkit/cache") / f".skill-{slug}-{uuid4().hex}.tmp"
|
|
807
|
+
)
|
|
808
|
+
staging.mkdir(parents=True, exist_ok=False)
|
|
809
|
+
try:
|
|
810
|
+
for info in files:
|
|
811
|
+
source_path = PurePosixPath(info.filename)
|
|
812
|
+
relative = (
|
|
813
|
+
PurePosixPath(*source_path.parts[len(prefix.parts) :])
|
|
814
|
+
if prefix.parts
|
|
815
|
+
else source_path
|
|
816
|
+
)
|
|
817
|
+
target = staging / Path(*relative.parts)
|
|
818
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
819
|
+
target.write_bytes(archive.read(info))
|
|
820
|
+
self.workspace.atomic_write_yaml(
|
|
821
|
+
staging / "skill.yaml",
|
|
822
|
+
{
|
|
823
|
+
"name": slug,
|
|
824
|
+
"displayName": name,
|
|
825
|
+
"description": description,
|
|
826
|
+
"version": version,
|
|
827
|
+
"instructionsFile": "SKILL.md",
|
|
828
|
+
},
|
|
829
|
+
)
|
|
830
|
+
shutil.move(str(staging), str(destination))
|
|
831
|
+
finally:
|
|
832
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
833
|
+
descriptor = next(
|
|
834
|
+
(
|
|
835
|
+
item
|
|
836
|
+
for item in self._local_skills()
|
|
837
|
+
if item.name == slug and item.version == version
|
|
838
|
+
),
|
|
839
|
+
None,
|
|
840
|
+
)
|
|
841
|
+
if descriptor is None:
|
|
842
|
+
raise StudioError(
|
|
843
|
+
"SKILL_IMPORT_FAILED",
|
|
844
|
+
"Skill 安装后无法解析",
|
|
845
|
+
status_code=500,
|
|
846
|
+
)
|
|
847
|
+
return descriptor
|
|
848
|
+
|
|
849
|
+
def discover_skills(
|
|
850
|
+
self,
|
|
851
|
+
*,
|
|
852
|
+
scan_paths: builtins.list[str] | None = None,
|
|
853
|
+
) -> dict[str, Any]:
|
|
854
|
+
return self.skill_discovery.discover(scan_paths=scan_paths)
|
|
855
|
+
|
|
856
|
+
def commit_discovered_skill(
|
|
857
|
+
self,
|
|
858
|
+
inspection_token: str,
|
|
859
|
+
candidate_id: str,
|
|
860
|
+
*,
|
|
861
|
+
overwrite: bool = False,
|
|
862
|
+
) -> ResourceDescriptor:
|
|
863
|
+
slug, version = self.skill_discovery.commit(
|
|
864
|
+
inspection_token,
|
|
865
|
+
candidate_id,
|
|
866
|
+
overwrite=overwrite,
|
|
867
|
+
)
|
|
868
|
+
descriptor = next(
|
|
869
|
+
(
|
|
870
|
+
item
|
|
871
|
+
for item in self._local_skills()
|
|
872
|
+
if item.name == slug and item.version == version
|
|
873
|
+
),
|
|
874
|
+
None,
|
|
875
|
+
)
|
|
876
|
+
if descriptor is None:
|
|
877
|
+
raise StudioError(
|
|
878
|
+
"SKILL_IMPORT_FAILED",
|
|
879
|
+
"Skill 导入后无法解析",
|
|
880
|
+
status_code=500,
|
|
881
|
+
)
|
|
882
|
+
return descriptor
|
|
883
|
+
|
|
884
|
+
def preview_discovered_skill(
|
|
885
|
+
self,
|
|
886
|
+
inspection_token: str,
|
|
887
|
+
candidate_id: str,
|
|
888
|
+
*,
|
|
889
|
+
path: str | None = None,
|
|
890
|
+
) -> dict[str, Any]:
|
|
891
|
+
return self.skill_discovery.preview_candidate(
|
|
892
|
+
inspection_token,
|
|
893
|
+
candidate_id,
|
|
894
|
+
path=path,
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
def preview_installed_skill(
|
|
898
|
+
self,
|
|
899
|
+
resource_id_value: str,
|
|
900
|
+
*,
|
|
901
|
+
path: str | None = None,
|
|
902
|
+
) -> dict[str, Any]:
|
|
903
|
+
descriptor = self.get(resource_id_value)
|
|
904
|
+
if descriptor.kind != "skill" or descriptor.source != "local":
|
|
905
|
+
raise StudioError(
|
|
906
|
+
"RESOURCE_KIND_INVALID",
|
|
907
|
+
"仅支持预览工作区已安装的 Skill",
|
|
908
|
+
status_code=422,
|
|
909
|
+
)
|
|
910
|
+
directory = self.workspace.resolve(
|
|
911
|
+
Path("capabilities/skills") / descriptor.name,
|
|
912
|
+
must_exist=True,
|
|
913
|
+
)
|
|
914
|
+
return self.skill_discovery.preview_directory(directory, path=path)
|
|
915
|
+
|
|
916
|
+
def _ready_binding(
|
|
917
|
+
self,
|
|
918
|
+
binding: CapabilityBinding,
|
|
919
|
+
*,
|
|
920
|
+
expected_kind: str,
|
|
921
|
+
) -> ResourceDescriptor:
|
|
922
|
+
descriptor = self.get(binding.resource_id)
|
|
923
|
+
if descriptor.kind != expected_kind:
|
|
924
|
+
raise StudioError(
|
|
925
|
+
"RESOURCE_KIND_INVALID",
|
|
926
|
+
f"Binding 必须引用 {expected_kind} Resource",
|
|
927
|
+
status_code=422,
|
|
928
|
+
details={"resourceId": binding.resource_id},
|
|
929
|
+
)
|
|
930
|
+
if descriptor.status != "ready":
|
|
931
|
+
raise StudioError(
|
|
932
|
+
"RESOURCE_NOT_READY",
|
|
933
|
+
"不可用的 Resource 不能绑定",
|
|
934
|
+
status_code=409,
|
|
935
|
+
details={
|
|
936
|
+
"resourceId": descriptor.resource_id,
|
|
937
|
+
"status": descriptor.status,
|
|
938
|
+
},
|
|
939
|
+
)
|
|
940
|
+
return descriptor
|
|
941
|
+
|
|
942
|
+
@staticmethod
|
|
943
|
+
def _effective_approval(
|
|
944
|
+
tool: ToolContract,
|
|
945
|
+
template: str,
|
|
946
|
+
binding: CapabilityBinding,
|
|
947
|
+
) -> str:
|
|
948
|
+
if binding.approval:
|
|
949
|
+
return binding.approval
|
|
950
|
+
if template == "loose":
|
|
951
|
+
return "never"
|
|
952
|
+
if template == "strict":
|
|
953
|
+
return (
|
|
954
|
+
"never"
|
|
955
|
+
if tool.side_effect in {"none", "read"}
|
|
956
|
+
and "process:execute" not in tool.permissions
|
|
957
|
+
else "always"
|
|
958
|
+
)
|
|
959
|
+
return tool.approval
|
|
960
|
+
|
|
961
|
+
def _builtin_tools(self) -> Iterable[ResourceDescriptor]:
|
|
962
|
+
cached = self._builtin_tools_cache
|
|
963
|
+
now = time.monotonic()
|
|
964
|
+
if cached is not None and now - cached[0] < 5.0:
|
|
965
|
+
return cached[1]
|
|
966
|
+
built = list(self._build_builtin_tools())
|
|
967
|
+
self._builtin_tools_cache = (now, built)
|
|
968
|
+
return built
|
|
969
|
+
|
|
970
|
+
def _build_builtin_tools(self) -> Iterable[ResourceDescriptor]:
|
|
971
|
+
runtime_tools = {
|
|
972
|
+
str(getattr(tool, "name", None) or getattr(tool, "__name__", "")): tool
|
|
973
|
+
for tool in get_agentengine_tools(profile="coding", mode="direct")
|
|
974
|
+
}
|
|
975
|
+
for descriptor in describe_agentengine_tools(profile="coding", mode="direct"):
|
|
976
|
+
name = str(descriptor["name"])
|
|
977
|
+
group = str(descriptor.get("group") or "general")
|
|
978
|
+
side_effects = [str(item) for item in descriptor.get("side_effects") or []]
|
|
979
|
+
side_effect = _tool_side_effect(side_effects)
|
|
980
|
+
permissions = [str(item) for item in descriptor.get("approval_scopes") or []]
|
|
981
|
+
if group == "workspace":
|
|
982
|
+
workspace_permission = (
|
|
983
|
+
"workspace:file:write" if side_effect == "write" else "workspace:file:read"
|
|
984
|
+
)
|
|
985
|
+
if workspace_permission not in permissions:
|
|
986
|
+
permissions.append(workspace_permission)
|
|
987
|
+
runtime_tool = runtime_tools.get(name)
|
|
988
|
+
args_schema = getattr(runtime_tool, "args_schema", None)
|
|
989
|
+
input_schema = (
|
|
990
|
+
args_schema.model_json_schema()
|
|
991
|
+
if args_schema is not None and hasattr(args_schema, "model_json_schema")
|
|
992
|
+
else {"type": "object", "properties": {}}
|
|
993
|
+
)
|
|
994
|
+
tool = ToolContract(
|
|
995
|
+
name=name,
|
|
996
|
+
version="1.0.0",
|
|
997
|
+
description=str(descriptor.get("description") or ""),
|
|
998
|
+
input_schema=input_schema,
|
|
999
|
+
permissions=permissions,
|
|
1000
|
+
side_effect=cast(Any, side_effect),
|
|
1001
|
+
approval="always" if descriptor.get("requires_approval") else "never",
|
|
1002
|
+
executor="builtin",
|
|
1003
|
+
group=group,
|
|
1004
|
+
risk_level=str(descriptor.get("risk_level") or "low"),
|
|
1005
|
+
boundary=str(descriptor.get("boundary") or "ksadk-runtime"),
|
|
1006
|
+
backend=str(descriptor.get("backend") or "") or None,
|
|
1007
|
+
enabled=bool(descriptor.get("enabled", True)),
|
|
1008
|
+
)
|
|
1009
|
+
resolved = self.resolver.resolve_tool(tool)
|
|
1010
|
+
resource = self._descriptor(
|
|
1011
|
+
kind="tool",
|
|
1012
|
+
source="builtin",
|
|
1013
|
+
name=tool.name,
|
|
1014
|
+
display_name=name,
|
|
1015
|
+
version=tool.version,
|
|
1016
|
+
description=tool.description,
|
|
1017
|
+
category=group,
|
|
1018
|
+
contract=resolved.model_dump(
|
|
1019
|
+
by_alias=True,
|
|
1020
|
+
exclude_none=True,
|
|
1021
|
+
mode="json",
|
|
1022
|
+
),
|
|
1023
|
+
)
|
|
1024
|
+
if not tool.enabled:
|
|
1025
|
+
resource = resource.model_copy(update={"status": "unresolved"})
|
|
1026
|
+
yield resource
|
|
1027
|
+
|
|
1028
|
+
def _local_skills(self) -> Iterable[ResourceDescriptor]:
|
|
1029
|
+
root = self.workspace.resolve("capabilities/skills")
|
|
1030
|
+
for directory in sorted(root.iterdir()):
|
|
1031
|
+
if not directory.is_dir() or directory.is_symlink():
|
|
1032
|
+
continue
|
|
1033
|
+
manifest_path = directory / "skill.yaml"
|
|
1034
|
+
manifest = load_yaml_file(manifest_path) if manifest_path.is_file() else {}
|
|
1035
|
+
version = str(manifest.get("version") or "unversioned")
|
|
1036
|
+
status: Literal["ready", "unresolved"]
|
|
1037
|
+
try:
|
|
1038
|
+
resolved = self.resolver.resolve_skill(
|
|
1039
|
+
CapabilityRef(name=directory.name, version=version)
|
|
1040
|
+
)
|
|
1041
|
+
digest = str(resolved["digest"])
|
|
1042
|
+
status = "ready"
|
|
1043
|
+
except StudioError:
|
|
1044
|
+
digest = sha256_digest(directory.name.encode("utf-8"))
|
|
1045
|
+
status = "unresolved"
|
|
1046
|
+
yield ResourceDescriptor(
|
|
1047
|
+
resource_id=resource_id("skill", "local", directory.name, version),
|
|
1048
|
+
kind="skill",
|
|
1049
|
+
name=directory.name,
|
|
1050
|
+
display_name=str(
|
|
1051
|
+
manifest.get("displayName") or manifest.get("name") or directory.name
|
|
1052
|
+
),
|
|
1053
|
+
version=version,
|
|
1054
|
+
digest=digest,
|
|
1055
|
+
source="local",
|
|
1056
|
+
status=cast(Any, status),
|
|
1057
|
+
description=str(manifest.get("description") or ""),
|
|
1058
|
+
category=str(manifest.get("category") or "general"),
|
|
1059
|
+
installed=True,
|
|
1060
|
+
contract={
|
|
1061
|
+
"name": directory.name,
|
|
1062
|
+
"version": version,
|
|
1063
|
+
"digest": digest,
|
|
1064
|
+
},
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
def _persisted(self, directory: str) -> Iterable[ResourceDescriptor]:
|
|
1068
|
+
root = self.workspace.resolve(Path(".agentkit/catalog") / directory)
|
|
1069
|
+
for path in sorted(root.glob("*.yaml")):
|
|
1070
|
+
try:
|
|
1071
|
+
yield ResourceDescriptor.model_validate(load_yaml_file(path))
|
|
1072
|
+
except (StudioError, ValidationError):
|
|
1073
|
+
continue
|
|
1074
|
+
|
|
1075
|
+
def _persist_descriptor(
|
|
1076
|
+
self,
|
|
1077
|
+
directory: str,
|
|
1078
|
+
descriptor: ResourceDescriptor,
|
|
1079
|
+
*,
|
|
1080
|
+
overwrite: bool = False,
|
|
1081
|
+
) -> ResourceDescriptor:
|
|
1082
|
+
target = self.workspace.resolve(
|
|
1083
|
+
Path(".agentkit/catalog")
|
|
1084
|
+
/ directory
|
|
1085
|
+
/ f"{resource_slug(descriptor.name)}-{resource_slug(descriptor.version)}.yaml"
|
|
1086
|
+
)
|
|
1087
|
+
if target.exists() and not overwrite:
|
|
1088
|
+
raise StudioError(
|
|
1089
|
+
"RESOURCE_ALREADY_EXISTS",
|
|
1090
|
+
"同名同版本资源已存在",
|
|
1091
|
+
status_code=409,
|
|
1092
|
+
details={"resourceId": descriptor.resource_id},
|
|
1093
|
+
)
|
|
1094
|
+
self.workspace.atomic_write_yaml(
|
|
1095
|
+
target,
|
|
1096
|
+
descriptor.model_dump(
|
|
1097
|
+
by_alias=True,
|
|
1098
|
+
exclude_none=True,
|
|
1099
|
+
mode="json",
|
|
1100
|
+
),
|
|
1101
|
+
)
|
|
1102
|
+
return descriptor
|
|
1103
|
+
|
|
1104
|
+
def delete_resource(self, resource_id: str) -> None:
|
|
1105
|
+
"""Remove a persisted catalog resource (model/tool/mcp/skill) by id."""
|
|
1106
|
+
descriptor = self.get(resource_id)
|
|
1107
|
+
kind = descriptor.kind
|
|
1108
|
+
if kind == "skill":
|
|
1109
|
+
# Skill 以目录形式安装在 capabilities/skills/{name}
|
|
1110
|
+
target_dir = self.workspace.resolve(Path("capabilities/skills") / descriptor.name)
|
|
1111
|
+
if target_dir.is_dir():
|
|
1112
|
+
shutil.rmtree(target_dir)
|
|
1113
|
+
return
|
|
1114
|
+
directory = {
|
|
1115
|
+
"model": "models",
|
|
1116
|
+
"mcp": "mcp",
|
|
1117
|
+
"tool": "tools",
|
|
1118
|
+
"tool-source": "tool-sources",
|
|
1119
|
+
}.get(kind, kind)
|
|
1120
|
+
target = self.workspace.resolve(
|
|
1121
|
+
Path(".agentkit/catalog")
|
|
1122
|
+
/ directory
|
|
1123
|
+
/ f"{resource_slug(descriptor.name)}-{resource_slug(descriptor.version)}.yaml"
|
|
1124
|
+
)
|
|
1125
|
+
if not target.exists():
|
|
1126
|
+
raise StudioError(
|
|
1127
|
+
"RESOURCE_NOT_FOUND",
|
|
1128
|
+
"资源的持久化文件不存在,无法删除",
|
|
1129
|
+
status_code=404,
|
|
1130
|
+
details={"resourceId": resource_id, "path": str(target)},
|
|
1131
|
+
)
|
|
1132
|
+
target.unlink()
|
|
1133
|
+
|
|
1134
|
+
def _descriptor(
|
|
1135
|
+
self,
|
|
1136
|
+
*,
|
|
1137
|
+
kind: str,
|
|
1138
|
+
name: str,
|
|
1139
|
+
display_name: str,
|
|
1140
|
+
version: str,
|
|
1141
|
+
description: str,
|
|
1142
|
+
category: str,
|
|
1143
|
+
contract: dict[str, Any],
|
|
1144
|
+
source: str = "local",
|
|
1145
|
+
required_secret_refs: builtins.list[str] | None = None,
|
|
1146
|
+
) -> ResourceDescriptor:
|
|
1147
|
+
digest = str(contract.get("digest") or sha256_digest(canonical_json(contract)))
|
|
1148
|
+
return ResourceDescriptor(
|
|
1149
|
+
resource_id=resource_id(kind, source, name, version),
|
|
1150
|
+
kind=cast(Any, kind),
|
|
1151
|
+
name=name,
|
|
1152
|
+
display_name=display_name,
|
|
1153
|
+
version=version,
|
|
1154
|
+
digest=digest,
|
|
1155
|
+
source=cast(Any, source),
|
|
1156
|
+
status="ready",
|
|
1157
|
+
description=description,
|
|
1158
|
+
category=category,
|
|
1159
|
+
required_secret_refs=required_secret_refs or [],
|
|
1160
|
+
contract=contract,
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
@staticmethod
|
|
1164
|
+
def _skill_entry(
|
|
1165
|
+
files: builtins.list[zipfile.ZipInfo],
|
|
1166
|
+
) -> tuple[zipfile.ZipInfo, PurePosixPath]:
|
|
1167
|
+
candidates = [info for info in files if PurePosixPath(info.filename).name == "SKILL.md"]
|
|
1168
|
+
if len(candidates) != 1:
|
|
1169
|
+
raise StudioError(
|
|
1170
|
+
"SKILL_MANIFEST_REQUIRED",
|
|
1171
|
+
"Skill ZIP 必须且只能包含一个 SKILL.md",
|
|
1172
|
+
status_code=422,
|
|
1173
|
+
)
|
|
1174
|
+
path = PurePosixPath(candidates[0].filename)
|
|
1175
|
+
prefix = PurePosixPath(*path.parts[:-1])
|
|
1176
|
+
if len(prefix.parts) > 1:
|
|
1177
|
+
raise StudioError(
|
|
1178
|
+
"SKILL_ARCHIVE_INVALID",
|
|
1179
|
+
"SKILL.md 必须位于 ZIP 根目录或单一顶层目录",
|
|
1180
|
+
status_code=422,
|
|
1181
|
+
)
|
|
1182
|
+
return candidates[0], prefix
|
|
1183
|
+
|
|
1184
|
+
@staticmethod
|
|
1185
|
+
def _skill_frontmatter(content: str) -> dict[str, Any]:
|
|
1186
|
+
if not content.startswith("---\n"):
|
|
1187
|
+
raise StudioError(
|
|
1188
|
+
"SKILL_MANIFEST_INVALID",
|
|
1189
|
+
"SKILL.md 必须包含 YAML frontmatter",
|
|
1190
|
+
status_code=422,
|
|
1191
|
+
)
|
|
1192
|
+
parts = content.split("\n---\n", 1)
|
|
1193
|
+
if len(parts) != 2:
|
|
1194
|
+
raise StudioError(
|
|
1195
|
+
"SKILL_MANIFEST_INVALID",
|
|
1196
|
+
"SKILL.md frontmatter 未闭合",
|
|
1197
|
+
status_code=422,
|
|
1198
|
+
)
|
|
1199
|
+
try:
|
|
1200
|
+
payload = yaml.safe_load(parts[0][4:]) or {}
|
|
1201
|
+
except yaml.YAMLError as exc:
|
|
1202
|
+
raise StudioError(
|
|
1203
|
+
"SKILL_MANIFEST_INVALID",
|
|
1204
|
+
"SKILL.md frontmatter 无法解析",
|
|
1205
|
+
status_code=422,
|
|
1206
|
+
) from exc
|
|
1207
|
+
if (
|
|
1208
|
+
not isinstance(payload, dict)
|
|
1209
|
+
or not payload.get("name")
|
|
1210
|
+
or not payload.get("description")
|
|
1211
|
+
):
|
|
1212
|
+
raise StudioError(
|
|
1213
|
+
"SKILL_MANIFEST_INVALID",
|
|
1214
|
+
"SKILL.md frontmatter 必须包含 name 和 description",
|
|
1215
|
+
status_code=422,
|
|
1216
|
+
)
|
|
1217
|
+
return cast(dict[str, Any], payload)
|