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,629 @@
1
+ """Safe OpenAI-compatible model client for local Studio runs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import ipaddress
7
+ import os
8
+ import socket
9
+ from dataclasses import dataclass, field
10
+ from threading import RLock
11
+ from typing import Any, Awaitable, Callable
12
+ from urllib.parse import urlparse
13
+
14
+ import httpx
15
+
16
+ from ksadk.studio.contracts import NetworkPolicy, ResolvedModel, Usage
17
+ from ksadk.studio.errors import StudioError
18
+
19
+ _DENIED_METADATA_HOSTS = {
20
+ "169.254.169.254",
21
+ "metadata.google.internal",
22
+ "metadata.azure.internal",
23
+ "fd00:ec2::254",
24
+ }
25
+
26
+ #: 表示输出被 token 上限截断的 finishReason 集合(chat 与 Responses 两种 wire)。
27
+ _LENGTH_FINISH_REASONS = {"length", "incomplete", "max_output_tokens"}
28
+ #: length 截断重试时把 max_tokens 提到的目标值;未配置 max_tokens 的 profile
29
+ #: 首次截断重试也用该值兜底(authoring 输出完整 JSON 需要宽松上限)。
30
+ _LENGTH_RETRY_TARGET_TOKENS = 16384
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ToolCall:
35
+ id: str
36
+ name: str
37
+ arguments: str
38
+ raw: dict[str, Any] = field(default_factory=dict)
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class ModelResponse:
43
+ content: str
44
+ finish_reason: str
45
+ usage: Usage
46
+ tool_calls: list[ToolCall]
47
+ raw_message: dict[str, Any]
48
+
49
+
50
+ class CredentialResolver:
51
+ """Resolve immutable Secret references with a workspace-persisted overlay.
52
+
53
+ Resolution order: in-memory session values → workspace secrets file
54
+ (``.agentkit/secrets.env``) → process environment (with paired fallback).
55
+ """
56
+
57
+ _FALLBACK_PAIRS: tuple[tuple[str, str], ...] = (
58
+ ("AGENTKIT_MODEL_API_KEY", "OPENAI_API_KEY"),
59
+ ("OPENAI_API_KEY", "AGENTKIT_MODEL_API_KEY"),
60
+ )
61
+
62
+ def __init__(self, workspace: Any = None) -> None:
63
+ self._session_values: dict[str, bytearray] = {}
64
+ self._lock = RLock()
65
+ self._workspace = workspace
66
+ self._persisted: dict[str, str] | None = None
67
+
68
+ def _secrets_path(self) -> Any | None:
69
+ if self._workspace is None:
70
+ return None
71
+ try:
72
+ return self._workspace.resolve(".agentkit/secrets.env")
73
+ except Exception:
74
+ return None
75
+
76
+ def _load_persisted(self) -> dict[str, str]:
77
+ if self._persisted is not None:
78
+ return self._persisted
79
+ self._persisted = {}
80
+ path = self._secrets_path()
81
+ if path is not None and path.is_file():
82
+ try:
83
+ for line in path.read_text(encoding="utf-8").splitlines():
84
+ if not line or line.startswith("#") or "=" not in line:
85
+ continue
86
+ key, _, value = line.partition("=")
87
+ key = key.strip()
88
+ if key:
89
+ self._persisted[key] = value
90
+ except Exception:
91
+ pass
92
+ return self._persisted
93
+
94
+ def _write_persisted(self) -> None:
95
+ path = self._secrets_path()
96
+ if path is None:
97
+ return
98
+ lines = [f"{k}={v}" for k, v in sorted(self._persisted.items())]
99
+ try:
100
+ path.parent.mkdir(parents=True, exist_ok=True)
101
+ path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
102
+ try:
103
+ path.chmod(0o600)
104
+ except Exception:
105
+ pass
106
+ except Exception:
107
+ pass
108
+
109
+ @staticmethod
110
+ def _validate_name(name: str) -> str:
111
+ allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
112
+ if not name or any(char not in allowed for char in name):
113
+ raise StudioError(
114
+ "SECRET_REFERENCE_INVALID",
115
+ "环境变量 Secret 引用格式无效",
116
+ status_code=422,
117
+ field="credentialName",
118
+ )
119
+ return name
120
+
121
+ @classmethod
122
+ def _environment_name(cls, reference: str) -> str:
123
+ if not reference.startswith("env://"):
124
+ raise StudioError(
125
+ "SECRET_BACKEND_UNAVAILABLE",
126
+ "当前本地 Runtime 仅支持 env:// Secret 引用",
127
+ status_code=501,
128
+ details={"scheme": reference.partition("://")[0]},
129
+ )
130
+ return cls._validate_name(reference.removeprefix("env://"))
131
+
132
+ @staticmethod
133
+ def _zero(value: bytearray | None) -> None:
134
+ if value is not None:
135
+ value[:] = b"\x00" * len(value)
136
+
137
+ def put_session(self, name: str, value: str) -> dict[str, str | bool]:
138
+ name = self._validate_name(name)
139
+ if (
140
+ not value
141
+ or len(value) > 16_384
142
+ or value != value.strip()
143
+ or any(ord(char) < 32 or ord(char) == 127 for char in value)
144
+ ):
145
+ raise StudioError(
146
+ "SECRET_VALUE_INVALID",
147
+ "凭证不能为空、包含控制字符或超过 16 KiB",
148
+ status_code=422,
149
+ field="value",
150
+ )
151
+ encoded = bytearray(value.encode("utf-8"))
152
+ with self._lock:
153
+ previous = self._session_values.get(name)
154
+ self._session_values[name] = encoded
155
+ self._zero(previous)
156
+ if self._secrets_path() is not None:
157
+ persisted = self._load_persisted()
158
+ if persisted.get(name) != value:
159
+ persisted[name] = value
160
+ self._write_persisted()
161
+ return self.status(f"env://{name}")
162
+
163
+ def delete_session(self, name: str) -> dict[str, str | bool]:
164
+ name = self._validate_name(name)
165
+ with self._lock:
166
+ previous = self._session_values.pop(name, None)
167
+ self._zero(previous)
168
+ if self._secrets_path() is not None:
169
+ persisted = self._load_persisted()
170
+ if name in persisted:
171
+ persisted.pop(name, None)
172
+ self._write_persisted()
173
+ return self.status(f"env://{name}")
174
+
175
+ def clear_session(self) -> None:
176
+ with self._lock:
177
+ values = list(self._session_values.values())
178
+ self._session_values.clear()
179
+ for value in values:
180
+ self._zero(value)
181
+
182
+ def _fallback_name(self, name: str) -> str | None:
183
+ for primary, alternate in self._FALLBACK_PAIRS:
184
+ if name == primary and alternate != primary:
185
+ return alternate
186
+ return None
187
+
188
+ def _resolve_source(self, name: str) -> tuple[bool, str]:
189
+ """Return (configured, source) considering session, persisted file, env, and fallback."""
190
+ fallback = self._fallback_name(name)
191
+ with self._lock:
192
+ if name in self._session_values:
193
+ return True, "session"
194
+ if fallback and fallback in self._session_values:
195
+ return True, "session-alias"
196
+ persisted = self._load_persisted()
197
+ if name in persisted:
198
+ return True, "workspace"
199
+ if fallback and fallback in persisted:
200
+ return True, "workspace-alias"
201
+ if os.environ.get(name):
202
+ return True, "environment"
203
+ if fallback and os.environ.get(fallback):
204
+ return True, "fallback"
205
+ return False, "missing"
206
+
207
+ def _session_value(self, name: str) -> str | None:
208
+ with self._lock:
209
+ value = self._session_values.get(name)
210
+ return value.decode("utf-8") if value is not None else None
211
+
212
+ def status(self, reference: str) -> dict[str, str | bool]:
213
+ name = self._environment_name(reference)
214
+ configured, source = self._resolve_source(name)
215
+ return {
216
+ "reference": reference,
217
+ "name": name,
218
+ "configured": configured,
219
+ "source": source,
220
+ "persistence": source,
221
+ }
222
+
223
+ def resolve(self, reference: str) -> str:
224
+ name = self._environment_name(reference)
225
+ fallback = self._fallback_name(name)
226
+ aliases = [name, *([fallback] if fallback else [])]
227
+ for candidate in aliases:
228
+ value = self._session_value(candidate)
229
+ if value is not None:
230
+ return value
231
+ persisted = self._load_persisted()
232
+ for candidate in aliases:
233
+ if candidate in persisted:
234
+ return persisted[candidate]
235
+ for candidate in aliases:
236
+ value = os.environ.get(candidate)
237
+ if value:
238
+ return value
239
+ raise StudioError(
240
+ "SECRET_NOT_FOUND",
241
+ "模型凭证尚未配置",
242
+ status_code=422,
243
+ details={"reference": reference},
244
+ )
245
+
246
+ def exists(self, reference: str) -> bool:
247
+ try:
248
+ return bool(self.status(reference)["configured"])
249
+ except StudioError:
250
+ return False
251
+
252
+
253
+ class NetworkGuard:
254
+ async def check(self, endpoint_url: str, policy: NetworkPolicy) -> None:
255
+ parsed = urlparse(endpoint_url)
256
+ host = (parsed.hostname or "").lower().rstrip(".")
257
+ if parsed.scheme not in {"http", "https"} or not host:
258
+ raise StudioError(
259
+ "MODEL_ENDPOINT_INVALID",
260
+ "模型地址必须是有效的 HTTP(S) URL",
261
+ status_code=422,
262
+ )
263
+ allowed = {value.lower().rstrip(".") for value in policy.allowed_hosts}
264
+ if policy.mode == "restricted" and host not in allowed:
265
+ raise StudioError(
266
+ "NETWORK_TARGET_DENIED",
267
+ "目标 hostname 不在网络允许清单中",
268
+ status_code=403,
269
+ details={"host": host},
270
+ )
271
+ if host in _DENIED_METADATA_HOSTS:
272
+ raise self._denied(host)
273
+ port = parsed.port or (443 if parsed.scheme == "https" else 80)
274
+ try:
275
+ records = await asyncio.to_thread(
276
+ socket.getaddrinfo,
277
+ host,
278
+ port,
279
+ 0,
280
+ socket.SOCK_STREAM,
281
+ )
282
+ except socket.gaierror as exc:
283
+ raise StudioError(
284
+ "NETWORK_TARGET_UNRESOLVED",
285
+ "模型 hostname 无法解析",
286
+ status_code=422,
287
+ details={"host": host},
288
+ ) from exc
289
+ for record in records:
290
+ address = str(record[4][0]).split("%", 1)[0]
291
+ ip = ipaddress.ip_address(address)
292
+ if str(ip) in _DENIED_METADATA_HOSTS or ip.is_link_local or ip.is_multicast:
293
+ raise self._denied(host)
294
+ if (
295
+ ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_unspecified
296
+ ) and not policy.allow_private_network:
297
+ raise self._denied(host)
298
+
299
+ @staticmethod
300
+ def _denied(host: str) -> StudioError:
301
+ return StudioError(
302
+ "NETWORK_TARGET_DENIED",
303
+ "目标地址不满足本地 Runtime 网络策略",
304
+ status_code=403,
305
+ details={"host": host},
306
+ )
307
+
308
+
309
+ class OpenAICompatibleModelClient:
310
+ def __init__(
311
+ self,
312
+ *,
313
+ credential_resolver: CredentialResolver | None = None,
314
+ network_guard: NetworkGuard | None = None,
315
+ transport: httpx.AsyncBaseTransport | None = None,
316
+ sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
317
+ ) -> None:
318
+ self.credential_resolver = credential_resolver or CredentialResolver()
319
+ self.network_guard = network_guard or NetworkGuard()
320
+ self.transport = transport
321
+ self.sleep = sleep
322
+
323
+ async def complete(
324
+ self,
325
+ model: ResolvedModel,
326
+ *,
327
+ messages: list[dict[str, Any]],
328
+ network_policy: NetworkPolicy,
329
+ timeout_seconds: int,
330
+ max_attempts: int,
331
+ backoff_seconds: float,
332
+ tools: list[dict[str, Any]] | None = None,
333
+ allow_empty: bool = False,
334
+ response_format: dict[str, Any] | None = None,
335
+ retry_on_length: bool = False,
336
+ ) -> ModelResponse:
337
+ await self.network_guard.check(model.endpoint_url, network_policy)
338
+ credential = self.credential_resolver.resolve(model.credential_ref)
339
+ wire_api = (model.wire_api or "chat").strip().lower()
340
+ payload: dict[str, Any]
341
+ if wire_api == "responses":
342
+ payload = self._responses_payload(model, messages, tools)
343
+ else:
344
+ payload = {
345
+ "model": model.model,
346
+ "messages": messages,
347
+ "stream": False,
348
+ }
349
+ # 未显式配置的采样参数一律不携带字段,交给服务端默认,
350
+ # 避免触碰各模型族的硬约束(kimi 温度、各家 max_tokens 上限等)。
351
+ if model.parameters.temperature is not None:
352
+ payload["temperature"] = model.parameters.temperature
353
+ if model.parameters.max_tokens is not None:
354
+ payload["max_tokens"] = model.parameters.max_tokens
355
+ if model.parameters.top_p is not None:
356
+ payload["top_p"] = model.parameters.top_p
357
+ if tools:
358
+ payload["tools"] = tools
359
+ payload["tool_choice"] = "auto"
360
+ if response_format and model.parameters.allow_json_response_format:
361
+ payload["response_format"] = response_format
362
+ headers = {
363
+ "Authorization": f"Bearer {credential}",
364
+ "Content-Type": "application/json",
365
+ }
366
+ timeout = httpx.Timeout(timeout_seconds, connect=min(10, timeout_seconds))
367
+ dropped_response_format = False
368
+ length_retried = False
369
+ last_length_error: StudioError | None = None
370
+ async with httpx.AsyncClient(
371
+ transport=self.transport,
372
+ timeout=timeout,
373
+ follow_redirects=False,
374
+ ) as client:
375
+ # 额外 1 次迭代仅用于 response_format 400 降级重发,
376
+ # 其余失败路径仍受 max_attempts 约束(会在原上限处 raise)。
377
+ for attempt in range(1, max_attempts + 2):
378
+ try:
379
+ response = await client.post(
380
+ model.endpoint_url,
381
+ headers=headers,
382
+ json=payload,
383
+ )
384
+ except (httpx.TimeoutException, httpx.TransportError) as exc:
385
+ if attempt >= max_attempts:
386
+ raise StudioError(
387
+ "MODEL_REQUEST_FAILED",
388
+ "模型请求网络失败",
389
+ status_code=502,
390
+ details={"attempts": attempt, "errorType": type(exc).__name__},
391
+ ) from exc
392
+ await self.sleep(backoff_seconds * attempt)
393
+ continue
394
+ if response.is_redirect:
395
+ raise StudioError(
396
+ "NETWORK_TARGET_DENIED",
397
+ "模型 endpoint 不允许重定向",
398
+ status_code=403,
399
+ details={"statusCode": response.status_code},
400
+ )
401
+ if response.status_code == 429 or response.status_code >= 500:
402
+ if attempt < max_attempts:
403
+ await self.sleep(backoff_seconds * attempt)
404
+ continue
405
+ if (
406
+ response.status_code == 400
407
+ and "response_format" in payload
408
+ and not dropped_response_format
409
+ ):
410
+ # 网关不支持 response_format:去掉该字段重发一次。
411
+ dropped_response_format = True
412
+ payload.pop("response_format")
413
+ continue
414
+ if response.status_code >= 400:
415
+ if response.status_code == 429:
416
+ # A selected authoring profile must not silently fall
417
+ # back to a different model. Preserve the actual
418
+ # upstream condition so Studio can offer the user a
419
+ # useful retry/switch decision instead of reporting a
420
+ # misleading generic 502.
421
+ raise StudioError(
422
+ "MODEL_RATE_LIMITED",
423
+ "所选生成模型当前限流,请稍后重试或切换模型 Profile",
424
+ status_code=429,
425
+ details={"upstreamStatus": response.status_code},
426
+ )
427
+ upstream_detail = ""
428
+ try:
429
+ upstream_detail = response.text[:200]
430
+ except Exception: # noqa: BLE001 - 诊断信息尽力而为
431
+ upstream_detail = ""
432
+ raise StudioError(
433
+ "MODEL_REQUEST_FAILED",
434
+ "模型服务返回错误",
435
+ status_code=502,
436
+ details={
437
+ "upstreamStatus": response.status_code,
438
+ "upstreamError": upstream_detail,
439
+ },
440
+ )
441
+ try:
442
+ if wire_api == "responses":
443
+ parsed = self._parse_responses_response(response, allow_empty=allow_empty)
444
+ else:
445
+ parsed = self._parse_response(response, allow_empty=allow_empty)
446
+ except StudioError as exc:
447
+ # finishReason=length 的空响应(大 JSON 被 max_tokens 截断):
448
+ # 一次性扩容 max_tokens 重发;再次截断则按原错误上抛。
449
+ if retry_on_length and not length_retried and self._is_length_truncation(exc):
450
+ length_retried = True
451
+ last_length_error = exc
452
+ payload = self._expand_length_budget(payload)
453
+ continue
454
+ raise
455
+ if (
456
+ retry_on_length
457
+ and not length_retried
458
+ and parsed.finish_reason in _LENGTH_FINISH_REASONS
459
+ ):
460
+ # 有内容但被 length 截断(结构化输出必然残缺),同样扩容重发一次。
461
+ length_retried = True
462
+ payload = self._expand_length_budget(payload)
463
+ continue
464
+ return parsed
465
+ if last_length_error is not None:
466
+ raise last_length_error
467
+ raise AssertionError("unreachable")
468
+
469
+ @staticmethod
470
+ def _is_length_truncation(exc: StudioError) -> bool:
471
+ if exc.code != "MODEL_EMPTY_RESPONSE":
472
+ return False
473
+ return str(exc.details.get("finishReason") or "") in _LENGTH_FINISH_REASONS
474
+
475
+ @staticmethod
476
+ def _expand_length_budget(payload: dict[str, Any]) -> dict[str, Any]:
477
+ key = "max_output_tokens" if "max_output_tokens" in payload else "max_tokens"
478
+ current = int(payload.get(key) or 0)
479
+ payload[key] = max(current, _LENGTH_RETRY_TARGET_TOKENS)
480
+ return payload
481
+
482
+ @staticmethod
483
+ def _responses_payload(
484
+ model: ResolvedModel,
485
+ messages: list[dict[str, Any]],
486
+ tools: list[dict[str, Any]] | None,
487
+ ) -> dict[str, Any]:
488
+ """把 chat 语义消息映射到 Responses API 最小可用载荷。"""
489
+ if tools:
490
+ raise StudioError(
491
+ "MODEL_REQUEST_FAILED",
492
+ "Responses 端点暂不支持 tools 参数",
493
+ status_code=422,
494
+ )
495
+ instructions: list[str] = []
496
+ items: list[dict[str, Any]] = []
497
+ for message in messages:
498
+ role = str(message.get("role") or "user")
499
+ text = str(message.get("content") or "")
500
+ if role == "system":
501
+ instructions.append(text)
502
+ continue
503
+ part_type = "output_text" if role == "assistant" else "input_text"
504
+ items.append({"role": role, "content": [{"type": part_type, "text": text}]})
505
+ payload: dict[str, Any] = {
506
+ "model": model.model,
507
+ "input": items,
508
+ }
509
+ if model.parameters.max_tokens is not None:
510
+ payload["max_output_tokens"] = model.parameters.max_tokens
511
+ if instructions:
512
+ payload["instructions"] = "\n\n".join(instructions)
513
+ return payload
514
+
515
+ @staticmethod
516
+ def _parse_responses_response(
517
+ response: httpx.Response, allow_empty: bool = False
518
+ ) -> ModelResponse:
519
+ if len(response.content) > 16 * 1024 * 1024:
520
+ raise StudioError(
521
+ "MODEL_RESPONSE_TOO_LARGE",
522
+ "模型响应超过 16 MiB 限制",
523
+ status_code=502,
524
+ )
525
+ try:
526
+ payload = response.json()
527
+ except ValueError as exc:
528
+ raise StudioError(
529
+ "MODEL_RESPONSE_INVALID",
530
+ "模型响应不符合 Responses 协议",
531
+ status_code=502,
532
+ ) from exc
533
+ content = payload.get("output_text")
534
+ if not isinstance(content, str) or not content:
535
+ parts: list[str] = []
536
+ for item in payload.get("output") or []:
537
+ if not isinstance(item, dict):
538
+ continue
539
+ for part in item.get("content") or []:
540
+ if isinstance(part, dict) and part.get("type") in {"output_text", "text"}:
541
+ parts.append(str(part.get("text") or ""))
542
+ content = "".join(parts)
543
+ status = str(payload.get("status") or "")
544
+ if not content and not allow_empty:
545
+ raise StudioError(
546
+ "MODEL_EMPTY_RESPONSE",
547
+ "模型未返回可用内容",
548
+ status_code=502,
549
+ details={"status": status, "finishReason": status},
550
+ )
551
+ raw_usage = payload.get("usage") or {}
552
+ usage = Usage(
553
+ input_tokens=int(raw_usage.get("input_tokens") or 0),
554
+ output_tokens=int(raw_usage.get("output_tokens") or 0),
555
+ total_tokens=int(raw_usage.get("total_tokens") or 0),
556
+ cached_input_tokens=int(
557
+ (raw_usage.get("input_tokens_details") or {}).get("cached_tokens") or 0
558
+ ),
559
+ reasoning_output_tokens=int(
560
+ (raw_usage.get("output_tokens_details") or {}).get("reasoning_tokens") or 0
561
+ ),
562
+ )
563
+ return ModelResponse(
564
+ content=content,
565
+ finish_reason="stop" if status == "completed" else status,
566
+ usage=usage,
567
+ tool_calls=[],
568
+ raw_message={"output": payload.get("output")},
569
+ )
570
+
571
+ @staticmethod
572
+ def _parse_response(response: httpx.Response, allow_empty: bool = False) -> ModelResponse:
573
+ if len(response.content) > 16 * 1024 * 1024:
574
+ raise StudioError(
575
+ "MODEL_RESPONSE_TOO_LARGE",
576
+ "模型响应超过 16 MiB 限制",
577
+ status_code=502,
578
+ )
579
+ try:
580
+ payload = response.json()
581
+ choice = payload["choices"][0]
582
+ message = choice["message"]
583
+ except (ValueError, KeyError, IndexError, TypeError) as exc:
584
+ raise StudioError(
585
+ "MODEL_RESPONSE_INVALID",
586
+ "模型响应不符合 OpenAI-compatible 协议",
587
+ status_code=502,
588
+ ) from exc
589
+ calls = []
590
+ for raw in message.get("tool_calls") or []:
591
+ function = raw.get("function") or {}
592
+ calls.append(
593
+ ToolCall(
594
+ id=str(raw.get("id") or ""),
595
+ name=str(function.get("name") or ""),
596
+ arguments=str(function.get("arguments") or "{}"),
597
+ raw=raw,
598
+ )
599
+ )
600
+ content = str(message.get("content") or "")
601
+ finish_reason = str(choice.get("finish_reason") or "")
602
+ if not content and not calls and not allow_empty:
603
+ raise StudioError(
604
+ "MODEL_EMPTY_RESPONSE",
605
+ "模型未返回可用内容",
606
+ status_code=502,
607
+ details={"finishReason": finish_reason},
608
+ )
609
+ raw_usage = payload.get("usage") or {}
610
+ usage = Usage(
611
+ input_tokens=int(raw_usage.get("prompt_tokens") or 0),
612
+ output_tokens=int(raw_usage.get("completion_tokens") or 0),
613
+ total_tokens=int(raw_usage.get("total_tokens") or 0),
614
+ cached_input_tokens=int(
615
+ (raw_usage.get("prompt_tokens_details") or {}).get("cached_tokens") or 0
616
+ ),
617
+ reasoning_output_tokens=int(
618
+ (raw_usage.get("completion_tokens_details") or {}).get("reasoning_tokens") or 0
619
+ ),
620
+ reported=bool(raw_usage),
621
+ source="model-provider" if raw_usage else None,
622
+ )
623
+ return ModelResponse(
624
+ content=content,
625
+ finish_reason=finish_reason,
626
+ usage=usage,
627
+ tool_calls=calls,
628
+ raw_message=message,
629
+ )