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
ksadk/tracing/setup.py CHANGED
@@ -1,11 +1,15 @@
1
1
  """
2
- Tracing 初始化 - 支持多 Exporter (InMemory + Langfuse + OTLP)
2
+ Tracing 初始化 - 标准 OTLP 双写(Langfuse 主 + CloudMonitor 次)
3
+
4
+ 每个 Python Agent 进程一个 TracerProvider,挂两个 OTLP/HTTP protobuf BatchSpanProcessor,
5
+ 消费同一批 ReadableSpan。仅为 CloudMonitor 克隆并补充兼容属性,不修改原始 span。
6
+
3
7
  支持 ADK 自动插桩 via openinference-instrumentation-google-adk
4
8
  """
5
9
 
6
10
  import atexit
7
- import base64
8
11
  import importlib
12
+ import json
9
13
  import logging
10
14
  import os
11
15
  from collections.abc import Callable, Sequence
@@ -13,6 +17,7 @@ from dataclasses import dataclass
13
17
  from typing import Any, Optional
14
18
  from urllib.parse import unquote
15
19
 
20
+ from opentelemetry.sdk.resources import Resource
16
21
  from opentelemetry.sdk.trace import ReadableSpan
17
22
  from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
18
23
 
@@ -20,6 +25,25 @@ from ksadk.tracing.exporters.inmemory_exporter import InMemoryExporter
20
25
 
21
26
  logger = logging.getLogger(__name__)
22
27
 
28
+ _CLOUD_MONITOR_RESOURCE_SPAN_ATTRIBUTES = (
29
+ "agentengine.account_id",
30
+ "agentengine.agent_id",
31
+ "agentengine.framework",
32
+ "agentengine.langfuse_project_id",
33
+ "agentengine.region",
34
+ )
35
+
36
+ _LANGFUSE_OBSERVATION_TYPES = {
37
+ "LLM": "generation",
38
+ "EMBEDDING": "embedding",
39
+ "CHAIN": "chain",
40
+ "AGENT": "agent",
41
+ "TOOL": "tool",
42
+ "RETRIEVER": "retriever",
43
+ "EVALUATOR": "evaluator",
44
+ "GUARDRAIL": "guardrail",
45
+ }
46
+
23
47
 
24
48
  def _batch_span_processor_kwargs() -> dict:
25
49
  """BatchSpanProcessor 参数:限制单次 export 体积,避免 OTLP collector 413。
@@ -41,9 +65,9 @@ def _batch_span_processor_kwargs() -> dict:
41
65
 
42
66
 
43
67
  _exporter_instance: Optional[InMemoryExporter] = None
44
- _langfuse_exporter: Optional[Any] = None
45
68
  _tracing_initialized: bool = False
46
69
  _adk_instrumented: bool = False
70
+ _managed_span_processors: list[Any] = []
47
71
 
48
72
 
49
73
  @dataclass(frozen=True)
@@ -55,7 +79,7 @@ class _OtlpHttpConfig:
55
79
 
56
80
 
57
81
  class _LoggingSpanExporter(SpanExporter):
58
- """Small delegating exporter that logs external trace export flow."""
82
+ """Delegating exporter that logs external trace export flow."""
59
83
 
60
84
  def __init__(
61
85
  self,
@@ -137,12 +161,7 @@ def _span_context(span: Any) -> Any:
137
161
  if context is not None:
138
162
  return context
139
163
  get_context = getattr(span, "get_span_context", None)
140
- if callable(get_context):
141
- try:
142
- return get_context()
143
- except Exception:
144
- return None
145
- return None
164
+ return get_context() if callable(get_context) else None
146
165
 
147
166
 
148
167
  def _span_id(span: Any) -> Optional[int]:
@@ -159,230 +178,244 @@ def _parent_span_id(span: Any) -> Optional[int]:
159
178
 
160
179
  def _span_token_usage(span: Any) -> dict[str, int]:
161
180
  attributes = getattr(span, "attributes", None) or {}
181
+ return _token_usage(attributes)
182
+
183
+
184
+ def _token_usage(attributes: dict[str, Any]) -> dict[str, int]:
185
+ input_tokens = _coerce_int(
186
+ attributes.get("gen_ai.usage.input_tokens")
187
+ or attributes.get("llm.usage.prompt_tokens")
188
+ or attributes.get("llm.token_count.prompt")
189
+ )
190
+ output_tokens = _coerce_int(
191
+ attributes.get("gen_ai.usage.output_tokens")
192
+ or attributes.get("llm.usage.completion_tokens")
193
+ or attributes.get("llm.token_count.completion")
194
+ )
195
+ total_tokens = _coerce_int(
196
+ attributes.get("gen_ai.usage.total_tokens")
197
+ or attributes.get("llm.usage.total_tokens")
198
+ or attributes.get("llm.token_count.total")
199
+ )
162
200
  return {
163
- "input": _coerce_int(attributes.get("gen_ai.usage.input_tokens")),
164
- "output": _coerce_int(attributes.get("gen_ai.usage.output_tokens")),
165
- "cache_read": _coerce_int(attributes.get("gen_ai.usage.cache_read.input_tokens")),
166
- "reasoning_output": _coerce_int(attributes.get("gen_ai.usage.reasoning.output_tokens")),
201
+ "input": input_tokens,
202
+ "output": output_tokens,
203
+ "total": total_tokens or input_tokens + output_tokens,
204
+ "cache_read": _coerce_int(
205
+ attributes.get("gen_ai.usage.cache_read.input_tokens")
206
+ or attributes.get("llm.usage.cache_read.input_tokens")
207
+ ),
208
+ "reasoning_output": _coerce_int(
209
+ attributes.get("gen_ai.usage.reasoning.output_tokens")
210
+ or attributes.get("llm.usage.reasoning_tokens")
211
+ ),
167
212
  }
168
213
 
169
214
 
170
- def _has_nonzero_token_usage(usage: dict[str, int]) -> bool:
171
- return any(value > 0 for value in usage.values())
215
+ def _first_span_attribute(attributes: dict[str, Any], *keys: str) -> Any:
216
+ for key in keys:
217
+ value = attributes.get(key)
218
+ if value not in (None, ""):
219
+ return value
220
+ return None
172
221
 
173
222
 
174
- def _instrumentation_scope_name(span: Any) -> str:
175
- scope = getattr(span, "instrumentation_scope", None)
176
- name = getattr(scope, "name", None)
177
- if name:
178
- return str(name)
179
- info = getattr(span, "instrumentation_info", None)
180
- name = getattr(info, "name", None)
181
- return str(name or "")
223
+ def _add_langfuse_compatibility_attributes(attributes: dict[str, Any]) -> None:
224
+ usage = _token_usage(attributes)
225
+ observation_kind = str(attributes.get("openinference.span.kind") or "").upper()
226
+ observation_type = _LANGFUSE_OBSERVATION_TYPES.get(observation_kind)
227
+ if observation_type is None:
228
+ observation_type = "generation" if _has_nonzero_token_usage(usage) else "span"
229
+ attributes.setdefault("langfuse.observation.type", observation_type)
182
230
 
231
+ provider = _first_span_attribute(
232
+ attributes,
233
+ "gen_ai.provider.name",
234
+ "gen_ai.system",
235
+ "llm.provider",
236
+ )
237
+ if provider is not None:
238
+ attributes.setdefault("langfuse.observation.metadata.ls_provider", provider)
183
239
 
184
- def _clone_span_with_attributes(span: Any, attributes: dict[str, Any]) -> Any:
185
- try:
186
- from opentelemetry.sdk.trace import ReadableSpan
187
-
188
- return ReadableSpan(
189
- name=span.name,
190
- context=span.context,
191
- parent=span.parent,
192
- resource=span.resource,
193
- attributes=attributes,
194
- events=span.events,
195
- links=span.links,
196
- kind=span.kind,
197
- instrumentation_info=getattr(span, "instrumentation_info", None),
198
- status=span.status,
199
- start_time=span.start_time,
200
- end_time=span.end_time,
201
- instrumentation_scope=getattr(span, "instrumentation_scope", None),
240
+ model = _first_span_attribute(
241
+ attributes,
242
+ "gen_ai.request.model",
243
+ "llm.model_name",
244
+ )
245
+ if model is not None:
246
+ attributes.setdefault("langfuse.observation.model.name", model)
247
+ attributes.setdefault("langfuse.observation.metadata.ls_model_name", model)
248
+
249
+ if _has_nonzero_token_usage(usage):
250
+ attributes.setdefault("gen_ai.usage.total_tokens", usage["total"])
251
+ usage_details = {
252
+ "input": usage["input"],
253
+ "output": usage["output"],
254
+ "total": usage["total"],
255
+ "input_cache_read": usage["cache_read"],
256
+ }
257
+ attributes["langfuse.observation.usage_details"] = json.dumps(
258
+ usage_details, separators=(",", ":"), sort_keys=True
202
259
  )
203
- except Exception:
204
- logger.debug("Failed to clone span for CloudMonitor token compatibility", exc_info=True)
205
- return span
206
260
 
207
261
 
208
- def _prepare_langfuse_spans(spans: Any) -> Any:
209
- """Keep ksadk response usage authoritative for Langfuse exports.
210
-
211
- OpenInference LangChain instrumentation can emit llm.token_count.* on nested
212
- ChatOpenAI spans. In LangGraph streaming these counts can be cumulative and
213
- disagree with the usage returned by ksadk to RunAgent/session state. When a
214
- trace already has a ksadk.conversations span with gen_ai.usage.*, strip token
215
- attrs from nested OpenInference LangChain spans before exporting to
216
- Langfuse. The child spans remain useful for timing and structure, but cannot
217
- pollute token accounting.
218
- """
219
- if not spans:
220
- return spans
221
- span_list = list(spans)
222
- authoritative_trace_ids = {
223
- _trace_id(span)
224
- for span in span_list
225
- if _trace_id(span) is not None
226
- and _instrumentation_scope_name(span) == "ksadk.conversations"
227
- and _has_nonzero_token_usage(_span_token_usage(span))
228
- }
229
- if not authoritative_trace_ids:
230
- return spans
231
-
232
- strip_prefixes = ("llm.token_count.", "llm.usage.", "gen_ai.usage.")
233
- replacements: dict[int, Any] = {}
234
- for span in span_list:
235
- if _trace_id(span) not in authoritative_trace_ids:
236
- continue
237
- if _instrumentation_scope_name(span) != "openinference.instrumentation.langchain":
238
- continue
239
- attributes = dict(getattr(span, "attributes", None) or {})
240
- stripped = {
241
- key: value
242
- for key, value in attributes.items()
243
- if not any(str(key).startswith(prefix) for prefix in strip_prefixes)
244
- }
245
- if len(stripped) != len(attributes):
246
- if str(stripped.get("openinference.span.kind") or "").upper() == "LLM":
247
- stripped["openinference.span.kind"] = "CHAIN"
248
- replacements[id(span)] = _clone_span_with_attributes(span, stripped)
262
+ def _has_nonzero_token_usage(usage: dict[str, int]) -> bool:
263
+ return any(value > 0 for value in usage.values())
249
264
 
250
- if not replacements:
251
- return spans
252
- return [replacements.get(id(span), span) for span in span_list]
253
265
 
266
+ def _clone_cloud_monitor_resource(resource: Resource | None) -> Resource | None:
267
+ if resource is None:
268
+ return None
269
+ attributes = dict(resource.attributes)
270
+ agent_id = str(attributes.get("agentengine.agent_id") or "").strip()
271
+ if not agent_id or attributes.get("service.name") == agent_id:
272
+ return resource
273
+ attributes["service.name"] = agent_id
274
+ return Resource(attributes, schema_url=resource.schema_url)
275
+
276
+
277
+ def _clone_span(
278
+ span: ReadableSpan,
279
+ *,
280
+ attributes: dict[str, Any],
281
+ resource: Resource | None,
282
+ ) -> ReadableSpan:
283
+ return ReadableSpan(
284
+ name=span.name,
285
+ context=span.context,
286
+ parent=span.parent,
287
+ resource=resource,
288
+ attributes=attributes,
289
+ events=span.events,
290
+ links=span.links,
291
+ kind=span.kind,
292
+ status=span.status,
293
+ start_time=span.start_time,
294
+ end_time=span.end_time,
295
+ instrumentation_scope=getattr(span, "instrumentation_scope", None),
296
+ )
254
297
 
255
- def _prepare_cloud_monitor_spans(spans: Any) -> Any:
256
- """Add root token rollups for CloudMonitor without changing other exporters.
257
298
 
258
- CloudMonitor already parses gen_ai.usage.* on LLM child spans, but its root-span
259
- and overview APIs currently use root-level token fields. ADK/OpenInference often
260
- emits token usage on nested LLM spans only, and sometimes duplicates the same LLM
261
- usage on a parent and a child span. To avoid double counting, aggregate only
262
- token-bearing leaf spans and copy the result to the root span for this exporter.
263
- """
299
+ def _prepare_cloud_monitor_spans(
300
+ spans: Sequence[ReadableSpan],
301
+ ) -> Sequence[ReadableSpan]:
302
+ """Clone CloudMonitor spans with resource identity and root usage compatibility."""
264
303
  if not spans:
265
304
  return spans
266
305
  span_list = list(spans)
267
- span_ids = {_span_id(span) for span in span_list if _span_id(span) is not None}
268
- if not span_ids:
269
- return spans
270
-
271
- children_by_parent: dict[tuple[Optional[int], int], list[Any]] = {}
272
- roots: list[Any] = []
306
+ session_ids_by_trace: dict[Optional[int], Any] = {}
307
+ for span in span_list:
308
+ attributes = dict(getattr(span, "attributes", None) or {})
309
+ session_id = _first_span_attribute(
310
+ attributes,
311
+ "session.id",
312
+ "langfuse.session.id",
313
+ "ksadk.session_id",
314
+ )
315
+ if session_id is not None:
316
+ session_ids_by_trace.setdefault(_trace_id(span), session_id)
317
+ span_keys = {
318
+ (_trace_id(span), _span_id(span)) for span in span_list if _span_id(span) is not None
319
+ }
320
+ children_by_parent: dict[tuple[Optional[int], int], list[ReadableSpan]] = {}
321
+ roots: list[ReadableSpan] = []
273
322
  for span in span_list:
274
- sid = _span_id(span)
275
- if sid is None:
323
+ span_id = _span_id(span)
324
+ if span_id is None:
276
325
  continue
277
- parent_id = _parent_span_id(span)
278
326
  trace_id = _trace_id(span)
279
- if parent_id is None or parent_id not in span_ids:
327
+ parent_id = _parent_span_id(span)
328
+ if parent_id is None or (trace_id, parent_id) not in span_keys:
280
329
  roots.append(span)
281
330
  else:
282
331
  children_by_parent.setdefault((trace_id, parent_id), []).append(span)
283
332
 
284
- token_usage_by_id = {
333
+ usage_by_span = {
285
334
  (_trace_id(span), _span_id(span)): _span_token_usage(span)
286
335
  for span in span_list
287
336
  if _span_id(span) is not None
288
337
  }
289
-
290
338
  has_token_descendant_cache: dict[tuple[Optional[int], int], bool] = {}
291
339
 
292
- def has_token_descendant(span: Any) -> bool:
293
- sid = _span_id(span)
294
- if sid is None:
340
+ def has_token_descendant(span: ReadableSpan) -> bool:
341
+ key = (_trace_id(span), _span_id(span))
342
+ if key[1] is None:
295
343
  return False
296
- key = (_trace_id(span), sid)
297
344
  if key in has_token_descendant_cache:
298
345
  return has_token_descendant_cache[key]
299
- for child in children_by_parent.get(key, []):
300
- child_sid = _span_id(child)
301
- if child_sid is None:
302
- continue
303
- child_key = (_trace_id(child), child_sid)
304
- if _has_nonzero_token_usage(token_usage_by_id.get(child_key, {})):
305
- has_token_descendant_cache[key] = True
306
- return True
307
- if has_token_descendant(child):
308
- has_token_descendant_cache[key] = True
309
- return True
310
- has_token_descendant_cache[key] = False
311
- return False
312
-
313
- def collect_leaf_usage(span: Any, usage: dict[str, int]) -> None:
314
- sid = _span_id(span)
315
- if sid is None:
316
- return
317
- key = (_trace_id(span), sid)
318
- own_usage = token_usage_by_id.get(key, {})
346
+ result = any(
347
+ _has_nonzero_token_usage(usage_by_span.get((_trace_id(child), _span_id(child)), {}))
348
+ or has_token_descendant(child)
349
+ for child in children_by_parent.get(key, [])
350
+ )
351
+ has_token_descendant_cache[key] = result
352
+ return result
353
+
354
+ def collect_leaf_usage(span: ReadableSpan, aggregate: dict[str, int]) -> None:
355
+ key = (_trace_id(span), _span_id(span))
356
+ own_usage = usage_by_span.get(key, {})
319
357
  if _has_nonzero_token_usage(own_usage) and not has_token_descendant(span):
320
358
  for usage_key, value in own_usage.items():
321
- usage[usage_key] += value
359
+ aggregate[usage_key] += value
322
360
  return
323
361
  for child in children_by_parent.get(key, []):
324
- collect_leaf_usage(child, usage)
362
+ collect_leaf_usage(child, aggregate)
325
363
 
326
- replacements: dict[int, Any] = {}
364
+ root_usage: dict[int, dict[str, int]] = {}
327
365
  for root in roots:
328
- aggregate = {"input": 0, "output": 0, "cache_read": 0, "reasoning_output": 0}
329
- collect_leaf_usage(root, aggregate)
330
- if not _has_nonzero_token_usage(aggregate):
331
- continue
332
-
333
- attributes = dict(getattr(root, "attributes", None) or {})
334
- updated = False
335
- attr_map = {
336
- "gen_ai.usage.input_tokens": "input",
337
- "gen_ai.usage.output_tokens": "output",
338
- "gen_ai.usage.cache_read.input_tokens": "cache_read",
339
- "gen_ai.usage.reasoning.output_tokens": "reasoning_output",
366
+ aggregate = {
367
+ "input": 0,
368
+ "output": 0,
369
+ "total": 0,
370
+ "cache_read": 0,
371
+ "reasoning_output": 0,
340
372
  }
341
- for attr_name, usage_key in attr_map.items():
342
- if _coerce_int(attributes.get(attr_name)) <= 0 and aggregate[usage_key] > 0:
343
- attributes[attr_name] = aggregate[usage_key]
344
- updated = True
345
-
346
- if updated:
347
- replacements[id(root)] = _clone_span_with_attributes(root, attributes)
348
-
349
- if not replacements:
350
- return spans
351
- return [replacements.get(id(span), span) for span in span_list]
352
-
353
-
354
- def _build_langfuse_otlp_config(
355
- langfuse_config: dict[str, Any] | None = None,
356
- ) -> dict[str, Any] | None:
357
- """Build Langfuse OTLP direct exporter config from explicit config or env."""
358
- if langfuse_config:
359
- public_key = langfuse_config.get("public_key") or ""
360
- secret_key = langfuse_config.get("secret_key") or ""
361
- host = langfuse_config.get("host") or "http://localhost:3000"
362
- else:
363
- public_key = os.getenv("LANGFUSE_PUBLIC_KEY", "")
364
- secret_key = os.getenv("LANGFUSE_SECRET_KEY", "")
365
- host = (
366
- os.getenv("LANGFUSE_BASE_URL") or os.getenv("LANGFUSE_HOST") or "http://localhost:3000"
367
- )
368
-
369
- if not public_key or not secret_key:
370
- return None
371
-
372
- headers = _build_langfuse_otlp_headers(public_key, secret_key)
373
- return {
374
- "endpoint": f"{host.rstrip('/')}/api/public/otel/v1/traces",
375
- "headers": headers,
376
- "protocol": "http/protobuf",
377
- }
378
-
379
-
380
- def _build_langfuse_otlp_headers(public_key: str, secret_key: str) -> dict[str, str]:
381
- auth = base64.b64encode(f"{public_key}:{secret_key}".encode("utf-8")).decode("ascii")
382
- return {
383
- "Authorization": f"Basic {auth}",
384
- "x-langfuse-ingestion-version": "4",
373
+ collect_leaf_usage(root, aggregate)
374
+ root_usage[id(root)] = aggregate
375
+
376
+ resource_cache: dict[int, Resource | None] = {}
377
+ transformed: list[ReadableSpan] = []
378
+ usage_attributes = {
379
+ "gen_ai.usage.input_tokens": "input",
380
+ "gen_ai.usage.output_tokens": "output",
381
+ "gen_ai.usage.total_tokens": "total",
382
+ "gen_ai.usage.cache_read.input_tokens": "cache_read",
383
+ "gen_ai.usage.reasoning.output_tokens": "reasoning_output",
385
384
  }
385
+ for span in span_list:
386
+ original_resource = getattr(span, "resource", None)
387
+ original_resource_attributes = getattr(original_resource, "attributes", {}) or {}
388
+ resource_key = id(original_resource)
389
+ if resource_key not in resource_cache:
390
+ resource_cache[resource_key] = _clone_cloud_monitor_resource(original_resource)
391
+ resource = resource_cache[resource_key]
392
+ attributes = dict(getattr(span, "attributes", None) or {})
393
+ resource_attributes = getattr(resource, "attributes", {}) or {}
394
+ for key in _CLOUD_MONITOR_RESOURCE_SPAN_ATTRIBUTES:
395
+ value = resource_attributes.get(key)
396
+ if value not in (None, ""):
397
+ attributes.setdefault(f"gen_ai.{key}", value)
398
+ agent_name = original_resource_attributes.get(
399
+ "agentengine.agent_name"
400
+ ) or original_resource_attributes.get("service.name")
401
+ if agent_name not in (None, ""):
402
+ attributes.setdefault("gen_ai.agentengine.agent_name", agent_name)
403
+ aggregate = root_usage.get(id(span), {})
404
+ for attribute, usage_key in usage_attributes.items():
405
+ if _coerce_int(attributes.get(attribute)) <= 0 and aggregate.get(usage_key, 0) > 0:
406
+ attributes[attribute] = aggregate[usage_key]
407
+
408
+ session_id = session_ids_by_trace.get(_trace_id(span))
409
+ if session_id is not None:
410
+ attributes.setdefault("session.id", session_id)
411
+ _add_langfuse_compatibility_attributes(attributes)
412
+
413
+ original_attributes = dict(getattr(span, "attributes", None) or {})
414
+ if resource is original_resource and attributes == original_attributes:
415
+ transformed.append(span)
416
+ else:
417
+ transformed.append(_clone_span(span, attributes=attributes, resource=resource))
418
+ return transformed
386
419
 
387
420
 
388
421
  def _parse_otlp_headers(raw: str) -> dict[str, str]:
@@ -408,7 +441,7 @@ def _decode_otlp_header_value(key: str, value: str) -> str:
408
441
  for scheme in ("Basic", "Bearer"):
409
442
  prefix = f"{scheme}+"
410
443
  if decoded.startswith(prefix):
411
- return f"{scheme} {decoded[len(prefix):]}"
444
+ return f"{scheme} {decoded[len(prefix) :]}"
412
445
  return decoded
413
446
 
414
447
 
@@ -420,10 +453,6 @@ def _derive_otlp_traces_endpoint(endpoint: str) -> str:
420
453
  return f"{endpoint}/v1/traces"
421
454
 
422
455
 
423
- def _env_flag_disabled(value: str) -> bool:
424
- return value.strip().lower() in {"0", "false", "no", "off", "disabled"}
425
-
426
-
427
456
  def _env_flag_enabled(value: str) -> bool:
428
457
  return value.strip().lower() in {"1", "true", "yes", "on"}
429
458
 
@@ -449,53 +478,48 @@ def _get_service_name() -> str:
449
478
 
450
479
 
451
480
  def _build_cloud_monitor_otlp_http_config() -> Optional[_OtlpHttpConfig]:
452
- """Build CloudMonitor OTLP HTTP traces exporter config from CLOUD_MONITOR_* env."""
453
- enabled = os.getenv("CLOUD_MONITOR_OTLP_ENABLED", "").strip()
454
- if enabled and _env_flag_disabled(enabled):
455
- logger.info("CloudMonitor OTLP exporter disabled by CLOUD_MONITOR_OTLP_ENABLED=%s", enabled)
456
- return None
457
- cloud_monitor_langfuse_configured = all(
458
- os.getenv(name, "").strip()
459
- for name in (
460
- "CLOUD_MONITOR_LANGFUSE_PUBLIC_KEY",
461
- "CLOUD_MONITOR_LANGFUSE_SECRET_KEY",
462
- )
463
- ) and bool(
464
- os.getenv("CLOUD_MONITOR_LANGFUSE_HOST", "").strip()
465
- or os.getenv("CLOUD_MONITOR_OTLP_ENDPOINT", "").strip()
466
- )
467
- cloud_monitor_otlp_forced = _env_flag_enabled(enabled)
468
- cloud_monitor_callback_requested = _env_flag_enabled(
469
- os.getenv("LANGFUSE_USE_CALLBACK", "")
470
- ) and not _env_flag_disabled(os.getenv("CLOUD_MONITOR_LANGFUSE_ENABLED", ""))
471
- if (
472
- cloud_monitor_langfuse_configured
473
- and cloud_monitor_callback_requested
474
- and not cloud_monitor_otlp_forced
475
- ):
476
- logger.info(
477
- "CloudMonitor OTLP exporter skipped because CloudMonitor Langfuse SDK "
478
- "callback is requested"
479
- )
480
- return None
481
+ """Build CloudMonitor OTLP HTTP traces exporter config from CLOUD_MONITOR_OTLP_* env.
481
482
 
482
- app_key = os.getenv("CLOUD_MONITOR_APP_KEY", "").strip()
483
+ traces 专用 headers 优先于通用 headers。仅当两个 headers 环境变量整体缺失时,
484
+ 才把旧 CLOUD_MONITOR_APP_KEY 按过渡兼容翻译为 Ksc-Appkey header。
485
+ """
483
486
  traces_endpoint = os.getenv("CLOUD_MONITOR_OTLP_TRACES_ENDPOINT", "").strip()
484
487
  base_endpoint = os.getenv("CLOUD_MONITOR_OTLP_ENDPOINT", "").strip()
485
488
  endpoint = traces_endpoint
486
489
  if not endpoint and base_endpoint:
487
490
  endpoint = _derive_otlp_traces_endpoint(base_endpoint)
488
491
 
489
- if not app_key and not endpoint:
490
- logger.debug("CloudMonitor OTLP exporter not configured: missing app key and endpoint")
491
- return None
492
- if not app_key:
493
- logger.warning("CloudMonitor OTLP exporter skipped: CLOUD_MONITOR_APP_KEY is missing")
494
- return None
492
+ raw_traces_headers = os.getenv("CLOUD_MONITOR_OTLP_TRACES_HEADERS", "")
493
+ raw_generic_headers = os.getenv("CLOUD_MONITOR_OTLP_HEADERS", "")
494
+ raw_headers = raw_traces_headers.strip() or raw_generic_headers
495
+ headers = _parse_otlp_headers(raw_headers)
496
+ # One-version compatibility applies only when both header variables are
497
+ # absent. A present-but-invalid traces or generic header bundle must fail
498
+ # closed rather than silently mixing it with the legacy AppKey.
499
+ if not raw_traces_headers.strip() and not raw_generic_headers.strip():
500
+ app_key = os.getenv("CLOUD_MONITOR_APP_KEY", "").strip()
501
+ if app_key:
502
+ if not endpoint:
503
+ logger.warning(
504
+ "CloudMonitor OTLP exporter skipped: CLOUD_MONITOR_APP_KEY fallback "
505
+ "requires CLOUD_MONITOR_OTLP_ENDPOINT or CLOUD_MONITOR_OTLP_TRACES_ENDPOINT"
506
+ )
507
+ return None
508
+ headers["Ksc-Appkey"] = app_key
509
+ logger.info(
510
+ "CloudMonitor OTLP using deprecated CLOUD_MONITOR_APP_KEY -> Ksc-Appkey fallback; "
511
+ "server should inject CLOUD_MONITOR_OTLP_TRACES_HEADERS or "
512
+ "CLOUD_MONITOR_OTLP_HEADERS instead"
513
+ )
514
+
495
515
  if not endpoint:
516
+ logger.debug("CloudMonitor OTLP exporter not configured: missing endpoint")
517
+ return None
518
+ if not _has_header(headers, "Ksc-Appkey"):
496
519
  logger.warning(
497
- "CloudMonitor OTLP exporter skipped: CLOUD_MONITOR_OTLP_ENDPOINT or "
498
- "CLOUD_MONITOR_OTLP_TRACES_ENDPOINT is missing"
520
+ "CloudMonitor OTLP exporter skipped: Ksc-Appkey missing in "
521
+ "CLOUD_MONITOR_OTLP_TRACES_HEADERS/CLOUD_MONITOR_OTLP_HEADERS and no "
522
+ "CLOUD_MONITOR_APP_KEY fallback"
499
523
  )
500
524
  return None
501
525
 
@@ -511,16 +535,12 @@ def _build_cloud_monitor_otlp_http_config() -> Optional[_OtlpHttpConfig]:
511
535
  )
512
536
  return None
513
537
 
514
- headers = _parse_otlp_headers(os.getenv("CLOUD_MONITOR_OTLP_HEADERS", ""))
515
- headers["Ksc-Appkey"] = app_key
516
538
  service_name = _get_service_name()
517
539
  logger.info(
518
- "CloudMonitor OTLP config resolved: endpoint=%s protocol=%s service_name=%s "
519
- "app_key_present=%s header_keys=%s",
540
+ "CloudMonitor OTLP config resolved: endpoint=%s protocol=%s service_name=%s header_keys=%s",
520
541
  endpoint,
521
542
  protocol,
522
543
  service_name or "",
523
- bool(app_key),
524
544
  ",".join(sorted(headers)),
525
545
  )
526
546
  return _OtlpHttpConfig(
@@ -531,48 +551,12 @@ def _build_cloud_monitor_otlp_http_config() -> Optional[_OtlpHttpConfig]:
531
551
  )
532
552
 
533
553
 
534
- def _is_langfuse_otlp_endpoint(endpoint: str) -> bool:
535
- if not endpoint:
536
- return False
537
- normalized_endpoint = endpoint.strip().rstrip("/")
538
- langfuse_hosts = [
539
- os.getenv("LANGFUSE_HOST", "").strip().rstrip("/"),
540
- os.getenv("LANGFUSE_BASE_URL", "").strip().rstrip("/"),
541
- ]
542
- if any(host and normalized_endpoint.startswith(host) for host in langfuse_hosts):
543
- return True
544
- return "/api/public/otel" in normalized_endpoint
545
-
546
-
547
- def _is_langfuse_callback_endpoint(endpoint: str) -> bool:
548
- return _env_flag_enabled(os.getenv("LANGFUSE_USE_CALLBACK", "")) and _is_langfuse_otlp_endpoint(
549
- endpoint
550
- )
551
-
552
-
553
- def _apply_langfuse_auth_fallback(endpoint: str, headers: dict[str, str]) -> bool:
554
- """Add Langfuse OTLP auth when the platform provides endpoint + keys but no headers."""
555
- if not _is_langfuse_otlp_endpoint(endpoint) or _has_header(headers, "Authorization"):
556
- return False
557
-
558
- public_key = os.getenv("LANGFUSE_PUBLIC_KEY", "").strip()
559
- secret_key = os.getenv("LANGFUSE_SECRET_KEY", "").strip()
560
- if not public_key or not secret_key:
561
- logger.warning(
562
- "Generic OTLP endpoint looks like Langfuse but LANGFUSE_PUBLIC_KEY/SECRET_KEY "
563
- "are missing; Authorization header was not added"
564
- )
565
- return False
566
-
567
- langfuse_headers = _build_langfuse_otlp_headers(public_key, secret_key)
568
- headers["Authorization"] = langfuse_headers["Authorization"]
569
- if not _has_header(headers, "x-langfuse-ingestion-version"):
570
- headers["x-langfuse-ingestion-version"] = langfuse_headers["x-langfuse-ingestion-version"]
571
- return True
572
-
573
-
574
554
  def _build_generic_otlp_http_config() -> Optional[dict]:
575
- """Build generic OTLP HTTP traces exporter config from standard OTEL env."""
555
+ """Build generic OTLP HTTP traces exporter config from standard OTEL env.
556
+
557
+ This path consumes only the standard OTEL_EXPORTER_OTLP_* contract. Backend
558
+ identity and authentication are opaque to KsADK.
559
+ """
576
560
  traces_endpoint = os.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "").strip()
577
561
  base_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "").strip()
578
562
  endpoint = traces_endpoint
@@ -580,13 +564,6 @@ def _build_generic_otlp_http_config() -> Optional[dict]:
580
564
  endpoint = _derive_otlp_traces_endpoint(base_endpoint)
581
565
  if not endpoint:
582
566
  return None
583
- if _is_langfuse_callback_endpoint(endpoint):
584
- logger.info(
585
- "Generic OTLP HTTP exporter skipped because LANGFUSE_USE_CALLBACK is enabled "
586
- "for endpoint=%s",
587
- endpoint,
588
- )
589
- return None
590
567
 
591
568
  protocol = (
592
569
  os.getenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "").strip().lower()
@@ -603,53 +580,58 @@ def _build_generic_otlp_http_config() -> Optional[dict]:
603
580
  "OTEL_EXPORTER_OTLP_HEADERS", ""
604
581
  )
605
582
  headers = _parse_otlp_headers(raw_headers)
606
- langfuse_auth_fallback = _apply_langfuse_auth_fallback(endpoint, headers)
607
583
  logger.info(
608
- "Generic OTLP HTTP config resolved: endpoint=%s protocol=http/protobuf "
609
- "header_keys=%s langfuse_auth_fallback=%s",
584
+ "Generic OTLP HTTP config resolved: endpoint=%s protocol=http/protobuf header_keys=%s",
610
585
  endpoint,
611
586
  ",".join(sorted(headers)),
612
- langfuse_auth_fallback,
613
587
  )
614
588
  return {
615
589
  "endpoint": endpoint,
616
590
  "headers": headers,
617
591
  "protocol": "http/protobuf",
618
- "langfuse_auth_fallback": langfuse_auth_fallback,
619
592
  }
620
593
 
621
594
 
595
+ def _register_span_processor(provider: Any, processor: Any) -> None:
596
+ provider.add_span_processor(processor)
597
+ _managed_span_processors.append(processor)
598
+
599
+
622
600
  def setup_tracing(
623
601
  enable_inmemory: bool = True,
624
- enable_langfuse: bool | None = None, # Auto-detect from env
602
+ enable_langfuse: bool | None = None,
625
603
  langfuse_config: dict[str, Any] | None = None,
626
604
  enable_otlp: bool = False,
627
605
  otlp_endpoint: str = "localhost:4317",
628
606
  enable_adk_instrumentation: bool = True, # Auto-instrument ADK
629
- use_callback_only: bool | None = None, # Explicit override
630
607
  **kwargs,
631
608
  ) -> Optional[InMemoryExporter]:
632
609
  """初始化 Tracing (支持多 Exporter)
633
610
 
634
611
  Args:
635
612
  enable_inmemory: 是否启用内存 Exporter (Web UI 使用)
636
- enable_langfuse: 是否启用 Langfuse (None = 自动检测环境变量)
637
- langfuse_config: Langfuse 配置 {"public_key", "secret_key", "host"}
638
- enable_otlp: 是否启用 OTLP Exporter
639
- otlp_endpoint: OTLP 端点地址
613
+ enable_langfuse: 已废弃并忽略;请配置标准 OTEL_EXPORTER_OTLP_*
614
+ langfuse_config: 已废弃并忽略;认证信息放入标准 OTLP headers
615
+ enable_otlp: 是否启用 OTLP gRPC Exporter
616
+ otlp_endpoint: OTLP gRPC 端点地址
640
617
  enable_adk_instrumentation: 是否启用 ADK 自动插桩
641
- use_callback_only: 是否仅使用 CallbackHandler (防止 OTel 重复)
642
618
 
643
619
  Returns:
644
620
  InMemoryExporter 实例 (用于 Web UI 获取 traces)
645
621
  """
646
- global _exporter_instance, _langfuse_exporter, _tracing_initialized, _adk_instrumented
622
+ global _exporter_instance, _tracing_initialized, _adk_instrumented
647
623
 
648
624
  # 防止重复初始化
649
625
  if _tracing_initialized:
650
626
  logger.debug("Tracing already initialized, skipping")
651
627
  return _exporter_instance
652
628
 
629
+ if enable_langfuse or langfuse_config:
630
+ logger.warning(
631
+ "enable_langfuse/langfuse_config are ignored; configure the standard "
632
+ "OTEL_EXPORTER_OTLP_* contract instead"
633
+ )
634
+
653
635
  try:
654
636
  from opentelemetry import trace
655
637
  from opentelemetry.sdk.trace import TracerProvider
@@ -661,11 +643,9 @@ def setup_tracing(
661
643
  # 检查是否已有 TracerProvider (避免覆盖)
662
644
  existing_provider = trace.get_tracer_provider()
663
645
  if existing_provider and hasattr(existing_provider, "add_span_processor"):
664
- # 使用现有 provider,直接添加 processor
665
646
  provider = existing_provider
666
647
  logger.debug("Using existing TracerProvider")
667
648
  else:
668
- # 创建新的 TracerProvider
669
649
  provider = TracerProvider()
670
650
  trace.set_tracer_provider(provider)
671
651
  logger.debug("Created new TracerProvider")
@@ -674,11 +654,10 @@ def setup_tracing(
674
654
  if enable_inmemory:
675
655
  exporter = InMemoryExporter(max_traces=kwargs.get("max_traces", 1000))
676
656
  _exporter_instance = exporter
677
- provider.add_span_processor(SimpleSpanProcessor(exporter))
657
+ _register_span_processor(provider, SimpleSpanProcessor(exporter))
678
658
  logger.info("InMemory exporter enabled")
679
659
 
680
- # 2. Generic OTLP HTTP exporter from standard environment variables.
681
- # This keeps user code backend-agnostic; Langfuse is only one possible OTLP backend.
660
+ # 2. Generic OTLP HTTP exporter (标准 OTEL_* —— 托管 Agent 的 Langfuse 主路)
682
661
  generic_otlp_config = _build_generic_otlp_http_config()
683
662
  if generic_otlp_config:
684
663
  try:
@@ -696,17 +675,10 @@ def setup_tracing(
696
675
  endpoint=generic_otlp_config["endpoint"],
697
676
  service_name=_get_service_name(),
698
677
  header_keys=sorted(generic_otlp_config["headers"]),
699
- span_transform=(
700
- _prepare_langfuse_spans
701
- if _is_langfuse_otlp_endpoint(generic_otlp_config["endpoint"])
702
- else None
703
- ),
704
678
  )
705
- provider.add_span_processor(
706
- BatchSpanProcessor(
707
- otlp_exporter,
708
- **_batch_span_processor_kwargs(),
709
- )
679
+ _register_span_processor(
680
+ provider,
681
+ BatchSpanProcessor(otlp_exporter, **_batch_span_processor_kwargs()),
710
682
  )
711
683
  logger.info(
712
684
  "Generic OTLP HTTP exporter enabled: %s (%s) headers=%s",
@@ -719,9 +691,7 @@ def setup_tracing(
719
691
  except Exception as e:
720
692
  logger.error(f"Failed to initialize generic OTLP HTTP exporter: {e}")
721
693
 
722
- # 3. CloudMonitor OTLP HTTP exporter.
723
- # This uses CloudMonitor-specific env vars, so existing Langfuse / generic OTLP paths
724
- # continue to behave exactly as configured by the user or platform.
694
+ # 3. CloudMonitor OTLP HTTP exporter (次路 —— 平台 CLOUD_MONITOR_OTLP_*)
725
695
  cloud_monitor_config = _build_cloud_monitor_otlp_http_config()
726
696
  if cloud_monitor_config:
727
697
  try:
@@ -741,11 +711,9 @@ def setup_tracing(
741
711
  header_keys=sorted(cloud_monitor_config.headers),
742
712
  span_transform=_prepare_cloud_monitor_spans,
743
713
  )
744
- provider.add_span_processor(
745
- BatchSpanProcessor(
746
- cloud_monitor_exporter,
747
- **_batch_span_processor_kwargs(),
748
- )
714
+ _register_span_processor(
715
+ provider,
716
+ BatchSpanProcessor(cloud_monitor_exporter, **_batch_span_processor_kwargs()),
749
717
  )
750
718
  logger.info(
751
719
  "CloudMonitor OTLP exporter enabled: endpoint=%s protocol=%s service_name=%s",
@@ -758,64 +726,7 @@ def setup_tracing(
758
726
  except Exception as e:
759
727
  logger.error(f"Failed to initialize CloudMonitor OTLP exporter: {e}")
760
728
 
761
- # 4. Langfuse OTLP direct exporter (auto-detect or explicit config)
762
- # 注意: 对于 LangGraph/LangChain 框架,推荐使用 CallbackHandler 而非 OTel Exporter
763
- # 同时使用两者会导致重复的 trace
764
- langfuse_enabled = enable_langfuse
765
- if langfuse_enabled is None:
766
- # Auto-detect from environment variables
767
- langfuse_enabled = not generic_otlp_config and bool(
768
- os.getenv("LANGFUSE_PUBLIC_KEY") or (langfuse_config or {}).get("public_key")
769
- )
770
-
771
- # 检查是否应该禁用 LangfuseExporter (当使用 LangChain/LangGraph 时)
772
- # 优先使用显式参数,否则读取环境变量
773
- if use_callback_only is None:
774
- use_callback_only = os.getenv("LANGFUSE_USE_CALLBACK", "false").lower() == "true"
775
-
776
- if langfuse_enabled and not use_callback_only:
777
- try:
778
- from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
779
- OTLPSpanExporter as HttpOTLPSpanExporter,
780
- )
781
-
782
- config = _build_langfuse_otlp_config(langfuse_config)
783
- if config:
784
- raw_langfuse_otlp_exporter = HttpOTLPSpanExporter(
785
- endpoint=config["endpoint"],
786
- headers=config["headers"],
787
- )
788
- otlp_exporter = _LoggingSpanExporter(
789
- raw_langfuse_otlp_exporter,
790
- name="Langfuse OTLP",
791
- endpoint=config["endpoint"],
792
- service_name=_get_service_name(),
793
- header_keys=sorted(config["headers"]),
794
- span_transform=_prepare_langfuse_spans,
795
- )
796
- provider.add_span_processor(
797
- BatchSpanProcessor(
798
- otlp_exporter,
799
- **_batch_span_processor_kwargs(),
800
- )
801
- )
802
- logger.info(
803
- "Langfuse OTLP exporter enabled: %s (%s) headers=%s",
804
- config["endpoint"],
805
- config["protocol"],
806
- ",".join(sorted(config["headers"])),
807
- )
808
- else:
809
- logger.warning("Langfuse credentials not found, skipping")
810
-
811
- except ImportError as e:
812
- logger.warning(f"Langfuse OTLP exporter not available: {e}")
813
- except Exception as e:
814
- logger.error(f"Failed to initialize Langfuse OTLP exporter: {e}")
815
- elif langfuse_enabled:
816
- logger.info("Langfuse will use CallbackHandler (recommended for LangChain/LangGraph)")
817
-
818
- # 5. OTLP Exporter (optional)
729
+ # 4. OTLP gRPC Exporter (optional compatibility path)
819
730
  if enable_otlp:
820
731
  try:
821
732
  from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
@@ -823,17 +734,15 @@ def setup_tracing(
823
734
  )
824
735
 
825
736
  grpc_otlp_exporter = GrpcOTLPSpanExporter(endpoint=otlp_endpoint)
826
- provider.add_span_processor(
827
- BatchSpanProcessor(
828
- grpc_otlp_exporter,
829
- **_batch_span_processor_kwargs(),
830
- )
737
+ _register_span_processor(
738
+ provider,
739
+ BatchSpanProcessor(grpc_otlp_exporter, **_batch_span_processor_kwargs()),
831
740
  )
832
741
  logger.info(f"OTLP exporter enabled: {otlp_endpoint}")
833
742
  except ImportError:
834
743
  logger.warning("OTLP exporter not installed")
835
744
 
836
- # 6. ADK Auto-Instrumentation (for Google ADK projects)
745
+ # 5. ADK Auto-Instrumentation (for Google ADK projects)
837
746
  if enable_adk_instrumentation and not _adk_instrumented:
838
747
  try:
839
748
  instrumentation_module = importlib.import_module(
@@ -851,7 +760,7 @@ def setup_tracing(
851
760
  except Exception as e:
852
761
  logger.debug(f"ADK instrumentation failed: {e}")
853
762
 
854
- # 7. LangChain Auto-Instrumentation
763
+ # 6. LangChain Auto-Instrumentation (covers LangChain + LangGraph LLM/tool/node spans)
855
764
  if enable_adk_instrumentation:
856
765
  try:
857
766
  from openinference.instrumentation.langchain import LangChainInstrumentor
@@ -874,18 +783,28 @@ def setup_tracing(
874
783
 
875
784
 
876
785
  def shutdown_tracing():
877
- """Gracefully shutdown tracing to prevent Ctrl+C errors"""
878
- global _langfuse_exporter, _tracing_initialized
786
+ """Flush and stop every processor installed by this module.
879
787
 
880
- if _langfuse_exporter is not None:
788
+ The global provider may belong to another library, so KsADK never shuts the
789
+ provider itself. It owns and closes only the processors it registered.
790
+ """
791
+ processors = list(_managed_span_processors)
792
+ _managed_span_processors.clear()
793
+ for processor in processors:
881
794
  try:
882
- if hasattr(_langfuse_exporter, "_exporter"):
883
- _langfuse_exporter._exporter.shutdown()
884
- logger.debug("Langfuse exporter shutdown gracefully")
795
+ processor.force_flush(timeout_millis=30000)
796
+ except TypeError:
797
+ try:
798
+ processor.force_flush()
799
+ except Exception:
800
+ logger.exception("Tracing processor force_flush failed")
885
801
  except Exception:
886
- pass # Ignore shutdown errors
887
-
888
- _tracing_initialized = False
802
+ logger.exception("Tracing processor force_flush failed")
803
+ for processor in processors:
804
+ try:
805
+ processor.shutdown()
806
+ except Exception:
807
+ logger.exception("Tracing processor shutdown failed")
889
808
 
890
809
 
891
810
  def get_memory_exporter() -> Optional[InMemoryExporter]:
@@ -893,11 +812,6 @@ def get_memory_exporter() -> Optional[InMemoryExporter]:
893
812
  return _exporter_instance
894
813
 
895
814
 
896
- def get_langfuse_exporter():
897
- """获取 Langfuse Exporter 实例"""
898
- return _langfuse_exporter
899
-
900
-
901
815
  def get_tracer(name: str = "ksadk"):
902
816
  """获取 Tracer"""
903
817
  try: