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/client.py CHANGED
@@ -1,6 +1,6 @@
1
- """CodexRuntime 的 codex 后端接口 (goal-09)。
1
+ """CodexRuntimeAdapter 的 codex 后端接口 (goal-09)。
2
2
 
3
- 重托管模式:CodexRuntime 对执行生命周期负责,不把 cancel/进程管理薄委托给上层。
3
+ 重托管模式:CodexRuntimeAdapter 对执行生命周期负责,不把 cancel/进程管理薄委托给上层。
4
4
  ``CodexClient`` 是 codex 后端(thread/turn 生命周期)的最小接口,方法集对齐 OpenAI
5
5
  官方 ``openai-codex`` SDK 的**真实**线程模型(``thread_start`` / ``thread.turn`` /
6
6
  ``handle.stream`` / ``handle.interrupt`` / ``thread_resume``):
@@ -11,27 +11,75 @@
11
11
  - 测试实现:用 fake(见 tests/runners/test_codex_runtime.py / test_adapter_contract.py),
12
12
  不需要真 CLI 二进制。
13
13
 
14
- 诚实边界:本模块的 SDK **方法面**已对安装的 ``openai-codex==0.144.4`` 实证(方法存在性 +
14
+ 诚实边界:本模块的 SDK **方法面**已对安装的 ``openai-codex==0.147.0`` 实证(方法存在性 +
15
15
  协程/asyncgen 形态);Notification → RuntimeEvent 的**字段级** phase 映射需在接真实 codex
16
16
  后端时按实况对齐(结构已按生成的 payload 类型映射,见 ``_notification_to_event_dict``)。
17
17
  """
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ import asyncio
21
22
  import os
23
+ import queue
22
24
  import secrets
25
+ import threading
23
26
  from abc import ABC, abstractmethod
27
+ from dataclasses import dataclass, field
24
28
  from importlib.metadata import PackageNotFoundError, version
25
29
  from typing import Any, AsyncIterator, Optional
30
+ from uuid import uuid4
26
31
 
27
32
  from ksadk.model_proxy import ProxyConfig, ProxyServer
28
33
  from ksadk.model_proxy.cache import CapabilityCache, credential_scope
29
- from ksadk.model_proxy.detect import probe_responses_capability
34
+ from ksadk.model_proxy.detect import (
35
+ CODEX_DIRECT_REQUIRED_TOOL_TYPES,
36
+ CODEX_OPTIONAL_TOOL_TYPES,
37
+ ModelCapabilities,
38
+ probe_responses_capability,
39
+ )
30
40
 
31
41
  # 探测缓存单例:能力判定跨 client 共享,按 (model, base, credential_scope) 长缓存
32
42
  _CAPABILITY_CACHE = CapabilityCache(ttl=3600)
33
43
 
34
44
 
45
+ class CodexCapabilityUnavailableError(RuntimeError):
46
+ """A caller-required Codex capability cannot be preserved by this route."""
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class _CapabilityRoute:
51
+ use_proxy: bool
52
+ disabled_tool_types: frozenset[str] = frozenset()
53
+ unavailable_required_tool_types: frozenset[str] = frozenset()
54
+
55
+ def require_available(self) -> None:
56
+ if self.unavailable_required_tool_types:
57
+ names = ", ".join(sorted(self.unavailable_required_tool_types))
58
+ raise CodexCapabilityUnavailableError(
59
+ f"required Codex capabilities unavailable on selected route: {names}"
60
+ )
61
+
62
+
63
+ @dataclass
64
+ class _PendingApproval:
65
+ approval_id: str
66
+ thread_id: str
67
+ method: str
68
+ params: dict[str, Any]
69
+ resolved: threading.Event = field(default_factory=threading.Event)
70
+ response: dict[str, Any] = field(default_factory=dict)
71
+
72
+
73
+ @dataclass
74
+ class _PendingInteraction:
75
+ interaction_id: str
76
+ thread_id: str
77
+ method: str
78
+ params: dict[str, Any]
79
+ resolved: threading.Event = field(default_factory=threading.Event)
80
+ response: dict[str, Any] = field(default_factory=dict)
81
+
82
+
35
83
  def _is_openai_official(base: str) -> bool:
36
84
  """OpenAI 官方 base_url(直连,不探测不代理)。"""
37
85
  from urllib.parse import urlsplit
@@ -56,11 +104,48 @@ def _upgrade_http_to_https(upstream: str) -> str:
56
104
  return upstream
57
105
 
58
106
 
59
- def _probe_requires_proxy(model: str, base: str, key: str) -> bool:
60
- """探测上游:只有**确凿不支持 responses** 才返回 True(走代理)。
107
+ def _route_for_capabilities(
108
+ caps: ModelCapabilities,
109
+ *,
110
+ required_tool_types: set[str] | frozenset[str] = frozenset(),
111
+ ) -> _CapabilityRoute:
112
+ """Split protocol requirements from optional tool degradation.
113
+
114
+ The current Studio/Codex launch contract has no user-facing required-tool
115
+ declaration, so callers pass the default empty set. This explicit input is
116
+ the fail-closed seam for a future ``web_search=required`` contract.
117
+ """
118
+
119
+ native_ready = caps.responses_supported is True and CODEX_DIRECT_REQUIRED_TOOL_TYPES.issubset(
120
+ caps.tool_types
121
+ )
122
+ use_proxy = not native_ready
123
+ disabled = (
124
+ CODEX_OPTIONAL_TOOL_TYPES
125
+ if use_proxy
126
+ else CODEX_OPTIONAL_TOOL_TYPES.difference(caps.tool_types)
127
+ )
128
+ required = frozenset(required_tool_types)
129
+ unavailable = (required.difference(caps.tool_types)) | required.intersection(disabled)
130
+ return _CapabilityRoute(
131
+ use_proxy=use_proxy,
132
+ disabled_tool_types=frozenset(disabled),
133
+ unavailable_required_tool_types=frozenset(unavailable),
134
+ )
135
+
61
136
 
62
- supported/unknown 都返回 False(直连)。unknown(故障)保守直连——故障 ≠ 模型
63
- 不支持 responses,不 silent 改变接入方式。结果经 CapabilityCache 缓存(singleflight)。
137
+ def _probe_capability_route(
138
+ model: str,
139
+ base: str,
140
+ key: str,
141
+ *,
142
+ required_tool_types: set[str] | frozenset[str] = frozenset(),
143
+ ) -> _CapabilityRoute:
144
+ """Probe once and return protocol plus optional-tool routing decisions.
145
+
146
+ 纯文本 Responses 成功不代表能接收 Codex 0.147 的完整
147
+ ``additional_tools`` 方言。namespace/custom 缺失或未知走兼容代理;仅缺
148
+ web_search 时保留原生 Responses,并在 Codex 生成请求前禁用该可选工具。
64
149
  """
65
150
 
66
151
  def probe(m: str, b: str):
@@ -70,7 +155,13 @@ def _probe_requires_proxy(model: str, base: str, key: str) -> bool:
70
155
  return probe_responses_capability(client, b, key, m, timeout=15.0)
71
156
 
72
157
  caps = _CAPABILITY_CACHE.get_or_probe(model, base, credential_scope(key), probe)
73
- return caps.verdict == "unsupported"
158
+ return _route_for_capabilities(caps, required_tool_types=required_tool_types)
159
+
160
+
161
+ def _probe_requires_proxy(model: str, base: str, key: str) -> bool:
162
+ """Compatibility predicate for callers/tests that only need protocol choice."""
163
+
164
+ return _probe_capability_route(model, base, key).use_proxy
74
165
 
75
166
 
76
167
  class CodexClient(ABC):
@@ -93,6 +184,17 @@ class CodexClient(ABC):
93
184
  产出规范化事件 dict(``{"method": ..., "params": ...}``)。"""
94
185
  raise NotImplementedError
95
186
 
187
+ def run_goal(
188
+ self,
189
+ thread_id: str,
190
+ objective: str,
191
+ *,
192
+ config: Optional[dict[str, Any]] = None,
193
+ ) -> AsyncIterator[dict[str, Any]]:
194
+ """Run one persisted Codex goal as a logical notification stream."""
195
+
196
+ raise RuntimeError("connected Codex client does not support goal operations")
197
+
96
198
  @abstractmethod
97
199
  async def interrupt_active_turn(self, thread_id: str) -> bool:
98
200
  """interrupt 该 thread 当前活跃 turn(真实 SDK ``handle.interrupt``);
@@ -104,6 +206,35 @@ class CodexClient(ABC):
104
206
  """恢复 thread(真实 SDK ``thread_resume``),返回 thread_id。"""
105
207
  raise NotImplementedError
106
208
 
209
+ async def resolve_approval(self, approval_id: str, decision: str) -> bool:
210
+ """Resolve one live native approval request.
211
+
212
+ Older/custom clients may not expose interactive approvals; returning
213
+ ``False`` keeps that capability boundary explicit without breaking the
214
+ existing client protocol.
215
+ """
216
+
217
+ return False
218
+
219
+ async def resolve_interaction(
220
+ self,
221
+ interaction_id: str,
222
+ data: dict[str, Any],
223
+ ) -> bool:
224
+ """Resolve one live structured user-input request."""
225
+
226
+ return False
227
+
228
+ async def pause_goal(self, thread_id: str) -> bool:
229
+ """Pause an active native goal when supported."""
230
+
231
+ return False
232
+
233
+ async def cancel_goal(self, thread_id: str) -> bool:
234
+ """Cancel an active native goal operation when supported."""
235
+
236
+ return False
237
+
107
238
  @abstractmethod
108
239
  async def close(self) -> None:
109
240
  """释放后端资源(真实 SDK ``close``)。"""
@@ -112,7 +243,7 @@ class CodexClient(ABC):
112
243
 
113
244
  def _missing_codex_error() -> RuntimeError:
114
245
  return RuntimeError(
115
- "CodexRuntime 需要 openai-codex SDK;请安装 ksadk[codex]:"
246
+ "CodexRuntimeAdapter 需要 openai-codex SDK;请安装 ksadk[codex]:"
116
247
  " pip install 'ksadk[codex]'(openai-codex 为可选 extra,不进默认依赖)"
117
248
  )
118
249
 
@@ -126,7 +257,7 @@ class AsyncCodexClient(CodexClient):
126
257
  ``AsyncThread.turn`` / ``AsyncTurnHandle.stream``/``interrupt``),版本漂移 fail-fast。
127
258
  """
128
259
 
129
- def __init__(self, config: Any = None) -> None:
260
+ def __init__(self, config: Any = None, *, proxy_observer: Any = None) -> None:
130
261
  try:
131
262
  from openai_codex import ( # type: ignore[import-not-found]
132
263
  AsyncCodex,
@@ -155,50 +286,358 @@ class AsyncCodexClient(CodexClient):
155
286
  f"{owner.__name__}.{method_name}(版本不兼容)"
156
287
  )
157
288
 
158
- # AsyncCodex 0.144.4 only accepts one CodexConfig positional/keyword.
159
- config, self._proxy = self._maybe_apply_proxy(config)
289
+ # AsyncCodex 0.147.0 only accepts one CodexConfig positional/keyword.
290
+ config, self._proxy = self._maybe_apply_proxy(
291
+ config,
292
+ proxy_observer=proxy_observer,
293
+ )
160
294
  self._codex = AsyncCodex(config=config)
161
295
  self.sdk_version = sdk_version
162
296
  self._threads: dict[str, Any] = {} # thread_id -> AsyncThread
163
297
  self._active_handles: dict[str, Any] = {} # thread_id -> 活跃 AsyncTurnHandle
298
+ self._goal_states: dict[str, Any] = {}
299
+ self._approval_queues: dict[str, queue.Queue[Any]] = {}
300
+ self._pending_approvals: dict[str, _PendingApproval] = {}
301
+ self._pending_interactions: dict[str, _PendingInteraction] = {}
302
+ self._approval_lock = threading.Lock()
303
+ self._install_approval_bridge()
304
+
305
+ def _install_approval_bridge(self) -> None:
306
+ """Replace the SDK's unconditional accept handler with a HITL bridge.
307
+
308
+ ``openai-codex==0.147.0`` exposes approval callbacks only on its sync
309
+ JSON-RPC client. The public ``AsyncCodex`` wrapper owns that client, so
310
+ this pinned compatibility seam is validated eagerly instead of silently
311
+ auto-accepting tool and file changes.
312
+ """
313
+
314
+ async_client = getattr(self._codex, "_client", None)
315
+ sync_client = getattr(async_client, "_sync", None)
316
+ if sync_client is None or not hasattr(sync_client, "_approval_handler"):
317
+ raise RuntimeError(
318
+ f"openai-codex {self.sdk_version} 不支持交互式审批桥接(内部 API 不兼容)"
319
+ )
320
+ sync_client._approval_handler = self._handle_server_request
321
+
322
+ def _active_request_queue(self, raw: dict[str, Any]) -> tuple[str, queue.Queue[Any] | None]:
323
+ thread_id = str(raw.get("threadId") or raw.get("thread_id") or "")
324
+ if not thread_id:
325
+ with self._approval_lock:
326
+ candidates = list(self._approval_queues)
327
+ if len(candidates) == 1:
328
+ thread_id = candidates[0]
329
+ with self._approval_lock:
330
+ return thread_id, self._approval_queues.get(thread_id)
331
+
332
+ def _handle_server_request(
333
+ self,
334
+ method: str,
335
+ params: dict[str, Any] | None,
336
+ ) -> dict[str, Any]:
337
+ if method in {
338
+ "item/commandExecution/requestApproval",
339
+ "item/fileChange/requestApproval",
340
+ }:
341
+ return self._handle_approval_request(method, params)
342
+ if method == "item/tool/requestUserInput":
343
+ return self._handle_user_input_request(method, params)
344
+ return {}
345
+
346
+ def _handle_approval_request(
347
+ self,
348
+ method: str,
349
+ params: dict[str, Any] | None,
350
+ ) -> dict[str, Any]:
351
+ raw = dict(params or {})
352
+ if method not in {
353
+ "item/commandExecution/requestApproval",
354
+ "item/fileChange/requestApproval",
355
+ }:
356
+ return {}
357
+ thread_id, approval_queue = self._active_request_queue(raw)
358
+ approval_id = str(
359
+ raw.get("itemId") or raw.get("item_id") or raw.get("id") or f"approval_{uuid4().hex}"
360
+ )
361
+ pending = _PendingApproval(
362
+ approval_id=approval_id,
363
+ thread_id=thread_id,
364
+ method=method,
365
+ params=raw,
366
+ )
367
+ with self._approval_lock:
368
+ if approval_queue is None:
369
+ # Fail closed when a request cannot be associated with an
370
+ # active run; never fall back to the SDK's auto-accept default.
371
+ return {"decision": "decline"}
372
+ self._pending_approvals[approval_id] = pending
373
+ # 下发原生 JSON-RPC requestApproval 消息(synthetic id=approval_id):
374
+ # canonical mapper 只认原生方法(unsupported_method fail-closed),
375
+ # 旧合成 ``item/approval/requested`` 事件会让整个 run 失败。
376
+ approval_queue.put(
377
+ {
378
+ "id": approval_id,
379
+ "method": method,
380
+ "params": raw,
381
+ }
382
+ )
383
+ pending.resolved.wait()
384
+ with self._approval_lock:
385
+ self._pending_approvals.pop(approval_id, None)
386
+ # 回包也以 JSON-RPC response 形态下发,mapper 才会产出
387
+ # InteractionResolved(原 call_id 闭环)并释放 continuation。
388
+ response = pending.response or {"decision": "decline"}
389
+ if approval_queue is not None:
390
+ approval_queue.put(
391
+ {
392
+ "id": approval_id,
393
+ "result": dict(response),
394
+ }
395
+ )
396
+ return pending.response or {"decision": "decline"}
397
+
398
+ def _handle_user_input_request(
399
+ self,
400
+ method: str,
401
+ params: dict[str, Any] | None,
402
+ ) -> dict[str, Any]:
403
+ raw = dict(params or {})
404
+ thread_id, request_queue = self._active_request_queue(raw)
405
+ interaction_id = str(
406
+ raw.get("itemId")
407
+ or raw.get("item_id")
408
+ or raw.get("requestId")
409
+ or raw.get("request_id")
410
+ or raw.get("id")
411
+ or f"input_{uuid4().hex}"
412
+ )
413
+ pending = _PendingInteraction(
414
+ interaction_id=interaction_id,
415
+ thread_id=thread_id,
416
+ method=method,
417
+ params=raw,
418
+ )
419
+ if request_queue is None:
420
+ return {"answers": {}}
421
+
422
+ surface_id = f"input-{interaction_id}"
423
+ components: list[dict[str, Any]] = []
424
+ question_components: list[dict[str, Any]] = []
425
+ input_schema: dict[str, Any] = {"type": "object", "properties": {}}
426
+ questions = raw.get("questions") if isinstance(raw.get("questions"), list) else []
427
+ for index, raw_question in enumerate(questions):
428
+ if not isinstance(raw_question, dict):
429
+ continue
430
+ question_id = str(raw_question.get("id") or f"question_{index + 1}")
431
+ raw_options = raw_question.get("options")
432
+ options = (
433
+ [dict(option) for option in raw_options if isinstance(option, dict)]
434
+ if isinstance(raw_options, list)
435
+ else []
436
+ )
437
+ multiple = bool(
438
+ raw_question.get("multiple")
439
+ or raw_question.get("isMultiple")
440
+ or raw_question.get("is_multiple")
441
+ or raw_question.get("isMultiSelect")
442
+ or raw_question.get("is_multi_select")
443
+ )
444
+ question_components.append(
445
+ {
446
+ "id": question_id,
447
+ "component": "MultipleChoice",
448
+ "props": {
449
+ "name": question_id,
450
+ "label": str(
451
+ raw_question.get("header")
452
+ or raw_question.get("question")
453
+ or question_id
454
+ ),
455
+ "description": str(raw_question.get("question") or ""),
456
+ "options": options,
457
+ "multiple": multiple,
458
+ "allow_other": bool(
459
+ raw_question.get("isOther") or raw_question.get("is_other")
460
+ ),
461
+ "secret": bool(
462
+ raw_question.get("isSecret") or raw_question.get("is_secret")
463
+ ),
464
+ },
465
+ }
466
+ )
467
+ option_labels = [
468
+ str(option.get("label") or "") for option in options if option.get("label")
469
+ ]
470
+ input_schema["properties"][question_id] = (
471
+ {
472
+ "type": "array",
473
+ "items": {"type": "string", "enum": option_labels},
474
+ }
475
+ if multiple
476
+ else {"type": "string", "enum": option_labels}
477
+ )
478
+ components.append(
479
+ {
480
+ "id": "form",
481
+ "component": "Form",
482
+ "props": {"title": "需要你的反馈", "submit_label": "提交"},
483
+ "children": question_components,
484
+ }
485
+ )
486
+ with self._approval_lock:
487
+ self._pending_interactions[interaction_id] = pending
488
+ request_queue.put(
489
+ {
490
+ "method": "a2ui/surface",
491
+ "params": {
492
+ "surface_id": surface_id,
493
+ "surface": {
494
+ "catalog_id": "https://a2ui.org/specification/v0_9/basic_catalog.json",
495
+ "components": components,
496
+ "data_model": {},
497
+ },
498
+ },
499
+ }
500
+ )
501
+ request_queue.put(
502
+ {
503
+ "method": "a2ui/interaction",
504
+ "params": {
505
+ "surface_id": surface_id,
506
+ "interaction_id": interaction_id,
507
+ "kind": "form",
508
+ "input_schema": input_schema,
509
+ "is_blocking": bool(raw.get("isBlocking", raw.get("is_blocking", True))),
510
+ },
511
+ }
512
+ )
513
+ pending.resolved.wait()
514
+ with self._approval_lock:
515
+ self._pending_interactions.pop(interaction_id, None)
516
+ return pending.response or {"answers": {}}
164
517
 
165
518
  @staticmethod
166
- def _maybe_apply_proxy(config: Any) -> tuple[Any, Any]:
519
+ def _maybe_apply_proxy(
520
+ config: Any,
521
+ *,
522
+ proxy_observer: Any = None,
523
+ ) -> tuple[Any, Any]:
167
524
  """codex 代理启用:**智能探测 fallback(带显式覆盖)**。
168
525
 
169
526
  - ``KSADK_CODEX_USE_PROXY=1`` → 强制开代理;``=0`` → 强制直连(可人工覆盖误判)。
170
527
  - **未设 env 时智能探测**:OpenAI 官方 base_url 直连(不探测);自定义上游
171
528
  (星流等)探测 responses 能力(detect.py + CapabilityCache 缓存,一次探测长缓存):
172
- - ``supported`` → 直连(原生 responses 可用)
173
- - ``unsupported`` → 自动启用代理(chat 模型,经转换层)
174
- - ``unknown``(故障/超时)→ **保守直连**,不 silent 改变接入方式
529
+ - namespace/custom 支持 → 原生 Responses 直连
530
+ - 仅 web_search 缺失 → 仍直连,并关闭该可选能力
531
+ - namespace/custom 缺失或无法确认 → 自动启用代理
175
532
  - 凭证闭合:codex 子进程只拿随机 KSADK_PROXY_TOKEN;上游 key 留父进程。
176
533
  - 互斥:launch_args_override 已设时 raise(override 整体覆盖命令行)。
534
+ - P1:直连分支(协议必需工具面确认、env=0)遇到自定义 base 也注入
535
+ ``ksadk_direct`` provider——否则 codex 子进程回落默认 OpenAI 官方
536
+ 端点,自定义上游(OPENAI_API_BASE)静默失效。已显式设
537
+ ``model_provider=`` 的 config 不覆盖;官方 base 不注入。
177
538
 
178
539
  返回 (新 config, ProxyServer | None)。staticmethod 便于单测。
179
540
  """
180
- env_val = os.environ.get("KSADK_CODEX_USE_PROXY")
181
- if env_val == "0":
182
- return config, None
183
- if env_val == "1":
184
- return AsyncCodexClient._start_proxy_and_inject(config)
541
+ runtime_env = {**os.environ, **(getattr(config, "env", None) or {})}
542
+ env_val = runtime_env.get("KSADK_CODEX_USE_PROXY")
543
+ if env_val in {"0", "direct"}:
544
+ return AsyncCodexClient._inject_direct_provider(config), None
545
+ if env_val in {"1", "forced"}:
546
+ return AsyncCodexClient._start_proxy_and_inject(
547
+ config,
548
+ proxy_observer=proxy_observer,
549
+ )
185
550
  # 未设:智能探测
186
551
  base = (
187
- os.environ.get("KSADK_PROXY_UPSTREAM_BASE")
188
- or os.environ.get("OPENAI_BASE_URL")
189
- or os.environ.get("OPENAI_API_BASE")
552
+ runtime_env.get("KSADK_PROXY_UPSTREAM_BASE")
553
+ or runtime_env.get("OPENAI_BASE_URL")
554
+ or runtime_env.get("OPENAI_API_BASE")
190
555
  or ""
191
556
  )
192
557
  if not base or _is_openai_official(base):
193
558
  return config, None # OpenAI 官方:直连,不探测
194
- model = os.environ.get("OPENAI_MODEL_NAME") or os.environ.get("MODEL_NAME") or ""
195
- key = os.environ.get("KSADK_PROXY_UPSTREAM_KEY") or os.environ.get("OPENAI_API_KEY") or ""
196
- if _probe_requires_proxy(model, base, key):
197
- return AsyncCodexClient._start_proxy_and_inject(config)
198
- return config, None
559
+ model = runtime_env.get("OPENAI_MODEL_NAME") or runtime_env.get("MODEL_NAME") or ""
560
+ key = runtime_env.get("KSADK_PROXY_UPSTREAM_KEY") or runtime_env.get("OPENAI_API_KEY") or ""
561
+ # Probe the exact URL scheme that Codex/proxy will use. Managed
562
+ # runtimes discover KSPMAS through its historical ``http://`` internal
563
+ # URL, while the provider is upgraded to HTTPS before execution. A
564
+ # probe against HTTP can see only a redirect and incorrectly classify
565
+ # an HTTPS ``/responses`` 404 as unknown, causing a broken direct path.
566
+ probe_base = _upgrade_http_to_https(base)
567
+ route = _probe_capability_route(model, probe_base, key)
568
+ # Future required-tool declarations must be checked here before either
569
+ # proxying or suppressing optional tools.
570
+ if isinstance(route, bool): # compatibility for injected test doubles
571
+ route = _CapabilityRoute(use_proxy=route)
572
+ route.require_available()
573
+ if route.use_proxy:
574
+ return AsyncCodexClient._start_proxy_and_inject(
575
+ config,
576
+ proxy_observer=proxy_observer,
577
+ )
578
+ # 探测确认协议必需工具面:直连,但必须把自定义 base 配成 provider。
579
+ return (
580
+ AsyncCodexClient._inject_direct_provider(
581
+ config,
582
+ disabled_tool_types=route.disabled_tool_types,
583
+ ),
584
+ None,
585
+ )
199
586
 
200
587
  @staticmethod
201
- def _start_proxy_and_inject(config: Any) -> tuple[Any, Any]:
588
+ def _inject_direct_provider(
589
+ config: Any,
590
+ *,
591
+ disabled_tool_types: frozenset[str] = frozenset(),
592
+ ) -> Any:
593
+ """直连模式注入 ``ksadk_direct`` provider(P1:非 proxy 不丢自定义 base)。
594
+
595
+ - 无自定义 base / 官方 OpenAI base → 原样返回。
596
+ - 已设 ``model_provider=`` → 保留 provider,仅追加必要的可选工具关闭项。
597
+ - 否则追加 ``model_provider=ksadk_direct`` + base_url/env_key/wire_api
598
+ (responses;该分支只在探测确认或显式强制直连时到达,其余走 proxy)。
599
+ """
600
+ import dataclasses
601
+
602
+ from openai_codex import CodexConfig # type: ignore[import-not-found]
603
+
604
+ cfg = config if isinstance(config, CodexConfig) else CodexConfig()
605
+ overrides = list(cfg.config_overrides or ())
606
+ if any(str(o).startswith("model_provider=") for o in overrides):
607
+ if "web_search" in disabled_tool_types and "web_search=disabled" not in overrides:
608
+ return dataclasses.replace(
609
+ cfg,
610
+ config_overrides=tuple([*overrides, "web_search=disabled"]),
611
+ )
612
+ return config
613
+ runtime_env = {**os.environ, **(cfg.env or {})}
614
+ base = (
615
+ runtime_env.get("KSADK_PROXY_UPSTREAM_BASE")
616
+ or runtime_env.get("OPENAI_BASE_URL")
617
+ or runtime_env.get("OPENAI_API_BASE")
618
+ or ""
619
+ )
620
+ if not base or _is_openai_official(base):
621
+ return config
622
+ base = _upgrade_http_to_https(base)
623
+ overrides += [
624
+ "model_provider=ksadk_direct",
625
+ "model_providers.ksadk_direct.name=ksadk_direct",
626
+ f"model_providers.ksadk_direct.base_url={base}",
627
+ "model_providers.ksadk_direct.env_key=OPENAI_API_KEY",
628
+ "model_providers.ksadk_direct.wire_api=responses",
629
+ "model_providers.ksadk_direct.supports_websockets=false",
630
+ ]
631
+ if "web_search" in disabled_tool_types and "web_search=disabled" not in overrides:
632
+ overrides.append("web_search=disabled")
633
+ return dataclasses.replace(cfg, config_overrides=tuple(overrides))
634
+
635
+ @staticmethod
636
+ def _start_proxy_and_inject(
637
+ config: Any,
638
+ *,
639
+ proxy_observer: Any = None,
640
+ ) -> tuple[Any, Any]:
202
641
  """起进程内 ProxyServer + 注入 codex provider(opt-in/探测判定走代理时)。"""
203
642
  import dataclasses
204
643
 
@@ -210,20 +649,39 @@ class AsyncCodexClient(CodexClient):
210
649
  "KSADK_CODEX_USE_PROXY 与 CodexConfig.launch_args_override 互斥:"
211
650
  "代理注入靠 config_overrides,而 launch_args_override 会整体覆盖命令行"
212
651
  )
652
+ runtime_env = {**os.environ, **(cfg.env or {})}
213
653
  upstream = (
214
- os.environ.get("KSADK_PROXY_UPSTREAM_BASE")
215
- or os.environ.get("OPENAI_BASE_URL")
216
- or os.environ.get("OPENAI_API_BASE")
654
+ runtime_env.get("KSADK_PROXY_UPSTREAM_BASE")
655
+ or runtime_env.get("OPENAI_BASE_URL")
656
+ or runtime_env.get("OPENAI_API_BASE")
217
657
  or "https://kspmas.ksyun.com/v1"
218
658
  )
219
659
  upstream = _upgrade_http_to_https(upstream)
220
660
  api_key = (
221
- os.environ.get("KSADK_PROXY_UPSTREAM_KEY") or os.environ.get("OPENAI_API_KEY") or ""
661
+ runtime_env.get("KSADK_PROXY_UPSTREAM_KEY") or runtime_env.get("OPENAI_API_KEY") or ""
222
662
  )
223
663
  token = secrets.token_hex(16)
224
- proxy = ProxyServer(ProxyConfig(upstream_base=upstream, api_key=api_key, local_token=token))
664
+ proxy = ProxyServer(
665
+ ProxyConfig(
666
+ upstream_base=upstream,
667
+ api_key=api_key,
668
+ local_token=token,
669
+ upstream_model=runtime_env.get("OPENAI_MODEL_NAME")
670
+ or runtime_env.get("MODEL_NAME")
671
+ or "",
672
+ event_callback=proxy_observer,
673
+ )
674
+ )
225
675
  proxy.start()
226
676
  overrides = list(cfg.config_overrides or ())
677
+ # 默认模型必须注入:codex 未配置 model= 时会发自家默认名(如 gpt-5.6-terra),
678
+ # 上游(kspmas)按未知模型 403。注入后 thread 级 model 覆盖仍优先生效
679
+ # (实测 codex thread model > config model=),RunAgent 的 Model 透传靠它。
680
+ default_model = (
681
+ runtime_env.get("OPENAI_MODEL_NAME") or runtime_env.get("MODEL_NAME") or ""
682
+ ).strip()
683
+ if default_model and not any(str(o).startswith("model=") for o in overrides):
684
+ overrides.append(f"model={default_model}")
227
685
  overrides += [
228
686
  "model_provider=ksadk_proxy",
229
687
  "model_providers.ksadk_proxy.name=ksadk_proxy",
@@ -235,13 +693,24 @@ class AsyncCodexClient(CodexClient):
235
693
  "features.multi_agent=false",
236
694
  "features.multi_agent_v2=false",
237
695
  ]
238
- env = dict(cfg.env or {})
696
+ # The local proxy owns the upstream credential. The Codex child only
697
+ # receives a random loopback token, so prompt-driven commands cannot
698
+ # read the real provider key from their environment.
699
+ env = dict(runtime_env)
700
+ for secret_name in (
701
+ "OPENAI_API_KEY",
702
+ "AGENTKIT_MODEL_API_KEY",
703
+ "KSADK_PROXY_UPSTREAM_KEY",
704
+ ):
705
+ env.pop(secret_name, None)
239
706
  env["KSADK_PROXY_TOKEN"] = token
240
707
  return dataclasses.replace(cfg, config_overrides=tuple(overrides), env=env), proxy
241
708
 
242
-
243
709
  async def start_thread(self, config: Optional[dict[str, Any]] = None) -> str:
244
- thread = await self._codex.thread_start(**self._thread_kwargs(config))
710
+ if self._uses_manual_approval(config):
711
+ thread = await self._start_manual_thread(config)
712
+ else:
713
+ thread = await self._codex.thread_start(**self._thread_kwargs(config))
245
714
  self._threads[thread.id] = thread
246
715
  return str(thread.id)
247
716
 
@@ -251,13 +720,91 @@ class AsyncCodexClient(CodexClient):
251
720
  cached = self._threads.get(thread_id)
252
721
  if cached is not None:
253
722
  return str(cached.id)
254
- kwargs = self._thread_kwargs(config)
255
- # AsyncCodex.thread_resume 0.144.4 has no ephemeral parameter.
256
- kwargs.pop("ephemeral", None)
257
- thread = await self._codex.thread_resume(thread_id, **kwargs)
723
+ if self._uses_manual_approval(config):
724
+ thread = await self._resume_manual_thread(thread_id, config)
725
+ else:
726
+ kwargs = self._thread_kwargs(config)
727
+ # AsyncCodex.thread_resume 0.144.4 has no ephemeral parameter.
728
+ kwargs.pop("ephemeral", None)
729
+ thread = await self._codex.thread_resume(thread_id, **kwargs)
258
730
  self._threads[thread.id] = thread
259
731
  return str(thread.id)
260
732
 
733
+ @staticmethod
734
+ def _uses_manual_approval(config: Optional[dict[str, Any]]) -> bool:
735
+ return str((config or {}).get("approval_mode") or "").strip().lower() == "manual"
736
+
737
+ async def _start_manual_thread(self, config: Optional[dict[str, Any]]) -> Any:
738
+ """Start a thread whose native approvals are reviewed by Studio users.
739
+
740
+ ``openai-codex==0.147.0`` exposes ``ApprovalsReviewer.user`` on the
741
+ generated app-server contract but omits it from the public
742
+ ``ApprovalMode`` enum. Use that pinned wire contract explicitly rather
743
+ than falling back to ``auto_review``.
744
+ """
745
+
746
+ from openai_codex import AsyncThread # type: ignore[import-not-found]
747
+ from openai_codex._sandbox import _sandbox_mode # type: ignore[import-not-found]
748
+ from openai_codex.generated.v2_all import ( # type: ignore[import-not-found]
749
+ ApprovalsReviewer,
750
+ AskForApproval,
751
+ AskForApprovalValue,
752
+ ThreadStartParams,
753
+ )
754
+
755
+ kwargs = self._thread_kwargs(self._without_manual_approval(config))
756
+ kwargs.pop("approval_mode", None)
757
+ sandbox = kwargs.pop("sandbox", None)
758
+ await self._codex._ensure_initialized()
759
+ started = await self._codex._client.thread_start(
760
+ ThreadStartParams(
761
+ approval_policy=AskForApproval(root=AskForApprovalValue.on_request),
762
+ approvals_reviewer=ApprovalsReviewer.user,
763
+ sandbox=_sandbox_mode(sandbox),
764
+ **kwargs,
765
+ )
766
+ )
767
+ return AsyncThread(self._codex, started.thread.id)
768
+
769
+ async def _resume_manual_thread(
770
+ self,
771
+ thread_id: str,
772
+ config: Optional[dict[str, Any]],
773
+ ) -> Any:
774
+ from openai_codex import AsyncThread # type: ignore[import-not-found]
775
+ from openai_codex._sandbox import _sandbox_mode # type: ignore[import-not-found]
776
+ from openai_codex.generated.v2_all import ( # type: ignore[import-not-found]
777
+ ApprovalsReviewer,
778
+ AskForApproval,
779
+ AskForApprovalValue,
780
+ ThreadResumeParams,
781
+ )
782
+
783
+ kwargs = self._thread_kwargs(self._without_manual_approval(config))
784
+ kwargs.pop("approval_mode", None)
785
+ kwargs.pop("ephemeral", None)
786
+ sandbox = kwargs.pop("sandbox", None)
787
+ await self._codex._ensure_initialized()
788
+ resumed = await self._codex._client.thread_resume(
789
+ thread_id,
790
+ ThreadResumeParams(
791
+ thread_id=thread_id,
792
+ approval_policy=AskForApproval(root=AskForApprovalValue.on_request),
793
+ approvals_reviewer=ApprovalsReviewer.user,
794
+ sandbox=_sandbox_mode(sandbox),
795
+ **kwargs,
796
+ ),
797
+ )
798
+ return AsyncThread(self._codex, resumed.thread.id)
799
+
800
+ @staticmethod
801
+ def _without_manual_approval(
802
+ config: Optional[dict[str, Any]],
803
+ ) -> dict[str, Any]:
804
+ result = dict(config or {})
805
+ result.pop("approval_mode", None)
806
+ return result
807
+
261
808
  def run_turn(
262
809
  self,
263
810
  thread_id: str,
@@ -267,6 +814,15 @@ class AsyncCodexClient(CodexClient):
267
814
  ) -> AsyncIterator[dict[str, Any]]:
268
815
  return self._run_turn_gen(thread_id, prompt, config)
269
816
 
817
+ def run_goal(
818
+ self,
819
+ thread_id: str,
820
+ objective: str,
821
+ *,
822
+ config: Optional[dict[str, Any]] = None,
823
+ ) -> AsyncIterator[dict[str, Any]]:
824
+ return self._run_goal_gen(thread_id, objective, config)
825
+
270
826
  async def _run_turn_gen(
271
827
  self, thread_id: str, prompt: Any, config: Optional[dict[str, Any]]
272
828
  ) -> AsyncIterator[dict[str, Any]]:
@@ -275,15 +831,157 @@ class AsyncCodexClient(CodexClient):
275
831
  # 未显式 start/resume 的 thread_id:按 resume 语义接入真实后端。
276
832
  await self.resume_thread(thread_id, config)
277
833
  thread = self._threads[thread_id]
278
- handle = await thread.turn(self._coerce_input(prompt), **self._turn_kwargs(config))
834
+ approval_queue: queue.Queue[Any] = queue.Queue()
835
+ with self._approval_lock:
836
+ self._approval_queues[thread_id] = approval_queue
837
+ handle = await self._start_turn(thread, self._coerce_input(prompt), config)
279
838
  self._active_handles[thread_id] = handle
839
+ notifications = handle.stream()
840
+ notification_task: Any = None
841
+ approval_task: Any = None
280
842
  try:
281
- async for notification in handle.stream():
282
- event = self._notification_to_event_dict(notification)
283
- if event is not None:
284
- yield event
843
+ while True:
844
+ if notification_task is None:
845
+ notification_task = asyncio.create_task(notifications.__anext__())
846
+ if approval_task is None:
847
+ approval_task = asyncio.create_task(asyncio.to_thread(approval_queue.get))
848
+ done, _pending = await asyncio.wait(
849
+ {notification_task, approval_task},
850
+ return_when=asyncio.FIRST_COMPLETED,
851
+ )
852
+ if approval_task in done:
853
+ approval_event = approval_task.result()
854
+ approval_task = None
855
+ if approval_event is not None:
856
+ yield approval_event
857
+ if notification_task in done:
858
+ try:
859
+ notification = notification_task.result()
860
+ except StopAsyncIteration:
861
+ break
862
+ notification_task = None
863
+ event = self._notification_to_event_dict(notification)
864
+ if event is not None:
865
+ yield event
285
866
  finally:
867
+ approval_queue.put(None)
868
+ for task in (notification_task, approval_task):
869
+ if task is not None and not task.done():
870
+ task.cancel()
871
+ await asyncio.gather(
872
+ *(task for task in (notification_task, approval_task) if task is not None),
873
+ return_exceptions=True,
874
+ )
286
875
  self._active_handles.pop(thread_id, None)
876
+ with self._approval_lock:
877
+ self._approval_queues.pop(thread_id, None)
878
+
879
+ async def _run_goal_gen(
880
+ self,
881
+ thread_id: str,
882
+ objective: str,
883
+ config: Optional[dict[str, Any]],
884
+ ) -> AsyncIterator[dict[str, Any]]:
885
+ if thread_id not in self._threads:
886
+ await self.resume_thread(thread_id, config)
887
+ from openai_codex._goal import _AsyncGoalNotificationStream
888
+
889
+ low_level = self._codex._client
890
+ approval_queue: queue.Queue[Any] = queue.Queue()
891
+ with self._approval_lock:
892
+ self._approval_queues[thread_id] = approval_queue
893
+ state, _logical_turn_id = await low_level.start_goal_operation(thread_id, objective)
894
+ self._goal_states[thread_id] = state
895
+ notifications = _AsyncGoalNotificationStream(
896
+ state=state,
897
+ next_notification=lambda: low_level.next_goal_notification(state),
898
+ unregister=lambda: low_level.unregister_goal_operation(state),
899
+ cancel_goal=lambda: low_level.cancel_goal_operation(state),
900
+ )
901
+ notification_task: Any = None
902
+ approval_task: Any = None
903
+ try:
904
+ while True:
905
+ if notification_task is None:
906
+ notification_task = asyncio.create_task(notifications.__anext__())
907
+ if approval_task is None:
908
+ approval_task = asyncio.create_task(asyncio.to_thread(approval_queue.get))
909
+ done, _pending = await asyncio.wait(
910
+ {notification_task, approval_task},
911
+ return_when=asyncio.FIRST_COMPLETED,
912
+ )
913
+ if approval_task in done:
914
+ approval_event = approval_task.result()
915
+ approval_task = None
916
+ if approval_event is not None:
917
+ yield approval_event
918
+ if notification_task in done:
919
+ try:
920
+ notification = notification_task.result()
921
+ except StopAsyncIteration:
922
+ break
923
+ notification_task = None
924
+ event = self._notification_to_event_dict(notification)
925
+ if event is not None:
926
+ yield event
927
+ finally:
928
+ approval_queue.put(None)
929
+ for task in (notification_task, approval_task):
930
+ if task is not None and not task.done():
931
+ task.cancel()
932
+ await asyncio.gather(
933
+ *(task for task in (notification_task, approval_task) if task is not None),
934
+ return_exceptions=True,
935
+ )
936
+ await notifications.aclose()
937
+ self._goal_states.pop(thread_id, None)
938
+ with self._approval_lock:
939
+ self._approval_queues.pop(thread_id, None)
940
+
941
+ async def _start_turn(
942
+ self,
943
+ thread: Any,
944
+ prompt: Any,
945
+ config: Optional[dict[str, Any]],
946
+ ) -> Any:
947
+ mode = str((config or {}).get("collaboration_mode") or "").strip().lower()
948
+ if not mode or mode == "default":
949
+ return await thread.turn(prompt, **self._turn_kwargs(config))
950
+ if mode != "plan":
951
+ raise ValueError("collaboration_mode must be default or plan")
952
+
953
+ # openai-codex 0.144.4 predates the public collaboration_mode argument,
954
+ # while the bundled app-server already accepts it. Keep this private
955
+ # compatibility seam small and covered by a wire-payload test.
956
+ from openai_codex import AsyncTurnHandle
957
+ from openai_codex._approval_mode import _approval_mode_override_settings
958
+ from openai_codex._inputs import _normalize_run_input, _to_wire_input
959
+ from openai_codex._sandbox import _sandbox_policy
960
+ from openai_codex.generated.v2_all import TurnStartParams
961
+
962
+ turn_config = self._turn_kwargs(config)
963
+ approval_mode = turn_config.pop("approval_mode", None)
964
+ sandbox = turn_config.pop("sandbox", None)
965
+ approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
966
+ wire_input = _to_wire_input(_normalize_run_input(prompt))
967
+ params = TurnStartParams(
968
+ thread_id=thread.id,
969
+ input=wire_input,
970
+ approval_policy=approval_policy,
971
+ approvals_reviewer=approvals_reviewer,
972
+ sandbox_policy=_sandbox_policy(sandbox),
973
+ **turn_config,
974
+ ).model_dump(mode="json", by_alias=True, exclude_none=True)
975
+ params["collaborationMode"] = {
976
+ "mode": "plan",
977
+ "settings": {
978
+ "model": str((config or {}).get("model") or ""),
979
+ "reasoning_effort": None,
980
+ "developer_instructions": None,
981
+ },
982
+ }
983
+ started = await self._codex._client.turn_start(thread.id, wire_input, params=params)
984
+ return AsyncTurnHandle(self._codex, thread.id, started.turn.id)
287
985
 
288
986
  async def interrupt_active_turn(self, thread_id: str) -> bool:
289
987
  handle = self._active_handles.get(thread_id)
@@ -292,7 +990,85 @@ class AsyncCodexClient(CodexClient):
292
990
  await handle.interrupt()
293
991
  return True
294
992
 
993
+ async def pause_goal(self, thread_id: str) -> bool:
994
+ state = self._goal_states.get(thread_id)
995
+ if state is None:
996
+ return False
997
+ await self._codex._client.pause_goal(thread_id)
998
+ return True
999
+
1000
+ async def cancel_goal(self, thread_id: str) -> bool:
1001
+ state = self._goal_states.get(thread_id)
1002
+ if state is None:
1003
+ return False
1004
+ await self._codex._client.cancel_goal_operation(state)
1005
+ return True
1006
+
1007
+ async def resolve_approval(self, approval_id: str, decision: str) -> bool:
1008
+ normalized = {
1009
+ "approve": "accept",
1010
+ "accept": "accept",
1011
+ "approve_session": "acceptForSession",
1012
+ "accept_for_session": "acceptForSession",
1013
+ "deny": "decline",
1014
+ "decline": "decline",
1015
+ "cancel": "cancel",
1016
+ }.get(str(decision).strip().lower())
1017
+ if normalized is None:
1018
+ raise ValueError("approval decision must be approve, approve_session, deny, or cancel")
1019
+ with self._approval_lock:
1020
+ pending = self._pending_approvals.get(approval_id)
1021
+ if pending is None:
1022
+ return False
1023
+ pending.response = {"decision": normalized}
1024
+ pending.resolved.set()
1025
+ return True
1026
+
1027
+ async def resolve_interaction(
1028
+ self,
1029
+ interaction_id: str,
1030
+ data: dict[str, Any],
1031
+ ) -> bool:
1032
+ answers: dict[str, dict[str, list[str]]] = {}
1033
+ for key, value in data.items():
1034
+ if key in {"decision", "name", "action"} or value is None:
1035
+ continue
1036
+ raw_answers = value if isinstance(value, list) else [value]
1037
+ normalized = [str(answer) for answer in raw_answers if str(answer)]
1038
+ answers[str(key)] = {"answers": normalized}
1039
+ with self._approval_lock:
1040
+ pending = self._pending_interactions.get(interaction_id)
1041
+ if pending is None:
1042
+ return False
1043
+ pending.response = {"answers": answers}
1044
+ pending.resolved.set()
1045
+ return True
1046
+
295
1047
  async def close(self) -> None:
1048
+ approval_lock = getattr(self, "_approval_lock", None)
1049
+ if approval_lock is None:
1050
+ pending: list[_PendingApproval] = []
1051
+ else:
1052
+ with approval_lock:
1053
+ pending = list(getattr(self, "_pending_approvals", {}).values())
1054
+ for approval in pending:
1055
+ approval.response = {"decision": "cancel"}
1056
+ approval.resolved.set()
1057
+ if approval_lock is None:
1058
+ interactions: list[_PendingInteraction] = []
1059
+ else:
1060
+ with approval_lock:
1061
+ interactions = list(getattr(self, "_pending_interactions", {}).values())
1062
+ for interaction in interactions:
1063
+ interaction.response = {"answers": {}}
1064
+ interaction.resolved.set()
1065
+ for thread_id, state in list(getattr(self, "_goal_states", {}).items()):
1066
+ try:
1067
+ await self._codex._client.cancel_goal_operation(state)
1068
+ except Exception:
1069
+ pass
1070
+ finally:
1071
+ self._goal_states.pop(thread_id, None)
296
1072
  try:
297
1073
  await self._codex.close()
298
1074
  finally:
@@ -373,7 +1149,7 @@ class AsyncCodexClient(CodexClient):
373
1149
  """
374
1150
  payload = notification.payload
375
1151
  if hasattr(payload, "model_dump"):
376
- params = payload.model_dump(mode="json")
1152
+ params = payload.model_dump(mode="json", by_alias=True)
377
1153
  else:
378
1154
  params = getattr(payload, "params", None)
379
1155
  if not isinstance(params, dict):
@@ -386,7 +1162,15 @@ class AsyncCodexClient(CodexClient):
386
1162
  "item/agentMessage/delta",
387
1163
  "item/autoApprovalReview/started",
388
1164
  "item/autoApprovalReview/completed",
1165
+ "thread/tokenUsage/updated",
1166
+ "turn/started",
1167
+ "turn/completed",
389
1168
  "error",
1169
+ "item/reasoning/summaryTextDelta",
1170
+ "item/reasoning/textDelta",
1171
+ "item/reasoning/summaryPartAdded",
1172
+ "thread/goal/updated",
1173
+ "thread/goal/cleared",
390
1174
  }
391
1175
  if method in supported_methods:
392
1176
  return {"method": method, "params": params}