agentengine-sdk-python 0.8.0__py3-none-any.whl → 0.8.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (494) hide show
  1. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/METADATA +43 -7
  2. agentengine_sdk_python-0.8.2.dist-info/RECORD +778 -0
  3. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/WHEEL +1 -1
  4. ksadk/a2a/__init__.py +2 -0
  5. ksadk/a2a/_space_client_events.py +381 -0
  6. ksadk/a2a/bootstrap.py +0 -2
  7. ksadk/a2a/card.py +6 -111
  8. ksadk/a2a/control_plane.py +239 -1
  9. ksadk/a2a/event_adapter.py +156 -64
  10. ksadk/a2a/executor.py +195 -172
  11. ksadk/a2a/routes.py +12 -5
  12. ksadk/a2a/server.py +1 -8
  13. ksadk/a2a/service_env.py +33 -0
  14. ksadk/a2a/space_client.py +43 -349
  15. ksadk/a2a_card.py +101 -0
  16. ksadk/a2ui/core.py +222 -53
  17. ksadk/agui/_agent_helpers.py +243 -0
  18. ksadk/agui/a2ui_projection.py +12 -5
  19. ksadk/agui/agent.py +474 -275
  20. ksadk/agui/config.py +11 -5
  21. ksadk/agui/routes.py +7 -15
  22. ksadk/api/client.py +530 -40
  23. ksadk/api/environment.py +27 -0
  24. ksadk/builders/code_builder.py +352 -53
  25. ksadk/builders/container_builder.py +71 -44
  26. ksadk/builders/framework_requirements.py +2 -1
  27. ksadk/builders/ks3_uploader.py +2 -2
  28. ksadk/builders/managed_runtime_builder.py +60 -28
  29. ksadk/cli/__init__.py +25 -0
  30. ksadk/cli/cmd_a2a.py +18 -31
  31. ksadk/cli/cmd_create.py +13 -26
  32. ksadk/cli/cmd_dashboard.py +15 -1
  33. ksadk/cli/cmd_deploy.py +28 -3
  34. ksadk/cli/cmd_eval.py +443 -0
  35. ksadk/cli/cmd_evalset.py +284 -0
  36. ksadk/cli/cmd_files.py +5 -1
  37. ksadk/cli/cmd_hermes.py +137 -163
  38. ksadk/cli/cmd_invoke.py +28 -38
  39. ksadk/cli/cmd_launch.py +12 -0
  40. ksadk/cli/cmd_managed_runtime.py +95 -0
  41. ksadk/cli/cmd_observe.py +85 -0
  42. ksadk/cli/cmd_openclaw.py +85 -479
  43. ksadk/cli/cmd_replay.py +28 -4
  44. ksadk/cli/cmd_run.py +29 -67
  45. ksadk/cli/cmd_studio.py +201 -0
  46. ksadk/cli/cmd_web.py +15 -6
  47. ksadk/cli/dry_run.py +2 -1
  48. ksadk/cli/env_options.py +83 -0
  49. ksadk/cli/global_options.py +3 -1
  50. ksadk/cli/hermes_env.py +151 -0
  51. ksadk/cli/invoke_payload.py +49 -0
  52. ksadk/cli/network_options.py +7 -5
  53. ksadk/cli/openclaw_env.py +458 -0
  54. ksadk/cli/runtime_bootstrap.py +51 -0
  55. ksadk/cli/ui.py +6 -0
  56. ksadk/cli/workflow_common.py +5 -5
  57. ksadk/codex/__init__.py +3 -3
  58. ksadk/codex/client.py +833 -49
  59. ksadk/codex/phase.py +1 -1
  60. ksadk/codex/runtime.py +720 -127
  61. ksadk/common/kop_client.py +232 -0
  62. ksadk/configs/__init__.py +1 -3
  63. ksadk/configs/env_registry.py +244 -45
  64. ksadk/configs/env_registry_pcm.py +181 -0
  65. ksadk/configs/env_var_spec.py +18 -0
  66. ksadk/configs/global_config.py +6 -1
  67. ksadk/configs/settings.py +5 -108
  68. ksadk/context_engine/__init__.py +98 -0
  69. ksadk/context_engine/assembler.py +176 -0
  70. ksadk/context_engine/baseline.py +419 -0
  71. ksadk/context_engine/cache_observability.py +228 -0
  72. ksadk/context_engine/capabilities.py +443 -0
  73. ksadk/context_engine/contributors.py +344 -0
  74. ksadk/context_engine/hosted_pipeline.py +417 -0
  75. ksadk/context_engine/models.py +117 -0
  76. ksadk/context_engine/planner.py +698 -0
  77. ksadk/context_engine/policies.py +319 -0
  78. ksadk/context_engine/projection.py +31 -0
  79. ksadk/context_engine/shadow_plan.py +266 -0
  80. ksadk/context_engine/tokenizer.py +164 -0
  81. ksadk/conversations/__init__.py +85 -94
  82. ksadk/conversations/context.py +126 -6
  83. ksadk/conversations/message_projection.py +292 -28
  84. ksadk/conversations/model_context.py +49 -3
  85. ksadk/conversations/runtime_compaction.py +207 -5
  86. ksadk/conversations/runtime_input.py +193 -3
  87. ksadk/conversations/runtime_invocation.py +81 -3
  88. ksadk/conversations/runtime_observability.py +157 -4
  89. ksadk/conversations/runtime_payloads.py +42 -0
  90. ksadk/conversations/runtime_preparation.py +280 -2
  91. ksadk/conversations/runtime_resume.py +71 -32
  92. ksadk/conversations/runtime_stream_events.py +94 -9
  93. ksadk/conversations/runtime_streaming.py +175 -22
  94. ksadk/conversations/semantic_summary.py +311 -1
  95. ksadk/conversations/session_lock.py +51 -0
  96. ksadk/deployment/env_forward.py +72 -0
  97. ksadk/deployment/managed_runtime.py +38 -0
  98. ksadk/deployment/manager.py +24 -8
  99. ksadk/deployment/providers/serverless.py +228 -92
  100. ksadk/evaluation/__init__.py +116 -0
  101. ksadk/evaluation/a2a_adapter.py +452 -0
  102. ksadk/evaluation/adapters.py +67 -0
  103. ksadk/evaluation/agent_eval_client.py +395 -0
  104. ksadk/evaluation/cloud_binding.py +80 -0
  105. ksadk/evaluation/cloud_converter.py +194 -0
  106. ksadk/evaluation/cloud_service.py +199 -0
  107. ksadk/evaluation/contracts.py +478 -0
  108. ksadk/evaluation/evalset.py +351 -0
  109. ksadk/evaluation/evaluators.py +627 -0
  110. ksadk/evaluation/evidence.py +258 -0
  111. ksadk/evaluation/executor.py +143 -0
  112. ksadk/evaluation/local_adapter.py +550 -0
  113. ksadk/evaluation/service_env.py +27 -0
  114. ksadk/evaluation/storage.py +68 -0
  115. ksadk/evaluation/studio_build_adapter.py +253 -0
  116. ksadk/evaluation/target.py +82 -0
  117. ksadk/events/__init__.py +13 -13
  118. ksadk/events/_v1_compat/__init__.py +1 -0
  119. ksadk/events/_v1_compat/models.py +299 -0
  120. ksadk/events/_v1_compat/parser.py +247 -0
  121. ksadk/events/_v1_compat/projection.py +753 -0
  122. ksadk/events/adapters/__init__.py +34 -0
  123. ksadk/events/adapters/_a2a_snapshot.py +622 -0
  124. ksadk/events/adapters/_a2a_support.py +275 -0
  125. ksadk/events/adapters/_codex_interactions.py +375 -0
  126. ksadk/events/adapters/_codex_items.py +801 -0
  127. ksadk/events/adapters/_codex_validators.py +311 -0
  128. ksadk/events/adapters/_langgraph_support.py +785 -0
  129. ksadk/events/adapters/a2a.py +841 -0
  130. ksadk/events/adapters/adk.py +503 -0
  131. ksadk/events/adapters/codex.py +717 -0
  132. ksadk/events/adapters/langgraph.py +745 -0
  133. ksadk/events/canonical.py +440 -0
  134. ksadk/events/canonical_replay.py +497 -0
  135. ksadk/events/canonical_store.py +468 -0
  136. ksadk/events/cold_recovery.py +257 -0
  137. ksadk/events/content.py +89 -0
  138. ksadk/events/identity.py +86 -0
  139. ksadk/events/pipeline.py +455 -0
  140. ksadk/events/projections.py +66 -0
  141. ksadk/events/reducer.py +661 -0
  142. ksadk/events/replay.py +3 -36
  143. ksadk/events/runtime_event.py +20 -256
  144. ksadk/events/session_event.py +324 -0
  145. ksadk/events/store.py +3 -316
  146. ksadk/events/v1_compat.py +41 -0
  147. ksadk/harness/__init__.py +3 -2
  148. ksadk/harness/app.py +114 -123
  149. ksadk/harness/config.py +1 -1
  150. ksadk/harness/runtime.py +467 -0
  151. ksadk/hermes_terminal.py +5 -0
  152. ksadk/identity/resolver.py +4 -3
  153. ksadk/interaction/__init__.py +28 -0
  154. ksadk/interaction/contracts.py +136 -0
  155. ksadk/interaction/ledger.py +189 -0
  156. ksadk/interaction/provider.py +122 -0
  157. ksadk/interaction/providers/__init__.py +52 -0
  158. ksadk/interaction/providers/adk.py +40 -0
  159. ksadk/interaction/providers/codex.py +94 -0
  160. ksadk/interaction/providers/langgraph.py +58 -0
  161. ksadk/kernel/__init__.py +117 -0
  162. ksadk/kernel/authorization.py +258 -0
  163. ksadk/kernel/bootstrap.py +1074 -0
  164. ksadk/kernel/contract_fingerprints.py +61 -0
  165. ksadk/kernel/contracts.py +362 -0
  166. ksadk/kernel/control.py +219 -0
  167. ksadk/kernel/errors.py +79 -0
  168. ksadk/kernel/ingress.py +1102 -0
  169. ksadk/kernel/mapping.py +111 -0
  170. ksadk/kernel/memory_store.py +1133 -0
  171. ksadk/kernel/postgres_store.py +1756 -0
  172. ksadk/kernel/recovery.py +452 -0
  173. ksadk/kernel/runtime_identity.py +103 -0
  174. ksadk/kernel/sql/001_agent_kernel.sql +120 -0
  175. ksadk/kernel/sqlite_store.py +1410 -0
  176. ksadk/kernel/state.py +145 -0
  177. ksadk/kernel/store.py +232 -0
  178. ksadk/kernel/worker.py +953 -0
  179. ksadk/knowledge_base/client.py +22 -4
  180. ksadk/knowledge_base/service.py +32 -5
  181. ksadk/managed_a2a_card.py +160 -0
  182. ksadk/managed_runtime.py +16 -9
  183. ksadk/memory/__init__.py +61 -0
  184. ksadk/memory/adk/backends/base_ltm_backend.py +96 -1
  185. ksadk/memory/adk/backends/http_ltm_backend.py +14 -5
  186. ksadk/memory/adk/backends/inmemory_ltm_backend.py +136 -3
  187. ksadk/memory/adk/backends/sdk_ltm_backend.py +394 -5
  188. ksadk/memory/adk/backends/sqlite_ltm_backend.py +95 -0
  189. ksadk/memory/adk/long_term_memory.py +3 -2
  190. ksadk/memory/coordinator.py +461 -0
  191. ksadk/memory/events.py +202 -0
  192. ksadk/memory/extraction.py +228 -0
  193. ksadk/memory/ltm_backend_factory.py +16 -3
  194. ksadk/memory/models.py +316 -0
  195. ksadk/memory/policy.py +215 -0
  196. ksadk/memory/provider.py +47 -0
  197. ksadk/memory/provider_adapter.py +159 -0
  198. ksadk/memory/provider_resolver.py +71 -0
  199. ksadk/memory/providers/__init__.py +5 -0
  200. ksadk/memory/providers/local_sqlite.py +490 -0
  201. ksadk/memory/resolved_policy.py +145 -0
  202. ksadk/memory/service.py +198 -8
  203. ksadk/memory/tool.py +4 -0
  204. ksadk/model_proxy/bootstrap.py +16 -9
  205. ksadk/model_proxy/config.py +22 -1
  206. ksadk/model_proxy/detect.py +202 -9
  207. ksadk/model_proxy/namespace.py +67 -15
  208. ksadk/model_proxy/server.py +168 -6
  209. ksadk/model_proxy/transform.py +105 -3
  210. ksadk/observability/__init__.py +25 -0
  211. ksadk/observability/session_log.py +420 -0
  212. ksadk/observability/trajectory.py +105 -0
  213. ksadk/prompts/__init__.py +71 -0
  214. ksadk/prompts/compiler.py +215 -0
  215. ksadk/prompts/models.py +82 -0
  216. ksadk/prompts/projection.py +89 -0
  217. ksadk/prompts/resolved.py +137 -0
  218. ksadk/prompts/sources.py +233 -0
  219. ksadk/runners/_langgraph_runner_streams.py +822 -0
  220. ksadk/runners/adk_runner.py +14 -31
  221. ksadk/runners/base_runner.py +44 -3
  222. ksadk/runners/factory.py +3 -3
  223. ksadk/runners/langgraph_runner.py +252 -433
  224. ksadk/runners/utils/__init__.py +0 -10
  225. ksadk/runners/utils/loader.py +16 -0
  226. ksadk/runtime/__init__.py +67 -41
  227. ksadk/runtime/_runner_adapter/__init__.py +1 -0
  228. ksadk/runtime/_runner_adapter/stream_mapping.py +788 -0
  229. ksadk/runtime/adapter.py +199 -16
  230. ksadk/runtime/conversation_execution.py +687 -0
  231. ksadk/runtime/executor.py +392 -0
  232. ksadk/runtime/factory.py +342 -0
  233. ksadk/runtime/framework_adapters.py +6 -12
  234. ksadk/runtime/hosted_finalizer.py +268 -0
  235. ksadk/runtime/launch.py +61 -0
  236. ksadk/runtime/preprocessing.py +49 -14
  237. ksadk/runtime/runner_adapter.py +330 -391
  238. ksadk/runtime/runner_loading.py +36 -0
  239. ksadk/runtime/usage.py +33 -0
  240. ksadk/sandbox/backends/e2b.py +36 -3
  241. ksadk/server/__init__.py +39 -4
  242. ksadk/server/app.py +18 -492
  243. ksadk/server/composition.py +36 -2
  244. ksadk/server/factory.py +210 -149
  245. ksadk/server/routes/common.py +15 -46
  246. ksadk/server/routes/control.py +13 -9
  247. ksadk/server/routes/kernel_ingress.py +258 -0
  248. ksadk/server/routes/misc.py +10 -33
  249. ksadk/server/routes/models.py +14 -6
  250. ksadk/server/routes/openai_compat.py +129 -27
  251. ksadk/server/routes/projection.py +84 -16
  252. ksadk/server/routes/run.py +347 -460
  253. ksadk/server/routes/sessions.py +37 -40
  254. ksadk/server/routes/streaming.py +52 -7
  255. ksadk/server/routes/workspace.py +54 -21
  256. ksadk/server/static/assets/{ArtifactsPanel-Cjnqt-8W.js → ArtifactsPanel-DSU1sWD_.js} +1 -1
  257. ksadk/server/static/assets/{CodeBlock-BqyqZfuB.js → CodeBlock-DOH6MVcn.js} +4 -4
  258. ksadk/server/static/assets/{MathMessageMarkdown-BpH7jiFM.js → MathMessageMarkdown-BL8my1R2.js} +4 -4
  259. ksadk/server/static/assets/MathMessageMarkdown-BorAY7qD.css +1 -0
  260. ksadk/server/static/assets/MermaidBlock-Dz4IP-Tx.js +312 -0
  261. ksadk/server/static/assets/NativeTerminalPanel-DUrK0JpZ.js +1 -0
  262. ksadk/server/static/assets/abnfDiagram-VCTEODGH-g20pFzNV.js +1 -0
  263. ksadk/server/static/assets/arc-C4FzinUA.js +1 -0
  264. ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +1 -0
  265. ksadk/server/static/assets/architectureDiagram-5GKGNRK7-DpIqL5h4.js +36 -0
  266. ksadk/server/static/assets/blockDiagram-NRAW4CY4-BjkljTNz.js +129 -0
  267. ksadk/server/static/assets/c4Diagram-UCG6FXSJ-Dk_ieq2X.js +38 -0
  268. ksadk/server/static/assets/channel-4cQHKtx1.js +1 -0
  269. ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +1 -0
  270. ksadk/server/static/assets/{chunk-FMBD7UC4-BGhvzDYt.js → chunk-5VM5RSS4-BVHAchFb.js} +1 -1
  271. ksadk/server/static/assets/{chunk-ND2GUHAM-D98vPd-E.js → chunk-F27PBJKO-C-ipQzuS.js} +1 -1
  272. ksadk/server/static/assets/chunk-G27WJ6UU-C2EyJbIK.js +231 -0
  273. ksadk/server/static/assets/{chunk-4BX2VUAB-CsWIQRhc.js → chunk-JWPE2WC7-vYvVJb_M.js} +1 -1
  274. ksadk/server/static/assets/chunk-LCL6LL3I-BcI6ysPT.js +206 -0
  275. ksadk/server/static/assets/chunk-POPQ4Y6H-C030x_Z1.js +1 -0
  276. ksadk/server/static/assets/chunk-RHFEMEQ7-yyGpsz4n.js +168 -0
  277. ksadk/server/static/assets/chunk-SVP7TREG-BLTlmMU7.js +88 -0
  278. ksadk/server/static/assets/chunk-XXDRQBXY-C_32ArgP.js +1 -0
  279. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +1 -0
  280. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +1 -0
  281. ksadk/server/static/assets/{cose-bilkent-S5V4N54A-CX3yotHq.js → cose-bilkent-JH36ORCC-9YQYwdl0.js} +1 -1
  282. ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +1 -0
  283. ksadk/server/static/assets/cynefinDiagram-5FMLGOSQ-ErLF5N13.js +62 -0
  284. ksadk/server/static/assets/cytoscape.esm-CyCl8rPi.js +321 -0
  285. ksadk/server/static/assets/dagre-3AP2YEHR-DFiQF-6f.js +4 -0
  286. ksadk/server/static/assets/dagre-BuhGnRI1.js +1 -0
  287. ksadk/server/static/assets/diagram-S7CK7UJ4-Bt1v8-GC.js +30 -0
  288. ksadk/server/static/assets/diagram-UQ7AKVKN-DSCxJdBK.js +41 -0
  289. ksadk/server/static/assets/diagram-VSXAHHWV-DHfYwp_9.js +3 -0
  290. ksadk/server/static/assets/diagram-VX7I27RA-DdzD-4Le.js +24 -0
  291. ksadk/server/static/assets/diagram-Z3DM3KII-Du-nAJ9F.js +24 -0
  292. ksadk/server/static/assets/{dist-D82mtD2s.js → dist-8hqcx0sU.js} +1 -1
  293. ksadk/server/static/assets/dist-B7seoj3d.js +1 -0
  294. ksadk/server/static/assets/{dist-DHn6nBzr.js → dist-BE7bu-DL.js} +1 -1
  295. ksadk/server/static/assets/{dist-eoW6tZXA.js → dist-BVDffZ0U.js} +1 -1
  296. ksadk/server/static/assets/{dist-C2xjE7cO.js → dist-BX8z72IC.js} +1 -1
  297. ksadk/server/static/assets/{dist-CgF3fj15.js → dist-Bf2M8m3N.js} +1 -1
  298. ksadk/server/static/assets/{dist-Bs-jQcRX.js → dist-BjU6y-A2.js} +1 -1
  299. ksadk/server/static/assets/{dist-Djgl2wVZ.js → dist-BytlxIDT.js} +1 -1
  300. ksadk/server/static/assets/{dist-CVQRZjPu.js → dist-CSh_DE14.js} +1 -1
  301. ksadk/server/static/assets/{dist-6CVKuTAe.js → dist-CXYYBw3_.js} +1 -1
  302. ksadk/server/static/assets/{dist-DlzCRy0S.js → dist-C_wsv-Qd.js} +1 -1
  303. ksadk/server/static/assets/{dist-DF06ORQo.js → dist-ClxGviKw.js} +1 -1
  304. ksadk/server/static/assets/{dist-Cf23NY4L.js → dist-CttYUqzS.js} +1 -1
  305. ksadk/server/static/assets/{dist-ivOya1LD.js → dist-DNp6rLRA.js} +1 -1
  306. ksadk/server/static/assets/{dist-BibcG1V9.js → dist-DUX-id_F.js} +1 -1
  307. ksadk/server/static/assets/{dist-BUHRxvDG.js → dist-DmldrHd_.js} +1 -1
  308. ksadk/server/static/assets/{dist-BZ9n9AjO.js → dist-DnfXs8Vn.js} +2 -2
  309. ksadk/server/static/assets/{dist-DqNnNFrw.js → dist-Dq9gLD7L.js} +1 -1
  310. ksadk/server/static/assets/{dist-CGcp2ex8.js → dist-W-TIVntx.js} +1 -1
  311. ksadk/server/static/assets/{dist-Cwt7koer.js → dist-dDdADKFA.js} +1 -1
  312. ksadk/server/static/assets/{dist-C7-3jInC.js → dist-dydu68Fl.js} +1 -1
  313. ksadk/server/static/assets/{dist-C99THtIA.js → dist-ngtN0DJB.js} +1 -1
  314. ksadk/server/static/assets/ebnfDiagram-PWID7BFC-Da9C9NO1.js +1 -0
  315. ksadk/server/static/assets/erDiagram-SSCWMZ5O-EAGqqR79.js +99 -0
  316. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +1 -0
  317. ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +1 -0
  318. ksadk/server/static/assets/ganttDiagram-EL5Y4UJY-DqJsKb59.js +292 -0
  319. ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +1 -0
  320. ksadk/server/static/assets/gitGraphDiagram-WWUBYQGX-B6g4dtDi.js +106 -0
  321. ksadk/server/static/assets/index-8ipRcQ-M.js +240 -0
  322. ksadk/server/static/assets/index-ByFCqjlh.css +2 -0
  323. ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +1 -0
  324. ksadk/server/static/assets/infoDiagram-RXCK75RN-Djm_nbd5.js +2 -0
  325. ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-DLOHGvWw.js → ishikawaDiagram-5VMMS53U-DWya9SG2.js} +6 -6
  326. ksadk/server/static/assets/{journeyDiagram-JHISSGLW-BIzKeejT.js → journeyDiagram-EYS64GPL-DutxhSNI.js} +6 -6
  327. ksadk/server/static/assets/{kanban-definition-UN3LZRKU-BWHoFSw2.js → kanban-definition-3QL26DDD-DJdOF8Fm.js} +10 -10
  328. ksadk/server/static/assets/katex-vFWytM5c.js +257 -0
  329. ksadk/server/static/assets/{linear-CvMBw9cA.js → linear-BHjG8b-J.js} +1 -1
  330. ksadk/server/static/assets/mermaid-parser.core-KGSy4jWT.js +166 -0
  331. ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-CSAQxEAe.js → mindmap-definition-FBJOCRG2-9-HC88D4.js} +29 -29
  332. ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +1 -0
  333. ksadk/server/static/assets/pegDiagram-XKGWAZYB-DrEZd4fD.js +1 -0
  334. ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +1 -0
  335. ksadk/server/static/assets/pieDiagram-E7YTZNPT-DpjqOFFp.js +39 -0
  336. ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-CxIR4JXI.js → quadrantDiagram-AXDQQJYC-BiNhgOXX.js} +6 -6
  337. ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +1 -0
  338. ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +1 -0
  339. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +1 -0
  340. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +1 -0
  341. ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +1 -0
  342. ksadk/server/static/assets/railroadDiagram-O6MQD6OU-_Y3Ojg6G.js +1 -0
  343. ksadk/server/static/assets/requirementDiagram-EFPCY7ZU-DeCFdg9K.js +84 -0
  344. ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-DpGa6EAV.js → sankeyDiagram-P5KCCOFB-CPH75rhw.js} +7 -7
  345. ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-NTKNGGe5.js → sequenceDiagram-WJ2MYXX4-OEpQQdr4.js} +10 -10
  346. ksadk/server/static/assets/sizeCapture-X5ZJPWSS-heUErzhI.js +1 -0
  347. ksadk/server/static/assets/stateDiagram-HBIQ2CUA-DvA3jSMB.js +1 -0
  348. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +1 -0
  349. ksadk/server/static/assets/swimlanes-XN3QIQJK-CR-dfN_t.js +1 -0
  350. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +8 -0
  351. ksadk/server/static/assets/{timeline-definition-PNZ67QCA-jqoYs3jQ.js → timeline-definition-24CTP7MA-DAVJf1TX.js} +7 -7
  352. ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +1 -0
  353. ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +1 -0
  354. ksadk/server/static/assets/vennDiagram-4TSXK5OY-DwM8eDkh.js +34 -0
  355. ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +1 -0
  356. ksadk/server/static/assets/wardleyDiagram-VM6X3IG4-DZL-Zz2t.js +78 -0
  357. ksadk/server/static/assets/xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js +7 -0
  358. ksadk/server/static/index.html +2 -2
  359. ksadk/sessions/__init__.py +26 -9
  360. ksadk/sessions/_local_service_sync.py +688 -0
  361. ksadk/sessions/_local_tables.py +21 -0
  362. ksadk/sessions/_postgres_schema.py +305 -0
  363. ksadk/sessions/_postgres_tables.py +15 -0
  364. ksadk/sessions/base.py +80 -1
  365. ksadk/sessions/in_memory.py +53 -2
  366. ksadk/sessions/local_service.py +63 -604
  367. ksadk/sessions/postgres_service.py +58 -148
  368. ksadk/sessions/resilient.py +35 -1
  369. ksadk/skills/runtime/backends/e2b.py +1 -1
  370. ksadk/skills/service_client.py +13 -3
  371. ksadk/studio/__init__.py +5 -0
  372. ksadk/studio/agent_avatar_assets.py +120 -0
  373. ksadk/studio/agent_lifecycle.py +51 -0
  374. ksadk/studio/api.py +1598 -0
  375. ksadk/studio/api_catalog_routes.py +324 -0
  376. ksadk/studio/api_contracts.py +286 -0
  377. ksadk/studio/api_helpers.py +132 -0
  378. ksadk/studio/api_memory_routes.py +82 -0
  379. ksadk/studio/authoring.py +749 -0
  380. ksadk/studio/authoring_coordinator.py +1017 -0
  381. ksadk/studio/builder.py +397 -0
  382. ksadk/studio/capabilities.py +368 -0
  383. ksadk/studio/cloud.py +2099 -0
  384. ksadk/studio/codex_agent_service.py +694 -0
  385. ksadk/studio/codex_authoring.py +469 -0
  386. ksadk/studio/codex_builder.py +437 -0
  387. ksadk/studio/codex_manifest.py +307 -0
  388. ksadk/studio/codex_run.py +395 -0
  389. ksadk/studio/compiler.py +190 -0
  390. ksadk/studio/contracts.py +755 -0
  391. ksadk/studio/errors.py +47 -0
  392. ksadk/studio/event_store.py +214 -0
  393. ksadk/studio/framework_run.py +342 -0
  394. ksadk/studio/hosted_kernel.py +317 -0
  395. ksadk/studio/identifiers.py +33 -0
  396. ksadk/studio/manifest_resolver.py +131 -0
  397. ksadk/studio/mcp_runtime.py +229 -0
  398. ksadk/studio/model_client.py +629 -0
  399. ksadk/studio/model_profile_service.py +214 -0
  400. ksadk/studio/operations.py +275 -0
  401. ksadk/studio/otel_trace.py +1060 -0
  402. ksadk/studio/pagination.py +182 -0
  403. ksadk/studio/pcm_memory.py +85 -0
  404. ksadk/studio/python_tool_inspection.py +183 -0
  405. ksadk/studio/repository.py +327 -0
  406. ksadk/studio/resource_catalog.py +1260 -0
  407. ksadk/studio/run_service.py +1133 -0
  408. ksadk/studio/runtime_catalog.py +74 -0
  409. ksadk/studio/runtime_source.py +365 -0
  410. ksadk/studio/service.py +2249 -0
  411. ksadk/studio/shared_web.py +1004 -0
  412. ksadk/studio/skill_discovery.py +628 -0
  413. ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +3 -0
  414. ksadk/studio/static/assets/index-BLpcdrgW.css +1 -0
  415. ksadk/studio/static/assets/index-CYekR2Xv.js +260 -0
  416. ksadk/studio/static/assets/react-vendor-BNmTiJ-I.js +9 -0
  417. ksadk/studio/static/assets/rolldown-runtime-hePW80VL.js +1 -0
  418. ksadk/studio/static/index.html +15 -0
  419. ksadk/studio/templates.py +722 -0
  420. ksadk/studio/validator.py +265 -0
  421. ksadk/studio/workspace.py +135 -0
  422. ksadk/tools/gateway.py +9 -0
  423. ksadk/toolsets/__init__.py +27 -1
  424. ksadk/toolsets/a2a.py +426 -0
  425. ksadk/tracing/__init__.py +19 -21
  426. ksadk/tracing/setup.py +317 -403
  427. ksadk/tracing/span_utils.py +0 -44
  428. ksadk/version.py +1 -1
  429. ksadk_runtime_common/memory_backend/registry.py +56 -56
  430. ksadk_runtime_common/schemas/runtime_event_v1.json +37 -0
  431. ksadk_runtime_common/schemas/runtime_event_v2.json +271 -0
  432. ksadk_runtime_common/workspace_files/router.py +8 -2
  433. agentengine_sdk_python-0.8.0.dist-info/RECORD +0 -578
  434. ksadk/a2a/card_builder.py +0 -64
  435. ksadk/a2a/client.py +0 -309
  436. ksadk/events/parser.py +0 -191
  437. ksadk/harness/runner.py +0 -211
  438. ksadk/runners/codex_runner.py +0 -132
  439. ksadk/runners/utils/langfuse.py +0 -230
  440. ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +0 -1
  441. ksadk/server/static/assets/MermaidBlock-DWuxETaJ.js +0 -303
  442. ksadk/server/static/assets/NativeTerminalPanel-MDTopWMO.js +0 -1
  443. ksadk/server/static/assets/arc-CFKwSk_B.js +0 -1
  444. ksadk/server/static/assets/architecture-7EHR7CIX-CynqOWjZ.js +0 -1
  445. ksadk/server/static/assets/architectureDiagram-3BPJPVTR-Dej8jX8c.js +0 -36
  446. ksadk/server/static/assets/blockDiagram-GPEHLZMM-1F64jqvy.js +0 -132
  447. ksadk/server/static/assets/c4Diagram-AAUBKEIU-9q7AUAKb.js +0 -10
  448. ksadk/server/static/assets/channel-iUGv2N24.js +0 -1
  449. ksadk/server/static/assets/chunk-2J33WTMH-6qgwJm7Q.js +0 -1
  450. ksadk/server/static/assets/chunk-55IACEB6-BK8paA20.js +0 -1
  451. ksadk/server/static/assets/chunk-727SXJPM-B-JEhy7h.js +0 -206
  452. ksadk/server/static/assets/chunk-AQP2D5EJ-DYBvMHAa.js +0 -231
  453. ksadk/server/static/assets/chunk-QZHKN3VN-YQIEXeF6.js +0 -1
  454. ksadk/server/static/assets/classDiagram-4FO5ZUOK-CDAUuoXK.js +0 -1
  455. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-CDAUuoXK.js +0 -1
  456. ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +0 -321
  457. ksadk/server/static/assets/dagre-BM42HDAG-C7TguAWo.js +0 -4
  458. ksadk/server/static/assets/dagre-Bx709z4p.js +0 -1
  459. ksadk/server/static/assets/diagram-2AECGRRQ-Dd17_kws.js +0 -43
  460. ksadk/server/static/assets/diagram-5GNKFQAL-DVIClIil.js +0 -10
  461. ksadk/server/static/assets/diagram-KO2AKTUF-Df_3gYQa.js +0 -3
  462. ksadk/server/static/assets/diagram-LMA3HP47-BhLk6ZtA.js +0 -24
  463. ksadk/server/static/assets/diagram-OG6HWLK6-D4GNGvuu.js +0 -24
  464. ksadk/server/static/assets/dist-DyvYLMW9.js +0 -1
  465. ksadk/server/static/assets/erDiagram-TEJ5UH35-CnfRcY2M.js +0 -85
  466. ksadk/server/static/assets/eventmodeling-FCH6USID-ChYO-ijM.js +0 -1
  467. ksadk/server/static/assets/flowDiagram-I6XJVG4X-DGaJjGNJ.js +0 -162
  468. ksadk/server/static/assets/ganttDiagram-6RSMTGT7-DpCoB2g9.js +0 -292
  469. ksadk/server/static/assets/gitGraph-WXDBUCRP-BN_0Fk-j.js +0 -1
  470. ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-9IKVFBVT.js +0 -106
  471. ksadk/server/static/assets/graphlib-B8gBHxth.js +0 -1
  472. ksadk/server/static/assets/index-QSwCHuB8.js +0 -177
  473. ksadk/server/static/assets/index-jOPmTOZH.css +0 -2
  474. ksadk/server/static/assets/info-J43DQDTF-Cp6o3vId.js +0 -1
  475. ksadk/server/static/assets/infoDiagram-5YYISTIA-CYIIsb8y.js +0 -2
  476. ksadk/server/static/assets/katex-DATssjp1.js +0 -265
  477. ksadk/server/static/assets/mermaid-parser.core-mGAcEwcq.js +0 -161
  478. ksadk/server/static/assets/packet-YPE3B663-wQzct3cT.js +0 -1
  479. ksadk/server/static/assets/pie-LRSECV5Y-BgTQiv-9.js +0 -1
  480. ksadk/server/static/assets/pieDiagram-4H26LBE5-By-ra07M.js +0 -30
  481. ksadk/server/static/assets/radar-GUYGQ44K-BIq5fXsn.js +0 -1
  482. ksadk/server/static/assets/requirementDiagram-4Y6WPE33-DpW90iu3.js +0 -84
  483. ksadk/server/static/assets/stateDiagram-AJRCARHV-DGpd9yFG.js +0 -1
  484. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-B3o579IQ.js +0 -1
  485. ksadk/server/static/assets/treeView-BLDUP644-Dqi-E0EM.js +0 -1
  486. ksadk/server/static/assets/treemap-LRROVOQU-ClQP9CJ9.js +0 -1
  487. ksadk/server/static/assets/vennDiagram-CIIHVFJN-l3Z7-wbP.js +0 -34
  488. ksadk/server/static/assets/wardley-L42UT6IY-8XLvnNIs.js +0 -1
  489. ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-BR54GPmr.js +0 -78
  490. ksadk/server/static/assets/xychartDiagram-2RQKCTM6-SkiH8GsK.js +0 -7
  491. ksadk/tracing/exporters/langfuse_exporter.py +0 -460
  492. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/entry_points.txt +0 -0
  493. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/licenses/LICENSE +0 -0
  494. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1260 @@
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
+ _PROXY_MODEL_FAMILIES = ("deepseek", "glm", "kimi", "minimax", "qwen")
51
+
52
+
53
+ def _proxy_model_family(model_id: str) -> str | None:
54
+ """Return the supported proxy family without changing the provider id.
55
+
56
+ The comparison deliberately treats ``.`` and ``-`` as equivalent only for
57
+ classification (for example ``glm-5.3`` and ``glm-5-3``). The returned
58
+ catalog descriptor keeps the exact identifier supplied by the provider,
59
+ which is the value used for deployment and model requests.
60
+ """
61
+
62
+ normalized = str(model_id or "").strip().lower()
63
+ if "/" in normalized:
64
+ normalized = normalized.split("/", 1)[1]
65
+ normalized = normalized.replace(".", "-")
66
+ for family in _PROXY_MODEL_FAMILIES:
67
+ # Qwen IDs are commonly emitted as ``qwen3-*`` rather than
68
+ # ``qwen-3-*``; the other approved families retain a word boundary.
69
+ boundary = family if family == "qwen" else f"{family}-"
70
+ if normalized == family or normalized.startswith(boundary):
71
+ return family
72
+ return None
73
+
74
+
75
+ def _proxy_model_sort_key(model_id: str) -> tuple[str, tuple[int, ...], str]:
76
+ """Group proxy models by vendor and put newer numbered releases first."""
77
+
78
+ raw = str(model_id or "").strip()
79
+ normalized = raw.lower().split("/", 1)[-1].replace(".", "-")
80
+ family = _proxy_model_family(raw) or "zz-unknown"
81
+ numbers = [int(value) for value in re.findall(r"\d+", normalized)]
82
+ # A fixed-width negative tuple makes 5.3.1 sort before 5.3 and 5.2,
83
+ # while keeping equal-version variants deterministic without inventing an
84
+ # ordering for provider suffixes such as ``-pro`` and ``-flash``.
85
+ version = tuple([-value for value in numbers[:8]] + [0] * (8 - len(numbers)))
86
+ return family, version, normalized
87
+
88
+
89
+ def _models_endpoint(api_base: str | None) -> str:
90
+ base = str(api_base or "").strip().rstrip("/")
91
+ if not base:
92
+ return ""
93
+ if base.endswith("/models"):
94
+ return base
95
+ if base.endswith("/v1"):
96
+ return f"{base}/models"
97
+ return f"{base}/v1/models"
98
+
99
+
100
+ def _provider_reports_context(raw: dict[str, Any]) -> bool:
101
+ direct = {
102
+ "context_window_tokens",
103
+ "context_length",
104
+ "context_window",
105
+ "input_max_length",
106
+ }
107
+ if direct.intersection(raw):
108
+ return True
109
+ for key in ("metadata", "limits"):
110
+ nested = raw.get(key)
111
+ if isinstance(nested, dict) and direct.intersection(nested):
112
+ return True
113
+ return False
114
+
115
+
116
+ def _provider_reports_modalities(raw: dict[str, Any]) -> bool:
117
+ architecture = raw.get("architecture")
118
+ if isinstance(architecture, dict) and isinstance(architecture.get("input_modalities"), list):
119
+ return True
120
+ capabilities = raw.get("capabilities")
121
+ return isinstance(capabilities, dict) and any(
122
+ key in capabilities
123
+ for key in (
124
+ "multimodal_input_image",
125
+ "multimodal_input_video",
126
+ "multimodal_input_file",
127
+ )
128
+ )
129
+
130
+
131
+ def _tool_side_effect(side_effects: list[str]) -> str:
132
+ if not side_effects:
133
+ return "none"
134
+ lowered = " ".join(side_effects).lower()
135
+ if any(marker in lowered for marker in ("write", "edit", "delete", "command", "code")):
136
+ return "write"
137
+ if any(marker in lowered for marker in ("network", "external", "http")):
138
+ return "external"
139
+ return "read"
140
+
141
+
142
+ def resource_slug(value: str) -> str:
143
+ slug = _SLUG.sub("-", value.strip().lower()).strip("-")
144
+ if not slug:
145
+ raise StudioError(
146
+ "RESOURCE_NAME_INVALID",
147
+ "资源名称必须至少包含一个字母或数字",
148
+ status_code=422,
149
+ field="name",
150
+ )
151
+ return slug[:80]
152
+
153
+
154
+ def resource_id(kind: str, source: str, name: str, version: str) -> str:
155
+ return f"{kind}:{source}:{resource_slug(name)}:{version}"
156
+
157
+
158
+ class LocalResourceCatalog:
159
+ def __init__(self, workspace: Workspace) -> None:
160
+ self.workspace = workspace
161
+ self.resolver = LocalCapabilityResolver(workspace)
162
+ self.skill_discovery = SkillDiscoveryService(workspace)
163
+ self.python_tool_inspector = PythonToolInspector(workspace)
164
+ self._provider_models: dict[str, ResourceDescriptor] = {}
165
+ # api_base -> (monotonic_ts, descriptors, source),见 discover_provider_models。
166
+ self._provider_catalog_cache: dict[
167
+ str, tuple[float, builtins.list[ResourceDescriptor], str]
168
+ ] = {}
169
+ # 内建工具描述构建一次约 40ms(导入 + schema 生成 + digest),
170
+ # catalog.list 调用方很多,用短 TTL 缓存避免页面加载期间重复构建;
171
+ # enabled 依赖 sandbox backend 配置,设置页改完后几秒自动生效。
172
+ self._builtin_tools_cache: tuple[float, builtins.list[ResourceDescriptor]] | None = None
173
+
174
+ def list(
175
+ self,
176
+ *,
177
+ kind: str | None = None,
178
+ query: str = "",
179
+ source: str | None = None,
180
+ status: str | None = None,
181
+ installed: bool | None = None,
182
+ limit: int = 50,
183
+ ) -> list[ResourceDescriptor]:
184
+ filtered = self._filtered_resources(
185
+ kind=kind,
186
+ query=query,
187
+ source=source,
188
+ status=status,
189
+ installed=installed,
190
+ )
191
+ filtered.sort(key=self._default_sort_key)
192
+ return filtered[:limit]
193
+
194
+ def list_page(
195
+ self,
196
+ *,
197
+ kind: str | None = None,
198
+ query: str = "",
199
+ source: str | None = None,
200
+ status: str | None = None,
201
+ installed: bool | None = None,
202
+ limit: int = 50,
203
+ cursor: str | None = None,
204
+ sort: str = "default",
205
+ status_resolver: Callable[[ResourceDescriptor], str] | None = None,
206
+ ) -> dict[str, Any]:
207
+ if sort not in {"default", "displayName:asc", "displayName:desc"}:
208
+ raise StudioError(
209
+ "PAGINATION_SORT_INVALID",
210
+ "Resource 排序字段无效",
211
+ status_code=422,
212
+ field="sort",
213
+ )
214
+ filtered = self._filtered_resources(
215
+ kind=kind,
216
+ query=query,
217
+ source=source,
218
+ status=status,
219
+ installed=installed,
220
+ status_resolver=status_resolver,
221
+ )
222
+ if sort == "default":
223
+ sort_key = self._default_sort_key
224
+ reverse = False
225
+ else:
226
+
227
+ def sort_key(item: ResourceDescriptor) -> tuple[str, str, int, str, str]:
228
+ return (
229
+ item.display_name.lower(),
230
+ item.kind,
231
+ 0 if item.source in {"builtin", "provider"} else 1,
232
+ item.version,
233
+ item.resource_id,
234
+ )
235
+
236
+ reverse = sort.endswith(":desc")
237
+ filters = {
238
+ "kind": kind or "",
239
+ "query": query.strip().lower(),
240
+ "source": source or "",
241
+ "status": status or "",
242
+ "installed": installed,
243
+ }
244
+ return keyset_page(
245
+ filtered,
246
+ key=sort_key,
247
+ reverse=reverse,
248
+ limit=max(1, min(limit, 200)),
249
+ cursor=cursor,
250
+ namespace="catalog-resources",
251
+ sort=sort,
252
+ filters=filters,
253
+ )
254
+
255
+ @staticmethod
256
+ def _default_sort_key(item: ResourceDescriptor) -> tuple[str, int, str, str, str]:
257
+ return (
258
+ item.kind,
259
+ 0 if item.source in {"builtin", "provider"} else 1,
260
+ item.display_name.lower(),
261
+ item.version,
262
+ item.resource_id,
263
+ )
264
+
265
+ def _filtered_resources(
266
+ self,
267
+ *,
268
+ kind: str | None,
269
+ query: str,
270
+ source: str | None,
271
+ status: str | None,
272
+ installed: bool | None,
273
+ status_resolver: Callable[[ResourceDescriptor], str] | None = None,
274
+ ) -> builtins.list[ResourceDescriptor]:
275
+ candidates = [
276
+ *self._provider_models.values(),
277
+ *self._builtin_tools(),
278
+ *self._persisted("models"),
279
+ *self._persisted("mcp"),
280
+ *self._persisted("tools"),
281
+ *self._local_skills(),
282
+ ]
283
+ resources = list({item.resource_id: item for item in candidates}.values())
284
+ normalized = query.strip().lower()
285
+ filtered: builtins.list[ResourceDescriptor] = []
286
+ for item in resources:
287
+ effective_status = status_resolver(item) if status_resolver else item.status
288
+ if kind is not None and item.kind != kind:
289
+ continue
290
+ if source is not None and item.source != source:
291
+ continue
292
+ if status is not None and effective_status != status:
293
+ continue
294
+ if installed is not None and item.installed is not installed:
295
+ continue
296
+ if normalized and not any(
297
+ normalized in value.lower()
298
+ for value in (item.name, item.display_name, item.description)
299
+ ):
300
+ continue
301
+ filtered.append(
302
+ item
303
+ if effective_status == item.status
304
+ else item.model_copy(update={"status": effective_status})
305
+ )
306
+ return filtered
307
+
308
+ async def discover_provider_models(
309
+ self,
310
+ *,
311
+ api_base: str | None,
312
+ api_key: str | None,
313
+ current_model: str | None,
314
+ timeout: float = 5.0,
315
+ ) -> tuple[builtins.list[ResourceDescriptor], str]:
316
+ """Project the same provider catalog used by ksadk runtimes into Studio.
317
+
318
+ The provider response is authoritative only for fields it actually
319
+ returns. Canonical fallback values remain annotated as ``ksadk-default``
320
+ so the UI never presents a guessed context window or modality as probed.
321
+ """
322
+
323
+ # /v1/models 是真实外网往返,catalog/models 等接口每次调用都走这里,
324
+ # 会话/资源页加载会连续触发多次。做 60s 进程内缓存;上游失败且
325
+ # 有缓存时直接回退缓存,避免上游抖动拖垮整个模型目录。
326
+ cache_key = (api_base or "").rstrip("/")
327
+ now = time.monotonic()
328
+ cached = self._provider_catalog_cache.get(cache_key)
329
+ if cached is not None and now - cached[0] < 60.0:
330
+ return cached[1], cached[2]
331
+
332
+ catalog = await fetch_provider_model_catalog(
333
+ api_base=api_base,
334
+ api_key=api_key,
335
+ timeout=timeout,
336
+ )
337
+ source = "provider" if catalog else "fallback"
338
+ if not catalog:
339
+ if cached is not None:
340
+ return cached[1], cached[2]
341
+ catalog = [normalize_model_metadata({"id": current_model or "glm-5.1"})]
342
+
343
+ descriptors: list[ResourceDescriptor] = []
344
+ for item in catalog:
345
+ normalized = dict(item)
346
+ raw = normalized.pop("_provider_raw_model", None)
347
+ raw_mapping = raw if isinstance(raw, dict) else {}
348
+ normalized = normalize_model_metadata(normalized)
349
+ model_id = str(normalized.get("id") or current_model or "unknown-model")
350
+ # The proxy has only been compatibility-validated for these five
351
+ # provider families. This is a presentation/selection policy, not
352
+ # a rewrite of the provider's model identifier.
353
+ if _proxy_model_family(model_id) is None:
354
+ continue
355
+ display_name = str(normalized.get("display_name") or model_id)
356
+ context_source = (
357
+ "provider" if _provider_reports_context(raw_mapping) else "ksadk-default"
358
+ )
359
+ modality_source = (
360
+ "provider" if _provider_reports_modalities(raw_mapping) else "ksadk-default"
361
+ )
362
+ spec = ModelSpec(
363
+ provider="openai-compatible",
364
+ model=model_id,
365
+ base_url=(api_base or "https://api.openai.com/v1").rstrip("/"),
366
+ credential_ref="env://OPENAI_API_KEY",
367
+ metadata=normalized,
368
+ discovery={
369
+ "source": source,
370
+ "endpoint": _models_endpoint(api_base),
371
+ "contextWindow": context_source,
372
+ "inputModalities": modality_source,
373
+ },
374
+ )
375
+ descriptor_source = "provider" if source == "provider" else "builtin"
376
+ descriptor = self._descriptor(
377
+ kind="model",
378
+ source=descriptor_source,
379
+ name=model_id,
380
+ display_name=display_name,
381
+ version=str(
382
+ normalized.get("version") or ("live" if source == "provider" else "1.0.0")
383
+ ),
384
+ description=(
385
+ "由模型服务 /v1/models 自动发现"
386
+ if source == "provider"
387
+ else "模型服务未返回目录,使用 ksadk 当前模型配置"
388
+ ),
389
+ category="provider-catalog",
390
+ contract=spec.model_dump(
391
+ by_alias=True,
392
+ exclude_none=True,
393
+ mode="json",
394
+ ),
395
+ required_secret_refs=[spec.credential_ref],
396
+ )
397
+ descriptors.append(descriptor)
398
+
399
+ descriptors.sort(key=lambda item: _proxy_model_sort_key(item.name))
400
+ self._provider_models = {item.resource_id: item for item in descriptors}
401
+ self._provider_catalog_cache[cache_key] = (now, descriptors, source)
402
+ return descriptors, source
403
+
404
+ def get(self, resource: str) -> ResourceDescriptor:
405
+ found = next(
406
+ (item for item in self.list(limit=10_000) if item.resource_id == resource),
407
+ None,
408
+ )
409
+ if found is None:
410
+ raise StudioError(
411
+ "RESOURCE_NOT_FOUND",
412
+ "Catalog Resource 不存在",
413
+ status_code=404,
414
+ details={"resourceId": resource},
415
+ )
416
+ return found
417
+
418
+ def create_model_profile(
419
+ self,
420
+ *,
421
+ name: str,
422
+ display_name: str,
423
+ version: str,
424
+ description: str,
425
+ spec: ModelSpec,
426
+ ) -> ResourceDescriptor:
427
+ require_exact_version(version, field="version")
428
+ return self._persist_descriptor(
429
+ "models",
430
+ self._descriptor(
431
+ kind="model",
432
+ name=name,
433
+ display_name=display_name,
434
+ version=version,
435
+ description=description,
436
+ category="model",
437
+ contract=spec.model_dump(
438
+ by_alias=True,
439
+ exclude_none=True,
440
+ mode="json",
441
+ ),
442
+ required_secret_refs=[spec.credential_ref],
443
+ ),
444
+ )
445
+
446
+ def create_mcp_server(
447
+ self,
448
+ *,
449
+ display_name: str,
450
+ description: str,
451
+ server: MCPServerRef,
452
+ ) -> ResourceDescriptor:
453
+ require_exact_version(server.version, field="version")
454
+ resolved = self.resolver.resolve_mcp(server)
455
+ descriptor = self._descriptor(
456
+ kind="mcp",
457
+ name=server.name,
458
+ display_name=display_name,
459
+ version=server.version,
460
+ description=description,
461
+ category="mcp",
462
+ contract=resolved,
463
+ required_secret_refs=sorted(set(server.env_refs.values())),
464
+ )
465
+ return self._persist_descriptor("mcp", descriptor)
466
+
467
+ def create_tool(
468
+ self,
469
+ *,
470
+ display_name: str,
471
+ category: str,
472
+ contract: ToolContract,
473
+ ) -> ResourceDescriptor:
474
+ require_exact_version(contract.version, field="version")
475
+ if contract.executor == "python":
476
+ contract = self._snapshot_python_tool(contract)
477
+ resolved = self.resolver.resolve_tool(contract)
478
+ descriptor = self._descriptor(
479
+ kind="tool",
480
+ name=contract.name,
481
+ display_name=display_name,
482
+ version=contract.version,
483
+ description=contract.description,
484
+ category=category,
485
+ contract=resolved.model_dump(
486
+ by_alias=True,
487
+ exclude_none=True,
488
+ mode="json",
489
+ ),
490
+ )
491
+ return self._persist_descriptor("tools", descriptor)
492
+
493
+ def inspect_python_tool(self, content: bytes, *, filename: str) -> dict[str, Any]:
494
+ return self.python_tool_inspector.inspect(content, filename=filename)
495
+
496
+ def commit_python_tool(
497
+ self,
498
+ inspection_token: str,
499
+ *,
500
+ display_name: str,
501
+ name: str,
502
+ callable_name: str,
503
+ description: str = "",
504
+ ) -> ResourceDescriptor:
505
+ inspection = self.python_tool_inspector.load(inspection_token)
506
+ callable_meta = next(
507
+ (
508
+ item
509
+ for item in inspection.get("callables") or []
510
+ if item.get("name") == callable_name
511
+ ),
512
+ None,
513
+ )
514
+ if callable_meta is None:
515
+ raise StudioError(
516
+ "PYTHON_TOOL_CALLABLE_INVALID",
517
+ "所选 Callable 不在已检查的源码中",
518
+ status_code=422,
519
+ field="callableName",
520
+ )
521
+ required = list(callable_meta.get("required") or [])
522
+ parameters = [
523
+ value
524
+ for value in callable_meta.get("parameters") or []
525
+ if not str(value).startswith(("*", "**"))
526
+ ]
527
+ resolved_description = str(description or callable_meta.get("description") or "").strip()
528
+ descriptor = self.create_tool(
529
+ display_name=display_name,
530
+ category="custom",
531
+ contract=ToolContract(
532
+ name=name,
533
+ version="1.0.0",
534
+ description=resolved_description,
535
+ input_schema={
536
+ "type": "object",
537
+ "properties": {parameter: {} for parameter in parameters},
538
+ **({"required": required} if required else {}),
539
+ },
540
+ output_schema={},
541
+ executor="python",
542
+ source_path=str(inspection["sourcePath"]),
543
+ callable_name=callable_name,
544
+ approval="policy",
545
+ side_effect="none",
546
+ ),
547
+ )
548
+ self.python_tool_inspector.consume(inspection_token)
549
+ return descriptor
550
+
551
+ def _snapshot_python_tool(self, contract: ToolContract) -> ToolContract:
552
+ assert contract.source_path is not None
553
+ try:
554
+ source = self.workspace.resolve(contract.source_path, must_exist=True)
555
+ except FileNotFoundError as exc:
556
+ raise StudioError(
557
+ "TOOL_SOURCE_NOT_FOUND",
558
+ f"Python Tool 源码文件不存在:{contract.source_path}(相对于当前工作区)",
559
+ status_code=422,
560
+ field="sourcePath",
561
+ ) from exc
562
+ if source.is_symlink() or not source.is_file() or source.suffix != ".py":
563
+ raise StudioError(
564
+ "TOOL_SOURCE_INVALID",
565
+ "Python Tool sourcePath 必须是工作区内的普通 .py 文件",
566
+ status_code=422,
567
+ field="sourcePath",
568
+ )
569
+ content = source.read_bytes()
570
+ if len(content) > 1024 * 1024:
571
+ raise StudioError(
572
+ "TOOL_SOURCE_TOO_LARGE",
573
+ "Python Tool 源码不能超过 1 MiB",
574
+ status_code=422,
575
+ field="sourcePath",
576
+ )
577
+ try:
578
+ text = content.decode("utf-8")
579
+ except UnicodeDecodeError as exc:
580
+ raise StudioError(
581
+ "TOOL_SOURCE_INVALID",
582
+ "Python Tool 源码必须使用 UTF-8 编码",
583
+ status_code=422,
584
+ field="sourcePath",
585
+ ) from exc
586
+ target = self.workspace.resolve(
587
+ Path(".agentkit/catalog/tool-sources")
588
+ / f"{resource_slug(contract.name)}-{resource_slug(contract.version)}"
589
+ / "tool.py"
590
+ )
591
+ self.workspace.atomic_write_text(target, text)
592
+ return cast(
593
+ ToolContract,
594
+ contract.model_copy(
595
+ update={
596
+ "source_path": self.workspace.relative(target),
597
+ "source_sha256": f"sha256:{hashlib.sha256(content).hexdigest()}",
598
+ }
599
+ ),
600
+ )
601
+
602
+ def save_probe(
603
+ self,
604
+ resource: str,
605
+ *,
606
+ result: dict[str, Any],
607
+ ) -> ResourceDescriptor:
608
+ descriptor = self.get(resource)
609
+ if descriptor.kind != "mcp" or descriptor.source != "local":
610
+ raise StudioError(
611
+ "RESOURCE_KIND_INVALID",
612
+ "只有本地 MCP Resource 可以保存探测结果",
613
+ status_code=422,
614
+ details={"resourceId": resource},
615
+ )
616
+ updated = descriptor.model_copy(deep=True)
617
+ updated.status = "ready"
618
+ updated.health = {
619
+ "status": "ready",
620
+ "toolCount": len(result.get("tools") or []),
621
+ "serverInfo": result.get("serverInfo") or {},
622
+ "probedAt": datetime.now(timezone.utc).isoformat(),
623
+ }
624
+ updated.contract["discoveredTools"] = result.get("tools") or []
625
+ updated.updated_at = datetime.now(timezone.utc)
626
+ return self._persist_descriptor("mcp", updated, overwrite=True)
627
+
628
+ def mark_probe_failed(
629
+ self,
630
+ resource: str,
631
+ *,
632
+ code: str,
633
+ detail: str | None = None,
634
+ ) -> ResourceDescriptor:
635
+ descriptor = self.get(resource)
636
+ updated = descriptor.model_copy(deep=True)
637
+ updated.status = "unhealthy"
638
+ updated.health = {
639
+ "status": "unhealthy",
640
+ "code": code,
641
+ "probedAt": datetime.now(timezone.utc).isoformat(),
642
+ }
643
+ if detail:
644
+ updated.health["message"] = detail[:500]
645
+ updated.updated_at = datetime.now(timezone.utc)
646
+ return self._persist_descriptor("mcp", updated, overwrite=True)
647
+
648
+ def policy_preview(
649
+ self,
650
+ bindings: AgentBindings,
651
+ ) -> tuple[builtins.list[ToolContract], builtins.list[str]]:
652
+ tools: builtins.list[ToolContract] = []
653
+ permissions: set[str] = set()
654
+ for binding in bindings.tools:
655
+ if not binding.enabled:
656
+ continue
657
+ descriptor = self.get(binding.resource_id)
658
+ if descriptor.kind != "tool":
659
+ raise StudioError(
660
+ "RESOURCE_KIND_INVALID",
661
+ "Tool binding 必须引用 Tool Resource",
662
+ status_code=422,
663
+ details={"resourceId": binding.resource_id},
664
+ )
665
+ if descriptor.status != "ready":
666
+ raise StudioError(
667
+ "RESOURCE_NOT_READY",
668
+ "不可用的 Tool Resource 不能绑定",
669
+ status_code=409,
670
+ details={
671
+ "resourceId": descriptor.resource_id,
672
+ "status": descriptor.status,
673
+ },
674
+ )
675
+ tool = ToolContract.model_validate(descriptor.contract)
676
+ approval = self._effective_approval(
677
+ tool,
678
+ bindings.policy_template,
679
+ binding,
680
+ )
681
+ tool = cast(
682
+ ToolContract,
683
+ tool.model_copy(update={"approval": approval}),
684
+ )
685
+ tools.append(tool)
686
+ permissions.update(tool.permissions)
687
+ tools.sort(key=lambda item: (item.name, item.version))
688
+ return tools, sorted(permissions)
689
+
690
+ def resolve_model(self, bindings: AgentBindings) -> ModelSpec | None:
691
+ if not bindings.model_profile_id:
692
+ return None
693
+ descriptor = self.get(bindings.model_profile_id)
694
+ if descriptor.kind != "model":
695
+ raise StudioError(
696
+ "RESOURCE_KIND_INVALID",
697
+ "modelProfileId 必须引用 Model Resource",
698
+ status_code=422,
699
+ )
700
+ if descriptor.status not in {"ready", "missing-secret"}:
701
+ raise StudioError(
702
+ "RESOURCE_NOT_READY",
703
+ "模型资源当前不可用",
704
+ status_code=409,
705
+ details={"resourceId": descriptor.resource_id, "status": descriptor.status},
706
+ )
707
+ resolved = cast(ModelSpec, ModelSpec.model_validate(descriptor.contract))
708
+ if bindings.model_parameters is not None:
709
+ resolved.parameters = bindings.model_parameters
710
+ return resolved
711
+
712
+ def resolve_models(self, bindings: AgentBindings) -> builtins.list[ModelSpec]:
713
+ resource_ids = builtins.list(bindings.model_profile_ids)
714
+ if not resource_ids and bindings.model_profile_id:
715
+ resource_ids = [bindings.model_profile_id]
716
+ resolved: builtins.list[ModelSpec] = []
717
+ for resource_id in resource_ids:
718
+ selected = bindings.model_copy(
719
+ update={"model_profile_id": resource_id, "model_profile_ids": []}
720
+ )
721
+ model = self.resolve_model(selected)
722
+ if model is not None:
723
+ resolved.append(model)
724
+ return resolved
725
+
726
+ def resolve_skills(self, bindings: AgentBindings) -> builtins.list[CapabilityRef]:
727
+ refs: builtins.list[CapabilityRef] = []
728
+ for binding in bindings.skills:
729
+ if not binding.enabled:
730
+ continue
731
+ descriptor = self._ready_binding(binding, expected_kind="skill")
732
+ refs.append(
733
+ CapabilityRef(
734
+ name=descriptor.name,
735
+ version=descriptor.version,
736
+ digest=descriptor.digest,
737
+ )
738
+ )
739
+ return refs
740
+
741
+ def resolve_mcp_servers(
742
+ self,
743
+ bindings: AgentBindings,
744
+ ) -> builtins.list[MCPServerRef]:
745
+ refs: builtins.list[MCPServerRef] = []
746
+ for binding in bindings.mcp_servers:
747
+ if not binding.enabled:
748
+ continue
749
+ descriptor = self._ready_binding(binding, expected_kind="mcp")
750
+ payload = {
751
+ key: value for key, value in descriptor.contract.items() if key != "discoveredTools"
752
+ }
753
+ refs.append(MCPServerRef.model_validate(payload))
754
+ return refs
755
+
756
+ def resolve_mcp_tools(
757
+ self,
758
+ bindings: AgentBindings,
759
+ ) -> builtins.list[ToolContract]:
760
+ tools: builtins.list[ToolContract] = []
761
+ for binding in bindings.mcp_servers:
762
+ if not binding.enabled:
763
+ continue
764
+ descriptor = self._ready_binding(binding, expected_kind="mcp")
765
+ for payload in descriptor.contract.get("discoveredTools") or []:
766
+ tool = ToolContract.model_validate(payload)
767
+ approval = self._effective_approval(
768
+ tool,
769
+ bindings.policy_template,
770
+ binding,
771
+ )
772
+ tools.append(
773
+ cast(
774
+ ToolContract,
775
+ tool.model_copy(update={"approval": approval}),
776
+ )
777
+ )
778
+ return tools
779
+
780
+ def import_skill_zip(self, content: bytes, *, filename: str) -> ResourceDescriptor:
781
+ if not filename.lower().endswith(".zip"):
782
+ raise StudioError(
783
+ "SKILL_ARCHIVE_INVALID",
784
+ "Skill 包必须是 .zip 文件",
785
+ status_code=422,
786
+ )
787
+ if len(content) > _MAX_SKILL_ARCHIVE_BYTES:
788
+ raise StudioError(
789
+ "SKILL_ARCHIVE_TOO_LARGE",
790
+ "Skill 包不能超过 50MB",
791
+ status_code=413,
792
+ )
793
+ try:
794
+ archive = zipfile.ZipFile(io.BytesIO(content))
795
+ except zipfile.BadZipFile as exc:
796
+ raise StudioError(
797
+ "SKILL_ARCHIVE_INVALID",
798
+ "Skill ZIP 无法解析",
799
+ status_code=422,
800
+ ) from exc
801
+ with archive:
802
+ files = [info for info in archive.infolist() if not info.is_dir()]
803
+ if not files or len(files) > _MAX_SKILL_FILES:
804
+ raise StudioError(
805
+ "SKILL_ARCHIVE_INVALID",
806
+ "Skill ZIP 文件数量不合法",
807
+ status_code=422,
808
+ )
809
+ expanded = sum(info.file_size for info in files)
810
+ if expanded > _MAX_SKILL_EXPANDED_BYTES:
811
+ raise StudioError(
812
+ "SKILL_ARCHIVE_TOO_LARGE",
813
+ "Skill ZIP 解压后不能超过 100MB",
814
+ status_code=413,
815
+ )
816
+ for info in files:
817
+ path = PurePosixPath(info.filename)
818
+ mode = (info.external_attr >> 16) & 0o170000
819
+ if (
820
+ path.is_absolute()
821
+ or ".." in path.parts
822
+ or mode == stat.S_IFLNK
823
+ or (info.compress_size == 0 and info.file_size > 0)
824
+ or (info.compress_size > 0 and info.file_size / info.compress_size > 200)
825
+ ):
826
+ raise StudioError(
827
+ "SKILL_ARCHIVE_UNSAFE",
828
+ "Skill ZIP 包含不安全路径、链接或压缩条目",
829
+ status_code=422,
830
+ details={"entry": info.filename},
831
+ )
832
+ skill_entry, prefix = self._skill_entry(files)
833
+ skill_text = archive.read(skill_entry).decode("utf-8")
834
+ metadata = self._skill_frontmatter(skill_text)
835
+ name = str(metadata["name"])
836
+ description = str(metadata["description"])
837
+ version = str(metadata.get("version") or "1.0.0")
838
+ require_exact_version(version, field="SKILL.md.version")
839
+ slug = resource_slug(name)
840
+ destination = self.workspace.resolve(Path("capabilities/skills") / slug)
841
+ if destination.exists():
842
+ raise StudioError(
843
+ "RESOURCE_ALREADY_EXISTS",
844
+ "同名 Skill 已安装",
845
+ status_code=409,
846
+ details={"name": slug},
847
+ )
848
+ staging = self.workspace.resolve(
849
+ Path(".agentkit/cache") / f".skill-{slug}-{uuid4().hex}.tmp"
850
+ )
851
+ staging.mkdir(parents=True, exist_ok=False)
852
+ try:
853
+ for info in files:
854
+ source_path = PurePosixPath(info.filename)
855
+ relative = (
856
+ PurePosixPath(*source_path.parts[len(prefix.parts) :])
857
+ if prefix.parts
858
+ else source_path
859
+ )
860
+ target = staging / Path(*relative.parts)
861
+ target.parent.mkdir(parents=True, exist_ok=True)
862
+ target.write_bytes(archive.read(info))
863
+ self.workspace.atomic_write_yaml(
864
+ staging / "skill.yaml",
865
+ {
866
+ "name": slug,
867
+ "displayName": name,
868
+ "description": description,
869
+ "version": version,
870
+ "instructionsFile": "SKILL.md",
871
+ },
872
+ )
873
+ shutil.move(str(staging), str(destination))
874
+ finally:
875
+ shutil.rmtree(staging, ignore_errors=True)
876
+ descriptor = next(
877
+ (
878
+ item
879
+ for item in self._local_skills()
880
+ if item.name == slug and item.version == version
881
+ ),
882
+ None,
883
+ )
884
+ if descriptor is None:
885
+ raise StudioError(
886
+ "SKILL_IMPORT_FAILED",
887
+ "Skill 安装后无法解析",
888
+ status_code=500,
889
+ )
890
+ return descriptor
891
+
892
+ def discover_skills(
893
+ self,
894
+ *,
895
+ scan_paths: builtins.list[str] | None = None,
896
+ ) -> dict[str, Any]:
897
+ return self.skill_discovery.discover(scan_paths=scan_paths)
898
+
899
+ def commit_discovered_skill(
900
+ self,
901
+ inspection_token: str,
902
+ candidate_id: str,
903
+ *,
904
+ overwrite: bool = False,
905
+ ) -> ResourceDescriptor:
906
+ slug, version = self.skill_discovery.commit(
907
+ inspection_token,
908
+ candidate_id,
909
+ overwrite=overwrite,
910
+ )
911
+ descriptor = next(
912
+ (
913
+ item
914
+ for item in self._local_skills()
915
+ if item.name == slug and item.version == version
916
+ ),
917
+ None,
918
+ )
919
+ if descriptor is None:
920
+ raise StudioError(
921
+ "SKILL_IMPORT_FAILED",
922
+ "Skill 导入后无法解析",
923
+ status_code=500,
924
+ )
925
+ return descriptor
926
+
927
+ def preview_discovered_skill(
928
+ self,
929
+ inspection_token: str,
930
+ candidate_id: str,
931
+ *,
932
+ path: str | None = None,
933
+ ) -> dict[str, Any]:
934
+ return self.skill_discovery.preview_candidate(
935
+ inspection_token,
936
+ candidate_id,
937
+ path=path,
938
+ )
939
+
940
+ def preview_installed_skill(
941
+ self,
942
+ resource_id_value: str,
943
+ *,
944
+ path: str | None = None,
945
+ ) -> dict[str, Any]:
946
+ descriptor = self.get(resource_id_value)
947
+ if descriptor.kind != "skill" or descriptor.source != "local":
948
+ raise StudioError(
949
+ "RESOURCE_KIND_INVALID",
950
+ "仅支持预览工作区已安装的 Skill",
951
+ status_code=422,
952
+ )
953
+ directory = self.workspace.resolve(
954
+ Path("capabilities/skills") / descriptor.name,
955
+ must_exist=True,
956
+ )
957
+ return self.skill_discovery.preview_directory(directory, path=path)
958
+
959
+ def _ready_binding(
960
+ self,
961
+ binding: CapabilityBinding,
962
+ *,
963
+ expected_kind: str,
964
+ ) -> ResourceDescriptor:
965
+ descriptor = self.get(binding.resource_id)
966
+ if descriptor.kind != expected_kind:
967
+ raise StudioError(
968
+ "RESOURCE_KIND_INVALID",
969
+ f"Binding 必须引用 {expected_kind} Resource",
970
+ status_code=422,
971
+ details={"resourceId": binding.resource_id},
972
+ )
973
+ if descriptor.status != "ready":
974
+ raise StudioError(
975
+ "RESOURCE_NOT_READY",
976
+ "不可用的 Resource 不能绑定",
977
+ status_code=409,
978
+ details={
979
+ "resourceId": descriptor.resource_id,
980
+ "status": descriptor.status,
981
+ },
982
+ )
983
+ return descriptor
984
+
985
+ @staticmethod
986
+ def _effective_approval(
987
+ tool: ToolContract,
988
+ template: str,
989
+ binding: CapabilityBinding,
990
+ ) -> str:
991
+ if binding.approval:
992
+ return binding.approval
993
+ if template == "loose":
994
+ return "never"
995
+ if template == "strict":
996
+ return (
997
+ "never"
998
+ if tool.side_effect in {"none", "read"}
999
+ and "process:execute" not in tool.permissions
1000
+ else "always"
1001
+ )
1002
+ return tool.approval
1003
+
1004
+ def _builtin_tools(self) -> Iterable[ResourceDescriptor]:
1005
+ cached = self._builtin_tools_cache
1006
+ now = time.monotonic()
1007
+ if cached is not None and now - cached[0] < 5.0:
1008
+ return cached[1]
1009
+ built = list(self._build_builtin_tools())
1010
+ self._builtin_tools_cache = (now, built)
1011
+ return built
1012
+
1013
+ def _build_builtin_tools(self) -> Iterable[ResourceDescriptor]:
1014
+ runtime_tools = {
1015
+ str(getattr(tool, "name", None) or getattr(tool, "__name__", "")): tool
1016
+ for tool in get_agentengine_tools(profile="coding", mode="direct")
1017
+ }
1018
+ for descriptor in describe_agentengine_tools(profile="coding", mode="direct"):
1019
+ name = str(descriptor["name"])
1020
+ group = str(descriptor.get("group") or "general")
1021
+ side_effects = [str(item) for item in descriptor.get("side_effects") or []]
1022
+ side_effect = _tool_side_effect(side_effects)
1023
+ permissions = [str(item) for item in descriptor.get("approval_scopes") or []]
1024
+ if group == "workspace":
1025
+ workspace_permission = (
1026
+ "workspace:file:write" if side_effect == "write" else "workspace:file:read"
1027
+ )
1028
+ if workspace_permission not in permissions:
1029
+ permissions.append(workspace_permission)
1030
+ runtime_tool = runtime_tools.get(name)
1031
+ args_schema = getattr(runtime_tool, "args_schema", None)
1032
+ input_schema = (
1033
+ args_schema.model_json_schema()
1034
+ if args_schema is not None and hasattr(args_schema, "model_json_schema")
1035
+ else {"type": "object", "properties": {}}
1036
+ )
1037
+ tool = ToolContract(
1038
+ name=name,
1039
+ version="1.0.0",
1040
+ description=str(descriptor.get("description") or ""),
1041
+ input_schema=input_schema,
1042
+ permissions=permissions,
1043
+ side_effect=cast(Any, side_effect),
1044
+ approval="always" if descriptor.get("requires_approval") else "never",
1045
+ executor="builtin",
1046
+ group=group,
1047
+ risk_level=str(descriptor.get("risk_level") or "low"),
1048
+ boundary=str(descriptor.get("boundary") or "ksadk-runtime"),
1049
+ backend=str(descriptor.get("backend") or "") or None,
1050
+ enabled=bool(descriptor.get("enabled", True)),
1051
+ )
1052
+ resolved = self.resolver.resolve_tool(tool)
1053
+ resource = self._descriptor(
1054
+ kind="tool",
1055
+ source="builtin",
1056
+ name=tool.name,
1057
+ display_name=name,
1058
+ version=tool.version,
1059
+ description=tool.description,
1060
+ category=group,
1061
+ contract=resolved.model_dump(
1062
+ by_alias=True,
1063
+ exclude_none=True,
1064
+ mode="json",
1065
+ ),
1066
+ )
1067
+ if not tool.enabled:
1068
+ resource = resource.model_copy(update={"status": "unresolved"})
1069
+ yield resource
1070
+
1071
+ def _local_skills(self) -> Iterable[ResourceDescriptor]:
1072
+ root = self.workspace.resolve("capabilities/skills")
1073
+ for directory in sorted(root.iterdir()):
1074
+ if not directory.is_dir() or directory.is_symlink():
1075
+ continue
1076
+ manifest_path = directory / "skill.yaml"
1077
+ manifest = load_yaml_file(manifest_path) if manifest_path.is_file() else {}
1078
+ version = str(manifest.get("version") or "unversioned")
1079
+ status: Literal["ready", "unresolved"]
1080
+ try:
1081
+ resolved = self.resolver.resolve_skill(
1082
+ CapabilityRef(name=directory.name, version=version)
1083
+ )
1084
+ digest = str(resolved["digest"])
1085
+ status = "ready"
1086
+ except StudioError:
1087
+ digest = sha256_digest(directory.name.encode("utf-8"))
1088
+ status = "unresolved"
1089
+ yield ResourceDescriptor(
1090
+ resource_id=resource_id("skill", "local", directory.name, version),
1091
+ kind="skill",
1092
+ name=directory.name,
1093
+ display_name=str(
1094
+ manifest.get("displayName") or manifest.get("name") or directory.name
1095
+ ),
1096
+ version=version,
1097
+ digest=digest,
1098
+ source="local",
1099
+ status=cast(Any, status),
1100
+ description=str(manifest.get("description") or ""),
1101
+ category=str(manifest.get("category") or "general"),
1102
+ installed=True,
1103
+ contract={
1104
+ "name": directory.name,
1105
+ "version": version,
1106
+ "digest": digest,
1107
+ },
1108
+ )
1109
+
1110
+ def _persisted(self, directory: str) -> Iterable[ResourceDescriptor]:
1111
+ root = self.workspace.resolve(Path(".agentkit/catalog") / directory)
1112
+ for path in sorted(root.glob("*.yaml")):
1113
+ try:
1114
+ yield ResourceDescriptor.model_validate(load_yaml_file(path))
1115
+ except (StudioError, ValidationError):
1116
+ continue
1117
+
1118
+ def _persist_descriptor(
1119
+ self,
1120
+ directory: str,
1121
+ descriptor: ResourceDescriptor,
1122
+ *,
1123
+ overwrite: bool = False,
1124
+ ) -> ResourceDescriptor:
1125
+ target = self.workspace.resolve(
1126
+ Path(".agentkit/catalog")
1127
+ / directory
1128
+ / f"{resource_slug(descriptor.name)}-{resource_slug(descriptor.version)}.yaml"
1129
+ )
1130
+ if target.exists() and not overwrite:
1131
+ raise StudioError(
1132
+ "RESOURCE_ALREADY_EXISTS",
1133
+ "同名同版本资源已存在",
1134
+ status_code=409,
1135
+ details={"resourceId": descriptor.resource_id},
1136
+ )
1137
+ self.workspace.atomic_write_yaml(
1138
+ target,
1139
+ descriptor.model_dump(
1140
+ by_alias=True,
1141
+ exclude_none=True,
1142
+ mode="json",
1143
+ ),
1144
+ )
1145
+ return descriptor
1146
+
1147
+ def delete_resource(self, resource_id: str) -> None:
1148
+ """Remove a persisted catalog resource (model/tool/mcp/skill) by id."""
1149
+ descriptor = self.get(resource_id)
1150
+ kind = descriptor.kind
1151
+ if kind == "skill":
1152
+ # Skill 以目录形式安装在 capabilities/skills/{name}
1153
+ target_dir = self.workspace.resolve(Path("capabilities/skills") / descriptor.name)
1154
+ if target_dir.is_dir():
1155
+ shutil.rmtree(target_dir)
1156
+ return
1157
+ directory = {
1158
+ "model": "models",
1159
+ "mcp": "mcp",
1160
+ "tool": "tools",
1161
+ "tool-source": "tool-sources",
1162
+ }.get(kind, kind)
1163
+ target = self.workspace.resolve(
1164
+ Path(".agentkit/catalog")
1165
+ / directory
1166
+ / f"{resource_slug(descriptor.name)}-{resource_slug(descriptor.version)}.yaml"
1167
+ )
1168
+ if not target.exists():
1169
+ raise StudioError(
1170
+ "RESOURCE_NOT_FOUND",
1171
+ "资源的持久化文件不存在,无法删除",
1172
+ status_code=404,
1173
+ details={"resourceId": resource_id, "path": str(target)},
1174
+ )
1175
+ target.unlink()
1176
+
1177
+ def _descriptor(
1178
+ self,
1179
+ *,
1180
+ kind: str,
1181
+ name: str,
1182
+ display_name: str,
1183
+ version: str,
1184
+ description: str,
1185
+ category: str,
1186
+ contract: dict[str, Any],
1187
+ source: str = "local",
1188
+ required_secret_refs: builtins.list[str] | None = None,
1189
+ ) -> ResourceDescriptor:
1190
+ digest = str(contract.get("digest") or sha256_digest(canonical_json(contract)))
1191
+ return ResourceDescriptor(
1192
+ resource_id=resource_id(kind, source, name, version),
1193
+ kind=cast(Any, kind),
1194
+ name=name,
1195
+ display_name=display_name,
1196
+ version=version,
1197
+ digest=digest,
1198
+ source=cast(Any, source),
1199
+ status="ready",
1200
+ description=description,
1201
+ category=category,
1202
+ required_secret_refs=required_secret_refs or [],
1203
+ contract=contract,
1204
+ )
1205
+
1206
+ @staticmethod
1207
+ def _skill_entry(
1208
+ files: builtins.list[zipfile.ZipInfo],
1209
+ ) -> tuple[zipfile.ZipInfo, PurePosixPath]:
1210
+ candidates = [info for info in files if PurePosixPath(info.filename).name == "SKILL.md"]
1211
+ if len(candidates) != 1:
1212
+ raise StudioError(
1213
+ "SKILL_MANIFEST_REQUIRED",
1214
+ "Skill ZIP 必须且只能包含一个 SKILL.md",
1215
+ status_code=422,
1216
+ )
1217
+ path = PurePosixPath(candidates[0].filename)
1218
+ prefix = PurePosixPath(*path.parts[:-1])
1219
+ if len(prefix.parts) > 1:
1220
+ raise StudioError(
1221
+ "SKILL_ARCHIVE_INVALID",
1222
+ "SKILL.md 必须位于 ZIP 根目录或单一顶层目录",
1223
+ status_code=422,
1224
+ )
1225
+ return candidates[0], prefix
1226
+
1227
+ @staticmethod
1228
+ def _skill_frontmatter(content: str) -> dict[str, Any]:
1229
+ if not content.startswith("---\n"):
1230
+ raise StudioError(
1231
+ "SKILL_MANIFEST_INVALID",
1232
+ "SKILL.md 必须包含 YAML frontmatter",
1233
+ status_code=422,
1234
+ )
1235
+ parts = content.split("\n---\n", 1)
1236
+ if len(parts) != 2:
1237
+ raise StudioError(
1238
+ "SKILL_MANIFEST_INVALID",
1239
+ "SKILL.md frontmatter 未闭合",
1240
+ status_code=422,
1241
+ )
1242
+ try:
1243
+ payload = yaml.safe_load(parts[0][4:]) or {}
1244
+ except yaml.YAMLError as exc:
1245
+ raise StudioError(
1246
+ "SKILL_MANIFEST_INVALID",
1247
+ "SKILL.md frontmatter 无法解析",
1248
+ status_code=422,
1249
+ ) from exc
1250
+ if (
1251
+ not isinstance(payload, dict)
1252
+ or not payload.get("name")
1253
+ or not payload.get("description")
1254
+ ):
1255
+ raise StudioError(
1256
+ "SKILL_MANIFEST_INVALID",
1257
+ "SKILL.md frontmatter 必须包含 name 和 description",
1258
+ status_code=422,
1259
+ )
1260
+ return cast(dict[str, Any], payload)