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/codex/runtime.py CHANGED
@@ -1,4 +1,4 @@
1
- """CodexRuntime — 非 ADK 体系的第三验证样本,按 Wegent 重托管模式 (goal-09)。
1
+ """CodexRuntimeAdapter — 非 ADK 体系的第三验证样本 (goal-09)。
2
2
 
3
3
  对执行生命周期负责(不做 veadk 式薄桥接),后端能力面对齐 ``openai-codex`` SDK 真实线程模型
4
4
  (``thread_start``/``thread.turn``/``handle.stream``/``handle.interrupt``/``thread_resume``):
@@ -19,19 +19,39 @@
19
19
  from __future__ import annotations
20
20
 
21
21
  import asyncio
22
+ import base64
23
+ import binascii
24
+ import hashlib
22
25
  import json
23
26
  import logging
27
+ import re
28
+ import time
29
+ from collections.abc import Mapping
24
30
  from dataclasses import dataclass, field
31
+ from pathlib import Path
25
32
  from typing import Any, AsyncIterator, Optional
26
33
 
27
34
  from ksadk.codex.client import CodexClient
28
- from ksadk.codex.phase import CodexPhaseTracker
29
- from ksadk.events.runtime_event import EventType, RuntimeEvent
35
+ from ksadk.events.adapters.codex import CodexAdapterContext, CodexEventAdapter
36
+ from ksadk.events.canonical import (
37
+ ErrorInfo,
38
+ InteractionRequested,
39
+ InteractionResolved,
40
+ RunCanceled,
41
+ RunCompleted,
42
+ RunFailed,
43
+ RunInterrupted,
44
+ RuntimeEvent,
45
+ SourceRef,
46
+ )
47
+ from ksadk.events.identity import stable_event_id, stable_item_id, stable_scope_id
48
+ from ksadk.kernel.contracts import RuntimeCapability, RuntimeCapabilityMatrix
30
49
  from ksadk.runtime.adapter import (
31
50
  BaseRuntime,
32
51
  CancelResult,
33
52
  CheckpointCapability,
34
53
  CheckpointDescriptor,
54
+ PauseResult,
35
55
  ResumePayload,
36
56
  ResumeTarget,
37
57
  RunHandle,
@@ -41,6 +61,8 @@ from ksadk.runtime.adapter import (
41
61
 
42
62
  logger = logging.getLogger(__name__)
43
63
 
64
+ _INTERRUPT_DRAIN_TIMEOUT_SECONDS = 5.0
65
+
44
66
 
45
67
  class _CodexAsBaseRuntime(BaseRuntime):
46
68
  """把 CodexClient 包装为 BaseRuntime(原生能力面)。"""
@@ -50,7 +72,13 @@ class _CodexAsBaseRuntime(BaseRuntime):
50
72
  self.runtime_type = "codex"
51
73
 
52
74
  def native_capabilities(self) -> dict[str, Any]:
53
- return {"Framework": "codex", "cancel": "thread", "resume": "thread_id"}
75
+ return {
76
+ "Framework": "codex",
77
+ "cancel": "thread",
78
+ "pause": "interrupt_then_resume_thread",
79
+ "resume": "thread_id",
80
+ "live_interaction": True,
81
+ }
54
82
 
55
83
 
56
84
  @dataclass
@@ -62,9 +90,15 @@ class _CodexThread:
62
90
  pending_approvals: set[str] = field(default_factory=set)
63
91
  done: bool = False
64
92
  interrupted: bool = False
93
+ paused: bool = False
94
+ started_at: int | None = None
95
+ completed_at: int | None = None
96
+ duration_ms: int | None = None
97
+ goal_mode: bool = False
98
+ continuation_preexisting: bool = False
65
99
 
66
100
 
67
- class CodexRuntime(RuntimeAdapter):
101
+ class CodexRuntimeAdapter(RuntimeAdapter):
68
102
  """Codex 的 RuntimeAdapter(重托管)。"""
69
103
 
70
104
  def __init__(
@@ -79,6 +113,7 @@ class CodexRuntime(RuntimeAdapter):
79
113
  self._sandbox_read_only = sandbox_read_only
80
114
  self._turn_timeout_seconds = turn_timeout_seconds
81
115
  self._threads: dict[str, _CodexThread] = {}
116
+ self._requests: dict[str, StartRequest] = {}
82
117
  self._known_threads: set[str] = set()
83
118
  self._pending_cancels: set[str] = set()
84
119
  # 被杀/interrupt 的 session 不持久化(goal-09 契约 1)。
@@ -86,6 +121,33 @@ class CodexRuntime(RuntimeAdapter):
86
121
  # 可观测:最近一次 cancel 级联丢弃的审批集(contract test 断言用)。
87
122
  self.last_cancel_dropped_approvals: set[str] = set()
88
123
  self._seq = 0
124
+ self._closed = False
125
+
126
+ # ---- capability matrix(v1,诚实声明) ----
127
+
128
+ def capabilities(self) -> RuntimeCapabilityMatrix:
129
+ """Codex 真实矩阵:thread 级 cancel/pause/resume + 审批 submit + snapshot
130
+ checkpoint 均为后端原生能力;attach/durable_restore 未实现(线程表在本进程,
131
+ attach seam 缺失),steer/inject 无原生通道。
132
+ """
133
+
134
+ def _unavailable(reason: str) -> RuntimeCapability:
135
+ return RuntimeCapability(supported=False, mode="unavailable", reason=reason)
136
+
137
+ return RuntimeCapabilityMatrix(
138
+ cancel=RuntimeCapability(supported=True, mode="native"),
139
+ pause=RuntimeCapability(supported=True, mode="native"),
140
+ resume=RuntimeCapability(supported=True, mode="native"),
141
+ submit_interaction=RuntimeCapability(supported=True, mode="native"),
142
+ attach=_unavailable("codex_process_local_thread_table"),
143
+ steer=_unavailable("runtime_no_native_steer"),
144
+ inject=_unavailable("runtime_no_native_inject"),
145
+ checkpoint=RuntimeCapability(supported=True, mode="native"),
146
+ durable_restore=_unavailable("codex_durable_restore_requires_attach_seam"),
147
+ goal=RuntimeCapability(supported=True, mode="native"),
148
+ loop=_unavailable("codex_loop_requires_run_control_spec"),
149
+ plan=RuntimeCapability(supported=True, mode="native"),
150
+ )
89
151
 
90
152
  # ---- 六动词 ----
91
153
 
@@ -98,16 +160,36 @@ class CodexRuntime(RuntimeAdapter):
98
160
  else:
99
161
  # 把 model + base_instructions 传给 codex thread(配置契约,见 plan C)
100
162
  thread_config: dict[str, Any] = {"sandbox_read_only": self._sandbox_read_only}
163
+ if request.config:
164
+ for key in ("sandbox", "approval_mode", "summary", "ephemeral"):
165
+ value = request.config.get(key)
166
+ if key == "ephemeral" and value is not None:
167
+ thread_config[key] = bool(value)
168
+ elif value:
169
+ thread_config[key] = value
101
170
  if request.model:
102
171
  thread_config["model"] = request.model
103
172
  base_instructions = request.config.get("base_instructions")
104
173
  if base_instructions:
105
174
  thread_config["base_instructions"] = base_instructions
175
+ cwd = request.config.get("cwd")
176
+ if cwd:
177
+ thread_config["cwd"] = str(cwd)
178
+ # AgentKernel creates one adapter/transport per durable turn and
179
+ # closes it after the canonical terminal event. The next turn
180
+ # therefore resumes the native thread from a new app-server
181
+ # process; an ephemeral Codex thread has no rollout and cannot be
182
+ # resumed across that transport boundary.
183
+ thread_config.setdefault("ephemeral", False)
106
184
  thread_id = await self._client.start_thread(thread_config)
107
185
  self._known_threads.add(thread_id)
108
- thread = _CodexThread(thread_id=thread_id)
186
+ thread = _CodexThread(
187
+ thread_id=thread_id,
188
+ continuation_preexisting=bool(provided),
189
+ )
109
190
  thread.__dict__["_start_request"] = request
110
191
  self._threads[thread_id] = thread
192
+ self._requests[thread_id] = request
111
193
  return RunHandle(
112
194
  run_id=thread_id,
113
195
  session_id=request.session_id,
@@ -135,21 +217,75 @@ class CodexRuntime(RuntimeAdapter):
135
217
  thread.pending_approvals.clear()
136
218
  # 真实中断:handle.interrupt() 停活跃 turn(真实 SDK 机制;无"杀进程"概念,
137
219
  # thread 由 codex 后端托管,interrupt 即终止当前 turn 的执行)。
138
- interrupted = await self._client.interrupt_active_turn(thread.thread_id)
220
+ # 先唤醒 Studio 的 stream;等待 request_user_input 时,中断 RPC
221
+ # 可能要等 turn 状态前进,否则取消接口与流会互相等待。
222
+ thread.interrupt_event.set()
223
+ interrupted = (
224
+ await self._client.cancel_goal(thread.thread_id)
225
+ if thread.goal_mode
226
+ else await self._client.interrupt_active_turn(thread.thread_id)
227
+ )
139
228
  if not interrupted:
140
229
  # 无活跃 handle 可 interrupt(竞态:turn 刚好结束)→ 视为未在运行。
141
230
  return CancelResult.NOT_RUNNING
142
- thread.interrupt_event.set()
143
231
  # 被中断的 session 不持久化(goal-09 契约 1)。
144
232
  self._do_not_persist.add(thread_id)
145
233
  thread.done = True
146
234
  self._threads.pop(thread_id, None)
235
+ self._requests.pop(thread_id, None)
147
236
  self._pending_cancels.discard(thread_id)
148
237
  return CancelResult.INTERRUPTED_ACTIVE_TURN
149
238
  except Exception: # noqa: BLE001
150
239
  logger.exception("codex cancel thread %s 失败", thread_id)
151
240
  return CancelResult.FAILED
152
241
 
242
+ async def pause(self, handle: RunHandle) -> PauseResult:
243
+ thread = self._threads.get(handle.run_id)
244
+ if thread is None or thread.done or not thread.streaming:
245
+ return PauseResult.NOT_RUNNING
246
+ if thread.pending_approvals:
247
+ # A blocked native approval must be answered through the interaction
248
+ # card. Interrupting while the SDK reader waits on that approval can
249
+ # deadlock the JSON-RPC command channel.
250
+ return PauseResult.FAILED
251
+ try:
252
+ interrupted = (
253
+ await self._client.pause_goal(thread.thread_id)
254
+ if thread.goal_mode
255
+ else await self._client.interrupt_active_turn(thread.thread_id)
256
+ )
257
+ if not interrupted:
258
+ return PauseResult.NOT_RUNNING
259
+ thread.paused = True
260
+ # ``thread/goal/set(status=paused)`` only prevents the next
261
+ # continuation. Wake the adapter stream as well so the current
262
+ # physical goal turn becomes an honest resumable pause now.
263
+ thread.interrupt_event.set()
264
+ return PauseResult.PAUSED_ACTIVE_TURN
265
+ except Exception: # noqa: BLE001
266
+ logger.exception("codex pause thread %s 失败", handle.run_id)
267
+ return PauseResult.FAILED
268
+
269
+ async def submit(self, handle: RunHandle, payload: ResumePayload) -> None:
270
+ if not payload.call_id:
271
+ raise ValueError("Codex live submit requires call_id")
272
+ raw = payload.data if isinstance(payload.data, dict) else {"decision": payload.data}
273
+ if payload.kind == "approval_decision":
274
+ decision = str(raw.get("decision") or raw.get("name") or "")
275
+ resolved = await self._client.resolve_approval(payload.call_id, decision)
276
+ elif payload.kind == "hitl_answer":
277
+ resolved = await self._client.resolve_interaction(
278
+ payload.call_id,
279
+ {key: value for key, value in raw.items() if key != "decision"},
280
+ )
281
+ else:
282
+ raise ValueError("Codex live submit requires approval_decision or hitl_answer")
283
+ if not resolved:
284
+ raise ValueError(f"interaction {payload.call_id!r} is not pending")
285
+ thread = self._threads.get(handle.run_id)
286
+ if thread is not None:
287
+ thread.pending_approvals.discard(payload.call_id)
288
+
153
289
  async def resume(
154
290
  self,
155
291
  handle: RunHandle,
@@ -158,16 +294,35 @@ class CodexRuntime(RuntimeAdapter):
158
294
  ) -> RunHandle:
159
295
  # resume 用 thread id 语义(resume_thread_id),不套 ADK invocation 模型。
160
296
  if target.kind != "thread_id":
161
- raise ValueError(f"CodexRuntime resume 仅支持 thread_id 目标,得到 {target.kind!r}")
297
+ raise ValueError(
298
+ f"CodexRuntimeAdapter resume 仅支持 thread_id 目标,得到 {target.kind!r}"
299
+ )
162
300
  if handle.run_id in self._do_not_persist:
163
301
  raise ValueError(f"thread {handle.run_id} 已被中断/杀进程,不持久化,不可 resume")
164
302
  self._pending_cancels.discard(handle.run_id)
165
303
  self._known_threads.add(target.id)
166
- thread = _CodexThread(thread_id=target.id)
304
+ thread = _CodexThread(
305
+ thread_id=target.id,
306
+ continuation_preexisting=True,
307
+ )
167
308
  thread.__dict__["_resume"] = {"target": target, "payload": payload}
309
+ request = self._requests.get(handle.run_id)
310
+ if request is not None:
311
+ thread.__dict__["_request_config"] = request
312
+ if request is not None and str(request.config.get("goal_objective") or "").strip():
313
+ # Restart the same native Goal operation on the persisted thread;
314
+ # its transcript remains in Codex, while the objective/config are
315
+ # needed to resume Goal rather than issuing a normal text turn.
316
+ thread.goal_mode = True
168
317
  self._threads[handle.run_id] = thread
169
318
  # 真实恢复:thread_resume 接入后端既有 thread(thread_id 语义,不套 ADK invocation)。
170
- await self._client.resume_thread(target.id, {"sandbox_read_only": self._sandbox_read_only})
319
+ resume_config: dict[str, Any] = {"sandbox_read_only": self._sandbox_read_only}
320
+ if request is not None and request.config:
321
+ for key in ("sandbox", "approval_mode"):
322
+ value = request.config.get(key)
323
+ if value:
324
+ resume_config[key] = value
325
+ await self._client.resume_thread(target.id, resume_config)
171
326
  handle.native_ref["thread_id"] = target.id
172
327
  handle.native_ref["resume_thread_id"] = target.id
173
328
  handle.native_ref["resume_payload"] = payload.data if payload else None
@@ -198,12 +353,17 @@ class CodexRuntime(RuntimeAdapter):
198
353
  )
199
354
 
200
355
  async def close(self, handle: RunHandle) -> None:
356
+ if self._closed:
357
+ return
358
+ self._closed = True
201
359
  thread = self._threads.pop(handle.run_id, None)
202
- if thread is not None:
360
+ self._requests.pop(handle.run_id, None)
361
+ active = thread is not None and thread.streaming and not thread.done
362
+ if active:
203
363
  thread.interrupt_event.set()
204
364
  try:
205
- active_thread_id = thread.thread_id if thread is not None else handle.run_id
206
- await self._client.interrupt_active_turn(active_thread_id)
365
+ if active:
366
+ await self._client.interrupt_active_turn(thread.thread_id)
207
367
  finally:
208
368
  # AsyncCodex.close owns terminate/wait/kill for the app-server child.
209
369
  await self._client.close()
@@ -229,34 +389,31 @@ class CodexRuntime(RuntimeAdapter):
229
389
 
230
390
  if handle.run_id in self._pending_cancels:
231
391
  self._pending_cancels.discard(handle.run_id)
232
- yield self._event(
392
+ yield self._make_run_canceled(
233
393
  handle,
234
- EventType.RUN_CANCELED,
235
- {
236
- "status": "cancelled",
237
- "cancel_result": CancelResult.PENDING_CANCEL_RECORDED.value,
238
- },
394
+ reason=f"pending_cancel:{CancelResult.PENDING_CANCEL_RECORDED.value}",
239
395
  )
240
396
  return
241
397
 
242
- yield self._event(handle, EventType.RUN_STARTED, {"status": "in_progress"})
243
- tracker = CodexPhaseTracker()
244
398
  request = thread.__dict__.get("_start_request")
245
399
  resume_state = thread.__dict__.get("_resume")
246
400
  if request is not None:
247
- prompt = request.input
401
+ prompt = _request_prompt(request)
248
402
  elif resume_state is not None:
249
403
  prompt = _resume_prompt(resume_state.get("payload"))
250
404
  else:
251
405
  prompt = ""
406
+ run_input = _build_run_input(request, prompt)
252
407
  thread.streaming = True
253
408
  thread.turn_id = thread.turn_id or f"turn_{thread.thread_id}"
254
409
  try:
255
- async for event in self._map_codex_stream(handle, thread, tracker, prompt):
410
+ async for event in self._map_codex_stream(handle, thread, run_input):
256
411
  yield event
257
- # 正常结束(非 interrupt):补 RUN_COMPLETED(AGUI 投射器据此发 RunFinished success)
258
- if not thread.interrupted:
259
- yield self._event(handle, EventType.RUN_COMPLETED, {"status": "completed"})
412
+ except asyncio.CancelledError:
413
+ thread.interrupted = True
414
+ self._do_not_persist.add(handle.run_id)
415
+ await self._client.interrupt_active_turn(thread.thread_id)
416
+ raise
260
417
  except TimeoutError:
261
418
  self.last_cancel_dropped_approvals = set(thread.pending_approvals)
262
419
  thread.pending_approvals.clear()
@@ -264,18 +421,10 @@ class CodexRuntime(RuntimeAdapter):
264
421
  # Closing the SDK transport terminates and waits for the app-server
265
422
  # child even when the stream is stuck between notifications.
266
423
  await self._client.close()
267
- yield self._event(
268
- handle,
269
- EventType.RUN_FAILED,
270
- {"status": "failed", "error": "codex turn timed out"},
271
- )
272
- except Exception as exc: # noqa: BLE001 通用兜底:任何异常都发 RUN_FAILED
424
+ yield self._make_run_failed(handle, "codex turn timed out")
425
+ except Exception as exc: # noqa: BLE001 通用兜底:任何异常都发 RunFailed
273
426
  self._do_not_persist.add(handle.run_id)
274
- yield self._event(
275
- handle,
276
- EventType.RUN_FAILED,
277
- {"status": "failed", "error": str(exc)},
278
- )
427
+ yield self._make_run_failed(handle, str(exc))
279
428
  finally:
280
429
  thread.streaming = False
281
430
  thread.done = True
@@ -285,19 +434,50 @@ class CodexRuntime(RuntimeAdapter):
285
434
  self,
286
435
  handle: RunHandle,
287
436
  thread: _CodexThread,
288
- tracker: CodexPhaseTracker,
289
437
  prompt: Any,
290
438
  ) -> AsyncIterator[RuntimeEvent]:
291
- codex_gen = self._client.run_turn(
292
- thread.thread_id,
293
- prompt,
294
- config={"sandbox_read_only": self._sandbox_read_only},
439
+ request = thread.__dict__.get("_start_request") or thread.__dict__.get("_request_config")
440
+ adapter = CodexEventAdapter(
441
+ known_thread_ids=(thread.thread_id,)
442
+ if thread.continuation_preexisting
443
+ else (),
295
444
  )
445
+ context = CodexAdapterContext(run_id=self._event_run_id(handle))
446
+ run_config: dict[str, Any] = {"sandbox_read_only": self._sandbox_read_only}
447
+ if request is not None and request.config:
448
+ for key in ("sandbox", "approval_mode", "summary", "collaboration_mode"):
449
+ value = request.config.get(key)
450
+ if key == "approval_mode" and value == "manual":
451
+ continue
452
+ if value:
453
+ run_config[key] = value
454
+ if request is not None and request.model:
455
+ run_config["model"] = request.model
456
+ goal_objective = str(
457
+ request.config.get("goal_objective") or ""
458
+ if request is not None and request.config
459
+ else ""
460
+ ).strip()
461
+ if goal_objective:
462
+ thread.goal_mode = True
463
+ codex_gen = self._client.run_goal(
464
+ thread.thread_id,
465
+ goal_objective,
466
+ config=run_config,
467
+ )
468
+ else:
469
+ codex_gen = self._client.run_turn(
470
+ thread.thread_id,
471
+ prompt,
472
+ config=run_config,
473
+ )
296
474
  deadline = (
297
475
  asyncio.get_running_loop().time() + self._turn_timeout_seconds
298
476
  if self._turn_timeout_seconds is not None
299
477
  else None
300
478
  )
479
+ chunk_task: asyncio.Task[Any] | None = None
480
+ interrupt_task: asyncio.Task[bool] | None = None
301
481
  try:
302
482
  while True:
303
483
  chunk_task = asyncio.ensure_future(_anext_or_stop(codex_gen))
@@ -321,23 +501,116 @@ class CodexRuntime(RuntimeAdapter):
321
501
  await self._client.close()
322
502
  await asyncio.gather(chunk_task, interrupt_task, return_exceptions=True)
323
503
  raise TimeoutError("codex turn timed out")
324
- for task in pending:
325
- task.cancel()
326
504
  if interrupt_task in done:
327
- chunk_task.cancel()
505
+ # ``openai-codex`` implements ``next_turn_notification``
506
+ # with ``asyncio.to_thread(queue.get)``. Cancelling that
507
+ # awaiter unregisters its queue while the worker thread is
508
+ # still blocked, so neither a later transport close nor
509
+ # event-loop teardown can wake it. After interrupting a
510
+ # turn, keep the queue registered and drain the SDK's
511
+ # terminal notifications instead. If the backend fails to
512
+ # finish promptly, close the transport *before* awaiting
513
+ # the waiter so MessageRouter.fail_all can release it.
514
+ drain_deadline = (
515
+ asyncio.get_running_loop().time()
516
+ + _INTERRUPT_DRAIN_TIMEOUT_SECONDS
517
+ )
518
+ while True:
519
+ drain_remaining = max(
520
+ 0.0,
521
+ drain_deadline - asyncio.get_running_loop().time(),
522
+ )
523
+ drained, _ = await asyncio.wait(
524
+ {chunk_task},
525
+ timeout=drain_remaining,
526
+ )
527
+ if not drained:
528
+ await self._client.close()
529
+ await asyncio.gather(chunk_task, return_exceptions=True)
530
+ break
531
+ try:
532
+ drained_chunk = chunk_task.result()
533
+ except Exception: # noqa: BLE001
534
+ break
535
+ if drained_chunk is _STREAM_STOP:
536
+ break
537
+ chunk_task = asyncio.ensure_future(_anext_or_stop(codex_gen))
538
+ chunk_task = None
328
539
  thread.interrupted = True
329
- # AGUI 投射器对 RUN_INTERRUPTED 无兜底,必须显式发,否则 raise
330
- yield self._event(
331
- handle, EventType.RUN_INTERRUPTED, {"status": "interrupted"}
540
+ # Runtime interrupt (user pause) — adapter doesn't know;
541
+ # emit canonical RunInterrupted explicitly.
542
+ yield self._make_run_interrupted(
543
+ handle,
544
+ reason="user_pause" if thread.paused else "runtime_interrupt",
332
545
  )
333
546
  return
547
+ for task in pending:
548
+ task.cancel()
549
+ if pending:
550
+ await asyncio.gather(*pending, return_exceptions=True)
334
551
  chunk = chunk_task.result()
335
552
  if chunk is _STREAM_STOP:
336
553
  return
337
- event = self._codex_chunk_to_event(handle, thread, tracker, chunk)
338
- if event is not None:
554
+ # TODO(runtime-event-v2): use real native cursor from chunk if
555
+ # available; fallback to thread:seq for now.
556
+ native_cursor = f"{thread.thread_id}:{self._next_seq()}"
557
+ # autoApprovalReview 不产生 canonical 事件(adapter 静默),但
558
+ # cancel 级联丢弃审批的契约依赖 runtime 的 pending 跟踪。
559
+ chunk_method = (
560
+ str((chunk or {}).get("method") or "")
561
+ if isinstance(chunk, dict)
562
+ else ""
563
+ )
564
+ if chunk_method in {
565
+ "item/autoApprovalReview/started",
566
+ "item/autoApprovalReview/completed",
567
+ }:
568
+ review_params = chunk.get("params") or {}
569
+ review_id = str(
570
+ review_params.get("reviewId")
571
+ or review_params.get("review_id")
572
+ or ""
573
+ )
574
+ if review_id:
575
+ if chunk_method.endswith("started"):
576
+ thread.pending_approvals.add(review_id)
577
+ else:
578
+ thread.pending_approvals.discard(review_id)
579
+ for event in adapter.map_protocol_message(
580
+ chunk,
581
+ context,
582
+ native_cursor=native_cursor,
583
+ timestamp=time.time(),
584
+ ):
585
+ event = self._with_caller_scope(event, request)
586
+ # 跟踪 pending 审批(cancel 级联丢弃契约依赖该集合)。
587
+ if isinstance(event, InteractionRequested):
588
+ if event.interaction_id:
589
+ thread.pending_approvals.add(event.interaction_id)
590
+ call_id = getattr(event.request, "call_id", None)
591
+ if call_id:
592
+ thread.pending_approvals.add(str(call_id))
593
+ elif isinstance(event, InteractionResolved):
594
+ thread.pending_approvals.discard(event.interaction_id)
595
+ call_id = getattr(event.response, "call_id", None)
596
+ if call_id:
597
+ thread.pending_approvals.discard(str(call_id))
598
+ if isinstance(event, (RunCompleted, RunFailed, RunCanceled)):
599
+ # The Kernel stops consuming as soon as it persists a
600
+ # canonical terminal fact, so generator ``finally`` may
601
+ # not run before worker cleanup calls ``close``. Mark the
602
+ # native turn terminal before yielding that fact; close
603
+ # must terminate the transport without sending a stale
604
+ # turn/interrupt RPC to an already-completed app-server.
605
+ thread.done = True
339
606
  yield event
340
607
  finally:
608
+ waiter_tasks = [task for task in (chunk_task, interrupt_task) if task is not None]
609
+ for task in waiter_tasks:
610
+ if not task.done():
611
+ task.cancel()
612
+ if waiter_tasks:
613
+ await asyncio.gather(*waiter_tasks, return_exceptions=True)
341
614
  aclose = getattr(codex_gen, "aclose", None)
342
615
  if callable(aclose):
343
616
  try:
@@ -345,88 +618,177 @@ class CodexRuntime(RuntimeAdapter):
345
618
  except Exception: # noqa: BLE001
346
619
  pass
347
620
 
348
- def _codex_chunk_to_event(
621
+ # ---- canonical run.* helpers (for runtime-owned lifecycle) ----
622
+
623
+ def _event_run_id(self, handle: RunHandle) -> str:
624
+ """事件的 canonical run_id:调用方 invocation_id 优先,退回 thread id。
625
+
626
+ ``handle.run_id`` 是 codex 原生 thread id(resume/cancel 按 thread 寻址);
627
+ 但 canonical RuntimeEvent 的 run_id 必须与调用方
628
+ ``StartRequest.metadata['invocation_id']`` 一致(conversation kernel 的
629
+ event scope 校验),否则 hosted/web 执行路径会在首个事件上 fail。
630
+ """
631
+ request = self._requests.get(handle.run_id)
632
+ if request is not None:
633
+ invocation_id = str(
634
+ (getattr(request, "metadata", None) or {}).get("invocation_id") or ""
635
+ ).strip()
636
+ if invocation_id:
637
+ return invocation_id
638
+ return handle.run_id
639
+
640
+ def _make_source(self, handle: RunHandle) -> SourceRef:
641
+ request = self._requests.get(handle.run_id)
642
+ return SourceRef(
643
+ framework="codex",
644
+ native_run_id=handle.run_id,
645
+ metadata={
646
+ "agent_id": (
647
+ str(request.agent_id or "codex") if request is not None else "codex"
648
+ ),
649
+ "user_id": (
650
+ request.user_id
651
+ if request is not None
652
+ else str(handle.native_ref.get("user_id") or "user")
653
+ ),
654
+ "session_id": handle.session_id,
655
+ "invocation_id": (
656
+ str(request.metadata.get("invocation_id") or handle.run_id)
657
+ if request is not None
658
+ else handle.run_id
659
+ ),
660
+ },
661
+ )
662
+
663
+ def _with_caller_scope(self, event: RuntimeEvent, request: Any) -> RuntimeEvent:
664
+ """把调用方 scope(request 的 agent/user/session/invocation)并入事件 source。"""
665
+
666
+ if request is None:
667
+ return event
668
+ caller_scope = {
669
+ "agent_id": str(getattr(request, "agent_id", "") or "codex"),
670
+ "user_id": str(getattr(request, "user_id", "") or "user"),
671
+ "session_id": str(getattr(request, "session_id", "") or ""),
672
+ "invocation_id": str(
673
+ (getattr(request, "metadata", None) or {}).get("invocation_id")
674
+ or ""
675
+ ),
676
+ }
677
+ merged = {**caller_scope, **dict(event.source.metadata or {})}
678
+ # adapter 自身字段优先;仅补齐缺失的调用方 scope 键。
679
+ for key, value in caller_scope.items():
680
+ if not merged.get(key):
681
+ merged[key] = value
682
+ source = event.source.model_copy(update={"metadata": merged})
683
+ return event.model_copy(update={"source": source})
684
+
685
+ def _canonical_kwargs(
349
686
  self,
350
687
  handle: RunHandle,
351
- thread: _CodexThread,
352
- tracker: CodexPhaseTracker,
353
- chunk: dict[str, Any],
354
- ) -> Optional[RuntimeEvent]:
355
- if not isinstance(chunk, dict):
356
- return None
357
- method = str(chunk.get("method") or chunk.get("type") or "")
358
- params = chunk.get("params") or chunk
359
-
360
- if method == "item/started":
361
- tracker.observe_item(params)
362
- return None
363
- if method == "item/completed":
364
- item = params.get("item") or params
365
- if item.get("type") != "agentMessage":
366
- tracker.forget_item(params)
367
- return None
368
- phase = tracker.runtime_phase_for_item(params)
369
- tracker.forget_item(params)
370
- text = str(item.get("text") or "")
371
- return self._event(
688
+ *,
689
+ scope_id: str,
690
+ item_id: str,
691
+ event_type: str,
692
+ part_id: str,
693
+ ) -> dict[str, Any]:
694
+ framework = "codex"
695
+ run_id = self._event_run_id(handle)
696
+ n = self._next_seq()
697
+ return {
698
+ "schema_version": 2,
699
+ "event_id": stable_event_id(
700
+ framework, scope_id, item_id, event_type, part_id, run_id, n
701
+ ),
702
+ "seq": n,
703
+ "timestamp": time.time(),
704
+ "run_id": run_id,
705
+ "scope_id": scope_id,
706
+ "source": self._make_source(handle),
707
+ }
708
+
709
+ def _make_run_canceled(
710
+ self, handle: RunHandle, *, reason: str | None = None
711
+ ) -> RunCanceled:
712
+ framework = "codex"
713
+ run_id = self._event_run_id(handle)
714
+ scope_id = stable_scope_id(framework, run_id)
715
+ item_id = stable_item_id(framework, run_id, "$run")
716
+ return RunCanceled(
717
+ **self._canonical_kwargs(
372
718
  handle,
373
- EventType.TEXT_COMPLETED,
374
- {"text": text},
375
- phase=phase or "final_answer",
376
- )
377
- if "delta" in method or method == "item/agentMessage/delta":
378
- phase = tracker.runtime_phase_for_delta(params)
379
- delta = str(params.get("delta") or "")
380
- if not delta:
381
- return None
382
- return self._event(
383
- handle, EventType.TEXT_DELTA, {"text": delta}, phase=phase or "commentary"
384
- )
385
- if method == "item/autoApprovalReview/started":
386
- review_id = str(params.get("review_id") or params.get("reviewId") or "")
387
- if review_id:
388
- thread.pending_approvals.add(review_id)
389
- return None
390
- if method == "item/autoApprovalReview/completed":
391
- review_id = str(params.get("review_id") or params.get("reviewId") or "")
392
- thread.pending_approvals.discard(review_id)
393
- return None
394
- if (
395
- "approval" in method
396
- or "requestPermission" in method
397
- or "approval" in str(chunk.get("type") or "")
398
- ):
399
- call_id = str(
400
- params.get("id") or params.get("call_id") or params.get("requestId") or ""
401
- )
402
- if call_id:
403
- thread.pending_approvals.add(call_id)
404
- return self._event(
719
+ scope_id=scope_id,
720
+ item_id=item_id,
721
+ event_type="run.canceled",
722
+ part_id="run",
723
+ ),
724
+ status="canceled",
725
+ reason=reason,
726
+ )
727
+
728
+ def _make_run_interrupted(
729
+ self, handle: RunHandle, *, reason: str | None = None
730
+ ) -> RunInterrupted:
731
+ framework = "codex"
732
+ run_id = self._event_run_id(handle)
733
+ scope_id = stable_scope_id(framework, run_id)
734
+ item_id = stable_item_id(framework, run_id, "$run")
735
+ return RunInterrupted(
736
+ **self._canonical_kwargs(
405
737
  handle,
406
- EventType.APPROVAL_REQUESTED,
407
- {"approval_id": call_id, "call_id": call_id, "kind": "tool", "detail": params},
408
- )
409
- return None
738
+ scope_id=scope_id,
739
+ item_id=item_id,
740
+ event_type="run.interrupted",
741
+ part_id="run",
742
+ ),
743
+ status="interrupted",
744
+ reason=reason,
745
+ )
410
746
 
411
- def _event(
412
- self,
413
- handle: RunHandle,
414
- event_type: str,
415
- payload: dict,
416
- *,
417
- phase: Optional[str] = None,
418
- ) -> RuntimeEvent:
419
- return RuntimeEvent.create(
420
- event_type,
421
- agent_id="codex",
422
- user_id=str(handle.native_ref.get("user_id") or "user"),
423
- session_id=handle.session_id,
424
- invocation_id=handle.run_id,
425
- seq_id=self._next_seq(),
426
- phase=phase,
427
- payload=payload,
747
+ def _make_run_failed(
748
+ self, handle: RunHandle, error_message: str
749
+ ) -> RunFailed:
750
+ framework = "codex"
751
+ run_id = self._event_run_id(handle)
752
+ scope_id = stable_scope_id(framework, run_id)
753
+ item_id = stable_item_id(framework, run_id, "$run")
754
+ return RunFailed(
755
+ **self._canonical_kwargs(
756
+ handle,
757
+ scope_id=scope_id,
758
+ item_id=item_id,
759
+ event_type="run.failed",
760
+ part_id="run",
761
+ ),
762
+ status="failed",
763
+ error=ErrorInfo(
764
+ code="codex_runtime_failed",
765
+ message=error_message,
766
+ source="codex",
767
+ scope_id=scope_id,
768
+ source_ref=self._make_source(handle),
769
+ ),
428
770
  )
429
771
 
772
+ @staticmethod
773
+ def _mcp_result_text(result: dict[str, Any]) -> str:
774
+ """把 MCP result.content 列表提取为可读文本;无文本时退化 structuredContent。"""
775
+ if not result:
776
+ return ""
777
+ content = result.get("content")
778
+ if isinstance(content, list):
779
+ texts = [
780
+ str(item.get("text") or "")
781
+ for item in content
782
+ if isinstance(item, dict) and item.get("type") == "text"
783
+ ]
784
+ texts = [text for text in texts if text]
785
+ if texts:
786
+ return "\n".join(texts)
787
+ structured = result.get("structuredContent", result.get("structured_content"))
788
+ if structured is not None:
789
+ return json.dumps(structured, ensure_ascii=False)
790
+ return ""
791
+
430
792
 
431
793
  _STREAM_STOP = object()
432
794
 
@@ -446,4 +808,235 @@ def _resume_prompt(payload: Optional[ResumePayload]) -> Any:
446
808
  return json.dumps(payload.data, ensure_ascii=False, sort_keys=True)
447
809
 
448
810
 
449
- __all__ = ["CodexRuntime"]
811
+ def _coerce_prompt_text(value: Any) -> Any:
812
+ """把 canonical message 形态的 input 压成 SDK 可接受的文本。
813
+
814
+ ``openai-codex`` 0.147 的 run input 只接受 TextInput/str;请求侧没有
815
+ conversation preprocessing 时 ``request.input`` 可能是
816
+ ``[{role, content}]`` 历史列表,直接透传会 ``unsupported input item``。
817
+ """
818
+ if isinstance(value, str) or value is None:
819
+ return value
820
+ if isinstance(value, dict):
821
+ content = value.get("content") if "role" in value else value.get("text")
822
+ if isinstance(content, str):
823
+ return content
824
+ if isinstance(content, dict):
825
+ text = content.get("text") or content.get("content")
826
+ if isinstance(text, str):
827
+ return text
828
+ return str(value)
829
+ if isinstance(value, list):
830
+ texts = [
831
+ text
832
+ for text in (_coerce_prompt_text(item) for item in value)
833
+ if isinstance(text, str) and text
834
+ ]
835
+ return "\n".join(texts) if texts else str(value)
836
+ return str(value)
837
+
838
+
839
+ def _request_prompt(request: StartRequest) -> Any:
840
+ """Render canonical conversation history for a native Codex turn.
841
+
842
+ 只传 user 消息历史 + 当前 input,不传 assistant 的完整回复(避免 codex 复述历史)。
843
+ assistant 回复用简短摘要代替,仅维持对话结构。
844
+ """
845
+ # A resumed Codex thread already owns its transcript. Re-sending Studio's
846
+ # transport-neutral history would duplicate every prior turn after refresh.
847
+ if str(request.metadata.get("thread_id") or "").strip():
848
+ return (
849
+ request.input
850
+ if _is_structured_turn_input(request.input)
851
+ else _coerce_prompt_text(request.input)
852
+ )
853
+
854
+ conversation = request.conversation_preprocessing()
855
+ if conversation is None or not conversation.messages:
856
+ # Keep native text/image/mention parts intact for _build_run_input().
857
+ # Flattening this list turns an image dict into user-visible text.
858
+ return (
859
+ request.input
860
+ if _is_structured_turn_input(request.input)
861
+ else _coerce_prompt_text(request.input)
862
+ )
863
+
864
+ lines: list[str] = []
865
+ for message in conversation.messages:
866
+ content = message.get("content")
867
+ if not isinstance(content, str):
868
+ continue
869
+ role = str(message.get("role") or "user").strip().lower()
870
+ if role == "assistant":
871
+ # 不传完整回复,只标注"已回复",避免 codex 复述
872
+ summary = content[:80] + ("..." if len(content) > 80 else "")
873
+ lines.append(f"[上一轮已回复: {summary}]")
874
+ elif role == "user":
875
+ lines.append(f"User: {content}")
876
+ return "\n".join(lines) or request.input
877
+
878
+
879
+ def _is_structured_turn_input(value: Any) -> bool:
880
+ return isinstance(value, list) and any(
881
+ isinstance(item, dict) and isinstance(item.get("type"), str)
882
+ for item in value
883
+ )
884
+
885
+
886
+ def _build_run_input(request: Optional[StartRequest], prompt: Any) -> Any:
887
+ """Compose Codex skills and native text/image/mention turn input."""
888
+ try:
889
+ from openai_codex import ImageInput, LocalImageInput, MentionInput, SkillInput, TextInput
890
+ except ImportError:
891
+ return prompt
892
+ skills: list[SkillInput] = []
893
+ if request is not None:
894
+ raw = request.config.get("skills") if request.config else None
895
+ if isinstance(raw, list):
896
+ for item in raw:
897
+ if not isinstance(item, dict):
898
+ continue
899
+ name = str(item.get("name") or "").strip()
900
+ path = str(item.get("path") or "").strip()
901
+ if name and path:
902
+ skills.append(SkillInput(name=name, path=path))
903
+ native_items: list[Any] = []
904
+ raw_input = request.input if request is not None else prompt
905
+ if isinstance(raw_input, list):
906
+ text_replaced = False
907
+ for item in raw_input:
908
+ if not isinstance(item, dict):
909
+ continue
910
+ kind = str(item.get("type") or "")
911
+ if not kind and "role" in item:
912
+ # canonical conversation message({role, content});当前 input
913
+ # 已由 prompt(或 conversation preprocessing)承载,跳过历史项。
914
+ continue
915
+ if kind in {"text", "input_text"}:
916
+ text = str(
917
+ prompt
918
+ if not text_replaced and isinstance(prompt, str)
919
+ else item.get("text") or ""
920
+ )
921
+ text_replaced = True
922
+ if text:
923
+ native_items.append(TextInput(text=text))
924
+ elif kind in {"image", "input_image"} and (
925
+ item.get("url") or item.get("image_url")
926
+ ):
927
+ native_items.append(
928
+ ImageInput(url=str(item.get("url") or item.get("image_url")))
929
+ )
930
+ elif kind == "localImage" and item.get("path"):
931
+ native_items.append(LocalImageInput(path=str(item["path"])))
932
+ elif kind == "input_file" and (
933
+ item.get("file_data")
934
+ or str(item.get("file_url") or "").startswith("data:")
935
+ ):
936
+ file_path = _materialize_inline_file(
937
+ str(item.get("file_data") or item.get("file_url")),
938
+ str(item.get("filename") or "attachment"),
939
+ )
940
+ if file_path is not None:
941
+ # App Server's ``mention`` input is presentation metadata:
942
+ # current Codex versions do not include it in the model's
943
+ # user message. Always add an explicit model-visible
944
+ # attachment context as well. Small textual files are
945
+ # inlined deterministically; binary/large files expose a
946
+ # sandbox-readable path that Codex can inspect with tools.
947
+ native_items.append(
948
+ TextInput(text=_attachment_context_text(file_path, item))
949
+ )
950
+ native_items.append(
951
+ MentionInput(name=file_path.name, path=str(file_path))
952
+ )
953
+ elif kind == "mention" and item.get("path"):
954
+ native_items.append(
955
+ MentionInput(
956
+ name=str(item.get("name") or item["path"]),
957
+ path=str(item["path"]),
958
+ )
959
+ )
960
+ else:
961
+ text = prompt if isinstance(prompt, str) else str(prompt or "")
962
+ if text:
963
+ native_items.append(TextInput(text=text))
964
+ combined = [*skills, *native_items]
965
+ if not combined:
966
+ return prompt
967
+ if len(combined) == 1 and isinstance(combined[0], TextInput) and not skills:
968
+ return combined[0].text
969
+ return combined
970
+
971
+
972
+ def _materialize_inline_file(data_url: str, filename: str) -> Path | None:
973
+ """Materialize a bounded Studio inline attachment for native Codex."""
974
+
975
+ match = re.fullmatch(r"data:([^;,]+)?;base64,([A-Za-z0-9+/=\s]+)", data_url)
976
+ encoded = match.group(2) if match is not None else data_url.strip()
977
+ try:
978
+ payload = base64.b64decode(encoded, validate=True)
979
+ except (ValueError, binascii.Error):
980
+ return None
981
+ if not payload or len(payload) > 10 * 1024 * 1024:
982
+ return None
983
+ safe_name = re.sub(r"[^A-Za-z0-9._-]+", "-", Path(filename).name).strip(".-")
984
+ safe_name = safe_name[:120] or "attachment"
985
+ digest = hashlib.sha256(payload).hexdigest()
986
+ path = Path("/tmp/ksadk-codex-attachments") / digest / safe_name
987
+ path.parent.mkdir(parents=True, exist_ok=True)
988
+ if not path.exists():
989
+ path.write_bytes(payload)
990
+ return path
991
+
992
+
993
+ _MAX_INLINE_ATTACHMENT_TEXT_BYTES = 64 * 1024
994
+ _TEXT_ATTACHMENT_SUFFIXES = {
995
+ ".csv",
996
+ ".html",
997
+ ".htm",
998
+ ".ini",
999
+ ".json",
1000
+ ".jsonl",
1001
+ ".log",
1002
+ ".md",
1003
+ ".py",
1004
+ ".rst",
1005
+ ".toml",
1006
+ ".tsv",
1007
+ ".txt",
1008
+ ".xml",
1009
+ ".yaml",
1010
+ ".yml",
1011
+ }
1012
+
1013
+
1014
+ def _attachment_context_text(path: Path, item: Mapping[str, Any]) -> str:
1015
+ """Build model-visible context for a materialized Responses input file."""
1016
+
1017
+ name = str(item.get("filename") or path.name).replace('"', "'")
1018
+ inline_data = item.get("inlineData")
1019
+ inline_mime = inline_data.get("mimeType") if isinstance(inline_data, Mapping) else None
1020
+ mime_type = str(item.get("mime_type") or inline_mime or "").strip().lower()
1021
+ source = str(item.get("file_data") or item.get("file_url") or "")
1022
+ data_url_match = re.match(r"data:([^;,]+)", source)
1023
+ if not mime_type and data_url_match is not None:
1024
+ mime_type = data_url_match.group(1).strip().lower()
1025
+ is_text = mime_type.startswith("text/") or path.suffix.lower() in _TEXT_ATTACHMENT_SUFFIXES
1026
+ header = f'<uploaded_attachment name="{name}" path="{path}">'
1027
+ if not is_text:
1028
+ return (
1029
+ f"{header}\n"
1030
+ "The uploaded file is available at the path above. Read it with an appropriate "
1031
+ "tool before answering questions about its contents.\n"
1032
+ "</uploaded_attachment>"
1033
+ )
1034
+
1035
+ raw = path.read_bytes()
1036
+ truncated = len(raw) > _MAX_INLINE_ATTACHMENT_TEXT_BYTES
1037
+ text = raw[:_MAX_INLINE_ATTACHMENT_TEXT_BYTES].decode("utf-8", errors="replace")
1038
+ suffix = "\n[attachment content truncated]" if truncated else ""
1039
+ return f"{header}\n{text}{suffix}\n</uploaded_attachment>"
1040
+
1041
+
1042
+ __all__ = ["CodexRuntimeAdapter"]