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,1060 @@
1
+ """OTLP-first trace persistence and Studio read models.
2
+
3
+ RuntimeEvent remains the live interaction contract. This module is the only
4
+ place that projects those events into telemetry: persisted Trace data is OTLP
5
+ JSON, while ``TraceView`` is a lossless, UI-oriented read model derived from
6
+ that canonical document.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import os
14
+ import re
15
+ import shutil
16
+ from datetime import datetime, timedelta, timezone
17
+ from pathlib import Path
18
+ from typing import Any, Iterable
19
+ from urllib.parse import quote
20
+
21
+ from ksadk.studio.contracts import RunEvent, RunRecord
22
+ from ksadk.studio.errors import StudioError, not_found
23
+ from ksadk.studio.pagination import keyset_page
24
+ from ksadk.studio.workspace import Workspace
25
+
26
+ _TRACE_ID = re.compile(r"^[0-9a-f]{32}$")
27
+ _SPAN_KIND = {1: "INTERNAL", 2: "SERVER", 3: "CLIENT", 4: "PRODUCER", 5: "CONSUMER"}
28
+ _STATUS_CODE = {0: "UNSET", 1: "OK", 2: "ERROR"}
29
+ _SENSITIVE_KEYS = {
30
+ "args",
31
+ "command",
32
+ "content",
33
+ "cwd",
34
+ "input",
35
+ "output",
36
+ "prompt",
37
+ "runtimeEvent",
38
+ "secret",
39
+ "text",
40
+ "token",
41
+ }
42
+
43
+ # 内容类字段:本地 Studio 默认放行(localhost 单用户调试),secret/token 永远脱敏
44
+ _CONTENT_KEYS = {"args", "command", "content", "input", "output", "prompt", "text", "delta"}
45
+
46
+
47
+ def _trace_content_enabled() -> bool:
48
+ return os.environ.get("KSADK_STUDIO_TRACE_CONTENT", "1") != "0"
49
+
50
+
51
+ def _canonical_trace_id(value: str) -> str:
52
+ normalized = value.strip().lower()
53
+ if _TRACE_ID.fullmatch(normalized):
54
+ return normalized
55
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()[:32]
56
+
57
+
58
+ def _span_id(trace_id: str, identity: str) -> str:
59
+ return hashlib.sha256(f"{trace_id}:{identity}".encode()).hexdigest()[:16]
60
+
61
+
62
+ def _unix_nano(value: datetime) -> int:
63
+ normalized = value.astimezone(timezone.utc)
64
+ return int(normalized.timestamp()) * 1_000_000_000 + normalized.microsecond * 1000
65
+
66
+
67
+ def _iso(value: datetime | None) -> str | None:
68
+ if value is None:
69
+ return None
70
+ return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
71
+
72
+
73
+ def _stringify(value: Any) -> str:
74
+ if isinstance(value, str):
75
+ return value
76
+ try:
77
+ return json.dumps(value, ensure_ascii=False, default=str)
78
+ except Exception:
79
+ return str(value)
80
+
81
+
82
+ def _truncate(text: str, limit: int = 4096) -> str:
83
+ if len(text) <= limit:
84
+ return text
85
+ return text[:limit] + "…"
86
+
87
+
88
+ def _otlp_value(value: Any) -> dict[str, Any]:
89
+ if isinstance(value, bool):
90
+ return {"boolValue": value}
91
+ if isinstance(value, int):
92
+ return {"intValue": str(value)}
93
+ if isinstance(value, float):
94
+ return {"doubleValue": value}
95
+ return {"stringValue": str(value)}
96
+
97
+
98
+ def _attributes(values: dict[str, Any]) -> list[dict[str, Any]]:
99
+ return [
100
+ {"key": key, "value": _otlp_value(value)}
101
+ for key, value in sorted(values.items())
102
+ if value is not None and value != ""
103
+ ]
104
+
105
+
106
+ def _decode_value(value: dict[str, Any]) -> Any:
107
+ if "intValue" in value:
108
+ return int(value["intValue"])
109
+ if "doubleValue" in value:
110
+ return float(value["doubleValue"])
111
+ if "boolValue" in value:
112
+ return bool(value["boolValue"])
113
+ if "arrayValue" in value:
114
+ return [_decode_value(item) for item in value["arrayValue"].get("values", [])]
115
+ return value.get("stringValue", "")
116
+
117
+
118
+ def _decoded_attributes(items: Iterable[dict[str, Any]]) -> dict[str, Any]:
119
+ return {item["key"]: _decode_value(item["value"]) for item in items}
120
+
121
+
122
+ def _safe_event_attributes(data: dict[str, Any]) -> dict[str, Any]:
123
+ safe: dict[str, Any] = {}
124
+ include_content = _trace_content_enabled()
125
+ for key, value in data.items():
126
+ if any(token in key.lower() for token in ("secret", "token")):
127
+ continue
128
+ if key in _SENSITIVE_KEYS:
129
+ if (
130
+ include_content
131
+ and key in _CONTENT_KEYS
132
+ and isinstance(value, (str, int, float, bool))
133
+ ):
134
+ safe[f"agentkit.event.{key}"] = _truncate(_stringify(value), 2048)
135
+ continue
136
+ if isinstance(value, (str, int, float, bool)):
137
+ safe[f"agentkit.event.{key}"] = value
138
+ return safe
139
+
140
+
141
+ def _duration_from(data: dict[str, Any]) -> int | None:
142
+ value = data.get("durationMs", data.get("duration_ms"))
143
+ return max(0, int(value)) if value is not None else None
144
+
145
+
146
+ def _enum_string(value: Any) -> str:
147
+ """Normalize both validated strings and assignment-time ``str`` enums."""
148
+
149
+ return str(getattr(value, "value", value))
150
+
151
+
152
+ def _token_value(attributes: dict[str, Any], *keys: str) -> int | None:
153
+ """Read one non-negative token counter from known OTLP attribute names."""
154
+
155
+ for key in keys:
156
+ value = attributes.get(key)
157
+ if value is None or isinstance(value, bool):
158
+ continue
159
+ try:
160
+ normalized = int(value)
161
+ except (TypeError, ValueError):
162
+ continue
163
+ if normalized >= 0:
164
+ return normalized
165
+ return None
166
+
167
+
168
+ def _usage_from_attributes(attributes: dict[str, Any]) -> dict[str, Any]:
169
+ """Normalize AgentKit and standard GenAI usage without trusting a side flag."""
170
+
171
+ input_tokens = _token_value(
172
+ attributes,
173
+ "gen_ai.usage.input_tokens",
174
+ "agentkit.usage.input_tokens",
175
+ )
176
+ output_tokens = _token_value(
177
+ attributes,
178
+ "gen_ai.usage.output_tokens",
179
+ "agentkit.usage.output_tokens",
180
+ )
181
+ total_tokens = _token_value(
182
+ attributes,
183
+ "agentkit.usage.total_tokens",
184
+ "gen_ai.usage.total_tokens",
185
+ )
186
+ cached_input_tokens = _token_value(
187
+ attributes,
188
+ "gen_ai.usage.cache_read.input_tokens",
189
+ "gen_ai.usage.cached_input_tokens",
190
+ "llm.usage.cache_read.input_tokens",
191
+ "agentkit.usage.cached_input_tokens",
192
+ )
193
+ reasoning_output_tokens = _token_value(
194
+ attributes,
195
+ "gen_ai.usage.reasoning.output_tokens",
196
+ "gen_ai.usage.reasoning_tokens",
197
+ "gen_ai.usage.reasoning_output_tokens",
198
+ "llm.usage.reasoning_tokens",
199
+ "agentkit.usage.reasoning_output_tokens",
200
+ )
201
+ if total_tokens is None and input_tokens is not None and output_tokens is not None:
202
+ total_tokens = input_tokens + output_tokens
203
+ reported = any(
204
+ value is not None
205
+ for value in (
206
+ input_tokens,
207
+ output_tokens,
208
+ total_tokens,
209
+ cached_input_tokens,
210
+ reasoning_output_tokens,
211
+ )
212
+ )
213
+ source = attributes.get("agentkit.usage.source")
214
+ if not source and reported:
215
+ source = "gen_ai.usage"
216
+ return {
217
+ "inputTokens": input_tokens,
218
+ "outputTokens": output_tokens,
219
+ "totalTokens": total_tokens,
220
+ "cachedInputTokens": cached_input_tokens,
221
+ "reasoningOutputTokens": reasoning_output_tokens,
222
+ "usageCompleteness": {
223
+ "inputTokens": input_tokens is not None,
224
+ "outputTokens": output_tokens is not None,
225
+ "totalTokens": total_tokens is not None,
226
+ "cachedInputTokens": cached_input_tokens is not None,
227
+ "reasoningOutputTokens": reasoning_output_tokens is not None,
228
+ },
229
+ "usageReported": reported,
230
+ "usageSource": source,
231
+ }
232
+
233
+
234
+ def _aggregate_span_usage(spans: Iterable[dict[str, Any]]) -> dict[str, Any]:
235
+ """Aggregate billing spans and collapse only exact compatibility wrappers."""
236
+
237
+ span_list = list(spans)
238
+ usage_by_id: dict[str, dict[str, Any]] = {}
239
+ children_by_parent: dict[str, list[str]] = {}
240
+ parent_by_id: dict[str, str] = {}
241
+ anonymous_usages: list[dict[str, Any]] = []
242
+ for span in span_list:
243
+ span_id = str(span.get("spanId") or "")
244
+ parent_id = str(span.get("parentSpanId") or "")
245
+ usage = _usage_from_attributes(_decoded_attributes(span.get("attributes", [])))
246
+ if span_id:
247
+ usage_by_id[span_id] = usage
248
+ parent_by_id[span_id] = parent_id
249
+ if parent_id:
250
+ children_by_parent.setdefault(parent_id, []).append(span_id)
251
+ elif usage["usageReported"]:
252
+ anonymous_usages.append(usage)
253
+
254
+ usage_fields = (
255
+ "inputTokens",
256
+ "outputTokens",
257
+ "totalTokens",
258
+ "cachedInputTokens",
259
+ "reasoningOutputTokens",
260
+ )
261
+
262
+ def empty_aggregate() -> dict[str, Any]:
263
+ return {
264
+ **{key: None for key in usage_fields},
265
+ "usageCompleteness": {key: False for key in usage_fields},
266
+ "billingSpanCount": 0,
267
+ "usageReported": False,
268
+ "usageSource": None,
269
+ }
270
+
271
+ def billed_usage(usage: dict[str, Any]) -> dict[str, Any]:
272
+ return {
273
+ **{key: usage[key] for key in usage_fields},
274
+ "usageCompleteness": dict(usage["usageCompleteness"]),
275
+ "billingSpanCount": 1,
276
+ "usageReported": True,
277
+ "usageSource": usage["usageSource"],
278
+ }
279
+
280
+ def combine(parts: Iterable[dict[str, Any]]) -> dict[str, Any]:
281
+ billed_parts = [part for part in parts if part["billingSpanCount"] > 0]
282
+ if not billed_parts:
283
+ return empty_aggregate()
284
+ aggregate = empty_aggregate()
285
+ aggregate["billingSpanCount"] = sum(part["billingSpanCount"] for part in billed_parts)
286
+ aggregate["usageReported"] = True
287
+ aggregate["usageSource"] = next(
288
+ (part["usageSource"] for part in billed_parts if part["usageSource"]),
289
+ "gen_ai.usage",
290
+ )
291
+ for key in usage_fields:
292
+ values = [part[key] for part in billed_parts if part[key] is not None]
293
+ aggregate[key] = sum(values) if values else None
294
+ aggregate["usageCompleteness"][key] = all(
295
+ part["usageCompleteness"][key] for part in billed_parts
296
+ )
297
+ return aggregate
298
+
299
+ def is_compatibility_wrapper(
300
+ own_usage: dict[str, Any], descendant_usage: dict[str, Any]
301
+ ) -> bool:
302
+ if descendant_usage["billingSpanCount"] == 0:
303
+ return False
304
+ if own_usage["inputTokens"] is None or own_usage["outputTokens"] is None:
305
+ return False
306
+ return all(
307
+ descendant_usage["usageCompleteness"][key] and descendant_usage[key] == own_usage[key]
308
+ for key in ("inputTokens", "outputTokens")
309
+ ) and all(
310
+ own_usage[key] is None
311
+ or (
312
+ descendant_usage["usageCompleteness"][key]
313
+ and descendant_usage[key] == own_usage[key]
314
+ )
315
+ for key in ("inputTokens", "outputTokens", "totalTokens")
316
+ )
317
+
318
+ memo: dict[str, dict[str, Any]] = {}
319
+
320
+ def aggregate_subtree(span_id: str, visiting: set[str] | None = None) -> dict[str, Any]:
321
+ if span_id in memo:
322
+ return memo[span_id]
323
+ active = set() if visiting is None else visiting
324
+ if span_id in active:
325
+ return empty_aggregate()
326
+ active.add(span_id)
327
+ descendants = combine(
328
+ aggregate_subtree(child_id, active) for child_id in children_by_parent.get(span_id, [])
329
+ )
330
+ active.remove(span_id)
331
+ own_usage = usage_by_id[span_id]
332
+ if not own_usage["usageReported"]:
333
+ result = descendants
334
+ elif is_compatibility_wrapper(own_usage, descendants):
335
+ result = descendants
336
+ for key in usage_fields:
337
+ if own_usage[key] is not None:
338
+ result[key] = own_usage[key]
339
+ result["usageCompleteness"][key] = True
340
+ else:
341
+ result = combine((billed_usage(own_usage), descendants))
342
+ memo[span_id] = result
343
+ return result
344
+
345
+ roots = [
346
+ span_id
347
+ for span_id in usage_by_id
348
+ if not parent_by_id[span_id] or parent_by_id[span_id] not in usage_by_id
349
+ ]
350
+ aggregate = combine(aggregate_subtree(span_id) for span_id in roots)
351
+ for span_id in usage_by_id:
352
+ if span_id not in memo:
353
+ aggregate = combine((aggregate, aggregate_subtree(span_id)))
354
+ aggregate = combine((aggregate, *(billed_usage(usage) for usage in anonymous_usages)))
355
+ if aggregate["billingSpanCount"] == 0:
356
+ return _usage_from_attributes({})
357
+ if not aggregate["usageCompleteness"]["totalTokens"]:
358
+ aggregate["totalTokens"] = None
359
+ aggregate.pop("billingSpanCount", None)
360
+ return aggregate
361
+
362
+
363
+ def _merge_usage(primary: dict[str, Any], fallback: dict[str, Any]) -> dict[str, Any]:
364
+ """Keep explicit root counters and fill only absent fields from leaf usage."""
365
+
366
+ merged: dict[str, Any] = {
367
+ key: primary[key] if primary[key] is not None else fallback[key]
368
+ for key in (
369
+ "inputTokens",
370
+ "outputTokens",
371
+ "cachedInputTokens",
372
+ "reasoningOutputTokens",
373
+ )
374
+ }
375
+ completeness = {
376
+ key: (
377
+ primary["usageCompleteness"][key]
378
+ if primary[key] is not None
379
+ else fallback["usageCompleteness"][key]
380
+ )
381
+ for key in (
382
+ "inputTokens",
383
+ "outputTokens",
384
+ "cachedInputTokens",
385
+ "reasoningOutputTokens",
386
+ )
387
+ }
388
+ if primary["totalTokens"] is not None:
389
+ merged["totalTokens"] = primary["totalTokens"]
390
+ completeness["totalTokens"] = primary["usageCompleteness"]["totalTokens"]
391
+ elif (
392
+ merged["inputTokens"] is not None
393
+ and merged["outputTokens"] is not None
394
+ and completeness["inputTokens"]
395
+ and completeness["outputTokens"]
396
+ ):
397
+ merged["totalTokens"] = merged["inputTokens"] + merged["outputTokens"]
398
+ completeness["totalTokens"] = True
399
+ elif primary["inputTokens"] is None and primary["outputTokens"] is None:
400
+ merged["totalTokens"] = (
401
+ fallback["totalTokens"] if fallback["usageCompleteness"]["totalTokens"] else None
402
+ )
403
+ completeness["totalTokens"] = fallback["usageCompleteness"]["totalTokens"]
404
+ else:
405
+ merged["totalTokens"] = None
406
+ completeness["totalTokens"] = False
407
+ reported = primary["usageReported"] or fallback["usageReported"]
408
+ return {
409
+ **merged,
410
+ "usageCompleteness": completeness,
411
+ "usageReported": reported,
412
+ "usageSource": primary["usageSource"] or fallback["usageSource"],
413
+ }
414
+
415
+
416
+ class OtlpTraceStore:
417
+ """Persist one canonical OTLP JSON document per local Trace."""
418
+
419
+ def __init__(self, workspace: Workspace) -> None:
420
+ self.workspace = workspace
421
+
422
+ def _path(self, trace_id: str) -> Path:
423
+ canonical = _canonical_trace_id(trace_id)
424
+ return self.workspace.resolve(Path(".agentkit/traces") / f"{canonical}.otlp.json")
425
+
426
+ def sync(self, record: RunRecord, events: list[RunEvent]) -> dict[str, Any]:
427
+ payload = self._build_otlp(record, events)
428
+ self.workspace.atomic_write_text(
429
+ self._path(record.trace_id),
430
+ json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
431
+ )
432
+ return payload
433
+
434
+ def get_otlp(self, trace_id: str) -> dict[str, Any]:
435
+ path = self._path(trace_id)
436
+ if not path.is_file():
437
+ raise not_found("trace", trace_id)
438
+ try:
439
+ payload = json.loads(path.read_text(encoding="utf-8"))
440
+ except (OSError, ValueError) as exc:
441
+ raise not_found("trace", trace_id) from exc
442
+ if not isinstance(payload, dict) or not isinstance(payload.get("resourceSpans"), list):
443
+ raise not_found("trace", trace_id)
444
+ return payload
445
+
446
+ def get_trace_view(self, trace_id: str) -> dict[str, Any]:
447
+ raw = self.get_otlp(trace_id)
448
+ spans = self._spans(raw)
449
+ if not spans:
450
+ raise not_found("trace", trace_id)
451
+ root = next((span for span in spans if not span.get("parentSpanId")), spans[0])
452
+ root_attributes = _decoded_attributes(root.get("attributes", []))
453
+ canonical = root["traceId"]
454
+ duration = root_attributes.get("agentkit.duration.ms")
455
+ root_usage = _usage_from_attributes(root_attributes)
456
+ leaf_usage = _aggregate_span_usage(
457
+ span for span in spans if span.get("spanId") != root.get("spanId")
458
+ )
459
+ usage = _merge_usage(root_usage, leaf_usage)
460
+ metrics = {
461
+ "durationMs": int(duration) if duration is not None else None,
462
+ "durationSource": root_attributes.get("agentkit.duration.source"),
463
+ **usage,
464
+ }
465
+ first_resource = (raw.get("resourceSpans") or [{}])[0]
466
+ first_scope = (first_resource.get("scopeSpans") or [{}])[0]
467
+ return {
468
+ "traceId": canonical,
469
+ "rootSpanId": root["spanId"],
470
+ "runId": root_attributes.get("agentkit.run.id", ""),
471
+ "agentId": root_attributes.get("gen_ai.agent.id", ""),
472
+ "sessionId": root_attributes.get("gen_ai.conversation.id", ""),
473
+ "runtimeType": root_attributes.get("agentkit.runtime.type", ""),
474
+ "model": root_attributes.get("gen_ai.request.model", ""),
475
+ "status": root_attributes.get("agentkit.run.status", ""),
476
+ "startedAt": root_attributes.get("agentkit.run.started_at"),
477
+ "target": {"type": "local", "name": "本地工作区"},
478
+ "resource": _decoded_attributes(
479
+ (first_resource.get("resource") or {}).get("attributes", [])
480
+ ),
481
+ "scope": dict(first_scope.get("scope") or {}),
482
+ "metrics": metrics,
483
+ "spans": [self._span_view(span) for span in spans],
484
+ "rawOtlpPath": f"/api/v1/traces/{quote(canonical, safe='')}/otlp",
485
+ }
486
+
487
+ def list_trace_summaries(
488
+ self,
489
+ *,
490
+ agent_id: str | None = None,
491
+ status: str | None = None,
492
+ limit: int = 200,
493
+ ) -> list[dict[str, Any]]:
494
+ summaries = self._trace_summaries(agent_id=agent_id, status=status)
495
+ summaries.sort(
496
+ key=lambda item: (item["startedAt"] or "", item["traceId"]),
497
+ reverse=True,
498
+ )
499
+ return summaries[: max(1, min(limit, 1000))]
500
+
501
+ def paginate_trace_summaries(
502
+ self,
503
+ *,
504
+ agent_id: str | None = None,
505
+ status: str | None = None,
506
+ query: str = "",
507
+ limit: int = 50,
508
+ cursor: str | None = None,
509
+ sort: str = "startedAt:desc",
510
+ ) -> dict[str, Any]:
511
+ if sort not in {"startedAt:desc", "startedAt:asc"}:
512
+ raise StudioError(
513
+ "PAGINATION_SORT_INVALID",
514
+ "Trace 排序字段无效",
515
+ status_code=422,
516
+ field="sort",
517
+ )
518
+ summaries = self._trace_summaries(
519
+ agent_id=agent_id,
520
+ status=status,
521
+ query=query,
522
+ )
523
+ filters = {
524
+ "agentId": agent_id or "",
525
+ "status": status or "",
526
+ "query": query.strip().lower(),
527
+ }
528
+ return keyset_page(
529
+ summaries,
530
+ key=lambda item: (item["startedAt"] or "", item["traceId"]),
531
+ reverse=sort.endswith(":desc"),
532
+ limit=max(1, min(limit, 1000)),
533
+ cursor=cursor,
534
+ namespace="traces",
535
+ sort=sort,
536
+ filters=filters,
537
+ )
538
+
539
+ def _trace_summaries(
540
+ self,
541
+ *,
542
+ agent_id: str | None = None,
543
+ status: str | None = None,
544
+ query: str = "",
545
+ ) -> list[dict[str, Any]]:
546
+ directory = self.workspace.resolve(".agentkit/traces")
547
+ summaries: list[dict[str, Any]] = []
548
+ normalized_query = query.strip().lower()
549
+ for path in directory.glob("*.otlp.json"):
550
+ try:
551
+ view = self.get_trace_view(path.name.removesuffix(".otlp.json"))
552
+ except Exception: # noqa: BLE001 - a corrupt local trace must not hide valid traces
553
+ continue
554
+ if agent_id and view["agentId"] != agent_id:
555
+ continue
556
+ if status and view["status"] != status:
557
+ continue
558
+ if normalized_query and not any(
559
+ normalized_query in str(view.get(field) or "").lower()
560
+ for field in (
561
+ "traceId",
562
+ "runId",
563
+ "agentId",
564
+ "sessionId",
565
+ "runtimeType",
566
+ "model",
567
+ "status",
568
+ )
569
+ ):
570
+ continue
571
+ metrics = view["metrics"]
572
+ summaries.append(
573
+ {
574
+ "traceId": view["traceId"],
575
+ "runId": view["runId"],
576
+ "agentId": view["agentId"],
577
+ "sessionId": view["sessionId"],
578
+ "runtimeType": view["runtimeType"],
579
+ "model": view["model"],
580
+ "status": view["status"],
581
+ "startedAt": view["startedAt"],
582
+ "durationMs": metrics["durationMs"],
583
+ "durationSource": metrics["durationSource"],
584
+ "inputTokens": metrics["inputTokens"],
585
+ "outputTokens": metrics["outputTokens"],
586
+ "totalTokens": metrics["totalTokens"],
587
+ "usageReported": metrics["usageReported"],
588
+ "usageCompleteness": metrics["usageCompleteness"],
589
+ "spanCount": len(view["spans"]),
590
+ "target": view["target"],
591
+ }
592
+ )
593
+ return summaries
594
+
595
+ def trace_overview(
596
+ self,
597
+ *,
598
+ range_name: str = "24h",
599
+ agent_id: str | None = None,
600
+ status: str | None = None,
601
+ now: datetime | None = None,
602
+ ) -> dict[str, Any]:
603
+ if range_name not in {"24h", "7d"}:
604
+ raise StudioError(
605
+ "TRACE_RANGE_INVALID",
606
+ "Trace 时间范围无效",
607
+ status_code=422,
608
+ field="range",
609
+ )
610
+ current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
611
+ if range_name == "24h":
612
+ bucket_delta = timedelta(hours=1)
613
+ bucket_count = 24
614
+ bucket_start = current.replace(minute=0, second=0, microsecond=0)
615
+ else:
616
+ bucket_delta = timedelta(days=1)
617
+ bucket_count = 7
618
+ bucket_start = current.replace(hour=0, minute=0, second=0, microsecond=0)
619
+ starts = [bucket_start - bucket_delta * index for index in range(bucket_count - 1, -1, -1)]
620
+ buckets = [
621
+ {
622
+ "startedAt": _iso(start),
623
+ "runs": 0,
624
+ "completed": 0,
625
+ }
626
+ for start in starts
627
+ ]
628
+ cutoff = starts[0]
629
+ summaries = self._trace_summaries(agent_id=agent_id, status=status)
630
+ in_range: list[dict[str, Any]] = []
631
+ for item in summaries:
632
+ raw_started_at = item.get("startedAt")
633
+ if not raw_started_at:
634
+ continue
635
+ try:
636
+ started_at = datetime.fromisoformat(
637
+ str(raw_started_at).replace("Z", "+00:00")
638
+ ).astimezone(timezone.utc)
639
+ except ValueError:
640
+ continue
641
+ if started_at < cutoff or started_at > current:
642
+ continue
643
+ index = int((started_at - cutoff) // bucket_delta)
644
+ if index < 0 or index >= bucket_count:
645
+ continue
646
+ in_range.append(item)
647
+ buckets[index]["runs"] += 1
648
+ if item["status"] == "COMPLETED":
649
+ buckets[index]["completed"] += 1
650
+
651
+ completed = [item for item in in_range if item["status"] == "COMPLETED"]
652
+ durations = [
653
+ int(item["durationMs"]) for item in completed if item.get("durationMs") is not None
654
+ ]
655
+ total = len(in_range)
656
+ return {
657
+ "range": range_name,
658
+ "total": total,
659
+ "completed": len(completed),
660
+ "successRate": len(completed) / total if total else None,
661
+ "averageDurationMs": (sum(durations) / len(durations) if durations else None),
662
+ "inputTokens": sum(int(item.get("inputTokens") or 0) for item in in_range),
663
+ "outputTokens": sum(int(item.get("outputTokens") or 0) for item in in_range),
664
+ "totalTokens": sum(int(item.get("totalTokens") or 0) for item in in_range),
665
+ "buckets": buckets,
666
+ }
667
+
668
+ def delete(
669
+ self,
670
+ trace_id: str,
671
+ *,
672
+ purge: bool,
673
+ trash_directory: Path | None = None,
674
+ ) -> None:
675
+ path = self._path(trace_id)
676
+ if not path.is_file():
677
+ return
678
+ if purge:
679
+ path.unlink()
680
+ return
681
+ if trash_directory is None:
682
+ raise ValueError("recoverable deletion requires a trash directory")
683
+ destination = self.workspace.resolve(trash_directory / "traces" / path.name)
684
+ destination.parent.mkdir(parents=True, exist_ok=True)
685
+ shutil.move(str(path), str(destination))
686
+
687
+ def _build_otlp(self, record: RunRecord, events: list[RunEvent]) -> dict[str, Any]:
688
+ trace_id = _canonical_trace_id(record.trace_id)
689
+ root_id = _span_id(trace_id, "root")
690
+ started_at = record.started_at or (
691
+ events[0].created_at if events else datetime.now(timezone.utc)
692
+ )
693
+ root_start = _unix_nano(started_at)
694
+ root_end = (
695
+ root_start + record.duration_ms * 1_000_000
696
+ if record.duration_ms is not None
697
+ else (
698
+ _unix_nano(record.completed_at)
699
+ if record.completed_at is not None
700
+ else _unix_nano(events[-1].created_at)
701
+ if events
702
+ else root_start
703
+ )
704
+ )
705
+ run_status = _enum_string(record.status)
706
+ root_attributes: dict[str, Any] = {
707
+ "gen_ai.operation.name": "invoke_agent",
708
+ "gen_ai.agent.id": record.agent_id,
709
+ "gen_ai.agent.name": record.agent_id,
710
+ "gen_ai.conversation.id": record.session_id,
711
+ "gen_ai.request.model": record.model,
712
+ "agentkit.run.id": record.id,
713
+ "agentkit.build.id": record.build_id,
714
+ "agentkit.run.status": run_status,
715
+ "agentkit.run.started_at": _iso(record.started_at),
716
+ "agentkit.runtime.type": record.runtime_type,
717
+ "agentkit.manifest.sha256": record.manifest_sha256,
718
+ "agentkit.usage.reported": record.usage.reported,
719
+ "agentkit.usage.source": record.usage.source,
720
+ "agentkit.duration.ms": record.duration_ms,
721
+ "agentkit.duration.source": record.duration_source,
722
+ }
723
+ if record.usage.reported:
724
+ root_attributes.update(
725
+ {
726
+ "gen_ai.usage.input_tokens": record.usage.input_tokens,
727
+ "gen_ai.usage.output_tokens": record.usage.output_tokens,
728
+ "gen_ai.usage.cached_input_tokens": record.usage.cached_input_tokens,
729
+ "gen_ai.usage.reasoning_tokens": record.usage.reasoning_output_tokens,
730
+ "agentkit.usage.total_tokens": record.usage.total_tokens,
731
+ }
732
+ )
733
+ root = {
734
+ "traceId": trace_id,
735
+ "spanId": root_id,
736
+ "name": f"invoke_agent {record.agent_id}",
737
+ "kind": 1,
738
+ "startTimeUnixNano": str(root_start),
739
+ "endTimeUnixNano": str(root_end),
740
+ "attributes": _attributes(root_attributes),
741
+ "events": self._root_events(events),
742
+ "status": {
743
+ "code": (
744
+ 1
745
+ if run_status == "COMPLETED"
746
+ else 2
747
+ if run_status in {"FAILED", "TIMED_OUT", "CANCELLED"}
748
+ else 0
749
+ )
750
+ },
751
+ }
752
+ spans = [
753
+ root,
754
+ *self._model_spans(trace_id, root_id, events, root_end),
755
+ *self._tool_spans(trace_id, root_id, events, root_end),
756
+ ]
757
+ spans.sort(key=lambda span: (int(span["startTimeUnixNano"]), span["spanId"]))
758
+ return {
759
+ "resourceSpans": [
760
+ {
761
+ "resource": {
762
+ "attributes": _attributes(
763
+ {
764
+ "service.name": "agentkit-studio",
765
+ "service.namespace": "ksadk",
766
+ "deployment.environment.name": "local",
767
+ "agentkit.runtime.target": "local",
768
+ }
769
+ )
770
+ },
771
+ "scopeSpans": [
772
+ {
773
+ "scope": {"name": "ksadk.studio.trace-adapter", "version": "1"},
774
+ "spans": spans,
775
+ }
776
+ ],
777
+ }
778
+ ]
779
+ }
780
+
781
+ @staticmethod
782
+ def _root_events(events: list[RunEvent]) -> list[dict[str, Any]]:
783
+ excluded = {
784
+ "proxy.requested",
785
+ "proxy.completed",
786
+ "model.requested",
787
+ "model.completed",
788
+ "command.started",
789
+ "command.completed",
790
+ "tool.started",
791
+ "tool.requested",
792
+ "tool.completed",
793
+ }
794
+ content_families = {
795
+ "thinking.delta": "thinking",
796
+ "thinking.completed": "thinking",
797
+ "message.delta": "message",
798
+ "message.completed": "message",
799
+ }
800
+ projected: list[dict[str, Any]] = []
801
+ content_groups: dict[tuple[str, str, str, str], list[RunEvent]] = {}
802
+ current_turn = ""
803
+ current_step = ""
804
+
805
+ for event in events:
806
+ runtime_event = event.data.get("runtimeEvent")
807
+ correlation = runtime_event if isinstance(runtime_event, dict) else {}
808
+ turn_id = str(correlation.get("turn_id") or "")
809
+ step_id = str(correlation.get("step_id") or "")
810
+ if turn_id:
811
+ current_turn = turn_id
812
+ if step_id and event.type in {"step.started", "model.call.begin"}:
813
+ current_step = step_id
814
+
815
+ family = content_families.get(event.type)
816
+ if family:
817
+ key = (
818
+ event.run_id,
819
+ turn_id or current_turn,
820
+ step_id or current_step,
821
+ family,
822
+ )
823
+ content_groups.setdefault(key, []).append(event)
824
+ continue
825
+ if event.type in excluded:
826
+ continue
827
+ projected.append(
828
+ {
829
+ "timeUnixNano": str(_unix_nano(event.created_at)),
830
+ "name": event.type,
831
+ "attributes": _attributes(_safe_event_attributes(event.data)),
832
+ }
833
+ )
834
+
835
+ for grouped in content_groups.values():
836
+ completed = next(
837
+ (event for event in reversed(grouped) if event.type.endswith(".completed")),
838
+ None,
839
+ )
840
+ source = completed or grouped[-1]
841
+ completed_text = source.data.get("text") if completed is not None else None
842
+ text = (
843
+ completed_text
844
+ if isinstance(completed_text, str) and completed_text
845
+ else "".join(
846
+ str(event.data.get("text") or "")
847
+ for event in grouped
848
+ if event.type.endswith(".delta")
849
+ )
850
+ )
851
+ data = dict(source.data)
852
+ data["text"] = text
853
+ data["delta_count"] = sum(event.type.endswith(".delta") for event in grouped)
854
+ projected.append(
855
+ {
856
+ "timeUnixNano": str(_unix_nano(source.created_at)),
857
+ "name": source.type,
858
+ "attributes": _attributes(_safe_event_attributes(data)),
859
+ }
860
+ )
861
+
862
+ projected.sort(key=lambda event: int(event["timeUnixNano"]))
863
+ return projected
864
+
865
+ def _model_spans(
866
+ self, trace_id: str, root_id: str, events: list[RunEvent], root_end: int
867
+ ) -> list[dict[str, Any]]:
868
+ starts: list[RunEvent] = []
869
+ by_response: dict[str, RunEvent] = {}
870
+ spans: list[dict[str, Any]] = []
871
+ for event in events:
872
+ if event.type in {"proxy.requested", "model.requested"}:
873
+ starts.append(event)
874
+ response_id = str(event.data.get("responseId") or "")
875
+ if response_id:
876
+ by_response[response_id] = event
877
+ continue
878
+ if event.type not in {"proxy.completed", "model.completed"}:
879
+ continue
880
+ response_id = str(event.data.get("responseId") or "")
881
+ start = by_response.pop(response_id, None) if response_id else None
882
+ if start is None and starts:
883
+ start = starts.pop(0)
884
+ elif start in starts:
885
+ starts.remove(start)
886
+ if start is None:
887
+ continue
888
+ spans.append(self._model_span(trace_id, root_id, start, event, root_end))
889
+ for start in starts:
890
+ spans.append(self._model_span(trace_id, root_id, start, None, root_end))
891
+ return spans
892
+
893
+ @staticmethod
894
+ def _model_span(
895
+ trace_id: str,
896
+ root_id: str,
897
+ start: RunEvent,
898
+ end: RunEvent | None,
899
+ root_end: int,
900
+ ) -> dict[str, Any]:
901
+ model = str(start.data.get("model") or "unknown")
902
+ response_id = str(start.data.get("responseId") or "")
903
+ start_ns = _unix_nano(start.created_at)
904
+ duration = _duration_from(end.data) if end is not None else None
905
+ end_ns = (
906
+ start_ns + duration * 1_000_000
907
+ if duration is not None
908
+ else _unix_nano(end.created_at)
909
+ if end is not None
910
+ else root_end
911
+ )
912
+ attrs: dict[str, Any] = {
913
+ "gen_ai.operation.name": "chat",
914
+ "gen_ai.request.model": model,
915
+ "gen_ai.response.id": response_id,
916
+ }
917
+ if end is not None:
918
+ raw_usage = end.data.get("usage")
919
+ usage: dict[str, Any] = raw_usage if isinstance(raw_usage, dict) else {}
920
+ for source_key, target_key in (
921
+ ("inputTokens", "gen_ai.usage.input_tokens"),
922
+ ("outputTokens", "gen_ai.usage.output_tokens"),
923
+ ("cachedInputTokens", "gen_ai.usage.cached_input_tokens"),
924
+ ("reasoningOutputTokens", "gen_ai.usage.reasoning_tokens"),
925
+ ):
926
+ if usage.get(source_key) is not None:
927
+ attrs[target_key] = int(usage[source_key])
928
+ status_code = int(end.data.get("statusCode") or 200) if end is not None else 0
929
+ return {
930
+ "traceId": trace_id,
931
+ "spanId": _span_id(trace_id, f"model:{start.id}:{response_id}"),
932
+ "parentSpanId": root_id,
933
+ "name": f"chat {model}",
934
+ "kind": 3,
935
+ "startTimeUnixNano": str(start_ns),
936
+ "endTimeUnixNano": str(max(start_ns, end_ns)),
937
+ "attributes": _attributes(attrs),
938
+ "events": [],
939
+ "status": {"code": 1 if 200 <= status_code < 400 else 2 if status_code else 0},
940
+ }
941
+
942
+ def _tool_spans(
943
+ self, trace_id: str, root_id: str, events: list[RunEvent], root_end: int
944
+ ) -> list[dict[str, Any]]:
945
+ starts: dict[str, RunEvent] = {}
946
+ spans: list[dict[str, Any]] = []
947
+ for event in events:
948
+ if event.type in {"command.started", "tool.started", "tool.requested"}:
949
+ call_id = str(event.data.get("callId") or event.data.get("call_id") or event.id)
950
+ starts[call_id] = event
951
+ continue
952
+ if event.type not in {"command.completed", "tool.completed"}:
953
+ continue
954
+ call_id = str(event.data.get("callId") or event.data.get("call_id") or "")
955
+ start = starts.pop(call_id, None)
956
+ if start is None:
957
+ continue
958
+ spans.append(self._tool_span(trace_id, root_id, call_id, start, event, root_end))
959
+ for call_id, start in starts.items():
960
+ spans.append(self._tool_span(trace_id, root_id, call_id, start, None, root_end))
961
+ return spans
962
+
963
+ @staticmethod
964
+ def _tool_span(
965
+ trace_id: str,
966
+ root_id: str,
967
+ call_id: str,
968
+ start: RunEvent,
969
+ end: RunEvent | None,
970
+ root_end: int,
971
+ ) -> dict[str, Any]:
972
+ name = str(
973
+ start.data.get("tool")
974
+ or ("codex.command" if start.type == "command.started" else "tool")
975
+ )
976
+ start_ns = _unix_nano(start.created_at)
977
+ duration = _duration_from(end.data) if end is not None else None
978
+ end_ns = (
979
+ start_ns + duration * 1_000_000
980
+ if duration is not None
981
+ else _unix_nano(end.created_at)
982
+ if end is not None
983
+ else root_end
984
+ )
985
+ exit_code = end.data.get("exitCode") if end is not None else None
986
+ status = str(end.data.get("status") or "") if end is not None else ""
987
+ failed = (exit_code not in (None, 0)) or status.lower() in {"failed", "error"}
988
+ include_content = _trace_content_enabled()
989
+ tool_input: Any = None
990
+ if include_content:
991
+ if start.type == "command.started":
992
+ tool_input = start.data.get("command") or start.data.get("commandActions")
993
+ elif start.type == "tool.started":
994
+ tool_input = (
995
+ start.data.get("args") or start.data.get("arguments") or start.data.get("input")
996
+ )
997
+ tool_output: Any = None
998
+ if include_content and end is not None:
999
+ tool_output = (
1000
+ end.data.get("result") or end.data.get("output") or end.data.get("content")
1001
+ )
1002
+ attrs: dict[str, Any] = {
1003
+ "gen_ai.operation.name": "execute_tool",
1004
+ "gen_ai.tool.name": name,
1005
+ "gen_ai.tool.call.id": call_id,
1006
+ "agentkit.tool.exit_code": exit_code,
1007
+ "agentkit.tool.status": status,
1008
+ }
1009
+ if tool_input is not None:
1010
+ attrs["agentkit.tool.input"] = _truncate(_stringify(tool_input))
1011
+ if tool_output is not None:
1012
+ attrs["agentkit.tool.output"] = _truncate(_stringify(tool_output))
1013
+ return {
1014
+ "traceId": trace_id,
1015
+ "spanId": _span_id(trace_id, f"tool:{call_id}:{start.id}"),
1016
+ "parentSpanId": root_id,
1017
+ "name": f"execute_tool {name}",
1018
+ "kind": 1,
1019
+ "startTimeUnixNano": str(start_ns),
1020
+ "endTimeUnixNano": str(max(start_ns, end_ns)),
1021
+ "attributes": _attributes(attrs),
1022
+ "events": [],
1023
+ "status": {"code": 2 if failed else 1 if end is not None else 0},
1024
+ }
1025
+
1026
+ @staticmethod
1027
+ def _spans(raw: dict[str, Any]) -> list[dict[str, Any]]:
1028
+ spans: list[dict[str, Any]] = []
1029
+ for resource in raw.get("resourceSpans", []):
1030
+ for scope in resource.get("scopeSpans", []):
1031
+ spans.extend(scope.get("spans", []))
1032
+ return spans
1033
+
1034
+ @staticmethod
1035
+ def _span_view(span: dict[str, Any]) -> dict[str, Any]:
1036
+ start_ns = int(span.get("startTimeUnixNano") or 0)
1037
+ end_ns = int(span.get("endTimeUnixNano") or start_ns)
1038
+ return {
1039
+ "spanId": span["spanId"],
1040
+ "parentSpanId": span.get("parentSpanId"),
1041
+ "name": span.get("name", ""),
1042
+ "kind": _SPAN_KIND.get(int(span.get("kind") or 0), "UNSPECIFIED"),
1043
+ "status": _STATUS_CODE.get(int((span.get("status") or {}).get("code") or 0), "UNSET"),
1044
+ "startTimeUnixNano": str(start_ns),
1045
+ "endTimeUnixNano": str(end_ns),
1046
+ "durationMs": max(0, round((end_ns - start_ns) / 1_000_000, 3)),
1047
+ "attributes": _decoded_attributes(span.get("attributes", [])),
1048
+ "events": [
1049
+ {
1050
+ "name": event.get("name", ""),
1051
+ "timeUnixNano": event.get("timeUnixNano", "0"),
1052
+ "attributes": _decoded_attributes(event.get("attributes", [])),
1053
+ }
1054
+ for event in span.get("events", [])
1055
+ ],
1056
+ "links": span.get("links", []),
1057
+ }
1058
+
1059
+
1060
+ __all__ = ["OtlpTraceStore"]