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.
Files changed (450) hide show
  1. {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/METADATA +52 -20
  2. agentengine_sdk_python-0.8.1.dist-info/RECORD +644 -0
  3. {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/WHEEL +1 -1
  4. ksadk/a2a/__init__.py +180 -11
  5. ksadk/a2a/bootstrap.py +332 -0
  6. ksadk/a2a/card.py +15 -0
  7. ksadk/a2a/context_store.py +203 -0
  8. ksadk/a2a/control_plane.py +1056 -0
  9. ksadk/a2a/credential.py +99 -0
  10. ksadk/a2a/event_adapter.py +149 -0
  11. ksadk/a2a/executor.py +361 -137
  12. ksadk/a2a/external_transport.py +516 -0
  13. ksadk/a2a/identity.py +304 -0
  14. ksadk/a2a/ids.py +22 -0
  15. ksadk/a2a/langgraph.py +285 -0
  16. ksadk/a2a/resume_store.py +201 -0
  17. ksadk/a2a/routes.py +112 -0
  18. ksadk/a2a/server.py +102 -62
  19. ksadk/a2a/service_env.py +33 -0
  20. ksadk/a2a/space_client.py +1122 -0
  21. ksadk/a2a/task_adapter.py +423 -0
  22. ksadk/a2a/task_event_dispatcher.py +167 -0
  23. ksadk/a2a/task_event_outbox.py +326 -0
  24. ksadk/a2a/task_store.py +206 -0
  25. ksadk/a2a_card.py +101 -0
  26. ksadk/a2ui/__init__.py +27 -0
  27. ksadk/a2ui/core.py +186 -0
  28. ksadk/a2ui/models.py +166 -0
  29. ksadk/a2ui/renderer.py +198 -0
  30. ksadk/agui/__init__.py +9 -0
  31. ksadk/agui/a2ui_projection.py +92 -0
  32. ksadk/agui/agent.py +872 -0
  33. ksadk/agui/config.py +92 -0
  34. ksadk/agui/routes.py +47 -0
  35. ksadk/api/client.py +238 -204
  36. ksadk/api/environment.py +27 -0
  37. ksadk/builders/__init__.py +2 -0
  38. ksadk/builders/base.py +16 -14
  39. ksadk/builders/code_builder.py +375 -270
  40. ksadk/builders/container_builder.py +240 -200
  41. ksadk/builders/framework_requirements.py +31 -12
  42. ksadk/builders/ks3_uploader.py +60 -29
  43. ksadk/builders/managed_runtime_builder.py +173 -0
  44. ksadk/builders/mcp_builder.py +91 -92
  45. ksadk/builders/requirements_utils.py +1 -5
  46. ksadk/cli/__init__.py +97 -171
  47. ksadk/cli/cmd_a2a.py +255 -84
  48. ksadk/cli/cmd_agent.py +19 -5
  49. ksadk/cli/cmd_build.py +158 -29
  50. ksadk/cli/cmd_completion.py +47 -35
  51. ksadk/cli/cmd_config.py +267 -198
  52. ksadk/cli/cmd_create.py +507 -740
  53. ksadk/cli/cmd_dashboard.py +120 -45
  54. ksadk/cli/cmd_deploy.py +135 -59
  55. ksadk/cli/cmd_destroy.py +44 -14
  56. ksadk/cli/cmd_eval.py +386 -0
  57. ksadk/cli/cmd_files.py +63 -24
  58. ksadk/cli/cmd_hermes.py +313 -135
  59. ksadk/cli/cmd_invoke.py +184 -122
  60. ksadk/cli/cmd_launch.py +76 -28
  61. ksadk/cli/cmd_mcp.py +98 -64
  62. ksadk/cli/cmd_model.py +14 -5
  63. ksadk/cli/cmd_openclaw.py +564 -268
  64. ksadk/cli/cmd_replay.py +71 -0
  65. ksadk/cli/cmd_run.py +58 -73
  66. ksadk/cli/cmd_status.py +80 -31
  67. ksadk/cli/cmd_studio.py +110 -0
  68. ksadk/cli/cmd_version.py +20 -13
  69. ksadk/cli/cmd_web.py +95 -18
  70. ksadk/cli/deploy_utils.py +20 -20
  71. ksadk/cli/dry_run.py +7 -4
  72. ksadk/cli/env_options.py +83 -0
  73. ksadk/cli/error_utils.py +37 -12
  74. ksadk/cli/global_options.py +4 -3
  75. ksadk/cli/local_runtime.py +1 -2
  76. ksadk/cli/model_catalog.py +6 -2
  77. ksadk/cli/network_options.py +41 -19
  78. ksadk/cli/resource_common.py +6 -2
  79. ksadk/cli/runtime_bootstrap.py +51 -0
  80. ksadk/cli/storage.py +0 -1
  81. ksadk/cli/ui.py +11 -3
  82. ksadk/cli/workflow_common.py +52 -18
  83. ksadk/codex/__init__.py +12 -0
  84. ksadk/codex/client.py +1042 -0
  85. ksadk/codex/phase.py +143 -0
  86. ksadk/codex/runtime.py +940 -0
  87. ksadk/common/aicp_env.py +0 -1
  88. ksadk/common/auth.py +2 -2
  89. ksadk/common/constants.py +1 -3
  90. ksadk/common/kop_client.py +232 -0
  91. ksadk/common/llm_utils.py +17 -13
  92. ksadk/compat/__init__.py +5 -0
  93. ksadk/compat/adk_compat.py +200 -0
  94. ksadk/compat/copilotkit_a2ui.py +330 -0
  95. ksadk/configs/__init__.py +12 -14
  96. ksadk/configs/env_registry.py +661 -116
  97. ksadk/configs/global_config.py +3 -2
  98. ksadk/configs/settings.py +133 -209
  99. ksadk/conversations/__init__.py +85 -92
  100. ksadk/conversations/attachment_storage.py +16 -11
  101. ksadk/conversations/attachments.py +44 -36
  102. ksadk/conversations/compaction_pipeline.py +12 -12
  103. ksadk/conversations/compaction_prompt.py +9 -5
  104. ksadk/conversations/context.py +221 -6
  105. ksadk/conversations/message_projection.py +438 -51
  106. ksadk/conversations/model_context.py +12 -6
  107. ksadk/conversations/model_options.py +0 -1
  108. ksadk/conversations/normalize.py +20 -16
  109. ksadk/conversations/reasoning_markup.py +1 -1
  110. ksadk/conversations/runtime.py +339 -5255
  111. ksadk/conversations/runtime_compaction.py +333 -0
  112. ksadk/conversations/runtime_constants.py +27 -0
  113. ksadk/conversations/runtime_governance.py +185 -0
  114. ksadk/conversations/runtime_input.py +660 -0
  115. ksadk/conversations/runtime_invocation.py +395 -0
  116. ksadk/conversations/runtime_metadata.py +475 -0
  117. ksadk/conversations/runtime_observability.py +367 -0
  118. ksadk/conversations/runtime_payloads.py +272 -0
  119. ksadk/conversations/runtime_persistence.py +440 -0
  120. ksadk/conversations/runtime_preparation.py +426 -0
  121. ksadk/conversations/runtime_resume.py +836 -0
  122. ksadk/conversations/runtime_stream_events.py +902 -0
  123. ksadk/conversations/runtime_streaming.py +700 -0
  124. ksadk/conversations/semantic_summary.py +17 -5
  125. ksadk/conversations/session_title.py +6 -3
  126. ksadk/deployment/__init__.py +15 -16
  127. ksadk/deployment/agent_access.py +12 -23
  128. ksadk/deployment/base.py +51 -38
  129. ksadk/deployment/managed_runtime.py +33 -0
  130. ksadk/deployment/manager.py +85 -71
  131. ksadk/deployment/providers/serverless.py +379 -210
  132. ksadk/deployment/registry.py +38 -32
  133. ksadk/deployment/state.py +11 -11
  134. ksadk/deployment/ui_config.py +1 -1
  135. ksadk/detection/__init__.py +1 -1
  136. ksadk/detection/detector.py +146 -83
  137. ksadk/detection/mcp_detector.py +9 -6
  138. ksadk/evaluation/__init__.py +84 -0
  139. ksadk/evaluation/a2a_adapter.py +444 -0
  140. ksadk/evaluation/adapters.py +54 -0
  141. ksadk/evaluation/contracts.py +409 -0
  142. ksadk/evaluation/evalset.py +351 -0
  143. ksadk/evaluation/evaluators.py +461 -0
  144. ksadk/evaluation/executor.py +118 -0
  145. ksadk/evaluation/storage.py +68 -0
  146. ksadk/evaluation/target.py +73 -0
  147. ksadk/events/__init__.py +25 -0
  148. ksadk/events/parser.py +191 -0
  149. ksadk/events/replay.py +37 -0
  150. ksadk/events/runtime_event.py +273 -0
  151. ksadk/events/store.py +317 -0
  152. ksadk/harness/__init__.py +35 -0
  153. ksadk/harness/app.py +269 -0
  154. ksadk/harness/config.py +181 -0
  155. ksadk/harness/reasoner.py +105 -0
  156. ksadk/harness/runtime.py +364 -0
  157. ksadk/harness/sandbox.py +141 -0
  158. ksadk/harness/tools.py +222 -0
  159. ksadk/hermes_terminal.py +28 -12
  160. ksadk/identity/resolver.py +15 -9
  161. ksadk/ids.py +32 -0
  162. ksadk/knowledge_base/adk_tool.py +1 -1
  163. ksadk/knowledge_base/client.py +38 -33
  164. ksadk/managed_a2a_card.py +160 -0
  165. ksadk/managed_runtime.py +195 -0
  166. ksadk/markdown.py +5 -2
  167. ksadk/mcp_runtime/__init__.py +9 -6
  168. ksadk/memory/__init__.py +1 -1
  169. ksadk/memory/adk/__init__.py +2 -0
  170. ksadk/memory/adk/backends/base_ltm_backend.py +2 -6
  171. ksadk/memory/adk/backends/http_ltm_backend.py +13 -21
  172. ksadk/memory/adk/backends/inmemory_ltm_backend.py +7 -13
  173. ksadk/memory/adk/backends/sdk_ltm_backend.py +36 -36
  174. ksadk/memory/adk/long_term_memory.py +34 -37
  175. ksadk/memory/adk/resilient_session_service.py +4 -5
  176. ksadk/memory/adk/short_term_memory.py +12 -21
  177. ksadk/memory/adk_tool.py +3 -2
  178. ksadk/memory/backends/base.py +19 -19
  179. ksadk/memory/backends/memory.py +24 -25
  180. ksadk/memory/backends/redis.py +31 -30
  181. ksadk/memory/langchain_tool.py +2 -1
  182. ksadk/memory/manager.py +52 -47
  183. ksadk/memory/service.py +16 -6
  184. ksadk/memory/tool.py +4 -0
  185. ksadk/model_policy.py +15 -7
  186. ksadk/model_proxy/__init__.py +51 -0
  187. ksadk/model_proxy/bootstrap.py +80 -0
  188. ksadk/model_proxy/cache.py +165 -0
  189. ksadk/model_proxy/config.py +66 -0
  190. ksadk/model_proxy/demo_webui.py +115 -0
  191. ksadk/model_proxy/detect.py +145 -0
  192. ksadk/model_proxy/gate.py +68 -0
  193. ksadk/model_proxy/namespace.py +201 -0
  194. ksadk/model_proxy/protocol_proxy.py +22 -0
  195. ksadk/model_proxy/server.py +443 -0
  196. ksadk/model_proxy/transform.py +735 -0
  197. ksadk/openclaw_gateway.py +29 -14
  198. ksadk/runners/adk_runner.py +354 -146
  199. ksadk/runners/base_runner.py +71 -29
  200. ksadk/runners/deepagents_runner.py +0 -1
  201. ksadk/runners/factory.py +18 -5
  202. ksadk/runners/langchain_runner.py +29 -742
  203. ksadk/runners/langgraph_runner.py +417 -97
  204. ksadk/runners/patch_langchain.py +16 -16
  205. ksadk/runners/remote_runner.py +134 -33
  206. ksadk/runners/usage_accumulator.py +1 -0
  207. ksadk/runners/utils/__init__.py +0 -10
  208. ksadk/runners/utils/loader.py +23 -7
  209. ksadk/runtime/__init__.py +67 -0
  210. ksadk/runtime/adapter.py +412 -0
  211. ksadk/runtime/conversation_execution.py +553 -0
  212. ksadk/runtime/executor.py +251 -0
  213. ksadk/runtime/factory.py +156 -0
  214. ksadk/runtime/framework_adapters.py +118 -0
  215. ksadk/runtime/launch.py +48 -0
  216. ksadk/runtime/preprocessing.py +131 -0
  217. ksadk/runtime/runner_adapter.py +1032 -0
  218. ksadk/runtime/runner_loading.py +36 -0
  219. ksadk/runtime_context.py +18 -3
  220. ksadk/runtime_state.py +0 -1
  221. ksadk/sandbox/__init__.py +4 -1
  222. ksadk/sandbox/backends/__init__.py +4 -1
  223. ksadk/sandbox/backends/e2b.py +13 -5
  224. ksadk/sandbox/backends/local_process.py +11 -3
  225. ksadk/sandbox/base.py +10 -15
  226. ksadk/sandbox/factory.py +14 -4
  227. ksadk/sandbox/registry.py +57 -6
  228. ksadk/server/__init__.py +39 -4
  229. ksadk/server/api_models.py +15 -5
  230. ksadk/server/app.py +24 -3928
  231. ksadk/server/composition.py +47 -0
  232. ksadk/server/factory.py +622 -0
  233. ksadk/server/routes/__init__.py +1 -0
  234. ksadk/server/routes/checkpoint_resolution.py +116 -0
  235. ksadk/server/routes/common.py +682 -0
  236. ksadk/server/routes/control.py +331 -0
  237. ksadk/server/routes/dependencies.py +98 -0
  238. ksadk/server/routes/misc.py +425 -0
  239. ksadk/server/routes/models.py +451 -0
  240. ksadk/server/routes/openai_compat.py +229 -0
  241. ksadk/server/routes/projection.py +848 -0
  242. ksadk/server/routes/routers.py +52 -0
  243. ksadk/server/routes/run.py +519 -0
  244. ksadk/server/routes/sessions.py +644 -0
  245. ksadk/server/routes/streaming.py +265 -0
  246. ksadk/server/routes/workspace.py +414 -0
  247. ksadk/server/static/assets/{ArtifactsPanel-D-SSn2ee.js → ArtifactsPanel-CakCqxEN.js} +1 -1
  248. ksadk/server/static/assets/CodeBlock-CynLknyO.js +9 -0
  249. ksadk/server/static/assets/MathMessageMarkdown-3sQdOysS.js +8 -0
  250. ksadk/server/static/assets/MermaidBlock-NERD_ef7.js +303 -0
  251. ksadk/server/static/assets/NativeTerminalPanel-CiHWf82q.js +1 -0
  252. ksadk/server/static/assets/{arc-BA13sHc9.js → arc-DrgjcKhl.js} +1 -1
  253. ksadk/server/static/assets/architecture-7EHR7CIX-CpZLRCcY.js +1 -0
  254. ksadk/server/static/assets/{architectureDiagram-3BPJPVTR-DzJzgVoL.js → architectureDiagram-3BPJPVTR-BXoVRbQ6.js} +1 -1
  255. ksadk/server/static/assets/{blockDiagram-GPEHLZMM-UTdVcZhn.js → blockDiagram-GPEHLZMM-BnaB3Ywv.js} +1 -1
  256. ksadk/server/static/assets/{c4Diagram-AAUBKEIU-Dix0GD4m.js → c4Diagram-AAUBKEIU-Dw4weZcp.js} +1 -1
  257. ksadk/server/static/assets/channel-Dt3rQTFI.js +1 -0
  258. ksadk/server/static/assets/{chunk-2J33WTMH-C2JxuZ42.js → chunk-2J33WTMH-ChjpLK7j.js} +1 -1
  259. ksadk/server/static/assets/{chunk-4BX2VUAB-Be57QPgD.js → chunk-4BX2VUAB-B9drTgOW.js} +1 -1
  260. ksadk/server/static/assets/{chunk-55IACEB6-WjSuy4nv.js → chunk-55IACEB6-CK4ROATy.js} +1 -1
  261. ksadk/server/static/assets/{chunk-727SXJPM-BOovP-b5.js → chunk-727SXJPM-B9KtTTgw.js} +1 -1
  262. ksadk/server/static/assets/{chunk-AQP2D5EJ-DOmZNmWj.js → chunk-AQP2D5EJ-1O1mki3m.js} +1 -1
  263. ksadk/server/static/assets/{chunk-FMBD7UC4-DzwlodkU.js → chunk-FMBD7UC4-B_fH2YuH.js} +1 -1
  264. ksadk/server/static/assets/{chunk-ND2GUHAM-DMqAikEU.js → chunk-ND2GUHAM-D6rFLaVV.js} +1 -1
  265. ksadk/server/static/assets/chunk-QZHKN3VN-iXEZtKQc.js +1 -0
  266. ksadk/server/static/assets/classDiagram-4FO5ZUOK-DIveV17Q.js +1 -0
  267. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-DIveV17Q.js +1 -0
  268. ksadk/server/static/assets/{cose-bilkent-S5V4N54A-Zhy2GBnP.js → cose-bilkent-S5V4N54A-ChNs_Mdk.js} +1 -1
  269. ksadk/server/static/assets/{dagre-BM42HDAG-CbKrtIUz.js → dagre-BM42HDAG-D0QXg_MQ.js} +1 -1
  270. ksadk/server/static/assets/{diagram-2AECGRRQ-BRApgQRK.js → diagram-2AECGRRQ-DBDDKwBQ.js} +1 -1
  271. ksadk/server/static/assets/{diagram-5GNKFQAL-D0xbnDiK.js → diagram-5GNKFQAL-BkwDOxmO.js} +1 -1
  272. ksadk/server/static/assets/{diagram-KO2AKTUF-CVugVI9L.js → diagram-KO2AKTUF-Bv8waYvY.js} +1 -1
  273. ksadk/server/static/assets/{diagram-LMA3HP47-DozUGgee.js → diagram-LMA3HP47-e1VECDdu.js} +1 -1
  274. ksadk/server/static/assets/{diagram-OG6HWLK6-ZOYsKlru.js → diagram-OG6HWLK6-Ds9adfRl.js} +1 -1
  275. ksadk/server/static/assets/{dist-DpKtt4Bo.js → dist-4N2NMVYg.js} +1 -1
  276. ksadk/server/static/assets/{dist-CkEKJFtk.js → dist-B-jNR6Vn.js} +1 -1
  277. ksadk/server/static/assets/{dist-D57wnGWd.js → dist-B5J2uhNk.js} +1 -1
  278. ksadk/server/static/assets/{dist-8_SfK1IP.js → dist-B5oS1zcj.js} +1 -1
  279. ksadk/server/static/assets/{dist-DMezvBGJ.js → dist-BB3-OwTQ.js} +1 -1
  280. ksadk/server/static/assets/{dist-2hInLS_J.js → dist-BIlKoW0k.js} +1 -1
  281. ksadk/server/static/assets/{dist-BrCcQ47T.js → dist-BjOdlDjj.js} +1 -1
  282. ksadk/server/static/assets/{dist-BmlMPsCc.js → dist-C1sz5Eo3.js} +1 -1
  283. ksadk/server/static/assets/{dist-B0NP_Z0z.js → dist-CWV4lDr1.js} +1 -1
  284. ksadk/server/static/assets/{dist-D-xOD048.js → dist-CmZHzlwZ.js} +1 -1
  285. ksadk/server/static/assets/{dist-DL_NKc_z.js → dist-D6MpCZtz.js} +1 -1
  286. ksadk/server/static/assets/{dist-DyOkZmze.js → dist-DJN_6Y39.js} +1 -1
  287. ksadk/server/static/assets/{dist-DmFaq_Iy.js → dist-DQU9EWw-.js} +1 -1
  288. ksadk/server/static/assets/{dist-gX6GOLKq.js → dist-DWKByxWV.js} +1 -1
  289. ksadk/server/static/assets/{dist-DyWsb78Q.js → dist-Dli7qW-B.js} +1 -1
  290. ksadk/server/static/assets/{dist-BIJVDMuB.js → dist-DsHPdk3W.js} +1 -1
  291. ksadk/server/static/assets/{dist-Dp0k7bWI.js → dist-X4TnqwOK.js} +1 -1
  292. ksadk/server/static/assets/{dist-8EEyGgDx.js → dist-_pHPTyZ-.js} +1 -1
  293. ksadk/server/static/assets/{dist-CPBtqRTW.js → dist-fhOuWds5.js} +1 -1
  294. ksadk/server/static/assets/{dist-D-JMdWPQ.js → dist-gLnrGsoU.js} +1 -1
  295. ksadk/server/static/assets/{dist-BssVmjIo.js → dist-j6Dxmf_j.js} +1 -1
  296. ksadk/server/static/assets/{dist-C3HIIaN5.js → dist-o2OVmKvt.js} +1 -1
  297. ksadk/server/static/assets/{erDiagram-TEJ5UH35-Bk-9bgE5.js → erDiagram-TEJ5UH35-BPItFKH6.js} +1 -1
  298. ksadk/server/static/assets/eventmodeling-FCH6USID-DYFSxbqr.js +1 -0
  299. ksadk/server/static/assets/{flowDiagram-I6XJVG4X-Dn9kZVuZ.js → flowDiagram-I6XJVG4X-BbYB9w5Y.js} +1 -1
  300. ksadk/server/static/assets/{ganttDiagram-6RSMTGT7-BGxXlJqm.js → ganttDiagram-6RSMTGT7-TQXa0KXv.js} +1 -1
  301. ksadk/server/static/assets/gitGraph-WXDBUCRP-DyNYp6BN.js +1 -0
  302. ksadk/server/static/assets/{gitGraphDiagram-PVQCEYII-C7Q0X-wi.js → gitGraphDiagram-PVQCEYII-bKblCnpa.js} +1 -1
  303. ksadk/server/static/assets/index-R52pJJvg.js +177 -0
  304. ksadk/server/static/assets/index-jJr3_955.css +2 -0
  305. ksadk/server/static/assets/info-J43DQDTF-WUx7qTde.js +1 -0
  306. ksadk/server/static/assets/{infoDiagram-5YYISTIA-BO_CSqq5.js → infoDiagram-5YYISTIA-BGVT7J_i.js} +1 -1
  307. ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-eYLPLd46.js → ishikawaDiagram-YF4QCWOH-K0OOfVna.js} +1 -1
  308. ksadk/server/static/assets/{journeyDiagram-JHISSGLW-De9geSb0.js → journeyDiagram-JHISSGLW-8LeOJaZp.js} +1 -1
  309. ksadk/server/static/assets/{kanban-definition-UN3LZRKU-Cs80kQto.js → kanban-definition-UN3LZRKU-C6uCgZrW.js} +1 -1
  310. ksadk/server/static/assets/{katex-DSrCFD8n.js → katex-CYWseY5E.js} +1 -1
  311. ksadk/server/static/assets/{linear-CN9sbKTF.js → linear-hdBcYzM8.js} +1 -1
  312. ksadk/server/static/assets/{mermaid-parser.core-DlgSBU8l.js → mermaid-parser.core-BWrDjSZM.js} +2 -2
  313. ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-9afz7Oug.js → mindmap-definition-RKZ34NQL-Bjy_2v4_.js} +1 -1
  314. ksadk/server/static/assets/packet-YPE3B663-BN13y_xl.js +1 -0
  315. ksadk/server/static/assets/pie-LRSECV5Y-ftSLXhhc.js +1 -0
  316. ksadk/server/static/assets/{pieDiagram-4H26LBE5-DPv_VstN.js → pieDiagram-4H26LBE5-GmmqNiVH.js} +1 -1
  317. ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-o77wZ1XK.js → quadrantDiagram-W4KKPZXB-BmkRbwCg.js} +1 -1
  318. ksadk/server/static/assets/radar-GUYGQ44K-jaICkIeO.js +1 -0
  319. ksadk/server/static/assets/{requirementDiagram-4Y6WPE33-BYVw81VC.js → requirementDiagram-4Y6WPE33-DSWFqL7c.js} +1 -1
  320. ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-B7nZFMKT.js → sankeyDiagram-5OEKKPKP-l6iXmU65.js} +1 -1
  321. ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-BGRRrFw5.js → sequenceDiagram-3UESZ5HK-Cq6UBeQB.js} +1 -1
  322. ksadk/server/static/assets/{stateDiagram-AJRCARHV-BUfHCoPs.js → stateDiagram-AJRCARHV-qzTQpg3Y.js} +1 -1
  323. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-BYCoYGAN.js +1 -0
  324. ksadk/server/static/assets/{timeline-definition-PNZ67QCA-FbCTGrf9.js → timeline-definition-PNZ67QCA-DEz24vV0.js} +1 -1
  325. ksadk/server/static/assets/treeView-BLDUP644-Cpib_vlz.js +1 -0
  326. ksadk/server/static/assets/treemap-LRROVOQU-DQxfmVHK.js +1 -0
  327. ksadk/server/static/assets/{vennDiagram-CIIHVFJN-DWgwtPsJ.js → vennDiagram-CIIHVFJN-CMCixki2.js} +1 -1
  328. ksadk/server/static/assets/wardley-L42UT6IY-BQYHnnkU.js +1 -0
  329. ksadk/server/static/assets/{wardleyDiagram-YWT4CUSO-2x8G-tTM.js → wardleyDiagram-YWT4CUSO-CVzB8F9K.js} +1 -1
  330. ksadk/server/static/assets/{xychartDiagram-2RQKCTM6-Drma3AYl.js → xychartDiagram-2RQKCTM6-BFBqBLc1.js} +1 -1
  331. ksadk/server/static/index.html +2 -2
  332. ksadk/server/terminal_sessions.py +111 -58
  333. ksadk/sessions/__init__.py +69 -19
  334. ksadk/sessions/base.py +31 -11
  335. ksadk/sessions/continuity.py +2 -3
  336. ksadk/sessions/in_memory.py +46 -2
  337. ksadk/sessions/local_service.py +148 -17
  338. ksadk/sessions/postgres_service.py +143 -25
  339. ksadk/sessions/resilient.py +36 -8
  340. ksadk/skills/models.py +17 -5
  341. ksadk/skills/package_store.py +5 -2
  342. ksadk/skills/runtime/agent.py +20 -5
  343. ksadk/skills/runtime/artifacts.py +1 -5
  344. ksadk/skills/runtime/backends/e2b.py +10 -4
  345. ksadk/skills/runtime/backends/local.py +15 -3
  346. ksadk/skills/runtime/base.py +1 -2
  347. ksadk/skills/runtime/executor.py +10 -3
  348. ksadk/skills/runtime/factory.py +4 -2
  349. ksadk/skills/runtime/loader.py +1 -2
  350. ksadk/skills/runtime/registry.py +15 -10
  351. ksadk/skills/runtime/request.py +3 -1
  352. ksadk/skills/service_client.py +47 -16
  353. ksadk/skills/tool_defs.py +15 -10
  354. ksadk/studio/__init__.py +5 -0
  355. ksadk/studio/agent_avatar_assets.py +120 -0
  356. ksadk/studio/agent_lifecycle.py +51 -0
  357. ksadk/studio/api.py +1046 -0
  358. ksadk/studio/api_catalog_routes.py +324 -0
  359. ksadk/studio/api_contracts.py +177 -0
  360. ksadk/studio/api_helpers.py +132 -0
  361. ksadk/studio/authoring.py +524 -0
  362. ksadk/studio/authoring_coordinator.py +285 -0
  363. ksadk/studio/builder.py +320 -0
  364. ksadk/studio/capabilities.py +351 -0
  365. ksadk/studio/cloud.py +278 -0
  366. ksadk/studio/codex_agent_service.py +583 -0
  367. ksadk/studio/codex_builder.py +324 -0
  368. ksadk/studio/codex_manifest.py +278 -0
  369. ksadk/studio/codex_run.py +352 -0
  370. ksadk/studio/compiler.py +189 -0
  371. ksadk/studio/contracts.py +644 -0
  372. ksadk/studio/errors.py +47 -0
  373. ksadk/studio/evaluation.py +192 -0
  374. ksadk/studio/event_store.py +284 -0
  375. ksadk/studio/framework_run.py +114 -0
  376. ksadk/studio/identifiers.py +33 -0
  377. ksadk/studio/mcp_runtime.py +229 -0
  378. ksadk/studio/model_client.py +543 -0
  379. ksadk/studio/model_profile_service.py +214 -0
  380. ksadk/studio/operations.py +243 -0
  381. ksadk/studio/otel_trace.py +754 -0
  382. ksadk/studio/pagination.py +182 -0
  383. ksadk/studio/python_tool_inspection.py +183 -0
  384. ksadk/studio/repository.py +306 -0
  385. ksadk/studio/resource_catalog.py +1217 -0
  386. ksadk/studio/run_service.py +707 -0
  387. ksadk/studio/runtime_catalog.py +74 -0
  388. ksadk/studio/runtime_source.py +339 -0
  389. ksadk/studio/service.py +1141 -0
  390. ksadk/studio/shared_web.py +994 -0
  391. ksadk/studio/skill_discovery.py +628 -0
  392. ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +3 -0
  393. ksadk/studio/static/assets/index-CaEN0eTR.css +1 -0
  394. ksadk/studio/static/assets/index-DI2eY_NH.js +252 -0
  395. ksadk/studio/static/assets/react-vendor-BNmTiJ-I.js +9 -0
  396. ksadk/studio/static/assets/rolldown-runtime-hePW80VL.js +1 -0
  397. ksadk/studio/static/index.html +15 -0
  398. ksadk/studio/templates.py +630 -0
  399. ksadk/studio/validator.py +246 -0
  400. ksadk/studio/workspace.py +137 -0
  401. ksadk/terminal_exec_policy.py +3 -4
  402. ksadk/tools/gateway.py +101 -15
  403. ksadk/tools/result_budget.py +12 -4
  404. ksadk/toolsets/__init__.py +161 -37
  405. ksadk/toolsets/a2a.py +426 -0
  406. ksadk/toolsets/platform.py +21 -5
  407. ksadk/toolsets/sandbox.py +82 -16
  408. ksadk/toolsets/skills.py +200 -39
  409. ksadk/toolsets/web.py +123 -26
  410. ksadk/toolsets/workspace.py +212 -101
  411. ksadk/tracing/__init__.py +19 -21
  412. ksadk/tracing/exporters/inmemory_exporter.py +62 -51
  413. ksadk/tracing/setup.py +135 -506
  414. ksadk/tracing/span_utils.py +0 -41
  415. ksadk/tui/loop.py +78 -30
  416. ksadk/tui/stream_render.py +1 -0
  417. ksadk/ui_config.py +5 -3
  418. ksadk/version.py +1 -1
  419. ksadk_runtime_common/memory_backend/registry.py +56 -56
  420. ksadk_runtime_common/schemas/runtime_event_v1.json +95 -0
  421. ksadk_runtime_common/workspace_files/router.py +8 -2
  422. agentengine_sdk_python-0.7.0.dist-info/RECORD +0 -482
  423. ksadk/a2a/card_builder.py +0 -64
  424. ksadk/a2a/client.py +0 -309
  425. ksadk/runners/utils/langfuse.py +0 -230
  426. ksadk/server/static/assets/CodeBlock-P16VOLYi.js +0 -9
  427. ksadk/server/static/assets/MathMessageMarkdown-CdGiQWHR.js +0 -8
  428. ksadk/server/static/assets/MermaidBlock-IOcal8cm.js +0 -303
  429. ksadk/server/static/assets/NativeTerminalPanel-DGSDWsei.js +0 -1
  430. ksadk/server/static/assets/architecture-7EHR7CIX-BvUlsA6l.js +0 -1
  431. ksadk/server/static/assets/channel-CTuydARX.js +0 -1
  432. ksadk/server/static/assets/chunk-QZHKN3VN-BnCydivS.js +0 -1
  433. ksadk/server/static/assets/classDiagram-4FO5ZUOK-BNHg45Be.js +0 -1
  434. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-BNHg45Be.js +0 -1
  435. ksadk/server/static/assets/eventmodeling-FCH6USID-DsAgWnlf.js +0 -1
  436. ksadk/server/static/assets/gitGraph-WXDBUCRP-BIao9Yjg.js +0 -1
  437. ksadk/server/static/assets/index-C00Lwg3L.js +0 -165
  438. ksadk/server/static/assets/index-DAURbCsP.css +0 -1
  439. ksadk/server/static/assets/info-J43DQDTF-CyipqGxU.js +0 -1
  440. ksadk/server/static/assets/packet-YPE3B663-CGkyRxlg.js +0 -1
  441. ksadk/server/static/assets/pie-LRSECV5Y-M0uWAf4n.js +0 -1
  442. ksadk/server/static/assets/radar-GUYGQ44K-Bdbm24-x.js +0 -1
  443. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-8xBN_gLM.js +0 -1
  444. ksadk/server/static/assets/treeView-BLDUP644-npMo0ak1.js +0 -1
  445. ksadk/server/static/assets/treemap-LRROVOQU-CHdzrrdo.js +0 -1
  446. ksadk/server/static/assets/wardley-L42UT6IY-BEQz7wyy.js +0 -1
  447. ksadk/tracing/exporters/langfuse_exporter.py +0 -445
  448. {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/entry_points.txt +0 -0
  449. {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/licenses/LICENSE +0 -0
  450. {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)