agentengine-sdk-python 0.8.4__py3-none-any.whl → 0.8.6__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 (1221) hide show
  1. {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/METADATA +17 -10
  2. agentengine_sdk_python-0.8.6.dist-info/RECORD +1731 -0
  3. ksadk/_async_iterators.py +79 -0
  4. ksadk/_build_provenance.json +1 -1
  5. ksadk/_model_anthropic.py +49 -0
  6. ksadk/_model_input.py +86 -0
  7. ksadk/_model_json.py +40 -0
  8. ksadk/_model_reasoning.py +17 -0
  9. ksadk/_model_refusal.py +20 -0
  10. ksadk/_model_sse.py +163 -0
  11. ksadk/_process.py +83 -0
  12. ksadk/_str_enum.py +17 -0
  13. ksadk/a2a/__init__.py +157 -167
  14. ksadk/a2a/_invocation_state.py +86 -0
  15. ksadk/a2a/_space_client_events.py +33 -22
  16. ksadk/a2a/adk.py +194 -0
  17. ksadk/a2a/adk_invocation_context.py +242 -0
  18. ksadk/a2a/adk_runtime.py +126 -0
  19. ksadk/a2a/adk_tools.py +319 -0
  20. ksadk/a2a/bootstrap.py +149 -23
  21. ksadk/a2a/build_validation.py +147 -0
  22. ksadk/a2a/card.py +23 -15
  23. ksadk/a2a/codex.py +160 -0
  24. ksadk/a2a/codex_checkpoint.py +111 -0
  25. ksadk/a2a/codex_recovery.py +96 -0
  26. ksadk/a2a/codex_recovery_events.py +123 -0
  27. ksadk/a2a/codex_runtime.py +198 -0
  28. ksadk/a2a/codex_session_checkpoint.py +162 -0
  29. ksadk/a2a/composition.py +88 -0
  30. ksadk/a2a/context_store.py +118 -19
  31. ksadk/a2a/control_plane.py +108 -106
  32. ksadk/a2a/event_adapter.py +92 -193
  33. ksadk/a2a/executor.py +257 -85
  34. ksadk/a2a/exposure.py +106 -0
  35. ksadk/a2a/external_transport.py +3 -4
  36. ksadk/a2a/identity.py +106 -12
  37. ksadk/a2a/ids.py +14 -3
  38. ksadk/a2a/inbound_kernel.py +133 -0
  39. ksadk/a2a/interaction_wire.py +76 -0
  40. ksadk/a2a/invocation_checkpoint.py +274 -0
  41. ksadk/a2a/invocation_policy.py +87 -0
  42. ksadk/a2a/invocation_publication.py +210 -0
  43. ksadk/a2a/invocation_service.py +972 -0
  44. ksadk/a2a/invocation_types.py +459 -0
  45. ksadk/a2a/kernel_task_adapter.py +259 -0
  46. ksadk/a2a/langgraph.py +20 -7
  47. ksadk/a2a/langgraph_invocation_context.py +136 -0
  48. ksadk/a2a/langgraph_managed.py +150 -0
  49. ksadk/a2a/langgraph_recovery.py +32 -0
  50. ksadk/a2a/langgraph_runtime.py +89 -0
  51. ksadk/a2a/langgraph_tool_runtime.py +132 -0
  52. ksadk/a2a/langgraph_tools.py +63 -0
  53. ksadk/a2a/message_digest.py +40 -0
  54. ksadk/a2a/platform.py +389 -0
  55. ksadk/a2a/policy_refs.py +13 -0
  56. ksadk/a2a/presentation.py +402 -0
  57. ksadk/a2a/product_kernel_facts.py +177 -0
  58. ksadk/a2a/product_key.py +99 -0
  59. ksadk/a2a/protocol_errors.py +116 -0
  60. ksadk/a2a/result_text.py +49 -0
  61. ksadk/a2a/resume_store.py +93 -3
  62. ksadk/a2a/routes.py +6 -1
  63. ksadk/a2a/runtime_approval.py +198 -0
  64. ksadk/a2a/runtime_input.py +151 -0
  65. ksadk/a2a/server.py +16 -7
  66. ksadk/a2a/space_client.py +101 -16
  67. ksadk/a2a/task_adapter.py +19 -5
  68. ksadk/a2a/task_event_dispatcher.py +39 -2
  69. ksadk/a2a/task_event_outbox.py +215 -0
  70. ksadk/a2a/task_event_outbox_owned.py +130 -0
  71. ksadk/a2a/task_store.py +37 -0
  72. ksadk/a2a/tool_activity.py +176 -0
  73. ksadk/a2a/workload_tokens.py +134 -0
  74. ksadk/a2ui/core.py +3 -3
  75. ksadk/api/client.py +38 -79
  76. ksadk/api/sse_stream.py +96 -0
  77. ksadk/builders/code_builder.py +121 -118
  78. ksadk/builders/container_builder.py +11 -29
  79. ksadk/builders/framework_requirements.py +22 -2
  80. ksadk/builders/managed_runtime_builder.py +12 -0
  81. ksadk/builders/source_provenance.py +87 -0
  82. ksadk/cli/__init__.py +36 -0
  83. ksadk/cli/cmd_a2a.py +43 -0
  84. ksadk/cli/cmd_build.py +9 -0
  85. ksadk/cli/cmd_config.py +4 -4
  86. ksadk/cli/cmd_create.py +12 -12
  87. ksadk/cli/cmd_deploy.py +27 -2
  88. ksadk/cli/cmd_hermes.py +24 -2
  89. ksadk/cli/cmd_invoke.py +64 -88
  90. ksadk/cli/cmd_openclaw.py +26 -1
  91. ksadk/cli/cmd_plugin.py +70 -12
  92. ksadk/cli/cmd_studio.py +69 -17
  93. ksadk/cli/cmd_web.py +2 -0
  94. ksadk/cli/hosted_invoke.py +206 -0
  95. ksadk/cli/invoke_content.py +60 -0
  96. ksadk/cli/openclaw_env.py +1 -1
  97. ksadk/cli/resource_common.py +26 -0
  98. ksadk/cli/runtime_bootstrap.py +27 -5
  99. ksadk/codex/_expert_request_reader.py +48 -0
  100. ksadk/codex/a2a_tools.py +140 -0
  101. ksadk/codex/async_approval.py +169 -0
  102. ksadk/codex/async_client.py +938 -0
  103. ksadk/codex/async_proxy.py +216 -0
  104. ksadk/codex/base_runtime.py +25 -0
  105. ksadk/codex/client.py +3 -1047
  106. ksadk/codex/client_payloads.py +85 -0
  107. ksadk/codex/rollout_checkpoint.py +264 -0
  108. ksadk/codex/runtime.py +290 -54
  109. ksadk/common/auth.py +2 -2
  110. ksadk/common/cloud_headers.py +36 -0
  111. ksadk/common/diagnostics.py +34 -0
  112. ksadk/common/kop_client.py +2 -1
  113. ksadk/configs/__init__.py +1 -1
  114. ksadk/configs/env_registry.py +216 -143
  115. ksadk/configs/env_registry_a2a.py +173 -0
  116. ksadk/configs/env_registry_studio.py +134 -0
  117. ksadk/configs/global_config.py +2 -1
  118. ksadk/configs/settings.py +9 -3
  119. ksadk/connector/__init__.py +17 -0
  120. ksadk/connector/channel.py +477 -0
  121. ksadk/context_engine/assembler.py +9 -0
  122. ksadk/context_engine/capabilities.py +52 -4
  123. ksadk/context_engine/contributors.py +80 -17
  124. ksadk/context_engine/instruction_authority.py +18 -0
  125. ksadk/conversations/attachment_storage.py +10 -3
  126. ksadk/conversations/attachments.py +69 -20
  127. ksadk/conversations/contracts.py +28 -9
  128. ksadk/conversations/message_projection.py +51 -155
  129. ksadk/conversations/message_projection_blocks.py +137 -0
  130. ksadk/conversations/model_context.py +11 -3
  131. ksadk/conversations/normalize.py +28 -10
  132. ksadk/conversations/projector.py +63 -0
  133. ksadk/conversations/runtime_baseline.py +37 -0
  134. ksadk/conversations/runtime_compaction.py +7 -2
  135. ksadk/conversations/runtime_input.py +16 -4
  136. ksadk/conversations/runtime_invocation.py +17 -4
  137. ksadk/conversations/runtime_observability.py +39 -10
  138. ksadk/conversations/runtime_payloads.py +12 -0
  139. ksadk/conversations/runtime_persistence.py +296 -32
  140. ksadk/conversations/runtime_preparation.py +98 -13
  141. ksadk/conversations/runtime_projection.py +217 -0
  142. ksadk/conversations/runtime_resume.py +141 -70
  143. ksadk/conversations/runtime_stream_events.py +29 -35
  144. ksadk/conversations/runtime_streaming.py +8 -0
  145. ksadk/conversations/tool_receipt_resume.py +387 -0
  146. ksadk/conversations/tool_receipts.py +32 -0
  147. ksadk/deployment/env_forward.py +5 -0
  148. ksadk/deployment/manager.py +15 -6
  149. ksadk/deployment/providers/serverless.py +76 -136
  150. ksadk/deployment/providers/serverless_payloads.py +122 -0
  151. ksadk/evaluation/agent_eval_client.py +2 -1
  152. ksadk/evaluation/contracts.py +78 -3
  153. ksadk/evaluation/evaluators.py +166 -1
  154. ksadk/evaluation/evidence.py +271 -4
  155. ksadk/evaluation/local_adapter.py +76 -12
  156. ksadk/evaluation/studio_build_adapter.py +73 -9
  157. ksadk/events/adapters/_a2a_checkpoint.py +151 -0
  158. ksadk/events/adapters/_a2a_snapshot.py +18 -14
  159. ksadk/events/adapters/_a2a_support.py +15 -3
  160. ksadk/events/adapters/_a2a_tool_activity.py +123 -0
  161. ksadk/events/adapters/_codex_errors.py +49 -0
  162. ksadk/events/adapters/_codex_items.py +16 -8
  163. ksadk/events/adapters/_langgraph_support.py +46 -1
  164. ksadk/events/adapters/a2a.py +54 -26
  165. ksadk/events/adapters/adk.py +188 -30
  166. ksadk/events/adapters/codex.py +33 -7
  167. ksadk/events/adapters/langgraph.py +16 -6
  168. ksadk/events/canonical.py +5 -0
  169. ksadk/events/canonical_store.py +200 -4
  170. ksadk/events/reducer.py +82 -0
  171. ksadk/events/run_authority.py +176 -0
  172. ksadk/events/session_event.py +2 -2
  173. ksadk/harness/__init__.py +18 -0
  174. ksadk/harness/app.py +11 -0
  175. ksadk/harness/artifact_adapters/__init__.py +1 -0
  176. ksadk/harness/artifact_adapters/native_page_budget.py +172 -0
  177. ksadk/harness/artifact_adapters/native_search.py +249 -0
  178. ksadk/harness/artifact_adapters/native_search_budget.py +134 -0
  179. ksadk/harness/artifact_adapters/native_store.py +115 -0
  180. ksadk/harness/artifact_adapters/native_tool.py +222 -0
  181. ksadk/harness/artifact_store.py +230 -0
  182. ksadk/harness/browser_adapters/__init__.py +6 -0
  183. ksadk/harness/browser_adapters/native_browser_tool.py +92 -0
  184. ksadk/harness/browser_adapters/native_dom_snapshot.py +187 -0
  185. ksadk/harness/browser_adapters/native_playwright.py +953 -0
  186. ksadk/harness/browser_adapters/native_vision.py +295 -0
  187. ksadk/harness/capabilities.py +115 -0
  188. ksadk/harness/capability_adapters/__init__.py +1 -0
  189. ksadk/harness/capability_adapters/native_dsh.py +289 -0
  190. ksadk/harness/capability_runtime.py +332 -0
  191. ksadk/harness/closure_contract.py +92 -0
  192. ksadk/harness/compaction_record.py +153 -0
  193. ksadk/harness/compiler.py +284 -0
  194. ksadk/harness/config.py +28 -5
  195. ksadk/harness/conformance/__init__.py +33 -0
  196. ksadk/harness/conformance/contract.py +504 -0
  197. ksadk/harness/conformance/fixtures.py +102 -0
  198. ksadk/harness/context_engine.py +388 -0
  199. ksadk/harness/context_maintenance.py +259 -0
  200. ksadk/harness/context_manifest.py +188 -0
  201. ksadk/harness/context_sections.py +397 -0
  202. ksadk/harness/context_tuning.py +168 -0
  203. ksadk/harness/diagnostic_adapters/__init__.py +1 -0
  204. ksadk/harness/diagnostic_adapters/contracts.py +70 -0
  205. ksadk/harness/diagnostic_adapters/lsp_rpc.py +210 -0
  206. ksadk/harness/diagnostic_adapters/native_lsp.py +286 -0
  207. ksadk/harness/draft_runtime.py +322 -0
  208. ksadk/harness/engine/__init__.py +40 -0
  209. ksadk/harness/engine/base.py +122 -0
  210. ksadk/harness/engine/budgets.py +108 -0
  211. ksadk/harness/engine/capability_declarations.py +59 -0
  212. ksadk/harness/engine/completion_guard.py +57 -0
  213. ksadk/harness/engine/context_pipeline.py +1351 -0
  214. ksadk/harness/engine/graph_builder.py +352 -0
  215. ksadk/harness/engine/hooked_reasoner.py +380 -0
  216. ksadk/harness/engine/langgraph.py +1049 -0
  217. ksadk/harness/engine/mcp_disclosure.py +701 -0
  218. ksadk/harness/engine/phases/__init__.py +6 -0
  219. ksadk/harness/engine/phases/context.py +48 -0
  220. ksadk/harness/engine/phases/interaction.py +31 -0
  221. ksadk/harness/engine/phases/reason.py +552 -0
  222. ksadk/harness/engine/phases/tools.py +638 -0
  223. ksadk/harness/engine/policy_runtime.py +81 -0
  224. ksadk/harness/engine/postgres_checkpointer.py +386 -0
  225. ksadk/harness/engine/run_control_bridge.py +100 -0
  226. ksadk/harness/engine/run_types.py +98 -0
  227. ksadk/harness/engine/skill_disclosure.py +275 -0
  228. ksadk/harness/engine/spans.py +41 -0
  229. ksadk/harness/engine/steering.py +177 -0
  230. ksadk/harness/engine/subagents.py +360 -0
  231. ksadk/harness/engine/thread_ids.py +112 -0
  232. ksadk/harness/engine/tool_dispatch.py +249 -0
  233. ksadk/harness/event_tree.py +212 -0
  234. ksadk/harness/events.py +498 -0
  235. ksadk/harness/execution_policy.py +104 -0
  236. ksadk/harness/hooks.py +259 -0
  237. ksadk/harness/insights.py +210 -0
  238. ksadk/harness/lifecycle.py +578 -0
  239. ksadk/harness/lifecycle_conformance.py +233 -0
  240. ksadk/harness/lifecycle_local_plane.py +104 -0
  241. ksadk/harness/loop/__init__.py +44 -0
  242. ksadk/harness/loop/reason.py +699 -0
  243. ksadk/harness/loop/tools.py +907 -0
  244. ksadk/harness/managed_runtime.py +1044 -0
  245. ksadk/harness/mcp_disclosure_contract.py +85 -0
  246. ksadk/harness/mcp_matrix.py +474 -0
  247. ksadk/harness/mcp_result_policy.py +44 -0
  248. ksadk/harness/mcp_runtime.py +504 -0
  249. ksadk/harness/memory_consolidation.py +157 -0
  250. ksadk/harness/memory_loop.py +156 -0
  251. ksadk/harness/memory_runtime.py +541 -0
  252. ksadk/harness/metrics.py +90 -0
  253. ksadk/harness/micro_compact.py +69 -0
  254. ksadk/harness/model_adapters/__init__.py +1 -0
  255. ksadk/harness/model_adapters/native_mcp_media.py +100 -0
  256. ksadk/harness/model_adapters/native_mcp_result.py +36 -0
  257. ksadk/harness/model_adapters/native_openai.py +703 -0
  258. ksadk/harness/model_capability.py +468 -0
  259. ksadk/harness/model_provider.py +430 -0
  260. ksadk/harness/multimodal.py +57 -0
  261. ksadk/harness/native/__init__.py +1 -0
  262. ksadk/harness/native/activation_contract.py +25 -0
  263. ksadk/harness/native/activation_recovery.py +29 -0
  264. ksadk/harness/native/adapters.py +291 -0
  265. ksadk/harness/native/artifact_contract.py +14 -0
  266. ksadk/harness/native/artifact_partial_read_capacity.py +171 -0
  267. ksadk/harness/native/artifact_read_capacity.py +182 -0
  268. ksadk/harness/native/artifact_read_contract.py +27 -0
  269. ksadk/harness/native/artifact_runtime.py +408 -0
  270. ksadk/harness/native/artifact_search_capacity.py +206 -0
  271. ksadk/harness/native/artifact_search_contract.py +40 -0
  272. ksadk/harness/native/artifact_single_read_capacity.py +170 -0
  273. ksadk/harness/native/browser_contract.py +229 -0
  274. ksadk/harness/native/browser_recovery.py +183 -0
  275. ksadk/harness/native/capabilities.py +204 -0
  276. ksadk/harness/native/child_approval.py +60 -0
  277. ksadk/harness/native/child_budget.py +136 -0
  278. ksadk/harness/native/child_cancellation.py +80 -0
  279. ksadk/harness/native/child_input.py +98 -0
  280. ksadk/harness/native/child_input_runtime.py +59 -0
  281. ksadk/harness/native/child_interruption.py +81 -0
  282. ksadk/harness/native/child_lifecycle.py +246 -0
  283. ksadk/harness/native/child_lifecycle_contract.py +91 -0
  284. ksadk/harness/native/child_notifications.py +75 -0
  285. ksadk/harness/native/child_promotion.py +66 -0
  286. ksadk/harness/native/command_runtime.py +99 -0
  287. ksadk/harness/native/completion.py +302 -0
  288. ksadk/harness/native/completion_text.py +116 -0
  289. ksadk/harness/native/context.py +358 -0
  290. ksadk/harness/native/context_artifact_page.py +75 -0
  291. ksadk/harness/native/context_compaction_projection.py +131 -0
  292. ksadk/harness/native/context_contributions.py +320 -0
  293. ksadk/harness/native/context_contributor_contract.py +133 -0
  294. ksadk/harness/native/context_driver.py +820 -0
  295. ksadk/harness/native/context_fresh_tools.py +49 -0
  296. ksadk/harness/native/context_history.py +272 -0
  297. ksadk/harness/native/context_microcompact.py +61 -0
  298. ksadk/harness/native/context_partial_summary.py +58 -0
  299. ksadk/harness/native/context_partition.py +492 -0
  300. ksadk/harness/native/context_pruning.py +275 -0
  301. ksadk/harness/native/context_reduction.py +60 -0
  302. ksadk/harness/native/context_replay.py +148 -0
  303. ksadk/harness/native/context_request_projection.py +526 -0
  304. ksadk/harness/native/context_runtime.py +976 -0
  305. ksadk/harness/native/context_runtime_contracts.py +409 -0
  306. ksadk/harness/native/context_search.py +112 -0
  307. ksadk/harness/native/context_source_fragments.py +188 -0
  308. ksadk/harness/native/context_source_progress.py +119 -0
  309. ksadk/harness/native/context_summary_planning.py +788 -0
  310. ksadk/harness/native/context_summary_recovery.py +105 -0
  311. ksadk/harness/native/delivery_review.py +331 -0
  312. ksadk/harness/native/delivery_review_contract.py +54 -0
  313. ksadk/harness/native/effect_admission.py +82 -0
  314. ksadk/harness/native/effect_control.py +318 -0
  315. ksadk/harness/native/effect_receipts.py +729 -0
  316. ksadk/harness/native/effect_runtime.py +303 -0
  317. ksadk/harness/native/effective_configuration.py +28 -0
  318. ksadk/harness/native/engine.py +945 -0
  319. ksadk/harness/native/final_output.py +34 -0
  320. ksadk/harness/native/final_submission.py +58 -0
  321. ksadk/harness/native/finalization_workspace.py +96 -0
  322. ksadk/harness/native/fork.py +151 -0
  323. ksadk/harness/native/guidance.py +91 -0
  324. ksadk/harness/native/history.py +381 -0
  325. ksadk/harness/native/hook_executor.py +205 -0
  326. ksadk/harness/native/hook_runtime_contract.py +282 -0
  327. ksadk/harness/native/interaction_observation.py +117 -0
  328. ksadk/harness/native/lease.py +103 -0
  329. ksadk/harness/native/machine.py +974 -0
  330. ksadk/harness/native/mcp_result_governance.py +156 -0
  331. ksadk/harness/native/model.py +570 -0
  332. ksadk/harness/native/model_feedback.py +63 -0
  333. ksadk/harness/native/model_input.py +30 -0
  334. ksadk/harness/native/model_recovery_contract.py +35 -0
  335. ksadk/harness/native/model_replay.py +128 -0
  336. ksadk/harness/native/model_request.py +133 -0
  337. ksadk/harness/native/model_runtime.py +196 -0
  338. ksadk/harness/native/model_takeover.py +419 -0
  339. ksadk/harness/native/model_tool_batch.py +40 -0
  340. ksadk/harness/native/observed_progress.py +46 -0
  341. ksadk/harness/native/operator_guidance.py +201 -0
  342. ksadk/harness/native/output_contract.py +80 -0
  343. ksadk/harness/native/output_recovery.py +24 -0
  344. ksadk/harness/native/ports.py +749 -0
  345. ksadk/harness/native/precompact_hook_runtime.py +171 -0
  346. ksadk/harness/native/replay_context.py +149 -0
  347. ksadk/harness/native/request_media.py +112 -0
  348. ksadk/harness/native/restore_contract.py +16 -0
  349. ksadk/harness/native/run_clock.py +120 -0
  350. ksadk/harness/native/state.py +436 -0
  351. ksadk/harness/native/steering.py +170 -0
  352. ksadk/harness/native/stop_hook_runtime.py +151 -0
  353. ksadk/harness/native/studio_admission.py +29 -0
  354. ksadk/harness/native/subagents.py +86 -0
  355. ksadk/harness/native/task_policy.py +99 -0
  356. ksadk/harness/native/task_progress.py +86 -0
  357. ksadk/harness/native/terminal_verification.py +43 -0
  358. ksadk/harness/native/tool_argument_feedback.py +178 -0
  359. ksadk/harness/native/tool_budget.py +253 -0
  360. ksadk/harness/native/tool_error_streak.py +89 -0
  361. ksadk/harness/native/tool_hook_runtime.py +343 -0
  362. ksadk/harness/native/tool_input_validation.py +58 -0
  363. ksadk/harness/native/tool_interruption.py +96 -0
  364. ksadk/harness/native/tool_observation.py +259 -0
  365. ksadk/harness/native/tool_progress.py +223 -0
  366. ksadk/harness/native/tool_recovery.py +25 -0
  367. ksadk/harness/native/tool_scheduling.py +33 -0
  368. ksadk/harness/native/tool_schema.py +126 -0
  369. ksadk/harness/native/transitions.py +151 -0
  370. ksadk/harness/native/turn_control.py +98 -0
  371. ksadk/harness/native/user_document.py +98 -0
  372. ksadk/harness/native/user_document_capture.py +49 -0
  373. ksadk/harness/native/user_input.py +87 -0
  374. ksadk/harness/native/verification.py +432 -0
  375. ksadk/harness/native/verification_binding.py +40 -0
  376. ksadk/harness/native/verification_runtime.py +233 -0
  377. ksadk/harness/native/verification_workspace.py +67 -0
  378. ksadk/harness/native_driver_failure.py +123 -0
  379. ksadk/harness/native_facade.py +977 -0
  380. ksadk/harness/native_factory.py +244 -0
  381. ksadk/harness/native_projection.py +503 -0
  382. ksadk/harness/native_runtime.py +621 -0
  383. ksadk/harness/native_shadow.py +250 -0
  384. ksadk/harness/native_shadow_inputs.py +146 -0
  385. ksadk/harness/observability/__init__.py +29 -0
  386. ksadk/harness/observability/inspection.py +528 -0
  387. ksadk/harness/observability/metrics.py +167 -0
  388. ksadk/harness/observability/otel_exporter.py +218 -0
  389. ksadk/harness/progress_adapters/__init__.py +5 -0
  390. ksadk/harness/progress_adapters/native_todo.py +75 -0
  391. ksadk/harness/prompt_cache.py +132 -0
  392. ksadk/harness/public_activity.py +213 -0
  393. ksadk/harness/public_output.py +30 -0
  394. ksadk/harness/public_summary.py +36 -0
  395. ksadk/harness/readiness.py +249 -0
  396. ksadk/harness/reasoner.py +779 -17
  397. ksadk/harness/release_readiness.py +140 -0
  398. ksadk/harness/resource_ref.py +69 -0
  399. ksadk/harness/rpc.py +200 -0
  400. ksadk/harness/run_control.py +773 -0
  401. ksadk/harness/runtime.py +157 -113
  402. ksadk/harness/runtime_capabilities.py +228 -0
  403. ksadk/harness/runtime_server.py +514 -0
  404. ksadk/harness/sandbox_adapters.py +739 -0
  405. ksadk/harness/sandbox_backend.py +733 -0
  406. ksadk/harness/sandbox_conformance.py +288 -0
  407. ksadk/harness/sandbox_lease.py +77 -0
  408. ksadk/harness/sandbox_lease_conformance.py +259 -0
  409. ksadk/harness/sandbox_matrix.py +170 -0
  410. ksadk/harness/sandbox_reconciliation.py +140 -0
  411. ksadk/harness/sandbox_recovery.py +586 -0
  412. ksadk/harness/sandbox_recovery_conformance.py +246 -0
  413. ksadk/harness/sdk.py +153 -0
  414. ksadk/harness/session_store.py +589 -0
  415. ksadk/harness/skill_adapters/__init__.py +1 -0
  416. ksadk/harness/skill_adapters/native_bundle.py +148 -0
  417. ksadk/harness/skill_adapters/native_disclosure.py +306 -0
  418. ksadk/harness/skill_composition.py +212 -0
  419. ksadk/harness/skill_feedback.py +123 -0
  420. ksadk/harness/skill_intent.py +47 -0
  421. ksadk/harness/skill_matrix.py +233 -0
  422. ksadk/harness/skill_runtime.py +317 -0
  423. ksadk/harness/spec.py +342 -0
  424. ksadk/harness/state.py +155 -0
  425. ksadk/harness/stores/__init__.py +14 -0
  426. ksadk/harness/stores/native_command_settlement.py +65 -0
  427. ksadk/harness/stores/native_contract.py +311 -0
  428. ksadk/harness/stores/native_memory.py +326 -0
  429. ksadk/harness/stores/native_memory_archive.py +70 -0
  430. ksadk/harness/stores/native_migration_cli.py +91 -0
  431. ksadk/harness/stores/native_outbox.py +56 -0
  432. ksadk/harness/stores/native_sqlite.py +782 -0
  433. ksadk/harness/stores/native_sqlite_audit.py +29 -0
  434. ksadk/harness/stores/native_sqlite_migration.py +252 -0
  435. ksadk/harness/strategies.py +149 -0
  436. ksadk/harness/subagent.py +263 -0
  437. ksadk/harness/subagent_adapters/checkpoint_recovery.py +120 -0
  438. ksadk/harness/subagent_adapters/native_background_child.py +803 -0
  439. ksadk/harness/subagent_adapters/native_child.py +819 -0
  440. ksadk/harness/subagent_adapters/native_child_approval.py +128 -0
  441. ksadk/harness/subagent_adapters/native_child_continuation.py +51 -0
  442. ksadk/harness/subagent_adapters/native_child_result.py +392 -0
  443. ksadk/harness/subagent_adapters/prepared_recovery.py +120 -0
  444. ksadk/harness/tool_display.py +75 -0
  445. ksadk/harness/tool_display_contract.py +134 -0
  446. ksadk/harness/tool_output.py +141 -0
  447. ksadk/harness/tool_policy.py +134 -0
  448. ksadk/harness/tool_receipts.py +96 -0
  449. ksadk/harness/tool_reliability.py +81 -0
  450. ksadk/harness/tool_result_policy.py +216 -0
  451. ksadk/harness/tools.py +52 -0
  452. ksadk/harness/user_errors.py +69 -0
  453. ksadk/harness/working_context.py +81 -0
  454. ksadk/harness/working_context_patch.py +99 -0
  455. ksadk/harness/workspace_adapters/__init__.py +6 -0
  456. ksadk/harness/workspace_adapters/native_directory.py +180 -0
  457. ksadk/harness/workspace_adapters/native_git.py +208 -0
  458. ksadk/harness/workspace_adapters/native_isolation.py +350 -0
  459. ksadk/identity/resolver.py +18 -0
  460. ksadk/interaction/__init__.py +1 -1
  461. ksadk/interaction/ledger.py +1 -1
  462. ksadk/interaction/provider.py +1 -1
  463. ksadk/interaction/providers/__init__.py +3 -1
  464. ksadk/interaction/providers/adk.py +1 -1
  465. ksadk/interaction/providers/codex.py +1 -1
  466. ksadk/interaction/providers/harness.py +50 -0
  467. ksadk/interaction/providers/langgraph.py +9 -2
  468. ksadk/kernel/__init__.py +10 -0
  469. ksadk/kernel/approval_presentation.py +91 -0
  470. ksadk/kernel/authorization.py +1 -1
  471. ksadk/kernel/bootstrap.py +220 -252
  472. ksadk/kernel/contract_fingerprints.py +45 -1
  473. ksadk/kernel/contracts.py +30 -0
  474. ksadk/kernel/control.py +180 -29
  475. ksadk/kernel/execution_grants.py +428 -0
  476. ksadk/kernel/execution_grants_memory.py +252 -0
  477. ksadk/kernel/execution_grants_postgres.py +395 -0
  478. ksadk/kernel/execution_grants_sqlite.py +435 -0
  479. ksadk/kernel/execution_host_ingress.py +1071 -0
  480. ksadk/kernel/history_projection.py +120 -0
  481. ksadk/kernel/ingress.py +139 -226
  482. ksadk/kernel/ingress_bootstrap.py +268 -0
  483. ksadk/kernel/legacy_history.py +179 -0
  484. ksadk/kernel/mapping.py +1 -1
  485. ksadk/kernel/memory_store.py +61 -58
  486. ksadk/kernel/memory_store_projection.py +63 -0
  487. ksadk/kernel/postgres_event_log.py +181 -0
  488. ksadk/kernel/postgres_store.py +230 -231
  489. ksadk/kernel/readiness.py +125 -0
  490. ksadk/kernel/recovery.py +166 -16
  491. ksadk/kernel/recovery_projection.py +124 -0
  492. ksadk/kernel/runtime_checkpoint.py +34 -0
  493. ksadk/kernel/sql/001_agent_kernel.sql +28 -0
  494. ksadk/kernel/sqlite_schema.py +100 -0
  495. ksadk/kernel/sqlite_store.py +410 -200
  496. ksadk/kernel/state.py +2 -3
  497. ksadk/kernel/store.py +104 -5
  498. ksadk/kernel/teams_artifact_tools.py +392 -0
  499. ksadk/kernel/teams_effects.py +851 -0
  500. ksadk/kernel/teams_execution_context.py +408 -0
  501. ksadk/kernel/teams_host_http.py +237 -0
  502. ksadk/kernel/teams_material_transport.py +245 -0
  503. ksadk/kernel/teams_materials.py +318 -0
  504. ksadk/kernel/teams_runtime_bootstrap.py +604 -0
  505. ksadk/kernel/teams_tool_runtime.py +273 -0
  506. ksadk/kernel/teams_workspace.py +259 -0
  507. ksadk/kernel/worker.py +369 -340
  508. ksadk/kernel/worker_controls.py +454 -0
  509. ksadk/kernel/worker_history.py +72 -0
  510. ksadk/kernel/worker_identity.py +74 -0
  511. ksadk/kernel/worker_interactions.py +197 -0
  512. ksadk/knowledge_base/client.py +15 -1
  513. ksadk/managed_runtime.py +29 -2
  514. ksadk/memory/adk/backends/http_ltm_backend.py +2 -1
  515. ksadk/memory/adk/backends/sdk_ltm_backend.py +15 -1
  516. ksadk/memory/adk/bounded_database_session_service.py +232 -0
  517. ksadk/memory/adk/resilient_session_service.py +115 -15
  518. ksadk/memory/adk/short_term_memory.py +8 -9
  519. ksadk/memory/coordinator.py +22 -6
  520. ksadk/memory/extraction.py +159 -7
  521. ksadk/memory/models.py +77 -1
  522. ksadk/memory/provider_resolver.py +5 -0
  523. ksadk/memory/providers/local_candidates.py +183 -0
  524. ksadk/memory/providers/local_postgres.py +200 -0
  525. ksadk/memory/providers/local_sqlite.py +280 -67
  526. ksadk/memory/resolved_policy.py +12 -7
  527. ksadk/memory/retrieval.py +189 -0
  528. ksadk/memory/store_adapter.py +150 -0
  529. ksadk/model_gateway/__init__.py +29 -0
  530. ksadk/model_gateway/client.py +253 -0
  531. ksadk/model_gateway/events.py +113 -0
  532. ksadk/model_gateway/matrix.py +297 -0
  533. ksadk/model_policy.py +2 -2
  534. ksadk/model_proxy/compat.py +670 -0
  535. ksadk/model_proxy/detect.py +254 -0
  536. ksadk/model_proxy/server.py +14 -9
  537. ksadk/model_proxy/transform.py +72 -6
  538. ksadk/plugins/__init__.py +1 -1
  539. ksadk/plugins/bridges/dsh.py +29 -20
  540. ksadk/plugins/bridges/dsh_profile_manifest.py +36 -0
  541. ksadk/plugins/builtin_resources.py +199 -0
  542. ksadk/plugins/builtins.py +417 -185
  543. ksadk/plugins/companion_artifacts.py +83 -0
  544. ksadk/plugins/companions.py +368 -0
  545. ksadk/plugins/context_contributor.py +15 -4
  546. ksadk/plugins/contracts.py +1 -1
  547. ksadk/plugins/dsh_channels.py +156 -0
  548. ksadk/plugins/dsh_dependency_policy.py +41 -0
  549. ksadk/plugins/dsh_home.py +140 -0
  550. ksadk/plugins/dsh_teams.py +105 -0
  551. ksadk/plugins/dsh_toolchain.py +92 -21
  552. ksadk/plugins/execution_host.py +107 -0
  553. ksadk/plugins/host.py +73 -5
  554. ksadk/plugins/kernel_adapter.py +44 -1
  555. ksadk/plugins/memory_provider.py +180 -0
  556. ksadk/plugins/providers/_process_group.py +50 -0
  557. ksadk/plugins/providers/bundles/dsh-channels-client/client.js +19 -0
  558. ksadk/plugins/providers/bundles/dsh-channels-client/cordis.patch.yml +3 -0
  559. ksadk/plugins/providers/bundles/dsh-channels-client/index.mjs +7 -0
  560. ksadk/plugins/providers/bundles/dsh-channels-client/package.json +12 -0
  561. ksadk/plugins/providers/bundles/dsh-knowledge/index.mjs +24 -2
  562. ksadk/plugins/providers/bundles/dsh-knowledge/package.json +1 -1
  563. ksadk/plugins/providers/bundles/dsh-memory/package.json +1 -1
  564. ksadk/plugins/providers/bundles/dsh-skill-center/package.json +1 -1
  565. ksadk/plugins/providers/bundles/dsh-teams-client/client.js +29 -0
  566. ksadk/plugins/providers/bundles/dsh-teams-client/cordis.patch.yml +3 -0
  567. ksadk/plugins/providers/bundles/dsh-teams-client/index.mjs +11 -0
  568. ksadk/plugins/providers/bundles/dsh-teams-client/package.json +35 -0
  569. ksadk/plugins/providers/bundles/dsh-teams-leader-policy/cordis.patch.yml +3 -0
  570. ksadk/plugins/providers/bundles/dsh-teams-leader-policy/index.mjs +13 -0
  571. ksadk/plugins/providers/bundles/dsh-teams-leader-policy/package.json +27 -0
  572. ksadk/plugins/providers/bundles/dsh-teams-profile/cordis.patch.yml +3 -0
  573. ksadk/plugins/providers/bundles/dsh-teams-profile/index.mjs +12 -0
  574. ksadk/plugins/providers/bundles/dsh-teams-profile/package.json +26 -0
  575. ksadk/plugins/providers/bundles/dsh-teams-router/cordis.patch.yml +3 -0
  576. ksadk/plugins/providers/bundles/dsh-teams-router/index.mjs +16 -0
  577. ksadk/plugins/providers/bundles/dsh-teams-router/package.json +27 -0
  578. ksadk/plugins/providers/bundles/dsh-teams-tasks/cordis.patch.yml +3 -0
  579. ksadk/plugins/providers/bundles/dsh-teams-tasks/index.mjs +16 -0
  580. ksadk/plugins/providers/bundles/dsh-teams-tasks/package.json +27 -0
  581. ksadk/plugins/providers/bundles/dsh-teams-tools/cordis.patch.yml +3 -0
  582. ksadk/plugins/providers/bundles/dsh-teams-tools/index.mjs +25 -0
  583. ksadk/plugins/providers/bundles/dsh-teams-tools/package.json +29 -0
  584. ksadk/plugins/providers/bundles/dsh-teams-tools/tools.json +235 -0
  585. ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +1 -1
  586. ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/index.mjs +51 -1
  587. ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/package.json +2 -2
  588. ksadk/plugins/providers/bundles/ksadk-dsh-companion-host/client.mjs +133 -0
  589. ksadk/plugins/providers/bundles/ksadk-dsh-companion-host/index.mjs +14 -0
  590. ksadk/plugins/providers/bundles/ksadk-dsh-studio/client.js +27 -4
  591. ksadk/plugins/providers/bundles/ksadk-dsh-studio/index.mjs +9 -5
  592. ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +1 -1
  593. ksadk/plugins/providers/codex.py +52 -40
  594. ksadk/plugins/providers/codex_native.py +20 -0
  595. ksadk/plugins/providers/codex_skills.py +46 -0
  596. ksadk/plugins/providers/dsh_capabilities.py +49 -143
  597. ksadk/plugins/providers/dsh_connector_lease.py +225 -0
  598. ksadk/plugins/providers/dsh_mcp.py +163 -1
  599. ksadk/plugins/providers/harness.py +413 -52
  600. ksadk/plugins/providers/harness_context.py +61 -0
  601. ksadk/plugins/providers/harness_contributions.py +241 -0
  602. ksadk/plugins/providers/harness_delegation.py +670 -0
  603. ksadk/plugins/providers/harness_hooks.py +121 -0
  604. ksadk/plugins/providers/harness_managed.py +550 -0
  605. ksadk/plugins/providers/harness_tools.py +455 -0
  606. ksadk/plugins/providers/legacy_catalog.py +1 -1
  607. ksadk/plugins/providers/mcp_projection.py +100 -3
  608. ksadk/plugins/providers/platform_resources.py +16 -6
  609. ksadk/plugins/providers/shipped_dsh.py +4 -1
  610. ksadk/plugins/subagent_providers/codex.py +28 -6
  611. ksadk/plugins/teams/__init__.py +5 -0
  612. ksadk/plugins/teams/api.py +405 -0
  613. ksadk/plugins/teams/application.py +540 -0
  614. ksadk/plugins/teams/artifacts.py +60 -0
  615. ksadk/plugins/teams/build_artifacts.py +460 -0
  616. ksadk/plugins/teams/cloud_contract_fingerprints.py +22 -0
  617. ksadk/plugins/teams/cloud_contracts.py +928 -0
  618. ksadk/plugins/teams/cloud_permits.py +250 -0
  619. ksadk/plugins/teams/context.py +96 -0
  620. ksadk/plugins/teams/contracts.py +136 -0
  621. ksadk/plugins/teams/domain.py +1480 -0
  622. ksadk/plugins/teams/domain_values.py +24 -0
  623. ksadk/plugins/teams/effect_contracts.py +255 -0
  624. ksadk/plugins/teams/errors.py +13 -0
  625. ksadk/plugins/teams/execution_projection.py +255 -0
  626. ksadk/plugins/teams/leader_decisions.py +106 -0
  627. ksadk/plugins/teams/migration.py +311 -0
  628. ksadk/plugins/teams/migration_cli.py +322 -0
  629. ksadk/plugins/teams/postgres_store.py +352 -0
  630. ksadk/plugins/teams/release_contracts.py +60 -0
  631. ksadk/plugins/teams/runtime.py +853 -0
  632. ksadk/plugins/teams/source_projection.py +84 -0
  633. ksadk/plugins/teams/store.py +519 -0
  634. ksadk/plugins/teams/task_decisions.py +436 -0
  635. ksadk/plugins/teams/tool_service.py +152 -0
  636. ksadk/plugins/teams/transport.py +96 -0
  637. ksadk/plugins/teams/workspaces.py +255 -0
  638. ksadk/resource_runtime/skill_runtime.py +3 -0
  639. ksadk/resource_runtime/socket_server.py +11 -0
  640. ksadk/resource_runtime/worker.py +28 -4
  641. ksadk/runners/_adk_a2a_stream.py +122 -0
  642. ksadk/runners/_adk_canonical_stream.py +474 -0
  643. ksadk/runners/_langgraph_canonical_streams.py +527 -0
  644. ksadk/runners/_langgraph_runner_streams.py +83 -401
  645. ksadk/runners/_langgraph_state_reader.py +106 -0
  646. ksadk/runners/_langgraph_tool_projection.py +86 -0
  647. ksadk/runners/_langgraph_tools_transformer.py +27 -0
  648. ksadk/runners/_session_identity.py +285 -0
  649. ksadk/runners/adk_runner.py +220 -350
  650. ksadk/runners/base_runner.py +6 -5
  651. ksadk/runners/langgraph_runner.py +208 -195
  652. ksadk/runners/patch_langchain.py +47 -0
  653. ksadk/runners/usage_accumulator.py +11 -3
  654. ksadk/runners/utils/loader.py +28 -14
  655. ksadk/runtime/_runner_adapter/a2ui_surface.py +145 -0
  656. ksadk/runtime/_runner_adapter/gateway_approvals.py +93 -0
  657. ksadk/runtime/_runner_adapter/stream_mapping.py +134 -129
  658. ksadk/runtime/adapter.py +40 -3
  659. ksadk/runtime/conversation_execution.py +29 -3
  660. ksadk/runtime/executor.py +105 -7
  661. ksadk/runtime/factory.py +45 -3
  662. ksadk/runtime/framework_adapters.py +184 -10
  663. ksadk/runtime/hosted_finalizer.py +4 -6
  664. ksadk/runtime/launch.py +2 -0
  665. ksadk/runtime/preprocessing.py +11 -3
  666. ksadk/runtime/runner_adapter.py +160 -21
  667. ksadk/runtime/skill_eval_result.py +36 -0
  668. ksadk/runtime/timing.py +46 -0
  669. ksadk/runtime/usage.py +49 -18
  670. ksadk/runtime_context.py +105 -1
  671. ksadk/sandbox/backends/e2b.py +272 -24
  672. ksadk/sandbox/backends/local_process.py +73 -39
  673. ksadk/sandbox/base.py +62 -1
  674. ksadk/sandbox/diagnostics.py +82 -0
  675. ksadk/sandbox/e2b_connection.py +9 -1
  676. ksadk/sandbox/local_controls.py +776 -0
  677. ksadk/sandbox/local_launcher.py +124 -0
  678. ksadk/scheduler/dispatcher.py +1 -1
  679. ksadk/server/factory.py +178 -25
  680. ksadk/server/history_deadline.py +158 -0
  681. ksadk/server/history_page_cache.py +129 -0
  682. ksadk/server/invocation_identity.py +118 -0
  683. ksadk/server/routes/checkpoint_audit.py +88 -0
  684. ksadk/server/routes/checkpoint_listing.py +294 -0
  685. ksadk/server/routes/checkpoint_resolution.py +73 -20
  686. ksadk/server/routes/common.py +32 -3
  687. ksadk/server/routes/control.py +112 -9
  688. ksadk/server/routes/dependencies.py +2 -1
  689. ksadk/server/routes/display_projection.py +154 -0
  690. ksadk/server/routes/kernel_ingress.py +26 -3
  691. ksadk/server/routes/misc.py +331 -33
  692. ksadk/server/routes/models.py +28 -4
  693. ksadk/server/routes/openai_compat.py +82 -13
  694. ksadk/server/routes/projection.py +159 -216
  695. ksadk/server/routes/run.py +112 -37
  696. ksadk/server/routes/sessions.py +493 -358
  697. ksadk/server/routes/streaming.py +68 -15
  698. ksadk/server/routes/tool_receipt_claim_routes.py +107 -0
  699. ksadk/server/routes/workspace.py +135 -23
  700. ksadk/server/static/assets/{ArtifactsPanel-CGbWlMNh.js → ArtifactsPanel-CILaW-fC.js} +1 -1
  701. ksadk/server/static/assets/{CodeBlock-b4C7upmO.js → CodeBlock-BVrNVtJC.js} +10 -7
  702. ksadk/server/static/assets/MathMessageMarkdown-Dp7c4wWD.js +8 -0
  703. ksadk/server/static/assets/{MermaidBlock-BEBN4Ksz.js → MermaidBlock-Dxop8Rd7.js} +9 -9
  704. ksadk/server/static/assets/{NativeTerminalPanel-zvL-WpAn.js → NativeTerminalPanel-Cjjz8B30.js} +1 -1
  705. ksadk/server/static/assets/{abnfDiagram-VCTEODGH-BYafW7Za.js → abnfDiagram-VCTEODGH-ckWDmOzw.js} +1 -1
  706. ksadk/server/static/assets/{arc-bTohInsS.js → arc-DqxJede-.js} +1 -1
  707. ksadk/server/static/assets/architecture-7GRP2DOG-pn_DuLy6.js +1 -0
  708. ksadk/server/static/assets/{architectureDiagram-5GKGNRK7-Ch5Lbzsi.js → architectureDiagram-5GKGNRK7-SEQA4bVi.js} +1 -1
  709. ksadk/server/static/assets/{blockDiagram-NRAW4CY4-kqF4MDOG.js → blockDiagram-NRAW4CY4-pM0B-6NJ.js} +1 -1
  710. ksadk/server/static/assets/{c4Diagram-UCG6FXSJ-8uSRIODv.js → c4Diagram-UCG6FXSJ-m2tuCNVq.js} +1 -1
  711. ksadk/server/static/assets/channel-fySsnkft.js +1 -0
  712. ksadk/server/static/assets/chunk-2Q5K7J3B-BheGxKRQ.js +1 -0
  713. ksadk/server/static/assets/{chunk-5VM5RSS4-DkhehZDr.js → chunk-5VM5RSS4-GJHDr9Ww.js} +1 -1
  714. ksadk/server/static/assets/{chunk-F27PBJKO-BdXhtPe9.js → chunk-F27PBJKO-CbAN45AV.js} +1 -1
  715. ksadk/server/static/assets/{chunk-G27WJ6UU-DPTpvpwi.js → chunk-G27WJ6UU-D5ngbbzF.js} +1 -1
  716. ksadk/server/static/assets/{chunk-JWPE2WC7-Pqoh38el.js → chunk-JWPE2WC7-Cc5IwuYC.js} +1 -1
  717. ksadk/server/static/assets/{chunk-LCL6LL3I-DeU00qC-.js → chunk-LCL6LL3I-CprgDDdJ.js} +1 -1
  718. ksadk/server/static/assets/{chunk-POPQ4Y6H-CzxScFkr.js → chunk-POPQ4Y6H-CDZhSy82.js} +1 -1
  719. ksadk/server/static/assets/{chunk-RHFEMEQ7-DpqMx-ux.js → chunk-RHFEMEQ7-CZchUNro.js} +1 -1
  720. ksadk/server/static/assets/{chunk-SVP7TREG-C6t0SypQ.js → chunk-SVP7TREG-BVruqk6T.js} +1 -1
  721. ksadk/server/static/assets/{chunk-XXDRQBXY-DVaeafZk.js → chunk-XXDRQBXY-4ld9SxPN.js} +1 -1
  722. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DSfLtNYm.js +1 -0
  723. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DSfLtNYm.js +1 -0
  724. ksadk/server/static/assets/{cose-bilkent-JH36ORCC-Bf9D3tzJ.js → cose-bilkent-JH36ORCC-Dmyyk_Vp.js} +1 -1
  725. ksadk/server/static/assets/cynefin-OW5HDTMX-PFWIGtvA.js +1 -0
  726. ksadk/server/static/assets/{cynefinDiagram-5FMLGOSQ-B9cY3iTz.js → cynefinDiagram-5FMLGOSQ-BIyq3EJb.js} +1 -1
  727. ksadk/server/static/assets/{dagre-3AP2YEHR-CSwlx3CB.js → dagre-3AP2YEHR-eBw08Qdk.js} +1 -1
  728. ksadk/server/static/assets/{dagre-UBpgon5q.js → dagre-DfvnEf45.js} +1 -1
  729. ksadk/server/static/assets/{diagram-S7CK7UJ4-C9akhN9a.js → diagram-S7CK7UJ4-CUSrE3Mg.js} +1 -1
  730. ksadk/server/static/assets/{diagram-UQ7AKVKN-CgedhTPI.js → diagram-UQ7AKVKN-Dq5Zw2gy.js} +1 -1
  731. ksadk/server/static/assets/{diagram-VSXAHHWV-Cjucz5BY.js → diagram-VSXAHHWV-DyUHfNwT.js} +1 -1
  732. ksadk/server/static/assets/{diagram-VX7I27RA-Dd4Vleyn.js → diagram-VX7I27RA-CREmM89O.js} +1 -1
  733. ksadk/server/static/assets/{diagram-Z3DM3KII-CZ2jGtYW.js → diagram-Z3DM3KII-Dw9zhHUK.js} +1 -1
  734. ksadk/server/static/assets/{dist-Bj730Ib4.js → dist-0uJ4tdx6.js} +1 -1
  735. ksadk/server/static/assets/{dist-BnaTiyzN.js → dist-AEv-vXf-.js} +1 -1
  736. ksadk/server/static/assets/{dist-BgV_owcB.js → dist-B49GzZDb.js} +1 -1
  737. ksadk/server/static/assets/{dist-D_ctSS2i.js → dist-B8Myiet9.js} +1 -1
  738. ksadk/server/static/assets/{dist-ufY3F5p6.js → dist-BBsYarrI.js} +2 -2
  739. ksadk/server/static/assets/{dist-ClYUxfeK.js → dist-BQMrUwdI.js} +1 -1
  740. ksadk/server/static/assets/{dist-CFzsn6BG.js → dist-BX_-FENW.js} +1 -1
  741. ksadk/server/static/assets/{dist-B3FR1SKK.js → dist-CD4L6ymH.js} +1 -1
  742. ksadk/server/static/assets/{dist-Cf125P_m.js → dist-CHpW1BHj.js} +1 -1
  743. ksadk/server/static/assets/{dist-DVCOWtha.js → dist-CVPau3tn.js} +1 -1
  744. ksadk/server/static/assets/{dist-DFD6Qa6T.js → dist-C_qdxcSH.js} +1 -1
  745. ksadk/server/static/assets/{dist-BIr4DSRq.js → dist-Co68Apq4.js} +1 -1
  746. ksadk/server/static/assets/{dist-DiixqL0J.js → dist-Cp2PaOMY.js} +1 -1
  747. ksadk/server/static/assets/{dist-fBO2v5Kt.js → dist-CuR5Bgme.js} +1 -1
  748. ksadk/server/static/assets/{dist-DoVSwoDb.js → dist-DDSyLEOX.js} +1 -1
  749. ksadk/server/static/assets/{dist-C5_ZAZsG.js → dist-DUxO0R99.js} +1 -1
  750. ksadk/server/static/assets/{dist-C1NMAY5a.js → dist-QzbGrED_.js} +1 -1
  751. ksadk/server/static/assets/dist-bupkyEBZ.js +1 -0
  752. ksadk/server/static/assets/{dist-DvFK7p5S.js → dist-cxTGfdqx.js} +1 -1
  753. ksadk/server/static/assets/{dist-CB9iJrvg.js → dist-hL5p1til.js} +1 -1
  754. ksadk/server/static/assets/{dist-btbmmX5O.js → dist-lOy23y43.js} +1 -1
  755. ksadk/server/static/assets/{dist-QfkEHOlb.js → dist-pq-nD33R.js} +1 -1
  756. ksadk/server/static/assets/{ebnfDiagram-PWID7BFC-Ci8NqqZu.js → ebnfDiagram-PWID7BFC-BS5LdNoR.js} +1 -1
  757. ksadk/server/static/assets/{erDiagram-SSCWMZ5O-CCIOhLZG.js → erDiagram-SSCWMZ5O-Bv1QtiY-.js} +1 -1
  758. ksadk/server/static/assets/eventmodeling-NTZA5JFV-DsM4upQX.js +1 -0
  759. ksadk/server/static/assets/flowDiagram-A5DVABFB-D5NA16k9.js +1 -0
  760. ksadk/server/static/assets/{ganttDiagram-EL5Y4UJY-Cp_r9Hb6.js → ganttDiagram-EL5Y4UJY-musi-e0G.js} +1 -1
  761. ksadk/server/static/assets/gitGraph-4MIJSDKK-DUCVz-ih.js +1 -0
  762. ksadk/server/static/assets/{gitGraphDiagram-WWUBYQGX-Dlhc8Wr8.js → gitGraphDiagram-WWUBYQGX-DH37EGkG.js} +1 -1
  763. ksadk/server/static/assets/index-CZdkAvsv.js +244 -0
  764. ksadk/server/static/assets/index-CvXdHgF3.css +2 -0
  765. ksadk/server/static/assets/info-A6RAGUB7-rw0ZNHZp.js +1 -0
  766. ksadk/server/static/assets/{infoDiagram-RXCK75RN-CO0rRTRA.js → infoDiagram-RXCK75RN-C1kEtv2y.js} +1 -1
  767. ksadk/server/static/assets/{ishikawaDiagram-5VMMS53U-CHiaedSS.js → ishikawaDiagram-5VMMS53U-BCNPyMka.js} +1 -1
  768. ksadk/server/static/assets/{journeyDiagram-EYS64GPL-CHcCIB09.js → journeyDiagram-EYS64GPL--cDbKnjP.js} +1 -1
  769. ksadk/server/static/assets/{kanban-definition-3QL26DDD-CJ_ebBZf.js → kanban-definition-3QL26DDD-R1rAVw2f.js} +1 -1
  770. ksadk/server/static/assets/{katex-B4weu383.js → katex-BT-X-V7U.js} +1 -1
  771. ksadk/server/static/assets/{linear-V-MgYHW1.js → linear-DTR631iG.js} +1 -1
  772. ksadk/server/static/assets/{mermaid-parser.core-CBijEX_c.js → mermaid-parser.core-DfFzJhX8.js} +2 -2
  773. ksadk/server/static/assets/{mindmap-definition-FBJOCRG2-Bc-pQvpp.js → mindmap-definition-FBJOCRG2-D4by_rmk.js} +1 -1
  774. ksadk/server/static/assets/packet-AYTQ26CC-Ca4Hvki9.js +1 -0
  775. ksadk/server/static/assets/{pegDiagram-XKGWAZYB-D7genGLl.js → pegDiagram-XKGWAZYB-xWXPEh0c.js} +1 -1
  776. ksadk/server/static/assets/pie-WAS4IAKB-hwiz6fbw.js +1 -0
  777. ksadk/server/static/assets/{pieDiagram-E7YTZNPT-Bxj32zwD.js → pieDiagram-E7YTZNPT-BudBDlCI.js} +1 -1
  778. ksadk/server/static/assets/{quadrantDiagram-AXDQQJYC-BURv2mGh.js → quadrantDiagram-AXDQQJYC-DXZ0PCJF.js} +1 -1
  779. ksadk/server/static/assets/radar-RG4KPBEZ-BR1oZXLM.js +1 -0
  780. ksadk/server/static/assets/railroad-74A4TZTK-DfgEISfX.js +1 -0
  781. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-CWOC3rdk.js +1 -0
  782. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CiRvHCLY.js +1 -0
  783. ksadk/server/static/assets/railroad-peg-WCYAUIDC-DSDDyJ2f.js +1 -0
  784. ksadk/server/static/assets/{railroadDiagram-O6MQD6OU-CCsh3avW.js → railroadDiagram-O6MQD6OU-BdAYRYB8.js} +1 -1
  785. ksadk/server/static/assets/{requirementDiagram-EFPCY7ZU-_FRRBKEd.js → requirementDiagram-EFPCY7ZU-CBBmaL70.js} +1 -1
  786. ksadk/server/static/assets/{sankeyDiagram-P5KCCOFB-C-gr2o9M.js → sankeyDiagram-P5KCCOFB-Doc3rdwp.js} +1 -1
  787. ksadk/server/static/assets/{sequenceDiagram-WJ2MYXX4-D97NmfKg.js → sequenceDiagram-WJ2MYXX4-0J8R8hWW.js} +1 -1
  788. ksadk/server/static/assets/{sizeCapture-X5ZJPWSS-DjrPC5FX.js → sizeCapture-X5ZJPWSS-CfFLlWsK.js} +1 -1
  789. ksadk/server/static/assets/{stateDiagram-HBIQ2CUA-DzMNg1hh.js → stateDiagram-HBIQ2CUA-jBwkjtTy.js} +1 -1
  790. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-Cw1fPb5w.js +1 -0
  791. ksadk/server/static/assets/{swimlanes-XN3QIQJK-Cssf_CsU.js → swimlanes-XN3QIQJK-B0WxTQf8.js} +1 -1
  792. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-Cc3JMBAF.js +8 -0
  793. ksadk/server/static/assets/{timeline-definition-24CTP7MA-BWTHzyS1.js → timeline-definition-24CTP7MA-BfCT_75A.js} +1 -1
  794. ksadk/server/static/assets/treeView-Q6P3EWNA-C05SJHxo.js +1 -0
  795. ksadk/server/static/assets/treemap-WGGIJYW6-BqNLAhc3.js +1 -0
  796. ksadk/server/static/assets/{vennDiagram-4TSXK5OY-BSKgJsmR.js → vennDiagram-4TSXK5OY-9gX1VMDC.js} +1 -1
  797. ksadk/server/static/assets/wardley-WFR3VGLG-DokNktqB.js +1 -0
  798. ksadk/server/static/assets/{wardleyDiagram-VM6X3IG4-eflHNoaZ.js → wardleyDiagram-VM6X3IG4-sEs_BP6O.js} +1 -1
  799. ksadk/server/static/assets/{xychartDiagram-S5SC5T6Z-eYqrQ-v4.js → xychartDiagram-S5SC5T6Z-Ll4jvUN9.js} +1 -1
  800. ksadk/server/static/index.html +2 -2
  801. ksadk/server/terminal_sessions.py +34 -2
  802. ksadk/sessions/__init__.py +8 -1
  803. ksadk/sessions/_checkpoint_resume_audit.py +44 -0
  804. ksadk/sessions/_in_memory_query.py +112 -0
  805. ksadk/sessions/_local_service_sync.py +157 -156
  806. ksadk/sessions/_local_state_sync.py +152 -0
  807. ksadk/sessions/_local_tables.py +2 -0
  808. ksadk/sessions/_postgres_checkpoint_read.py +323 -0
  809. ksadk/sessions/_postgres_checkpoint_stats.py +330 -0
  810. ksadk/sessions/_postgres_history_read.py +678 -0
  811. ksadk/sessions/_postgres_schema.py +44 -2
  812. ksadk/sessions/_postgres_tables.py +2 -0
  813. ksadk/sessions/_postgres_tool_receipts.py +169 -0
  814. ksadk/sessions/_resilient_read.py +630 -0
  815. ksadk/sessions/_tool_receipt_sync.py +119 -0
  816. ksadk/sessions/agent_identity.py +16 -0
  817. ksadk/sessions/base.py +278 -0
  818. ksadk/sessions/db_budget.py +332 -0
  819. ksadk/sessions/errors.py +12 -0
  820. ksadk/sessions/history_budget.py +36 -0
  821. ksadk/sessions/history_identity.py +27 -0
  822. ksadk/sessions/in_memory.py +376 -113
  823. ksadk/sessions/invocation_identity.py +235 -0
  824. ksadk/sessions/local_service.py +345 -0
  825. ksadk/sessions/postgres_service.py +515 -549
  826. ksadk/sessions/resilience.py +21 -14
  827. ksadk/sessions/resilient.py +611 -504
  828. ksadk/skills/__init__.py +26 -0
  829. ksadk/skills/events.py +464 -0
  830. ksadk/skills/loader.py +44 -2
  831. ksadk/skills/manifest_cache.py +263 -0
  832. ksadk/skills/mcp_server/__init__.py +0 -0
  833. ksadk/skills/mcp_server/_registry.py +33 -0
  834. ksadk/skills/mcp_server/_routing.py +26 -0
  835. ksadk/skills/mcp_server/_workspace_files.py +160 -0
  836. ksadk/skills/mcp_server/register.py +415 -0
  837. ksadk/skills/mcp_server/server.py +218 -0
  838. ksadk/skills/models.py +11 -0
  839. ksadk/skills/observability.py +132 -0
  840. ksadk/skills/package_store.py +75 -2
  841. ksadk/skills/runtime/agent.py +38 -5
  842. ksadk/skills/runtime/artifacts.py +87 -0
  843. ksadk/skills/runtime/backends/e2b.py +400 -56
  844. ksadk/skills/runtime/backends/local.py +621 -119
  845. ksadk/skills/runtime/base.py +78 -7
  846. ksadk/skills/runtime/executor.py +266 -1
  847. ksadk/skills/runtime/loader.py +108 -3
  848. ksadk/skills/runtime/request.py +18 -0
  849. ksadk/skills/service_client.py +6 -1
  850. ksadk/skills/tool_defs.py +108 -3
  851. ksadk/studio/a2a_bindings.py +407 -0
  852. ksadk/studio/a2a_orchestration.py +178 -0
  853. ksadk/studio/active_run_output.py +92 -0
  854. ksadk/studio/activity_records.py +542 -0
  855. ksadk/studio/agent_configuration.py +173 -0
  856. ksadk/studio/api.py +784 -419
  857. ksadk/studio/api_a2a_routes.py +93 -0
  858. ksadk/studio/api_catalog_routes.py +13 -0
  859. ksadk/studio/api_cloud_chat_routes.py +382 -0
  860. ksadk/studio/api_contracts.py +31 -2
  861. ksadk/studio/api_plugin_routes.py +39 -12
  862. ksadk/studio/api_resource_connections.py +79 -41
  863. ksadk/studio/attachment_store.py +14 -2
  864. ksadk/studio/authoring.py +12 -6
  865. ksadk/studio/authoring_coordinator.py +4 -2
  866. ksadk/studio/build_resolution.py +117 -0
  867. ksadk/studio/build_singleflight.py +37 -0
  868. ksadk/studio/builder.py +32 -2
  869. ksadk/studio/capabilities.py +31 -9
  870. ksadk/studio/channel_connections.py +333 -0
  871. ksadk/studio/cloud.py +113 -387
  872. ksadk/studio/cloud_gateways.py +424 -0
  873. ksadk/studio/cloud_shared_web.py +82 -2
  874. ksadk/studio/codex_agent_service.py +19 -2
  875. ksadk/studio/codex_builder.py +1 -1
  876. ksadk/studio/codex_manifest.py +5 -1
  877. ksadk/studio/codex_provider_build.py +65 -0
  878. ksadk/studio/codex_run.py +6 -1
  879. ksadk/studio/compiler.py +112 -6
  880. ksadk/studio/configuration.py +182 -0
  881. ksadk/studio/contract_base.py +19 -0
  882. ksadk/studio/contracts.py +260 -202
  883. ksadk/studio/dsh_application.py +183 -27
  884. ksadk/studio/dsh_capability_service.py +302 -189
  885. ksadk/studio/dsh_lifecycle.py +58 -0
  886. ksadk/studio/dsh_mcp_transport.py +191 -0
  887. ksadk/studio/dsh_provider_registration.py +55 -16
  888. ksadk/studio/entry.py +53 -0
  889. ksadk/studio/event_store.py +53 -5
  890. ksadk/studio/execution_host.py +598 -0
  891. ksadk/studio/framework_run.py +50 -10
  892. ksadk/studio/hosted_kernel.py +1 -1
  893. ksadk/studio/interaction_submission.py +265 -0
  894. ksadk/studio/kernel_registry.py +429 -0
  895. ksadk/studio/kernel_sqlite_migration.py +181 -0
  896. ksadk/studio/lifecycle_status.py +43 -0
  897. ksadk/studio/model_buffered_wire.py +227 -0
  898. ksadk/studio/model_chat_tools.py +143 -0
  899. ksadk/studio/model_client.py +1939 -345
  900. ksadk/studio/model_content_stream.py +129 -0
  901. ksadk/studio/model_credentials.py +196 -0
  902. ksadk/studio/model_profile_service.py +89 -40
  903. ksadk/studio/model_request_projection.py +284 -0
  904. ksadk/studio/model_responses_content.py +296 -0
  905. ksadk/studio/model_usage.py +109 -0
  906. ksadk/studio/model_wire_failure.py +110 -0
  907. ksadk/studio/model_wire_identity.py +138 -0
  908. ksadk/studio/model_wire_snapshot.py +45 -0
  909. ksadk/studio/model_wire_tools.py +283 -0
  910. ksadk/studio/native_acceptance.py +68 -0
  911. ksadk/studio/native_approval.py +111 -0
  912. ksadk/studio/native_browser_port.py +861 -0
  913. ksadk/studio/native_child_mcp.py +87 -0
  914. ksadk/studio/native_child_publication.py +68 -0
  915. ksadk/studio/native_child_skills.py +72 -0
  916. ksadk/studio/native_child_workspace.py +176 -0
  917. ksadk/studio/native_command_policy.py +53 -0
  918. ksadk/studio/native_context_port.py +326 -0
  919. ksadk/studio/native_context_registration.py +54 -0
  920. ksadk/studio/native_document_input.py +88 -0
  921. ksadk/studio/native_documents.py +172 -0
  922. ksadk/studio/native_final_shape.py +852 -0
  923. ksadk/studio/native_gateway_stream.py +69 -0
  924. ksadk/studio/native_hook_port.py +145 -0
  925. ksadk/studio/native_hook_registration.py +70 -0
  926. ksadk/studio/native_inventory_disclosure.py +223 -0
  927. ksadk/studio/native_kernel_admission.py +84 -0
  928. ksadk/studio/native_language_diagnostics.py +320 -0
  929. ksadk/studio/native_mcp_disclosure.py +198 -0
  930. ksadk/studio/native_mcp_tool_port.py +219 -0
  931. ksadk/studio/native_memory_acceptance.py +362 -0
  932. ksadk/studio/native_memory_context.py +266 -0
  933. ksadk/studio/native_memory_flush.py +174 -0
  934. ksadk/studio/native_memory_sources.py +80 -0
  935. ksadk/studio/native_memory_tool_port.py +360 -0
  936. ksadk/studio/native_memory_turn.py +183 -0
  937. ksadk/studio/native_model_port.py +765 -0
  938. ksadk/studio/native_model_recovery.py +126 -0
  939. ksadk/studio/native_options.py +75 -0
  940. ksadk/studio/native_output_fields.py +58 -0
  941. ksadk/studio/native_planning.py +156 -0
  942. ksadk/studio/native_product.py +988 -0
  943. ksadk/studio/native_read_budget.py +426 -0
  944. ksadk/studio/native_recovery.py +171 -0
  945. ksadk/studio/native_scope.py +35 -0
  946. ksadk/studio/native_session_context_port.py +141 -0
  947. ksadk/studio/native_session_engine.py +14 -0
  948. ksadk/studio/native_skill_source.py +74 -0
  949. ksadk/studio/native_subagent_port.py +700 -0
  950. ksadk/studio/native_terminal_recovery.py +95 -0
  951. ksadk/studio/native_test_action.py +610 -0
  952. ksadk/studio/native_tool_port.py +477 -0
  953. ksadk/studio/native_user_input.py +60 -0
  954. ksadk/studio/native_verification_presentation.py +65 -0
  955. ksadk/studio/native_workspace_diagnostics.py +183 -0
  956. ksadk/studio/native_workspace_port.py +43 -0
  957. ksadk/studio/operations.py +34 -9
  958. ksadk/studio/plugin_composition.py +39 -12
  959. ksadk/studio/plugin_kernel_adapter.py +89 -0
  960. ksadk/studio/plugin_lifecycle.py +80 -0
  961. ksadk/studio/plugin_native_activation.py +163 -0
  962. ksadk/studio/plugin_runtime.py +832 -99
  963. ksadk/studio/plugin_runtime_admission.py +57 -0
  964. ksadk/studio/presentation.py +92 -0
  965. ksadk/studio/provider_recovery.py +59 -0
  966. ksadk/studio/public_output_store.py +69 -0
  967. ksadk/studio/recovery_page.py +70 -0
  968. ksadk/studio/resource_authority.py +69 -34
  969. ksadk/studio/resource_catalog.py +14 -12
  970. ksadk/studio/run_activity.py +848 -0
  971. ksadk/studio/run_controls.py +185 -0
  972. ksadk/studio/run_documents.py +127 -0
  973. ksadk/studio/run_service.py +1220 -335
  974. ksadk/studio/run_submission_receipts.py +68 -0
  975. ksadk/studio/runtime_catalog.py +73 -11
  976. ksadk/studio/runtime_prewarm.py +22 -0
  977. ksadk/studio/runtime_source.py +1 -1
  978. ksadk/studio/scheduler_assistant.py +4 -1
  979. ksadk/studio/scheduler_runtime.py +32 -289
  980. ksadk/studio/service.py +615 -246
  981. ksadk/studio/service_settings.py +229 -0
  982. ksadk/studio/shared_web.py +518 -181
  983. ksadk/studio/shared_web_response_events.py +180 -0
  984. ksadk/studio/shared_web_run_state.py +133 -0
  985. ksadk/studio/skill_discovery.py +305 -3
  986. ksadk/studio/static/assets/ChannelsPage-xX5HlMId.js +8 -0
  987. ksadk/studio/static/assets/CodeBlock-DMIf4h33-CkufcPYo.js +44 -0
  988. ksadk/studio/static/assets/ExecutionTree-v3HexvPo-Bk8wzGSy.js +1 -0
  989. ksadk/studio/static/assets/MathMessageMarkdown-BesSeCYl-DYG1Wzbx.js +8 -0
  990. ksadk/studio/static/assets/MermaidBlock-H_0qzQDf-BdXtxug_.js +11 -0
  991. ksadk/studio/static/assets/MessageMarkdown-CwMTvgFj-BeyLyQHu.js +59 -0
  992. ksadk/studio/static/assets/PrismRenderer-5pK6tUdb.js +3 -0
  993. ksadk/studio/static/assets/TeamsPage-CCaaGaVp.js +3 -0
  994. ksadk/studio/static/assets/abnfDiagram-VCTEODGH-Bnb1O3tA.js +1 -0
  995. ksadk/studio/static/assets/architecture-7GRP2DOG-BMOPAkUC.js +1 -0
  996. ksadk/studio/static/assets/architectureDiagram-5GKGNRK7-5Zn1gqJv.js +36 -0
  997. ksadk/studio/static/assets/{blockDiagram-I7D4REHJ-CFmPl1de.js → blockDiagram-I7D4REHJ-CLB4L11u.js} +6 -6
  998. ksadk/studio/static/assets/{c4Diagram-7LVT6UL2-BRrGu8qt.js → c4Diagram-7LVT6UL2-BOma_Z_F.js} +7 -7
  999. ksadk/studio/static/assets/channel-r8itPEik.js +1 -0
  1000. ksadk/studio/static/assets/chunk-2E4U76K2-BFf_huql.js +2 -0
  1001. ksadk/studio/static/assets/chunk-2ZTRR5NV-C2qZJila.js +2 -0
  1002. ksadk/studio/static/assets/chunk-3Z5EZCMW-DMniOlPd.js +1 -0
  1003. ksadk/studio/static/assets/{chunk-4HAMMTFA-D220vUtq.js → chunk-4HAMMTFA-DVtDP4rm.js} +7 -7
  1004. ksadk/studio/static/assets/chunk-5V3GS4D5-i-ZDOEnP.js +1 -0
  1005. ksadk/studio/static/assets/chunk-6AZGARVD-BB6FqMl-.js +1 -0
  1006. ksadk/studio/static/assets/chunk-6EIED4P4-DdUJkP0f.js +1 -0
  1007. ksadk/studio/static/assets/chunk-6K3QC6MW-CqVsVn-y.js +1 -0
  1008. ksadk/studio/static/assets/chunk-6TQVIW2G-DKlLbNIP.js +1 -0
  1009. ksadk/studio/static/assets/chunk-747NJXEK-BFkZPlab.js +2 -0
  1010. ksadk/studio/static/assets/chunk-75Z2AOVW-BZNhM4vB.js +2 -0
  1011. ksadk/studio/static/assets/chunk-CLGD4ZFX-D-GTtyKs.js +1 -0
  1012. ksadk/studio/static/assets/{chunk-DU6HZSFF-C4DtVpCu.js → chunk-DU6HZSFF-CpEZCUgg.js} +1 -1
  1013. ksadk/studio/static/assets/chunk-F27PBJKO-D7pvKf0C.js +1 -0
  1014. ksadk/studio/static/assets/{chunk-GMAD6QVW-DiQA-u2N.js → chunk-GMAD6QVW-DE_8H717.js} +6 -6
  1015. ksadk/studio/static/assets/chunk-GVQU2GXP-BfEB0lNv.js +1 -0
  1016. ksadk/studio/static/assets/chunk-I5DQTOEV-Hm2LfiYI.js +1 -0
  1017. ksadk/studio/static/assets/chunk-ICYGCRZG-749o-pGJ.js +1 -0
  1018. ksadk/studio/static/assets/chunk-IH6LHLGP-CC6I9F3i.js +1 -0
  1019. ksadk/studio/static/assets/{chunk-IMKFNOWR-BSlbjT_b.js → chunk-IMKFNOWR-D38axYOR.js} +5 -5
  1020. ksadk/studio/static/assets/chunk-KI3K4JFJ-Rma-0k_M.js +1 -0
  1021. ksadk/studio/static/assets/chunk-L3NEJ4N5-Dq5xn7aL.js +1 -0
  1022. ksadk/studio/static/assets/chunk-LNGE3PJU-DWpGwHrW.js +34 -0
  1023. ksadk/studio/static/assets/{chunk-OSK3NFVY-M2m6G3j4.js → chunk-OSK3NFVY-DiCQjx9b.js} +5 -5
  1024. ksadk/studio/static/assets/chunk-OUJLGHUK-D0j2JRSg.js +2 -0
  1025. ksadk/studio/static/assets/{chunk-P2QGCYS3-DezaQxiK.js → chunk-P2QGCYS3-VdhKmfYq.js} +1 -1
  1026. ksadk/studio/static/assets/chunk-POPQ4Y6H-C1_rzO5M.js +1 -0
  1027. ksadk/studio/static/assets/chunk-PWAF6VOD-B45HvflI.js +1 -0
  1028. ksadk/studio/static/assets/{chunk-SHT3W25Y-DAyIVp5H.js → chunk-SHT3W25Y-DsCFnRVE.js} +9 -9
  1029. ksadk/studio/static/assets/{chunk-SVP7TREG-DulomOrS.js → chunk-SVP7TREG-DljApgRX.js} +1 -1
  1030. ksadk/studio/static/assets/{chunk-TICWLB2K-bHJYoflj.js → chunk-TICWLB2K-DWqRtyqa.js} +6 -6
  1031. ksadk/studio/static/assets/chunk-TLUHSLCS-DUeGgnD5.js +2 -0
  1032. ksadk/studio/static/assets/chunk-UY3FDG6J-CnmaAkG5.js +1 -0
  1033. ksadk/studio/static/assets/chunk-XHIXRSVI-BFQ6B8BE.js +1 -0
  1034. ksadk/studio/static/assets/chunk-XXDRQBXY-DqAYeljN.js +1 -0
  1035. ksadk/studio/static/assets/classDiagram-ZZMXUADV-DjeMNJmu.js +1 -0
  1036. ksadk/studio/static/assets/classDiagram-v2-VYDZK3BY-DjeMNJmu.js +1 -0
  1037. ksadk/studio/static/assets/clsx-DB0hHKMi.js +1 -0
  1038. ksadk/studio/static/assets/cose-bilkent-JH36ORCC-om8AY0Mc.js +1 -0
  1039. ksadk/studio/static/assets/createLucideIcon-DR0LGTP5.js +1 -0
  1040. ksadk/studio/static/assets/cynefin-OW5HDTMX-7tP7QfM3.js +1 -0
  1041. ksadk/studio/static/assets/cynefinDiagram-5FMLGOSQ-6BrmK52S.js +62 -0
  1042. ksadk/studio/static/assets/dagre-GXQ25YYZ-buZBRlwz.js +4 -0
  1043. ksadk/studio/static/assets/{diagram-S7CK7UJ4-0BZ98mya.js → diagram-S7CK7UJ4-C6aH78c4.js} +3 -3
  1044. ksadk/studio/static/assets/diagram-UQ7AKVKN-DY_tQ6C_.js +41 -0
  1045. ksadk/studio/static/assets/diagram-VSXAHHWV-DX9p5cQT.js +3 -0
  1046. ksadk/studio/static/assets/diagram-VX7I27RA-DB2WSH8X.js +24 -0
  1047. ksadk/studio/static/assets/diagram-Z3DM3KII-Ce1BJwHE.js +24 -0
  1048. ksadk/studio/static/assets/ebnfDiagram-PWID7BFC-Daukxwqf.js +1 -0
  1049. ksadk/studio/static/assets/{erDiagram-RLTQ6QDP-DLGry2Ap.js → erDiagram-RLTQ6QDP-CGthTMmJ.js} +6 -6
  1050. ksadk/studio/static/assets/eventmodeling-NTZA5JFV-BR2FzK4R.js +1 -0
  1051. ksadk/studio/static/assets/flowDiagram-HODETNUW-CZ1tEenp.js +1 -0
  1052. ksadk/studio/static/assets/ganttDiagram-EL5Y4UJY-BfoAZW9X.js +292 -0
  1053. ksadk/studio/static/assets/gitGraph-4MIJSDKK-DYlXv_yJ.js +1 -0
  1054. ksadk/studio/static/assets/gitGraphDiagram-WWUBYQGX-S985OIJF.js +106 -0
  1055. ksadk/studio/static/assets/iconfont-4HN3C9Pg.woff2 +0 -0
  1056. ksadk/studio/static/assets/index-C7DNDik2.css +2 -0
  1057. ksadk/studio/static/assets/index-Cy68gTlH.js +265 -0
  1058. ksadk/studio/static/assets/index.dom-BQenBtFm.js +1 -0
  1059. ksadk/studio/static/assets/info-A6RAGUB7-BlU-8_xu.js +1 -0
  1060. ksadk/studio/static/assets/infoDiagram-27XIBGKW-CO3W8tFn.js +2 -0
  1061. ksadk/studio/static/assets/{ishikawaDiagram-5VMMS53U-CljI2wyf.js → ishikawaDiagram-5VMMS53U-BUGEl8cO.js} +2 -2
  1062. ksadk/studio/static/assets/{journeyDiagram-3NMN7TZE-D50Fo2sL.js → journeyDiagram-3NMN7TZE-Ch2ZYxvr.js} +6 -6
  1063. ksadk/studio/static/assets/{kanban-definition-UXKFOSKX-DimfOTam.js → kanban-definition-UXKFOSKX-Da-Fp4vC.js} +3 -3
  1064. ksadk/studio/static/assets/kingsoft-cloud-BOcLiQ3e.ico +0 -0
  1065. ksadk/studio/static/assets/{line-CX5BOOUP.js → line-BDaeq300.js} +1 -1
  1066. ksadk/studio/static/assets/linear-DmucvK78.js +1 -0
  1067. ksadk/studio/static/assets/mermaid-parser.core-Cj5mYdBi.js +4 -0
  1068. ksadk/studio/static/assets/{mindmap-definition-YA3MSWOX-Bizzqy5R.js → mindmap-definition-YA3MSWOX-CvlsVQ38.js} +1 -1
  1069. ksadk/studio/static/assets/packet-AYTQ26CC-BSJEH9Lb.js +1 -0
  1070. ksadk/studio/static/assets/pegDiagram-XKGWAZYB-BuHuNgT5.js +1 -0
  1071. ksadk/studio/static/assets/pie-WAS4IAKB-qxv01Y8W.js +1 -0
  1072. ksadk/studio/static/assets/{pieDiagram-E7YTZNPT-B9mrFSDF.js → pieDiagram-E7YTZNPT-DlrWiSOv.js} +1 -1
  1073. ksadk/studio/static/assets/preload-helper-CO68xz6u.js +1 -0
  1074. ksadk/studio/static/assets/presentation-yYw7xK60-BmcZriri.js +1 -0
  1075. ksadk/studio/static/assets/{quadrantDiagram-AXDQQJYC-NzUfCbMR.js → quadrantDiagram-AXDQQJYC-BA3hpUZM.js} +6 -6
  1076. ksadk/studio/static/assets/radar-RG4KPBEZ-B6h6pWOs.js +1 -0
  1077. ksadk/studio/static/assets/railroad-74A4TZTK-CGtTWdGc.js +1 -0
  1078. ksadk/studio/static/assets/railroad-abnf-HS5TGJTU-Cq7y22IA.js +1 -0
  1079. ksadk/studio/static/assets/railroad-ebnf-LZEXJU2U-V0aQVv9m.js +1 -0
  1080. ksadk/studio/static/assets/railroad-peg-WCYAUIDC-BQiIsX_p.js +1 -0
  1081. ksadk/studio/static/assets/railroadDiagram-O6MQD6OU-BxpAazym.js +1 -0
  1082. ksadk/studio/static/assets/{requirementDiagram-BXWQKSXE-BF3d103U.js → requirementDiagram-BXWQKSXE-E-Ge0FcV.js} +1 -1
  1083. ksadk/studio/static/assets/sankeyDiagram-P5KCCOFB-CA6YKhQ3.js +40 -0
  1084. ksadk/studio/static/assets/{sequenceDiagram-WJ2MYXX4-Dh4wZtUm.js → sequenceDiagram-WJ2MYXX4-CcElXGg4.js} +9 -9
  1085. ksadk/studio/static/assets/src-B6xuSHsQ.js +1 -0
  1086. ksadk/studio/static/assets/stateDiagram-D77RDMKH-DdXZOFqY.js +1 -0
  1087. ksadk/studio/static/assets/stateDiagram-v2-MP3YSRHH-CJbOM-QK.js +1 -0
  1088. ksadk/studio/static/assets/swimlanes-42K2YHIH-NtgEEHLY.js +1 -0
  1089. ksadk/studio/static/assets/swimlanesDiagram-VR7AAH4N-D-AVlW8B.js +8 -0
  1090. ksadk/studio/static/assets/{timeline-definition-24CTP7MA-DxmSSjRB.js → timeline-definition-24CTP7MA-CIuYhNy3.js} +7 -7
  1091. ksadk/studio/static/assets/treeView-Q6P3EWNA-BTTYcvrF.js +1 -0
  1092. ksadk/studio/static/assets/treemap-WGGIJYW6-Cg7tyfH8.js +1 -0
  1093. ksadk/studio/static/assets/vennDiagram-4TSXK5OY-7HbApbrv.js +34 -0
  1094. ksadk/studio/static/assets/wardley-WFR3VGLG-C6N1rULY.js +1 -0
  1095. ksadk/studio/static/assets/{wardleyDiagram-VM6X3IG4-Ba1AOWoe.js → wardleyDiagram-VM6X3IG4-CMJiaedy.js} +1 -1
  1096. ksadk/studio/static/assets/{xychartDiagram-S5SC5T6Z-Cr5pt5Y-.js → xychartDiagram-S5SC5T6Z-BdgtMvW2.js} +6 -6
  1097. ksadk/studio/static/index.html +8 -2
  1098. ksadk/studio/teams_catalog.py +166 -0
  1099. ksadk/studio/teams_installation.py +217 -0
  1100. ksadk/studio/teams_node.py +549 -0
  1101. ksadk/studio/teams_node_factory.py +558 -0
  1102. ksadk/studio/teams_node_runner.py +72 -0
  1103. ksadk/studio/teams_node_v1.py +935 -0
  1104. ksadk/studio/teams_remote.py +496 -0
  1105. ksadk/studio/templates.py +41 -19
  1106. ksadk/studio/validator.py +13 -0
  1107. ksadk/studio/workspace.py +19 -4
  1108. ksadk/studio/workspace_plugins.py +138 -0
  1109. ksadk/studio/workspace_registry.py +286 -0
  1110. ksadk/tools/a2a_policy.py +5 -0
  1111. ksadk/tools/artifact_observation.py +53 -0
  1112. ksadk/tools/captured_source_artifact.py +68 -0
  1113. ksadk/tools/schema.py +34 -0
  1114. ksadk/toolsets/__init__.py +93 -88
  1115. ksadk/toolsets/a2a.py +89 -89
  1116. ksadk/toolsets/sandbox.py +377 -6
  1117. ksadk/toolsets/skill_management.py +315 -0
  1118. ksadk/toolsets/skills.py +446 -378
  1119. ksadk/toolsets/tool_selection.py +94 -0
  1120. ksadk/toolsets/workspace.py +344 -326
  1121. ksadk/toolsets/workspace_editing.py +291 -0
  1122. ksadk/toolsets/workspace_identity.py +15 -0
  1123. ksadk/toolsets/workspace_model_observation.py +104 -0
  1124. ksadk/toolsets/workspace_mutation.py +75 -0
  1125. ksadk/toolsets/workspace_read_budget.py +216 -0
  1126. ksadk/toolsets/workspace_search.py +192 -0
  1127. ksadk/toolsets/workspace_search_process.py +129 -0
  1128. ksadk/toolsets/workspace_state.py +6 -0
  1129. ksadk/version.py +1 -1
  1130. ksadk_runtime_common/http_headers.py +18 -0
  1131. ksadk_runtime_common/memory_backend/providers/lancedb.py +56 -2
  1132. ksadk_runtime_common/schemas/memory_backend_manifest.schema.json +148 -148
  1133. ksadk_runtime_common/workspace_files/router.py +28 -14
  1134. agentengine_sdk_python-0.8.4.dist-info/RECORD +0 -1070
  1135. ksadk/server/static/assets/MathMessageMarkdown-DbdW5A4_.js +0 -8
  1136. ksadk/server/static/assets/architecture-7GRP2DOG-BW5UtSnx.js +0 -1
  1137. ksadk/server/static/assets/channel-CF5Up3hQ.js +0 -1
  1138. ksadk/server/static/assets/chunk-2Q5K7J3B-KkXX47WI.js +0 -1
  1139. ksadk/server/static/assets/classDiagram-DTDB5LWJ-Cz81acRQ.js +0 -1
  1140. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Cz81acRQ.js +0 -1
  1141. ksadk/server/static/assets/cynefin-OW5HDTMX-DVWs8a2t.js +0 -1
  1142. ksadk/server/static/assets/dist-B2_CVgKv.js +0 -1
  1143. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CQqR2hg3.js +0 -1
  1144. ksadk/server/static/assets/flowDiagram-A5DVABFB-DDRRbGPm.js +0 -1
  1145. ksadk/server/static/assets/gitGraph-4MIJSDKK-fgWEf7UD.js +0 -1
  1146. ksadk/server/static/assets/index-DcTML1Up.css +0 -2
  1147. ksadk/server/static/assets/index-Dh2XHqPK.js +0 -244
  1148. ksadk/server/static/assets/info-A6RAGUB7-BUecLTdt.js +0 -1
  1149. ksadk/server/static/assets/packet-AYTQ26CC-BeW-btFv.js +0 -1
  1150. ksadk/server/static/assets/pie-WAS4IAKB-X0xh4SaT.js +0 -1
  1151. ksadk/server/static/assets/radar-RG4KPBEZ-DzRMSqjd.js +0 -1
  1152. ksadk/server/static/assets/railroad-74A4TZTK-Cc44icX-.js +0 -1
  1153. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-D6S4BdV-.js +0 -1
  1154. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-vD3aVE2o.js +0 -1
  1155. ksadk/server/static/assets/railroad-peg-WCYAUIDC-CYbW_b4T.js +0 -1
  1156. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BIcjKFhF.js +0 -1
  1157. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-tq3K3Ybl.js +0 -8
  1158. ksadk/server/static/assets/treeView-Q6P3EWNA-DBXt3GdU.js +0 -1
  1159. ksadk/server/static/assets/treemap-WGGIJYW6-DrCy4E_l.js +0 -1
  1160. ksadk/server/static/assets/wardley-WFR3VGLG-CefhOliM.js +0 -1
  1161. ksadk/studio/static/assets/CodeBlock-DCUUY0PL-BLjA7kbS.js +0 -41
  1162. ksadk/studio/static/assets/MathMessageMarkdown-CBcDrAw--CUYIXwgI.js +0 -8
  1163. ksadk/studio/static/assets/MermaidBlock-DBFTztfQ-BTYKWEbD.js +0 -44
  1164. ksadk/studio/static/assets/PrismRenderer-wiXRHURw.js +0 -3
  1165. ksadk/studio/static/assets/abnfDiagram-VCTEODGH-CLETGjKq.js +0 -1
  1166. ksadk/studio/static/assets/architecture-7GRP2DOG-hXMiO_AU.js +0 -1
  1167. ksadk/studio/static/assets/architectureDiagram-5GKGNRK7-B5EQysk6.js +0 -36
  1168. ksadk/studio/static/assets/channel-BqO-gtEw.js +0 -1
  1169. ksadk/studio/static/assets/chunk-75Z2AOVW-C8lLv_qc.js +0 -2
  1170. ksadk/studio/static/assets/chunk-F27PBJKO-Cpw14SNG.js +0 -1
  1171. ksadk/studio/static/assets/chunk-GVQU2GXP-DUdNBUCi.js +0 -1
  1172. ksadk/studio/static/assets/chunk-L3NEJ4N5-Db8Ab22W.js +0 -1
  1173. ksadk/studio/static/assets/chunk-POPQ4Y6H-DOpufJ3t.js +0 -1
  1174. ksadk/studio/static/assets/chunk-PWAF6VOD-CfLb1gSc.js +0 -1
  1175. ksadk/studio/static/assets/chunk-XXDRQBXY-7PcdR2B1.js +0 -1
  1176. ksadk/studio/static/assets/classDiagram-ZZMXUADV-D_bW8JjA.js +0 -1
  1177. ksadk/studio/static/assets/classDiagram-v2-VYDZK3BY-D_bW8JjA.js +0 -1
  1178. ksadk/studio/static/assets/cose-bilkent-JH36ORCC-snL88wkX.js +0 -1
  1179. ksadk/studio/static/assets/cynefin-OW5HDTMX-Bd1YhRo1.js +0 -1
  1180. ksadk/studio/static/assets/cynefinDiagram-5FMLGOSQ-BBrIG_mI.js +0 -62
  1181. ksadk/studio/static/assets/dagre-GXQ25YYZ-DtF1nGdY.js +0 -4
  1182. ksadk/studio/static/assets/diagram-UQ7AKVKN-6mLKEo6L.js +0 -41
  1183. ksadk/studio/static/assets/diagram-VSXAHHWV-CJBIaiZ2.js +0 -3
  1184. ksadk/studio/static/assets/diagram-VX7I27RA-BohLEE1C.js +0 -24
  1185. ksadk/studio/static/assets/diagram-Z3DM3KII-D8KIGonG.js +0 -24
  1186. ksadk/studio/static/assets/ebnfDiagram-PWID7BFC-C99HU61t.js +0 -1
  1187. ksadk/studio/static/assets/eventmodeling-NTZA5JFV-B1_5Ac8j.js +0 -1
  1188. ksadk/studio/static/assets/flowDiagram-HODETNUW-DD5hmicn.js +0 -1
  1189. ksadk/studio/static/assets/ganttDiagram-EL5Y4UJY-BPJrVLSG.js +0 -292
  1190. ksadk/studio/static/assets/gitGraph-4MIJSDKK-Cy_ZQBGi.js +0 -1
  1191. ksadk/studio/static/assets/gitGraphDiagram-WWUBYQGX-CZCUz4MT.js +0 -106
  1192. ksadk/studio/static/assets/index-CeHL9ARV.css +0 -2
  1193. ksadk/studio/static/assets/index-D9QEcVpL.js +0 -320
  1194. ksadk/studio/static/assets/info-A6RAGUB7-DBzDQB_I.js +0 -1
  1195. ksadk/studio/static/assets/infoDiagram-27XIBGKW-B-sq07Hi.js +0 -2
  1196. ksadk/studio/static/assets/linear-BCxN9JLv.js +0 -1
  1197. ksadk/studio/static/assets/mermaid-parser.core-BdCz8fkS.js +0 -7
  1198. ksadk/studio/static/assets/packet-AYTQ26CC-gPln-_kG.js +0 -1
  1199. ksadk/studio/static/assets/pegDiagram-XKGWAZYB-Dy3fB-gW.js +0 -1
  1200. ksadk/studio/static/assets/pie-WAS4IAKB-qZdHMr7B.js +0 -1
  1201. ksadk/studio/static/assets/radar-RG4KPBEZ-BZlnaea9.js +0 -1
  1202. ksadk/studio/static/assets/railroad-74A4TZTK-Ce3__H9c.js +0 -1
  1203. ksadk/studio/static/assets/railroad-abnf-HS5TGJTU-DqQn7EzD.js +0 -1
  1204. ksadk/studio/static/assets/railroad-ebnf-LZEXJU2U-B55kCA8F.js +0 -1
  1205. ksadk/studio/static/assets/railroad-peg-WCYAUIDC-DfQnvkHd.js +0 -1
  1206. ksadk/studio/static/assets/railroadDiagram-O6MQD6OU-DnhMjZCk.js +0 -1
  1207. ksadk/studio/static/assets/sankeyDiagram-P5KCCOFB-Bo-JTwt0.js +0 -40
  1208. ksadk/studio/static/assets/src-SFn9St9H.js +0 -1
  1209. ksadk/studio/static/assets/stateDiagram-D77RDMKH-DH774G6d.js +0 -1
  1210. ksadk/studio/static/assets/stateDiagram-v2-MP3YSRHH-DZvVWPBo.js +0 -1
  1211. ksadk/studio/static/assets/swimlanes-42K2YHIH-Bl13OvTX.js +0 -1
  1212. ksadk/studio/static/assets/swimlanesDiagram-VR7AAH4N-BnFOTALJ.js +0 -8
  1213. ksadk/studio/static/assets/treeView-Q6P3EWNA-D5Nn9Swz.js +0 -1
  1214. ksadk/studio/static/assets/treemap-WGGIJYW6-CgHgnQhE.js +0 -1
  1215. ksadk/studio/static/assets/vennDiagram-4TSXK5OY-B9QEXcJq.js +0 -34
  1216. ksadk/studio/static/assets/wardley-WFR3VGLG-DXrK2RRY.js +0 -1
  1217. {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/WHEEL +0 -0
  1218. {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/entry_points.txt +0 -0
  1219. {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/licenses/LICENSE +0 -0
  1220. {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/top_level.txt +0 -0
  1221. /ksadk/studio/static/assets/{MathMessageMarkdown-CBcDrAw--BEOyPC3w.css → MathMessageMarkdown-BesSeCYl-BEOyPC3w.css} +0 -0
@@ -3,18 +3,54 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import hashlib
7
+ import html
6
8
  import ipaddress
7
- import os
9
+ import json
10
+ import logging
11
+ import re
8
12
  import socket
9
- from dataclasses import dataclass, field
13
+ import time
14
+ from collections.abc import AsyncIterator
15
+ from contextlib import asynccontextmanager
16
+ from dataclasses import dataclass, field, replace
10
17
  from threading import RLock
11
- from typing import Any, Awaitable, Callable
18
+ from typing import Any, Awaitable, Callable, Literal
12
19
  from urllib.parse import urlparse
13
20
 
14
21
  import httpx
15
22
 
16
- from ksadk.studio.contracts import NetworkPolicy, ResolvedModel, Usage
23
+ from ksadk._async_iterators import owning_async_context, owning_async_iterator
24
+ from ksadk._model_input import ProviderInputObservation, input_observation
25
+ from ksadk._model_json import ModelJSONPolicyError, load_model_json
26
+ from ksadk._model_sse import ModelSSEIdleTimeout, SSEFrame, SSEFramingError, model_sse_frames
27
+ from ksadk.model_proxy import compat as provider_compat
28
+ from ksadk.studio.contracts import ANTHROPIC_VERSION, NetworkPolicy, ResolvedModel, Usage
17
29
  from ksadk.studio.errors import StudioError
30
+ from ksadk.studio.model_buffered_wire import (
31
+ buffered_projection,
32
+ validate_buffered_identity,
33
+ wire_refusal,
34
+ )
35
+ from ksadk.studio.model_chat_tools import ChatToolCalls
36
+ from ksadk.studio.model_content_stream import AnthropicContent, ChatContent
37
+ from ksadk.studio.model_request_projection import anthropic_messages, responses_messages
38
+ from ksadk.studio.model_responses_content import ResponsesContent
39
+ from ksadk.studio.model_usage import AnthropicUsageAccumulator, provider_usage
40
+ from ksadk.studio.model_wire_failure import ModelResponseError, provider_error
41
+ from ksadk.studio.model_wire_identity import (
42
+ ModelWireIdentity,
43
+ chat_choices,
44
+ decode_model_frame,
45
+ invalid_model_frame,
46
+ )
47
+ from ksadk.studio.model_wire_snapshot import reconcile_responses_snapshot
48
+ from ksadk.studio.model_wire_tools import AnthropicToolBlocks, ResponsesToolCalls
49
+
50
+ from .model_credentials import CredentialResolver as CredentialResolver
51
+
52
+ #: Anthropic Messages wire 把 max_tokens 设为必填;profile 未配置时的安全默认。
53
+ _ANTHROPIC_DEFAULT_MAX_TOKENS = 8192
18
54
 
19
55
  _DENIED_METADATA_HOSTS = {
20
56
  "169.254.169.254",
@@ -28,6 +64,132 @@ _LENGTH_FINISH_REASONS = {"length", "incomplete", "max_output_tokens"}
28
64
  #: length 截断重试时把 max_tokens 提到的目标值;未配置 max_tokens 的 profile
29
65
  #: 首次截断重试也用该值兜底(authoring 输出完整 JSON 需要宽松上限)。
30
66
  _LENGTH_RETRY_TARGET_TOKENS = 16384
67
+ # These explicit terminal reasons cannot authorize any partially assembled tool.
68
+ # Preserve them even when the provider ended before emitting its arguments.
69
+ _INCOMPLETE_CALL_NON_SUCCESS_FINISH_REASONS = {
70
+ "length", "incomplete", "max_tokens", "max_output_tokens", "output_limit",
71
+ "context_overflow", "context_length_exceeded", "policy_abort", "content_filter",
72
+ "user_abort", "canceled", "cancelled",
73
+ }
74
+ _REPETITION_MIN_UNIT_CHARS = 24
75
+ _REPETITION_MAX_UNIT_CHARS = 512
76
+ _REPETITION_COUNT = 4
77
+
78
+ _LOGGER = logging.getLogger(__name__)
79
+
80
+
81
+ async def _http_model_error(response: httpx.Response, *, wire_api: str) -> StudioError:
82
+ if response.is_redirect:
83
+ return StudioError("NETWORK_TARGET_DENIED", "模型 endpoint 不允许重定向", status_code=403,
84
+ details={"statusCode": response.status_code})
85
+ payload = {}
86
+ usage = None
87
+ try:
88
+ raw = await response.aread()
89
+ if len(raw) <= 16 * 1024 * 1024:
90
+ payload = decode_model_frame(raw)
91
+ validate_buffered_identity(payload, wire_api=wire_api)
92
+ usage = provider_usage(payload.get("usage"), wire_api=wire_api)
93
+ except (StudioError, httpx.HTTPError):
94
+ # An unreadable error body cannot erase the actual HTTP failure fact.
95
+ payload, usage = {}, None
96
+ failure = provider_error(
97
+ payload, wire_api=wire_api, status=response.status_code, usage=usage,
98
+ headers=response.headers, provider_request_id=_provider_request_id(response.headers),
99
+ )
100
+ assert failure is not None
101
+ return failure
102
+
103
+
104
+ @asynccontextmanager
105
+ async def _model_frames(
106
+ response: httpx.Response, *, idle_timeout_seconds: float,
107
+ known_policy_abort: Callable[[], bool] | None = None,
108
+ ) -> AsyncIterator[AsyncIterator[SSEFrame]]:
109
+ try:
110
+ async with model_sse_frames(
111
+ response.aiter_bytes(), max_bytes=16 * 1024 * 1024,
112
+ idle_timeout_seconds=idle_timeout_seconds,
113
+ ) as frames:
114
+ yield frames
115
+ except ModelSSEIdleTimeout:
116
+ if known_policy_abort is None or not known_policy_abort():
117
+ raise httpx.ReadTimeout("model stream idle timeout") from None
118
+ except (SSEFramingError, UnicodeError):
119
+ raise invalid_model_frame() from None
120
+ except httpx.TransportError:
121
+ # A typed refusal already observed in this attempt cannot become a
122
+ # retryable request just because the remaining transport tail is lost.
123
+ if known_policy_abort is None or not known_policy_abort():
124
+ raise
125
+
126
+ _DSML_INVOKE_RE = re.compile(
127
+ r"<\s*(?:\|\s*){1,2}DSML\s*(?:\|\s*){1,2}invoke\b(?P<attrs>[^>]*)>"
128
+ r"(?P<body>.*?)"
129
+ r"<\s*(?:\|\s*){1,2}DSML\s*(?:\|\s*){1,2}/invoke\s*>",
130
+ re.IGNORECASE | re.DOTALL,
131
+ )
132
+ _DSML_PARAMETER_RE = re.compile(
133
+ r"<\s*(?:\|\s*){1,2}DSML\s*(?:\|\s*){1,2}parameter\b(?P<attrs>[^>]*)>"
134
+ r"(?P<value>.*?)"
135
+ r"<\s*(?:\|\s*){1,2}DSML\s*(?:\|\s*){1,2}/parameter\s*>",
136
+ re.IGNORECASE | re.DOTALL,
137
+ )
138
+ _DSML_CALLS_WRAPPER_RE = re.compile(
139
+ r"<\s*(?:\|\s*){1,2}DSML\s*(?:\|\s*){1,2}/?calls\s*>",
140
+ re.IGNORECASE,
141
+ )
142
+ _DSML_ATTRIBUTE_RE = re.compile(
143
+ r"(?P<name>[A-Za-z_][\w.-]*)\s*=\s*(?P<quote>['\"])(?P<value>.*?)(?P=quote)",
144
+ re.DOTALL,
145
+ )
146
+ _DSML_SIMPLE_ARGUMENT_RE = re.compile(
147
+ r"<(?P<name>[A-Za-z_][\w.-]*)>(?P<value>.*?)</(?P=name)>",
148
+ re.DOTALL,
149
+ )
150
+ _DSML_STREAM_LOOKBEHIND = 64
151
+ _GLM_TOOL_CALL_RE = re.compile(
152
+ r"<tool_call>\s*(?P<name>[A-Za-z_][\w.-]*)\s*(?P<body>.*?)</tool_call>",
153
+ re.IGNORECASE | re.DOTALL,
154
+ )
155
+ _GLM_ARGUMENT_TAG_RE = re.compile(
156
+ r"<(?P<tag>arg_key|arg_value)>(?P<value>.*?)</(?:arg_key|arg_value)>",
157
+ re.IGNORECASE | re.DOTALL,
158
+ )
159
+ _GLM_ASSIGNMENT_RE = re.compile(
160
+ r"^(?P<name>[A-Za-z_][\w.-]*)\s*=\s*(?P<quote>['\"])(?P<value>.*)(?P=quote)$",
161
+ re.DOTALL,
162
+ )
163
+ _GLM_ARGUMENT_KEY_RESIDUE_RE = re.compile(
164
+ r"(?:</?\s*arg_(?:key|value)\s*>)+$",
165
+ re.IGNORECASE,
166
+ )
167
+ _GLM_MALFORMED_TOOL_START_RE = re.compile(
168
+ r"<\s*tool_call\s*>\s*(?P<name>[A-Za-z_][\w.-]*)",
169
+ re.IGNORECASE,
170
+ )
171
+ _GLM_MALFORMED_KEY_RE = re.compile(
172
+ r"(?:<\s*arg_value\s*>\s*)?(?P<name>[A-Za-z_][\w.-]*)\s*"
173
+ r"</\s*arg_key\s*>",
174
+ re.IGNORECASE,
175
+ )
176
+ _GLM_MALFORMED_ASSIGNMENT_RE = re.compile(
177
+ r"(?P<name>[A-Za-z_][\w.-]*)\s*=\s*(?P<quote>['\"])(?P<value>.*?)(?P=quote)"
178
+ r"\s*</\s*arg_value\s*>",
179
+ re.IGNORECASE | re.DOTALL,
180
+ )
181
+ _SAFE_TOOL_NAME_ALIASES = {
182
+ # GLM 5.2 has emitted this shorter DSML name for the explicitly
183
+ # advertised workspace writer. The alias is accepted only when the target
184
+ # tool is present in the current request's declared schema.
185
+ "writefile": "writeworkspacefile",
186
+ }
187
+ _SAFE_TOOL_ARGUMENT_ALIASES = {
188
+ "write_workspace_file": {
189
+ "filename": "path",
190
+ "filepath": "path",
191
+ },
192
+ }
31
193
 
32
194
 
33
195
  @dataclass(frozen=True)
@@ -45,209 +207,580 @@ class ModelResponse:
45
207
  usage: Usage
46
208
  tool_calls: list[ToolCall]
47
209
  raw_message: dict[str, Any]
210
+ #: 推理文本(chat wire 的 reasoning_content / Responses wire 的 reasoning 项)。
211
+ reasoning: str = ""
212
+ #: 工具回合只返回 reasoning_content、修复后仍无内容与工具调用(可见降级信号)。
213
+ empty_reasoning_only: bool = False
214
+ #: Upstream correlation id only; never contains authorization material.
215
+ provider_request_id: str = ""
216
+ provider_input: ProviderInputObservation | None = field(default=None, repr=False)
217
+
218
+
219
+ @dataclass(frozen=True)
220
+ class ModelStreamChunk:
221
+ """One OpenAI-compatible chat stream delta."""
222
+
223
+ text: str = ""
224
+ reasoning: str = ""
225
+ tool_calls: tuple[ToolCall, ...] = ()
226
+ usage: Usage | None = None
227
+ done: bool = False
228
+ # Preserve the provider terminal reason across the canonical gateway
229
+ # boundary. Dropping ``length`` made a truncated plan indistinguishable
230
+ # from a complete answer to Agent runtimes.
231
+ finish_reason: str = ""
232
+ provider_request_id: str = ""
233
+ # Complete provider-only replay at the terminal, never a public text delta.
234
+ provider_content: tuple[dict[str, Any], ...] | None = field(default=None, repr=False)
235
+ provider_input: ProviderInputObservation | None = field(default=None, repr=False)
236
+ public_summary: str = ""
237
+
238
+
239
+ def _provider_request_id(headers: Any) -> str:
240
+ """Extract a bounded upstream correlation id from common Gateway headers."""
241
+
242
+ for name in (
243
+ "x-request-id",
244
+ "request-id",
245
+ "x-amzn-requestid",
246
+ "x-trace-id",
247
+ "trace-id",
248
+ ):
249
+ try:
250
+ value = str(headers.get(name) or "").strip()
251
+ except Exception: # noqa: BLE001 - third-party header mappings vary
252
+ value = ""
253
+ if value:
254
+ return value[:256]
255
+ return ""
256
+
257
+
258
+ def _dsml_attributes(value: str) -> dict[str, str]:
259
+ return {
260
+ match.group("name"): html.unescape(match.group("value"))
261
+ for match in _DSML_ATTRIBUTE_RE.finditer(value)
262
+ }
263
+
48
264
 
265
+ def _declared_tool_names(tools: list[dict[str, Any]] | None) -> dict[str, str]:
266
+ declared: dict[str, str] = {}
267
+ for tool in tools or []:
268
+ function = tool.get("function") or tool
269
+ name = str(function.get("name") or "").strip()
270
+ if name:
271
+ declared[re.sub(r"[^a-z0-9]", "", name.lower())] = name
272
+ return declared
49
273
 
50
- class CredentialResolver:
51
- """Resolve immutable Secret references with a workspace-persisted overlay.
52
274
 
53
- Resolution order: in-memory session values → workspace secrets file
54
- (``.agentkit/secrets.env``) → process environment (with paired fallback).
275
+ def _delegation_reasoning_effort(
276
+ model: ResolvedModel,
277
+ tools: list[dict[str, Any]] | None,
278
+ compatibility_profile: provider_compat.CompatProfile | None = None,
279
+ ) -> str | None:
280
+ """Return the provider-declared effort for a delegation decision.
281
+
282
+ Provider-specific values live in ``model_proxy.compat``. Studio only
283
+ identifies the semantic turn (the controller exposes ``delegate_task``),
284
+ avoiding transport branches keyed directly on model names.
55
285
  """
56
286
 
57
- _FALLBACK_PAIRS: tuple[tuple[str, str], ...] = (
58
- ("AGENTKIT_MODEL_API_KEY", "OPENAI_API_KEY"),
59
- ("OPENAI_API_KEY", "AGENTKIT_MODEL_API_KEY"),
287
+ if "delegate_task" not in _declared_tool_names(tools).values():
288
+ return None
289
+ profile = compatibility_profile or provider_compat.compat_profile(
290
+ base_url=model.endpoint_url,
291
+ model=model.model,
292
+ )
293
+ return provider_compat.default_delegation_reasoning_effort(
294
+ profile,
295
+ model=model.model,
60
296
  )
61
297
 
62
- def __init__(self, workspace: Any = None) -> None:
63
- self._session_values: dict[str, bytearray] = {}
64
- self._lock = RLock()
65
- self._workspace = workspace
66
- self._persisted: dict[str, str] | None = None
67
298
 
68
- def _secrets_path(self) -> Any | None:
69
- if self._workspace is None:
70
- return None
71
- try:
72
- return self._workspace.resolve(".agentkit/secrets.env")
73
- except Exception:
74
- return None
75
-
76
- def _load_persisted(self) -> dict[str, str]:
77
- if self._persisted is not None:
78
- return self._persisted
79
- self._persisted = {}
80
- path = self._secrets_path()
81
- if path is not None and path.is_file():
82
- try:
83
- for line in path.read_text(encoding="utf-8").splitlines():
84
- if not line or line.startswith("#") or "=" not in line:
85
- continue
86
- key, _, value = line.partition("=")
87
- key = key.strip()
88
- if key:
89
- self._persisted[key] = value
90
- except Exception:
91
- pass
92
- return self._persisted
93
-
94
- def _write_persisted(self) -> None:
95
- path = self._secrets_path()
96
- if path is None:
97
- return
98
- lines = [f"{k}={v}" for k, v in sorted(self._persisted.items())]
299
+ def _effective_reasoning_effort(
300
+ model: ResolvedModel,
301
+ tools: list[dict[str, Any]] | None,
302
+ explicit: str | None,
303
+ compatibility_profile: provider_compat.CompatProfile | None = None,
304
+ ) -> str | None:
305
+ """Resolve effort with the same precedence as mature agent runtimes.
306
+
307
+ Explicit run/session selection wins. Only when it is absent may the
308
+ endpoint compatibility profile supply a narrow controller-turn default.
309
+ """
310
+
311
+ profile = compatibility_profile or provider_compat.compat_profile(
312
+ base_url=model.endpoint_url, model=model.model,
313
+ )
314
+ if not profile.supports_reasoning_effort:
315
+ return None
316
+ requested = explicit or _delegation_reasoning_effort(model, tools, profile)
317
+ if not requested:
318
+ return None
319
+ from ksadk.model_proxy.transform import clamp_reasoning_effort
320
+
321
+ return clamp_reasoning_effort(model.model, requested, policy=profile.reasoning_effort_policy)
322
+
323
+
324
+ def _canonical_declared_tool_name(
325
+ requested_name: str,
326
+ declared: dict[str, str],
327
+ ) -> str | None:
328
+ key = re.sub(r"[^a-z0-9]", "", requested_name.lower())
329
+ canonical = declared.get(key)
330
+ if canonical:
331
+ return canonical
332
+ alias_target = _SAFE_TOOL_NAME_ALIASES.get(key)
333
+ canonical = declared.get(alias_target or "")
334
+ if canonical:
335
+ return canonical
336
+ # GLM 5.2 occasionally appends a leaked textual call id to a declared
337
+ # name (for example ``write_file_ide49a``). Only strip this narrowly
338
+ # shaped suffix and only accept the prefix when it resolves to an
339
+ # explicitly advertised tool/alias.
340
+ leaked_id = re.fullmatch(r"(?P<name>.+?)_id[a-z0-9]+", requested_name, re.IGNORECASE)
341
+ if leaked_id:
342
+ prefix = re.sub(r"[^a-z0-9]", "", leaked_id.group("name").lower())
343
+ return declared.get(prefix) or declared.get(_SAFE_TOOL_NAME_ALIASES.get(prefix, ""))
344
+ return None
345
+
346
+
347
+ def _canonical_declared_argument_name(
348
+ requested_name: str,
349
+ canonical_tool_name: str,
350
+ declared_properties: frozenset[str],
351
+ ) -> str | None:
352
+ if requested_name in declared_properties:
353
+ return requested_name
354
+ key = re.sub(r"[^a-z0-9]", "", requested_name.lower())
355
+ for declared_name in declared_properties:
356
+ if re.sub(r"[^a-z0-9]", "", declared_name.lower()) == key:
357
+ return declared_name
358
+ alias = _SAFE_TOOL_ARGUMENT_ALIASES.get(canonical_tool_name, {}).get(key)
359
+ return alias if alias in declared_properties else None
360
+
361
+
362
+ def _declared_tool_properties(
363
+ tools: list[dict[str, Any]] | None,
364
+ ) -> dict[str, frozenset[str]]:
365
+ """Return declared JSON argument names keyed by canonical tool name.
366
+
367
+ A few OpenAI-compatible GLM gateways have emitted a native function-call
368
+ JSON key such as ``task</arg_key>``. We only repair that wire residue when
369
+ the cleaned name is an explicitly declared property; arbitrary model keys
370
+ are never guessed or renamed.
371
+ """
372
+
373
+ declared: dict[str, frozenset[str]] = {}
374
+ for tool in tools or []:
375
+ function = tool.get("function") or tool
376
+ name = str(function.get("name") or "").strip()
377
+ parameters = function.get("parameters") or {}
378
+ properties = parameters.get("properties") if isinstance(parameters, dict) else {}
379
+ if name:
380
+ declared[name] = (
381
+ frozenset(str(key) for key in properties if isinstance(key, str))
382
+ if isinstance(properties, dict)
383
+ else frozenset()
384
+ )
385
+ return declared
386
+
387
+
388
+ def _normalize_tool_call_arguments(
389
+ calls: tuple[ToolCall, ...] | list[ToolCall],
390
+ tools: list[dict[str, Any]] | None,
391
+ ) -> tuple[ToolCall, ...]:
392
+ """Normalize known provider wire residue in native function arguments."""
393
+
394
+ properties_by_tool = _declared_tool_properties(tools)
395
+ declared_names = _declared_tool_names(tools)
396
+ normalized_calls: list[ToolCall] = []
397
+ for call in calls:
398
+ canonical_name = _canonical_declared_tool_name(call.name, declared_names) or call.name
399
+ properties = properties_by_tool.get(canonical_name, frozenset())
400
+ if not properties:
401
+ normalized_calls.append(
402
+ ToolCall(
403
+ id=call.id,
404
+ name=canonical_name,
405
+ arguments=call.arguments,
406
+ raw=call.raw,
407
+ )
408
+ )
409
+ continue
99
410
  try:
100
- path.parent.mkdir(parents=True, exist_ok=True)
101
- path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
102
- try:
103
- path.chmod(0o600)
104
- except Exception:
105
- pass
106
- except Exception:
107
- pass
411
+ arguments = load_model_json(call.arguments or "{}")
412
+ except (TypeError, ValueError):
413
+ normalized_calls.append(call)
414
+ continue
415
+ if not isinstance(arguments, dict):
416
+ normalized_calls.append(call)
417
+ continue
418
+ repaired: dict[str, Any] = {}
419
+ changed = False
420
+ for raw_key, value in arguments.items():
421
+ key = str(raw_key)
422
+ candidate = _GLM_ARGUMENT_KEY_RESIDUE_RE.sub("", html.unescape(key).strip()).strip()
423
+ if key not in properties and candidate in properties:
424
+ key = candidate
425
+ changed = True
426
+ if key in repaired:
427
+ raise StudioError(
428
+ "MODEL_TOOL_PROTOCOL_INVALID",
429
+ "模型返回了重复的工具参数",
430
+ status_code=502,
431
+ details={"toolName": call.name, "argument": key},
432
+ )
433
+ repaired[key] = value
434
+ normalized_calls.append(
435
+ ToolCall(
436
+ id=call.id,
437
+ name=canonical_name,
438
+ arguments=(json.dumps(repaired, ensure_ascii=False) if changed else call.arguments),
439
+ raw=call.raw,
440
+ )
441
+ )
442
+ return tuple(normalized_calls)
108
443
 
109
- @staticmethod
110
- def _validate_name(name: str) -> str:
111
- allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
112
- if not name or any(char not in allowed for char in name):
444
+
445
+ class _AmbiguousToolInput(StudioError):
446
+ """A failed identity/value check cannot enter malformed-text recovery."""
447
+
448
+
449
+ def _textual_argument_error() -> _AmbiguousToolInput:
450
+ return _AmbiguousToolInput(
451
+ "MODEL_TOOL_PROTOCOL_INVALID", "模型返回了重复或非法的工具参数", status_code=502,
452
+ )
453
+
454
+
455
+ def _store_textual_argument(arguments: dict[str, Any], name: str, value: Any) -> None:
456
+ if name in arguments:
457
+ raise _textual_argument_error()
458
+ arguments[name] = value
459
+
460
+
461
+ def _dsml_argument_value(raw: str, attributes: dict[str, str]) -> Any:
462
+ value = html.unescape(raw.strip())
463
+ if attributes.get("string", "").lower() == "true":
464
+ return value
465
+ try:
466
+ return load_model_json(value)
467
+ except ModelJSONPolicyError:
468
+ raise _textual_argument_error() from None
469
+ except (TypeError, ValueError):
470
+ return value
471
+
472
+
473
+ def _recover_dsml_tool_calls(
474
+ content: str,
475
+ tools: list[dict[str, Any]] | None,
476
+ ) -> tuple[str, list[ToolCall], bool]:
477
+ """Recover DeepSeek's textual DSML tool protocol without exposing it as prose."""
478
+
479
+ normalized = content.replace("|", "|")
480
+ if "DSML" not in normalized.upper():
481
+ return content, [], False
482
+ declared = _declared_tool_names(tools)
483
+ calls: list[ToolCall] = []
484
+ spans: list[tuple[int, int]] = []
485
+ for index, match in enumerate(_DSML_INVOKE_RE.finditer(normalized)):
486
+ attrs = _dsml_attributes(match.group("attrs"))
487
+ requested_name = str(attrs.get("name") or "").strip()
488
+ canonical_name = _canonical_declared_tool_name(requested_name, declared)
489
+ if not canonical_name:
113
490
  raise StudioError(
114
- "SECRET_REFERENCE_INVALID",
115
- "环境变量 Secret 引用格式无效",
116
- status_code=422,
117
- field="credentialName",
491
+ "MODEL_TOOL_PROTOCOL_INVALID",
492
+ f"模型返回了未声明的工具调用:{requested_name or 'unknown'}",
493
+ status_code=502,
494
+ details={"toolName": requested_name or "unknown"},
495
+ )
496
+ body = match.group("body")
497
+ arguments: dict[str, Any] = {}
498
+ for parameter in _DSML_PARAMETER_RE.finditer(body):
499
+ parameter_attrs = _dsml_attributes(parameter.group("attrs"))
500
+ parameter_name = str(parameter_attrs.get("name") or "").strip()
501
+ if parameter_name:
502
+ _store_textual_argument(
503
+ arguments, parameter_name,
504
+ _dsml_argument_value(parameter.group("value"), parameter_attrs),
505
+ )
506
+ if not arguments:
507
+ for parameter in _DSML_SIMPLE_ARGUMENT_RE.finditer(body):
508
+ parameter_name = parameter.group("name")
509
+ _store_textual_argument(arguments, parameter_name,
510
+ _dsml_argument_value(parameter.group("value"), {}))
511
+ digest = hashlib.sha256(f"{canonical_name}:{index}:{match.group(0)}".encode()).hexdigest()[
512
+ :20
513
+ ]
514
+ calls.append(
515
+ ToolCall(
516
+ id=f"call_dsml_{digest}",
517
+ name=canonical_name,
518
+ arguments=json.dumps(arguments, ensure_ascii=False),
519
+ raw={"protocol": "dsml"},
118
520
  )
119
- return name
521
+ )
522
+ spans.append(match.span())
523
+ if not calls:
524
+ raise StudioError(
525
+ "MODEL_TOOL_PROTOCOL_INVALID",
526
+ "模型返回了无法解析的工具调用",
527
+ status_code=502,
528
+ )
529
+ cleaned = normalized
530
+ for start, end in reversed(spans):
531
+ cleaned = cleaned[:start] + cleaned[end:]
532
+ cleaned = _DSML_CALLS_WRAPPER_RE.sub("", cleaned).strip()
533
+ if "DSML" in cleaned.upper():
534
+ raise StudioError(
535
+ "MODEL_TOOL_PROTOCOL_INVALID",
536
+ "模型返回了不完整的工具调用",
537
+ status_code=502,
538
+ )
539
+ return cleaned, calls, True
540
+
541
+
542
+ def _recover_glm_tool_calls(
543
+ content: str,
544
+ tools: list[dict[str, Any]] | None,
545
+ ) -> tuple[str, list[ToolCall], bool]:
546
+ """Recover GLM's textual ``<tool_call>`` fallback protocol.
120
547
 
121
- @classmethod
122
- def _environment_name(cls, reference: str) -> str:
123
- if not reference.startswith("env://"):
548
+ Some OpenAI-compatible GLM endpoints put tool calls in ``content`` rather
549
+ than ``delta.tool_calls``. Real responses also occasionally encode an
550
+ argument as ``<arg_value>name=\"value\"</arg_value>`` (or close an
551
+ ``arg_key`` with ``</arg_value>``), so parsing is intentionally tolerant
552
+ about those two wire quirks while remaining fail-closed on tool names.
553
+ """
554
+
555
+ if "<tool_call" not in content.lower():
556
+ return content, [], False
557
+ declared = _declared_tool_names(tools)
558
+ calls: list[ToolCall] = []
559
+ spans: list[tuple[int, int]] = []
560
+ for index, match in enumerate(_GLM_TOOL_CALL_RE.finditer(content)):
561
+ requested_name = match.group("name").strip()
562
+ canonical_name = _canonical_declared_tool_name(requested_name, declared)
563
+ if not canonical_name:
124
564
  raise StudioError(
125
- "SECRET_BACKEND_UNAVAILABLE",
126
- "当前本地 Runtime 仅支持 env:// Secret 引用",
127
- status_code=501,
128
- details={"scheme": reference.partition("://")[0]},
565
+ "MODEL_TOOL_PROTOCOL_INVALID",
566
+ f"模型返回了未声明的工具调用:{requested_name or 'unknown'}",
567
+ status_code=502,
568
+ details={"toolName": requested_name or "unknown"},
129
569
  )
130
- return cls._validate_name(reference.removeprefix("env://"))
570
+ arguments: dict[str, Any] = {}
571
+ pending_key = ""
572
+ for token in _GLM_ARGUMENT_TAG_RE.finditer(match.group("body")):
573
+ tag = token.group("tag").lower()
574
+ raw_value = html.unescape(token.group("value").strip())
575
+ assignment = _GLM_ASSIGNMENT_RE.match(raw_value)
576
+ if assignment:
577
+ _store_textual_argument(
578
+ arguments, assignment.group("name"),
579
+ _dsml_argument_value(assignment.group("value"), {"string": "true"}),
580
+ )
581
+ pending_key = ""
582
+ elif tag == "arg_key":
583
+ pending_key = raw_value
584
+ elif pending_key:
585
+ _store_textual_argument(arguments, pending_key, _dsml_argument_value(raw_value, {}))
586
+ pending_key = ""
587
+ else:
588
+ raise StudioError(
589
+ "MODEL_TOOL_PROTOCOL_INVALID",
590
+ "模型返回了无法解析的工具参数",
591
+ status_code=502,
592
+ )
593
+ if pending_key or not arguments:
594
+ raise StudioError(
595
+ "MODEL_TOOL_PROTOCOL_INVALID",
596
+ "模型返回了不完整的工具调用",
597
+ status_code=502,
598
+ )
599
+ digest = hashlib.sha256(f"{canonical_name}:{index}:{match.group(0)}".encode()).hexdigest()[
600
+ :20
601
+ ]
602
+ calls.append(
603
+ ToolCall(
604
+ id=f"call_glm_{digest}",
605
+ name=canonical_name,
606
+ arguments=json.dumps(arguments, ensure_ascii=False),
607
+ raw={"protocol": "glm-text"},
608
+ )
609
+ )
610
+ spans.append(match.span())
611
+ if not calls:
612
+ raise StudioError(
613
+ "MODEL_TOOL_PROTOCOL_INVALID",
614
+ "模型返回了无法解析的工具调用",
615
+ status_code=502,
616
+ )
617
+ cleaned = content
618
+ for start, end in reversed(spans):
619
+ cleaned = cleaned[:start] + cleaned[end:]
620
+ if "<tool_call" in cleaned.lower():
621
+ raise StudioError(
622
+ "MODEL_TOOL_PROTOCOL_INVALID",
623
+ "模型返回了不完整的工具调用",
624
+ status_code=502,
625
+ )
626
+ return cleaned.strip(), calls, True
131
627
 
132
- @staticmethod
133
- def _zero(value: bytearray | None) -> None:
134
- if value is not None:
135
- value[:] = b"\x00" * len(value)
136
628
 
137
- def put_session(self, name: str, value: str) -> dict[str, str | bool]:
138
- name = self._validate_name(name)
139
- if (
140
- not value
141
- or len(value) > 16_384
142
- or value != value.strip()
143
- or any(ord(char) < 32 or ord(char) == 127 for char in value)
144
- ):
629
+ def _recover_malformed_glm_tool_call(
630
+ content: str,
631
+ tools: list[dict[str, Any]] | None,
632
+ ) -> tuple[str, list[ToolCall], bool]:
633
+ """Recover the bounded malformed fallback emitted by GLM 5.2.
634
+
635
+ Some GLM 5.2 compatible gateways emit a textual ``<tool_call>`` marker
636
+ but lose the closing marker and mix up ``arg_key``/``arg_value`` tags.
637
+ Recovery remains fail-closed: the tool must resolve to a declared schema,
638
+ every recovered argument must resolve to a declared property, and at
639
+ least one argument must be recovered. Text after the marker is discarded
640
+ so a hallucinated "file created" sentence cannot escape as visible output
641
+ before the tool actually executes.
642
+ """
643
+
644
+ start = _GLM_MALFORMED_TOOL_START_RE.search(content)
645
+ if start is None:
646
+ return content, [], False
647
+ declared_names = _declared_tool_names(tools)
648
+ requested_name = start.group("name").strip()
649
+ canonical_name = _canonical_declared_tool_name(requested_name, declared_names)
650
+ if not canonical_name:
651
+ raise StudioError(
652
+ "MODEL_TOOL_PROTOCOL_INVALID",
653
+ f"模型返回了未声明的工具调用:{requested_name or 'unknown'}",
654
+ status_code=502,
655
+ details={"toolName": requested_name or "unknown"},
656
+ )
657
+ properties = _declared_tool_properties(tools).get(canonical_name, frozenset())
658
+ body = content[start.end() :]
659
+ raw_arguments: list[tuple[str, str]] = []
660
+
661
+ key_matches = list(_GLM_MALFORMED_KEY_RE.finditer(body))
662
+ for index, key_match in enumerate(key_matches):
663
+ next_start = key_matches[index + 1].start() if index + 1 < len(key_matches) else len(body)
664
+ value = body[key_match.end() : next_start]
665
+ value = re.sub(r"^\s*<\s*arg_value\s*>\s*", "", value, flags=re.IGNORECASE)
666
+ value = re.split(r"</\s*arg_value\s*>", value, maxsplit=1, flags=re.IGNORECASE)[0]
667
+ raw_arguments.append((key_match.group("name"), html.unescape(value.strip())))
668
+
669
+ if not raw_arguments:
670
+ raw_arguments.extend(
671
+ (match.group("name"), html.unescape(match.group("value")))
672
+ for match in _GLM_MALFORMED_ASSIGNMENT_RE.finditer(body)
673
+ )
674
+
675
+ arguments: dict[str, Any] = {}
676
+ for raw_name, raw_value in raw_arguments:
677
+ argument_name = _canonical_declared_argument_name(
678
+ raw_name,
679
+ canonical_name,
680
+ properties,
681
+ )
682
+ if not argument_name:
145
683
  raise StudioError(
146
- "SECRET_VALUE_INVALID",
147
- "凭证不能为空、包含控制字符或超过 16 KiB",
148
- status_code=422,
149
- field="value",
684
+ "MODEL_TOOL_PROTOCOL_INVALID",
685
+ "模型返回了未声明的工具参数",
686
+ status_code=502,
687
+ details={"toolName": canonical_name, "argument": raw_name},
150
688
  )
151
- encoded = bytearray(value.encode("utf-8"))
152
- with self._lock:
153
- previous = self._session_values.get(name)
154
- self._session_values[name] = encoded
155
- self._zero(previous)
156
- if self._secrets_path() is not None:
157
- persisted = self._load_persisted()
158
- if persisted.get(name) != value:
159
- persisted[name] = value
160
- self._write_persisted()
161
- return self.status(f"env://{name}")
162
-
163
- def delete_session(self, name: str) -> dict[str, str | bool]:
164
- name = self._validate_name(name)
165
- with self._lock:
166
- previous = self._session_values.pop(name, None)
167
- self._zero(previous)
168
- if self._secrets_path() is not None:
169
- persisted = self._load_persisted()
170
- if name in persisted:
171
- persisted.pop(name, None)
172
- self._write_persisted()
173
- return self.status(f"env://{name}")
174
-
175
- def clear_session(self) -> None:
176
- with self._lock:
177
- values = list(self._session_values.values())
178
- self._session_values.clear()
179
- for value in values:
180
- self._zero(value)
181
-
182
- def _fallback_name(self, name: str) -> str | None:
183
- for primary, alternate in self._FALLBACK_PAIRS:
184
- if name == primary and alternate != primary:
185
- return alternate
186
- return None
187
-
188
- def _resolve_source(self, name: str) -> tuple[bool, str]:
189
- """Return (configured, source) considering session, persisted file, env, and fallback."""
190
- fallback = self._fallback_name(name)
191
- with self._lock:
192
- if name in self._session_values:
193
- return True, "session"
194
- if fallback and fallback in self._session_values:
195
- return True, "session-alias"
196
- persisted = self._load_persisted()
197
- if name in persisted:
198
- return True, "workspace"
199
- if fallback and fallback in persisted:
200
- return True, "workspace-alias"
201
- if os.environ.get(name):
202
- return True, "environment"
203
- if fallback and os.environ.get(fallback):
204
- return True, "fallback"
205
- return False, "missing"
206
-
207
- def _session_value(self, name: str) -> str | None:
208
- with self._lock:
209
- value = self._session_values.get(name)
210
- return value.decode("utf-8") if value is not None else None
211
-
212
- def status(self, reference: str) -> dict[str, str | bool]:
213
- name = self._environment_name(reference)
214
- configured, source = self._resolve_source(name)
215
- return {
216
- "reference": reference,
217
- "name": name,
218
- "configured": configured,
219
- "source": source,
220
- "persistence": source,
221
- }
689
+ if argument_name in arguments:
690
+ raise StudioError(
691
+ "MODEL_TOOL_PROTOCOL_INVALID",
692
+ "模型返回了重复的工具参数",
693
+ status_code=502,
694
+ details={"toolName": canonical_name, "argument": argument_name},
695
+ )
696
+ arguments[argument_name] = _dsml_argument_value(raw_value, {"string": "true"})
222
697
 
223
- def resolve(self, reference: str, *, allow_aliases: bool = True) -> str:
224
- name = self._environment_name(reference)
225
- fallback = self._fallback_name(name) if allow_aliases else None
226
- aliases = [name, *([fallback] if fallback else [])]
227
- for candidate in aliases:
228
- value = self._session_value(candidate)
229
- if value is not None:
230
- return value
231
- persisted = self._load_persisted()
232
- for candidate in aliases:
233
- if candidate in persisted:
234
- return persisted[candidate]
235
- for candidate in aliases:
236
- value = os.environ.get(candidate)
237
- if value:
238
- return value
698
+ if not arguments:
239
699
  raise StudioError(
240
- "SECRET_NOT_FOUND",
241
- "模型凭证尚未配置",
242
- status_code=422,
243
- details={"reference": reference},
700
+ "MODEL_TOOL_PROTOCOL_INVALID",
701
+ "模型返回了无法解析的工具调用",
702
+ status_code=502,
244
703
  )
704
+ digest = hashlib.sha256(
705
+ f"{canonical_name}:malformed:{content[start.start() :]}".encode()
706
+ ).hexdigest()[:20]
707
+ return (
708
+ content[: start.start()].strip(),
709
+ [
710
+ ToolCall(
711
+ id=f"call_glm_{digest}",
712
+ name=canonical_name,
713
+ arguments=json.dumps(arguments, ensure_ascii=False),
714
+ raw={"protocol": "glm-text-recovered"},
715
+ )
716
+ ],
717
+ True,
718
+ )
245
719
 
246
- def exists(self, reference: str) -> bool:
247
- try:
248
- return bool(self.status(reference)["configured"])
249
- except StudioError:
250
- return False
720
+
721
+ def _recover_textual_tool_calls(
722
+ content: str,
723
+ tools: list[dict[str, Any]] | None,
724
+ ) -> tuple[str, list[ToolCall], bool]:
725
+ if "DSML" in content.replace("|", "|").upper():
726
+ return _recover_dsml_tool_calls(content, tools)
727
+ try:
728
+ return _recover_glm_tool_calls(content, tools)
729
+ except StudioError as exc:
730
+ if isinstance(exc, _AmbiguousToolInput):
731
+ raise
732
+ if exc.code != "MODEL_TOOL_PROTOCOL_INVALID":
733
+ raise
734
+ return _recover_malformed_glm_tool_call(content, tools)
735
+
736
+
737
+ def _partition_tool_stream_text(value: str) -> tuple[str, str, bool]:
738
+ """Split a streamed text buffer before a possible textual tool marker.
739
+
740
+ Ordinary model prose must remain truly streaming even when tools are
741
+ available. Only the short suffix beginning at ``<`` is held long enough
742
+ to decide whether a provider is starting a textual DSML tool call.
743
+ """
744
+
745
+ normalized = value.replace("|", "|")
746
+ marker = re.search(
747
+ r"<\s*(?:\|\s*){1,2}DSML|<\s*tool_call\s*>",
748
+ normalized,
749
+ re.IGNORECASE,
750
+ )
751
+ if marker:
752
+ start = marker.start()
753
+ return value[:start], value[start:], True
754
+ candidate_start = value.rfind("<")
755
+ if candidate_start >= 0 and len(value) - candidate_start <= _DSML_STREAM_LOOKBEHIND:
756
+ return value[:candidate_start], value[candidate_start:], False
757
+ return value, "", False
758
+
759
+
760
+ def _repeated_output_unit(value: str) -> str:
761
+ """Return a repeated tail unit when a provider is stuck in a text loop."""
762
+
763
+ if len(value) < _REPETITION_MIN_UNIT_CHARS * _REPETITION_COUNT:
764
+ return ""
765
+ window = value[-(_REPETITION_MAX_UNIT_CHARS * _REPETITION_COUNT) :]
766
+ max_unit = min(_REPETITION_MAX_UNIT_CHARS, len(window) // _REPETITION_COUNT)
767
+ for unit_length in range(_REPETITION_MIN_UNIT_CHARS, max_unit + 1):
768
+ unit = window[-unit_length:]
769
+ if window.endswith(unit * _REPETITION_COUNT) and len(set(unit.strip())) >= 6:
770
+ return unit
771
+ return ""
772
+
773
+
774
+ def _raise_for_repetitive_output(value: str) -> None:
775
+ unit = _repeated_output_unit(value)
776
+ if not unit:
777
+ return
778
+ raise StudioError(
779
+ "MODEL_REPETITIVE_OUTPUT",
780
+ "模型输出出现重复循环,已停止本次生成",
781
+ status_code=502,
782
+ details={"repeatedChars": len(unit)},
783
+ )
251
784
 
252
785
 
253
786
  class NetworkGuard:
@@ -319,6 +852,34 @@ class OpenAICompatibleModelClient:
319
852
  self.network_guard = network_guard or NetworkGuard()
320
853
  self.transport = transport
321
854
  self.sleep = sleep
855
+ # Keep one AsyncClient for the lifetime of the Studio service. Creating
856
+ # a client for every turn defeats HTTP keep-alive and forces a fresh
857
+ # TCP/TLS handshake before the model can produce its first token.
858
+ self._client: httpx.AsyncClient | None = None
859
+ self._client_lock = RLock()
860
+
861
+ def _http_client(self) -> httpx.AsyncClient:
862
+ with self._client_lock:
863
+ if self._client is None or self._client.is_closed:
864
+ self._client = httpx.AsyncClient(
865
+ transport=self.transport,
866
+ follow_redirects=False,
867
+ limits=httpx.Limits(
868
+ max_connections=20,
869
+ max_keepalive_connections=10,
870
+ keepalive_expiry=30.0,
871
+ ),
872
+ )
873
+ return self._client
874
+
875
+ async def aclose(self) -> None:
876
+ """Close the shared connection pool during Studio shutdown."""
877
+
878
+ with self._client_lock:
879
+ client = self._client
880
+ self._client = None
881
+ if client is not None and not client.is_closed:
882
+ await client.aclose()
322
883
 
323
884
  async def complete(
324
885
  self,
@@ -326,20 +887,35 @@ class OpenAICompatibleModelClient:
326
887
  *,
327
888
  messages: list[dict[str, Any]],
328
889
  network_policy: NetworkPolicy,
329
- timeout_seconds: int,
890
+ timeout_seconds: float,
330
891
  max_attempts: int,
331
892
  backoff_seconds: float,
332
893
  tools: list[dict[str, Any]] | None = None,
333
894
  allow_empty: bool = False,
334
895
  response_format: dict[str, Any] | None = None,
335
896
  retry_on_length: bool = False,
897
+ retry_reasoning_only: bool = True,
898
+ max_output_tokens: int | None = None,
899
+ output_budget_mode: Literal["adaptive", "fixed"] = "adaptive",
900
+ reasoning_effort: str | None = None,
901
+ tool_choice: str | dict[str, Any] | None = None,
902
+ parallel_tool_calls: bool | None = None,
903
+ output_token_parameter: Literal["max_tokens", "max_completion_tokens"] | None = None,
904
+ compatibility_profile: provider_compat.CompatProfile | None = None,
905
+ thinking_mode: Literal["disabled"] | None = None,
336
906
  ) -> ModelResponse:
337
907
  await self.network_guard.check(model.endpoint_url, network_policy)
338
908
  credential = self.credential_resolver.resolve(model.credential_ref)
339
909
  wire_api = (model.wire_api or "chat").strip().lower()
910
+ # 端点差异只允许来自声明式兼容档位(不按模型名写传输/解析分支)。
911
+ profile = compatibility_profile or provider_compat.compat_profile(
912
+ base_url=model.endpoint_url, model=model.model
913
+ )
340
914
  payload: dict[str, Any]
341
915
  if wire_api == "responses":
342
916
  payload = self._responses_payload(model, messages, tools)
917
+ elif wire_api == "anthropic":
918
+ payload = self._anthropic_payload(model, messages, tools, compatibility_profile=profile)
343
919
  else:
344
920
  payload = {
345
921
  "model": model.model,
@@ -356,30 +932,75 @@ class OpenAICompatibleModelClient:
356
932
  payload["top_p"] = model.parameters.top_p
357
933
  if tools:
358
934
  payload["tools"] = tools
359
- payload["tool_choice"] = "auto"
935
+ payload["tool_choice"] = tool_choice or "auto"
936
+ # OpenAI-compatible gateways do not all share the same default.
937
+ # Declare this explicitly so one model turn may return several
938
+ # independent calls; the stream parser and Harness dispatcher
939
+ # already preserve and execute such batches.
940
+ payload["parallel_tool_calls"] = True
360
941
  if response_format and model.parameters.allow_json_response_format:
361
942
  payload["response_format"] = response_format
362
- headers = {
363
- "Authorization": f"Bearer {credential}",
364
- "Content-Type": "application/json",
365
- }
943
+ payload = provider_compat.apply_chat_compat(payload, profile, model=model.model)
944
+ self._apply_tool_choice(payload, wire_api, tool_choice)
945
+ self._apply_parallel_tool_policy(payload, wire_api, parallel_tool_calls)
946
+ if max_output_tokens is not None:
947
+ payload["max_output_tokens" if wire_api == "responses" else "max_tokens"] = (
948
+ max_output_tokens
949
+ )
950
+ # A Native call owns a hard output limit. Endpoint recommendations
951
+ # and legacy length recovery must not enlarge that allocation.
952
+ if (
953
+ output_budget_mode == "adaptive"
954
+ and wire_api != "responses" and max_output_tokens is not None
955
+ ):
956
+ # 不变量:max_output_tokens > thinking_budget。只在**引擎显式压预算**
957
+ # (委派决策回合)时抬:这是实测零产出事故的成因;agent 作者自己配置
958
+ # 的 maxTokens 不静默改写,否则等于替作者改配置。
959
+ effective_budget = provider_compat.required_output_budget(
960
+ profile, payload.get("max_tokens")
961
+ )
962
+ if effective_budget is not None:
963
+ payload["max_tokens"] = effective_budget
964
+ effective_effort = _effective_reasoning_effort(model, tools, reasoning_effort, profile)
965
+ self._apply_reasoning_effort(payload, wire_api, effective_effort)
966
+ # 思考决策必须在"有效输出预算"确定之后做,且**与是否带工具无关**:
967
+ # 预算不足以容纳思考预算时(如委派决策回合的 2048),必须关思考,
968
+ # 否则始终思考的模型会把预算全花在 reasoning 上、零产出。
969
+ self._apply_thinking_policy(payload, wire_api, profile, tools, effective_effort,
970
+ thinking_mode=thinking_mode)
971
+ if wire_api not in {"responses", "anthropic"}:
972
+ # Apply the complete endpoint/model policy after all host options,
973
+ # so a late effort or thinking switch cannot reintroduce a field
974
+ # rejected by this model. No adaptive retry or budget increase.
975
+ payload = provider_compat.apply_chat_compat(payload, profile, model=model.model)
976
+ self._apply_output_token_parameter(payload, wire_api, output_token_parameter)
977
+ if wire_api == "anthropic":
978
+ headers = self._anthropic_headers(credential, payload)
979
+ else:
980
+ headers = {
981
+ "Authorization": f"Bearer {credential}",
982
+ "Content-Type": "application/json",
983
+ }
366
984
  timeout = httpx.Timeout(timeout_seconds, connect=min(10, timeout_seconds))
367
985
  dropped_response_format = False
368
986
  length_retried = False
987
+ reasoning_only_retried = False
369
988
  last_length_error: StudioError | None = None
370
- async with httpx.AsyncClient(
371
- transport=self.transport,
372
- timeout=timeout,
373
- follow_redirects=False,
374
- ) as client:
989
+ client = self._http_client()
990
+ observation = None
991
+ observed_usage = None
992
+ try:
375
993
  # 额外 1 次迭代仅用于 response_format 400 降级重发,
376
994
  # 其余失败路径仍受 max_attempts 约束(会在原上限处 raise)。
377
995
  for attempt in range(1, max_attempts + 2):
996
+ observation = None
997
+ observed_usage = None
378
998
  try:
379
999
  response = await client.post(
380
1000
  model.endpoint_url,
381
1001
  headers=headers,
382
1002
  json=payload,
1003
+ timeout=timeout,
383
1004
  )
384
1005
  except (httpx.TimeoutException, httpx.TransportError) as exc:
385
1006
  if attempt >= max_attempts:
@@ -398,62 +1019,56 @@ class OpenAICompatibleModelClient:
398
1019
  status_code=403,
399
1020
  details={"statusCode": response.status_code},
400
1021
  )
401
- if response.status_code == 429 or response.status_code >= 500:
402
- if attempt < max_attempts:
403
- await self.sleep(backoff_seconds * attempt)
404
- continue
405
1022
  if (
406
1023
  response.status_code == 400
407
1024
  and "response_format" in payload
408
1025
  and not dropped_response_format
409
1026
  ):
410
- # 网关不支持 response_format:去掉该字段重发一次。
411
1027
  dropped_response_format = True
412
1028
  payload.pop("response_format")
413
1029
  continue
414
1030
  if response.status_code >= 400:
415
- if response.status_code == 429:
416
- # A selected authoring profile must not silently fall
417
- # back to a different model. Preserve the actual
418
- # upstream condition so Studio can offer the user a
419
- # useful retry/switch decision instead of reporting a
420
- # misleading generic 502.
421
- raise StudioError(
422
- "MODEL_RATE_LIMITED",
423
- "所选生成模型当前限流,请稍后重试或切换模型 Profile",
424
- status_code=429,
425
- details={"upstreamStatus": response.status_code},
426
- )
427
- upstream_detail = ""
1031
+ failure = await _http_model_error(response, wire_api=wire_api)
1032
+ from ksadk.harness.model_provider import classify_model_failure
1033
+
1034
+ if (classify_model_failure(failure).kind.value in {"rate_limit", "unavailable"}
1035
+ and attempt < max_attempts):
1036
+ await self.sleep(backoff_seconds * attempt)
1037
+ continue
1038
+ raise failure
1039
+ observation = None
1040
+ if wire_api == "anthropic" and len(response.content) <= 16 * 1024 * 1024:
428
1041
  try:
429
- upstream_detail = response.text[:200]
430
- except Exception: # noqa: BLE001 - 诊断信息尽力而为
431
- upstream_detail = ""
432
- raise StudioError(
433
- "MODEL_REQUEST_FAILED",
434
- "模型服务返回错误",
435
- status_code=502,
436
- details={
437
- "upstreamStatus": response.status_code,
438
- "upstreamError": upstream_detail,
439
- },
440
- )
1042
+ observation = input_observation(decode_model_frame(response.content),
1043
+ payload, "buffered")
1044
+ except (ValueError, TypeError):
1045
+ raise invalid_model_frame() from None
441
1046
  try:
442
1047
  if wire_api == "responses":
443
1048
  parsed = self._parse_responses_response(response, allow_empty=allow_empty)
1049
+ elif wire_api == "anthropic":
1050
+ parsed = self._parse_anthropic_response(response, allow_empty=allow_empty)
444
1051
  else:
445
1052
  parsed = self._parse_response(response, allow_empty=allow_empty)
446
1053
  except StudioError as exc:
1054
+ if type(exc) is ModelResponseError:
1055
+ exc.provider_input = observation
447
1056
  # finishReason=length 的空响应(大 JSON 被 max_tokens 截断):
448
1057
  # 一次性扩容 max_tokens 重发;再次截断则按原错误上抛。
449
- if retry_on_length and not length_retried and self._is_length_truncation(exc):
1058
+ if (
1059
+ retry_on_length and output_budget_mode == "adaptive"
1060
+ and not length_retried and self._is_length_truncation(exc)
1061
+ ):
450
1062
  length_retried = True
451
1063
  last_length_error = exc
452
1064
  payload = self._expand_length_budget(payload)
453
1065
  continue
454
1066
  raise
1067
+ parsed = replace(parsed, provider_input=observation)
1068
+ observed_usage = parsed.usage
455
1069
  if (
456
1070
  retry_on_length
1071
+ and output_budget_mode == "adaptive"
457
1072
  and not length_retried
458
1073
  and parsed.finish_reason in _LENGTH_FINISH_REASONS
459
1074
  ):
@@ -461,11 +1076,870 @@ class OpenAICompatibleModelClient:
461
1076
  length_retried = True
462
1077
  payload = self._expand_length_budget(payload)
463
1078
  continue
1079
+ if parsed.finish_reason == "content_filter":
1080
+ return parsed
1081
+ if not (
1082
+ "DSML" in parsed.content.replace("|", "|").upper()
1083
+ or "<tool_call" in parsed.content.lower()
1084
+ ):
1085
+ _raise_for_repetitive_output(parsed.content)
1086
+ if wire_api != "responses" and not parsed.tool_calls:
1087
+ try:
1088
+ cleaned, recovered_calls, recovered = _recover_textual_tool_calls(
1089
+ parsed.content, tools
1090
+ )
1091
+ except StudioError as error:
1092
+ raise ModelResponseError(error, parsed.usage) from None
1093
+ if recovered:
1094
+ parsed = ModelResponse(
1095
+ content=cleaned,
1096
+ finish_reason="tool_calls",
1097
+ usage=parsed.usage,
1098
+ tool_calls=recovered_calls,
1099
+ raw_message=parsed.raw_message,
1100
+ reasoning=parsed.reasoning,
1101
+ provider_request_id=parsed.provider_request_id,
1102
+ provider_input=parsed.provider_input,
1103
+ )
1104
+ if (
1105
+ wire_api != "responses"
1106
+ and tools
1107
+ and not parsed.content.strip()
1108
+ and not parsed.tool_calls
1109
+ and parsed.reasoning.strip()
1110
+ ):
1111
+ # 思考型 OpenAI 兼容端点(GLM / DeepSeek / MiMo 等)在工具
1112
+ # 回合可能只返回 reasoning_content。这是**已知的正常形态**,
1113
+ # 不是传输故障:按档位关掉思考修一次(而不是强推
1114
+ # tool_choice=required),仍为空则作为"可见空回合"返回,
1115
+ # 交给上层循环决定——绝不升级成致命错误,否则会被 provider
1116
+ # 重试链放大成整轮 ABORT(2026-09-18 实测事故)。
1117
+ if retry_reasoning_only and not reasoning_only_retried:
1118
+ reasoning_only_retried = True
1119
+ repair_payload = {
1120
+ **payload,
1121
+ "messages": [
1122
+ *list(payload.get("messages") or []),
1123
+ {
1124
+ "role": "system",
1125
+ "content": (
1126
+ "The previous response contained reasoning only. "
1127
+ "Return a concrete final answer, or call one of the "
1128
+ "declared tools using the native tool-call protocol; "
1129
+ "do not return reasoning alone."
1130
+ ),
1131
+ },
1132
+ ],
1133
+ }
1134
+ payload = provider_compat.with_thinking_disabled(
1135
+ repair_payload, profile, reasoning_effort=effective_effort,
1136
+ )
1137
+ continue
1138
+ return ModelResponse(
1139
+ content="",
1140
+ finish_reason=parsed.finish_reason,
1141
+ usage=parsed.usage,
1142
+ tool_calls=[],
1143
+ raw_message=parsed.raw_message,
1144
+ reasoning=parsed.reasoning,
1145
+ empty_reasoning_only=True,
1146
+ provider_request_id=parsed.provider_request_id,
1147
+ provider_input=parsed.provider_input,
1148
+ )
1149
+ if parsed.tool_calls:
1150
+ try:
1151
+ normalized_calls = list(_normalize_tool_call_arguments(
1152
+ parsed.tool_calls, tools,
1153
+ ))
1154
+ except StudioError as error:
1155
+ raise ModelResponseError(error, parsed.usage) from None
1156
+ parsed = ModelResponse(
1157
+ content=parsed.content,
1158
+ finish_reason=parsed.finish_reason,
1159
+ usage=parsed.usage,
1160
+ tool_calls=normalized_calls,
1161
+ raw_message=parsed.raw_message,
1162
+ reasoning=parsed.reasoning,
1163
+ provider_request_id=parsed.provider_request_id,
1164
+ provider_input=parsed.provider_input,
1165
+ )
464
1166
  return parsed
1167
+ except asyncio.CancelledError:
1168
+ raise
1169
+ except StudioError as error:
1170
+ if observation is not None:
1171
+ failure = (error if type(error) is ModelResponseError else
1172
+ ModelResponseError(error, observed_usage))
1173
+ failure.provider_input = observation
1174
+ raise failure from None
1175
+ raise
465
1176
  if last_length_error is not None:
466
1177
  raise last_length_error
467
1178
  raise AssertionError("unreachable")
468
1179
 
1180
+ async def stream(
1181
+ self,
1182
+ model: ResolvedModel,
1183
+ *,
1184
+ messages: list[dict[str, Any]],
1185
+ network_policy: NetworkPolicy,
1186
+ timeout_seconds: float,
1187
+ tools: list[dict[str, Any]] | None = None,
1188
+ max_output_tokens: int | None = None,
1189
+ output_budget_mode: Literal["adaptive", "fixed"] = "adaptive",
1190
+ reasoning_effort: str | None = None,
1191
+ retry_empty_stream: bool = True,
1192
+ tool_choice: str | dict[str, Any] | None = None,
1193
+ parallel_tool_calls: bool | None = None,
1194
+ output_token_parameter: Literal["max_tokens", "max_completion_tokens"] | None = None,
1195
+ compatibility_profile: provider_compat.CompatProfile | None = None,
1196
+ thinking_mode: Literal["disabled"] | None = None,
1197
+ ) -> AsyncIterator[ModelStreamChunk]:
1198
+ """Stream chat-completions deltas without buffering the response.
1199
+
1200
+ Studio's Harness path uses the same credential and network policy as
1201
+ ``complete``. Wire-specific decoders share byte framing and lifecycle
1202
+ ownership; provider events remain outside the Native decision core.
1203
+ """
1204
+ if (model.wire_api or "chat").strip().lower() == "responses":
1205
+ iterator = self._stream_responses(
1206
+ model,
1207
+ messages=messages,
1208
+ network_policy=network_policy,
1209
+ timeout_seconds=timeout_seconds,
1210
+ tools=tools,
1211
+ max_output_tokens=max_output_tokens,
1212
+ reasoning_effort=reasoning_effort,
1213
+ thinking_mode=thinking_mode,
1214
+ compatibility_profile=compatibility_profile,
1215
+ tool_choice=tool_choice,
1216
+ parallel_tool_calls=parallel_tool_calls,
1217
+ output_token_parameter=output_token_parameter,
1218
+ )
1219
+ async with owning_async_iterator(iterator, owner="studio.responses"):
1220
+ async for chunk in iterator:
1221
+ yield chunk
1222
+ return
1223
+ if (model.wire_api or "chat").strip().lower() == "anthropic":
1224
+ iterator = self._stream_anthropic(
1225
+ model,
1226
+ messages=messages,
1227
+ network_policy=network_policy,
1228
+ timeout_seconds=timeout_seconds,
1229
+ tools=tools,
1230
+ max_output_tokens=max_output_tokens,
1231
+ reasoning_effort=reasoning_effort,
1232
+ thinking_mode=thinking_mode,
1233
+ compatibility_profile=compatibility_profile,
1234
+ tool_choice=tool_choice,
1235
+ parallel_tool_calls=parallel_tool_calls,
1236
+ output_token_parameter=output_token_parameter,
1237
+ )
1238
+ async with owning_async_iterator(iterator, owner="studio.anthropic"):
1239
+ async for chunk in iterator:
1240
+ yield chunk
1241
+ return
1242
+ await self.network_guard.check(model.endpoint_url, network_policy)
1243
+ credential = self.credential_resolver.resolve(model.credential_ref)
1244
+ profile = compatibility_profile or provider_compat.compat_profile(
1245
+ base_url=model.endpoint_url, model=model.model
1246
+ )
1247
+ payload: dict[str, Any] = {
1248
+ "model": model.model,
1249
+ "messages": messages,
1250
+ "stream": True,
1251
+ # OpenAI-compatible chat streams do not have to include usage in
1252
+ # ordinary delta chunks. Request the terminal usage chunk when
1253
+ # the endpoint profile supports it; ``apply_chat_compat`` removes
1254
+ # this option for strict providers that reject the extension.
1255
+ "stream_options": {"include_usage": True},
1256
+ }
1257
+ if model.parameters.temperature is not None:
1258
+ payload["temperature"] = model.parameters.temperature
1259
+ if model.parameters.max_tokens is not None:
1260
+ payload["max_tokens"] = model.parameters.max_tokens
1261
+ if max_output_tokens is not None:
1262
+ payload["max_tokens"] = max_output_tokens
1263
+ if model.parameters.top_p is not None:
1264
+ payload["top_p"] = model.parameters.top_p
1265
+ if tools:
1266
+ payload["tools"] = tools
1267
+ payload["tool_choice"] = "auto"
1268
+ payload["parallel_tool_calls"] = True
1269
+ payload = provider_compat.apply_chat_compat(payload, profile, model=model.model)
1270
+ self._apply_tool_choice(payload, "chat", tool_choice)
1271
+ self._apply_parallel_tool_policy(payload, "chat", parallel_tool_calls)
1272
+ # Fixed allocations are owned by the caller, including empty-stream
1273
+ # fallback. Only the legacy adaptive path may apply recommendations.
1274
+ if max_output_tokens is not None and output_budget_mode == "adaptive":
1275
+ effective_budget = provider_compat.required_output_budget(
1276
+ profile, payload.get("max_tokens")
1277
+ )
1278
+ if effective_budget is not None:
1279
+ payload["max_tokens"] = effective_budget
1280
+ effective_effort = _effective_reasoning_effort(model, tools, reasoning_effort, profile)
1281
+ self._apply_reasoning_effort(payload, "chat", effective_effort)
1282
+ self._apply_thinking_policy(payload, "chat", profile, tools, effective_effort,
1283
+ thinking_mode=thinking_mode)
1284
+ payload = provider_compat.apply_chat_compat(payload, profile, model=model.model)
1285
+ self._apply_output_token_parameter(payload, "chat", output_token_parameter)
1286
+ headers = {
1287
+ "Authorization": f"Bearer {credential}",
1288
+ "Content-Type": "application/json",
1289
+ "Accept": "text/event-stream",
1290
+ }
1291
+ timeout = httpx.Timeout(timeout_seconds, connect=min(10, timeout_seconds))
1292
+ client = self._http_client()
1293
+ request_started = time.monotonic()
1294
+
1295
+ async def recover_empty_response() -> ModelResponse:
1296
+ """Ask once for a usable answer after a reasoning-only stream.
1297
+
1298
+ Some OpenAI-compatible reasoning models occasionally finish a
1299
+ stream after emitting only ``reasoning_content``. Replaying the
1300
+ exact same request through the outer ten-attempt provider policy
1301
+ is both slow and usually deterministic. Give the provider one
1302
+ explicit, non-streaming repair turn instead; if that is empty as
1303
+ well, preserve ``MODEL_EMPTY_RESPONSE`` so the Harness can stop
1304
+ immediately rather than presenting it as transient congestion.
1305
+ """
1306
+
1307
+ return await self.complete(
1308
+ model,
1309
+ messages=[
1310
+ *messages,
1311
+ {
1312
+ "role": "system",
1313
+ "content": (
1314
+ "The previous response contained no usable final answer or tool "
1315
+ "call. Return either a concrete final answer or one valid native "
1316
+ "tool call now. Do not return reasoning alone."
1317
+ ),
1318
+ },
1319
+ ],
1320
+ network_policy=network_policy,
1321
+ timeout_seconds=timeout_seconds,
1322
+ max_attempts=1,
1323
+ backoff_seconds=0,
1324
+ tools=tools,
1325
+ retry_on_length=True,
1326
+ max_output_tokens=max_output_tokens,
1327
+ output_budget_mode=output_budget_mode,
1328
+ reasoning_effort=reasoning_effort,
1329
+ thinking_mode=thinking_mode,
1330
+ compatibility_profile=profile,
1331
+ **({"tool_choice": tool_choice} if tool_choice is not None else {}),
1332
+ **({"parallel_tool_calls": parallel_tool_calls}
1333
+ if parallel_tool_calls is not None else {}),
1334
+ **({"output_token_parameter": output_token_parameter}
1335
+ if output_token_parameter is not None else {}),
1336
+ )
1337
+
1338
+ try:
1339
+ response_context = client.stream(
1340
+ "POST", model.endpoint_url, headers=headers, json=payload, timeout=timeout
1341
+ )
1342
+ async with owning_async_context(response_context, owner="studio.chat_http") as response:
1343
+ if response.is_redirect or response.status_code >= 400:
1344
+ raise await _http_model_error(response, wire_api="chat")
1345
+ provider_request_id = _provider_request_id(response.headers)
1346
+ tool_stream = ChatToolCalls()
1347
+ first_chunk_logged = False
1348
+ raw_text = ""
1349
+ content_stream = ChatContent()
1350
+ emitted_text = ""
1351
+ pending_visible_text = ""
1352
+ textual_tool_detected = False
1353
+ latest_usage: Usage | None = None
1354
+ pending_done_finish_reason = ""
1355
+ refused = False
1356
+ wire_identity = ModelWireIdentity()
1357
+ last_repetition_check_length = 0
1358
+ async with _model_frames(
1359
+ response, idle_timeout_seconds=timeout_seconds,
1360
+ known_policy_abort=lambda: refused,
1361
+ ) as frames:
1362
+ async for frame in frames:
1363
+ raw = frame.data.strip()
1364
+ if raw == "[DONE]":
1365
+ break
1366
+ if not raw:
1367
+ continue
1368
+ chunk = decode_model_frame(raw)
1369
+ wire_identity.accept(chunk, wire_api="chat")
1370
+ failure = provider_error(
1371
+ chunk, wire_api="chat", headers=response.headers,
1372
+ provider_request_id=provider_request_id,
1373
+ usage=provider_usage(chunk.get("usage"), wire_api="chat"),
1374
+ )
1375
+ if failure is not None:
1376
+ raise failure
1377
+ choices = chat_choices(chunk)
1378
+ finish_reason = next(
1379
+ (
1380
+ choice["finish_reason"]
1381
+ for choice in choices
1382
+ if choice.get("finish_reason")
1383
+ ),
1384
+ "",
1385
+ )
1386
+ if (
1387
+ finish_reason
1388
+ and pending_done_finish_reason
1389
+ and finish_reason != pending_done_finish_reason
1390
+ ):
1391
+ raise invalid_model_frame()
1392
+ usage_value = provider_usage(chunk.get("usage"), wire_api="chat")
1393
+ if usage_value is not None:
1394
+ latest_usage = usage_value
1395
+ if usage_value is not None and not choices:
1396
+ # A usage-only frame can precede or follow the finish.
1397
+ # Report it now, so later absent/invalid data cannot
1398
+ # erase known counters. Do not synthesize a terminal.
1399
+ yield ModelStreamChunk(
1400
+ usage=usage_value, provider_request_id=provider_request_id,
1401
+ )
1402
+ if not choices and chunk.get("choices"):
1403
+ # Explicit alternative-only packets have no selected
1404
+ # choice. Their root snapshots cannot be attributed to
1405
+ # choice zero; keep only this attempt's known usage.
1406
+ continue
1407
+ root_message = chunk.get("message") or {}
1408
+ try:
1409
+ # Deltas precede same-frame complete snapshots. A
1410
+ # snapshot may already include that last fragment.
1411
+ for choice in choices:
1412
+ tool_stream.accept(choice.get("delta"), snapshot=False)
1413
+ tool_stream.accept(choice.get("message"), snapshot=True)
1414
+ tool_stream.accept(choice, snapshot=True)
1415
+ tool_stream.accept(root_message, snapshot=True)
1416
+ tool_stream.accept(chunk, snapshot=True)
1417
+ except StudioError as error:
1418
+ raise ModelResponseError(error, latest_usage) from None
1419
+ refused = refused or wire_refusal(
1420
+ root_message.get("refusal"), wire_api="chat",
1421
+ )
1422
+ content_choices = choices or ([{}] if any(
1423
+ root_message.get(key) is not None
1424
+ for key in ("content", "reasoning_content", "reasoning")
1425
+ ) else [])
1426
+ for choice in content_choices:
1427
+ delta = choice.get("delta") or {}
1428
+ refused = wire_refusal(delta.get("refusal"), wire_api="chat") or refused
1429
+ try:
1430
+ text, reasoning = content_stream.accept(
1431
+ delta, choice.get("message") or {}, root_message,
1432
+ )
1433
+ except StudioError as error:
1434
+ raise ModelResponseError(error, latest_usage) from None
1435
+ raw_text += text
1436
+ pending_visible_text += text
1437
+ message = choice.get("message") or {}
1438
+ refused = wire_refusal(message.get("refusal"), wire_api="chat") \
1439
+ or refused
1440
+ visible_text = ""
1441
+ if text and not textual_tool_detected:
1442
+ visible_text, pending_visible_text, marker_found = (
1443
+ _partition_tool_stream_text(pending_visible_text)
1444
+ )
1445
+ textual_tool_detected = marker_found
1446
+ emitted_text += visible_text
1447
+ visible_length = len(emitted_text) + len(pending_visible_text)
1448
+ if (
1449
+ not textual_tool_detected
1450
+ and visible_length - last_repetition_check_length >= 64
1451
+ ):
1452
+ last_repetition_check_length = visible_length
1453
+ _raise_for_repetitive_output(
1454
+ emitted_text + pending_visible_text
1455
+ )
1456
+ if visible_text or reasoning or usage_value is not None:
1457
+ if not first_chunk_logged and (text or reasoning):
1458
+ first_chunk_logged = True
1459
+ _LOGGER.info(
1460
+ "model stream first delta: model=%s ttfb_ms=%d",
1461
+ model.model,
1462
+ int((time.monotonic() - request_started) * 1000),
1463
+ )
1464
+ yield ModelStreamChunk(
1465
+ text=visible_text,
1466
+ reasoning=reasoning,
1467
+ usage=usage_value,
1468
+ provider_request_id=provider_request_id,
1469
+ )
1470
+ if finish_reason:
1471
+ pending_done_finish_reason = finish_reason
1472
+ content_stream.finished = True
1473
+ tool_stream.finished = True
1474
+ # A finish notification does not seal the HTTP response. Keep
1475
+ # consuming its usage and snapshots until [DONE] or natural
1476
+ # EOF, then publish exactly one final tool batch. A read error
1477
+ # before that boundary cannot authorize tool effects.
1478
+ if refused:
1479
+ yield ModelStreamChunk(
1480
+ usage=latest_usage, done=True, finish_reason="content_filter",
1481
+ provider_request_id=provider_request_id,
1482
+ )
1483
+ return
1484
+ empty_output_limit_retry = (
1485
+ retry_empty_stream and not tool_stream.calls and not raw_text.strip()
1486
+ and pending_done_finish_reason in {
1487
+ "length", "incomplete", "max_tokens", "max_output_tokens", "output_limit",
1488
+ }
1489
+ )
1490
+ if (pending_done_finish_reason in _INCOMPLETE_CALL_NON_SUCCESS_FINISH_REASONS
1491
+ and not empty_output_limit_retry):
1492
+ # An explicit non-success termination cannot authorize any
1493
+ # partially assembled tool. Preserve its reason; do not
1494
+ # retry. Explicit empty-stream compatibility repair is
1495
+ # limited to output caps with no tool envelope. An unfilled
1496
+ # call under a success/missing reason
1497
+ # still falls through to tool_stream.finish() and surfaces
1498
+ # as a provider_retryable_error.
1499
+ yield ModelStreamChunk(
1500
+ tool_calls=(), usage=latest_usage, done=True,
1501
+ finish_reason=pending_done_finish_reason,
1502
+ provider_request_id=provider_request_id,
1503
+ )
1504
+ return
1505
+ try:
1506
+ sealed_tools = tool_stream.finish()
1507
+ except StudioError as error:
1508
+ raise ModelResponseError(error, latest_usage) from None
1509
+ calls = tuple(ToolCall(id=value.call_id, name=value.name, arguments=value.arguments)
1510
+ for value in sealed_tools)
1511
+ final_text = raw_text
1512
+ if not calls and (
1513
+ textual_tool_detected
1514
+ or "DSML" in raw_text.replace("|", "|").upper()
1515
+ or "<tool_call" in raw_text.lower()
1516
+ ):
1517
+ final_text, recovered_calls, _ = _recover_textual_tool_calls(raw_text, tools)
1518
+ calls = tuple(recovered_calls)
1519
+ calls = _normalize_tool_call_arguments(calls, tools)
1520
+ if not final_text.strip() and not calls and retry_empty_stream:
1521
+ repair = await recover_empty_response()
1522
+ final_text = repair.content
1523
+ calls = tuple(repair.tool_calls)
1524
+ latest_usage = repair.usage
1525
+ if not calls and not textual_tool_detected:
1526
+ _raise_for_repetitive_output(final_text)
1527
+ remaining_text = final_text
1528
+ if emitted_text and final_text.startswith(emitted_text):
1529
+ remaining_text = final_text[len(emitted_text) :]
1530
+ if remaining_text:
1531
+ yield ModelStreamChunk(
1532
+ text=remaining_text,
1533
+ provider_request_id=provider_request_id,
1534
+ )
1535
+ yield ModelStreamChunk(
1536
+ tool_calls=calls,
1537
+ usage=latest_usage,
1538
+ done=True,
1539
+ finish_reason=pending_done_finish_reason,
1540
+ provider_request_id=provider_request_id,
1541
+ )
1542
+ except asyncio.CancelledError:
1543
+ raise
1544
+
1545
+ async def _stream_responses(
1546
+ self,
1547
+ model: ResolvedModel,
1548
+ *,
1549
+ messages: list[dict[str, Any]],
1550
+ network_policy: NetworkPolicy,
1551
+ timeout_seconds: float,
1552
+ tools: list[dict[str, Any]] | None = None,
1553
+ max_output_tokens: int | None = None,
1554
+ reasoning_effort: str | None = None,
1555
+ compatibility_profile: provider_compat.CompatProfile | None = None,
1556
+ thinking_mode: Literal["disabled"] | None = None,
1557
+ tool_choice: str | dict[str, Any] | None = None,
1558
+ parallel_tool_calls: bool | None = None,
1559
+ output_token_parameter: Literal["max_tokens", "max_completion_tokens"] | None = None,
1560
+ ) -> AsyncIterator[ModelStreamChunk]:
1561
+ """Adapt the native Responses SSE event stream to Harness chunks."""
1562
+
1563
+ await self.network_guard.check(model.endpoint_url, network_policy)
1564
+ credential = self.credential_resolver.resolve(model.credential_ref)
1565
+ payload = self._responses_payload(model, messages, tools)
1566
+ self._apply_tool_choice(payload, "responses", tool_choice)
1567
+ self._apply_parallel_tool_policy(payload, "responses", parallel_tool_calls)
1568
+ self._apply_output_token_parameter(payload, "responses", output_token_parameter)
1569
+ payload["stream"] = True
1570
+ if max_output_tokens is not None:
1571
+ payload["max_output_tokens"] = max_output_tokens
1572
+ if reasoning_effort:
1573
+ profile = compatibility_profile or provider_compat.compat_profile(
1574
+ base_url=model.endpoint_url, model=model.model,
1575
+ )
1576
+ self._apply_reasoning_effort(
1577
+ payload, "responses", _effective_reasoning_effort(
1578
+ model, None, reasoning_effort, profile,
1579
+ ),
1580
+ )
1581
+ headers = {
1582
+ "Authorization": f"Bearer {credential}",
1583
+ "Content-Type": "application/json",
1584
+ "Accept": "text/event-stream",
1585
+ }
1586
+ timeout = httpx.Timeout(timeout_seconds, connect=min(10, timeout_seconds))
1587
+ client = self._http_client()
1588
+ request_started = time.monotonic()
1589
+ first_delta_logged = False
1590
+ tool_items = ResponsesToolCalls()
1591
+ content_items = ResponsesContent(tool_items.items)
1592
+ from ksadk.harness.public_summary import PublicSummaryBuffer
1593
+
1594
+ summary_buffer = PublicSummaryBuffer()
1595
+ raw_text = ""
1596
+ raw_reasoning = ""
1597
+ refused = False
1598
+ wire_identity = ModelWireIdentity()
1599
+ response_context = client.stream(
1600
+ "POST", model.endpoint_url, headers=headers, json=payload, timeout=timeout
1601
+ )
1602
+ async with owning_async_context(
1603
+ response_context, owner="studio.responses_http"
1604
+ ) as response:
1605
+ if response.is_redirect or response.status_code >= 400:
1606
+ raise await _http_model_error(response, wire_api="responses")
1607
+ provider_request_id = _provider_request_id(response.headers)
1608
+ async with _model_frames(
1609
+ response, idle_timeout_seconds=timeout_seconds,
1610
+ known_policy_abort=lambda: refused,
1611
+ ) as frames:
1612
+ async for frame in frames:
1613
+ raw = frame.data.strip()
1614
+ if not raw or raw == "[DONE]":
1615
+ continue
1616
+ event = decode_model_frame(raw)
1617
+ wire_identity.accept(event, wire_api="responses")
1618
+ if event.get("type") in {"error", "response.failed"}:
1619
+ failed_body = event.get("response")
1620
+ if failed_body is not None and not isinstance(failed_body, dict):
1621
+ raise invalid_model_frame()
1622
+ if event["type"] == "response.failed":
1623
+ failed_body = dict(failed_body or {})
1624
+ if failed_body.get("status") not in (None, "failed"):
1625
+ raise invalid_model_frame()
1626
+ failed_body["status"] = "failed"
1627
+ else:
1628
+ failed_body = {"error": event.get("error", event),
1629
+ "usage": event.get("usage")}
1630
+ failure = provider_error(
1631
+ failed_body, wire_api="responses", headers=response.headers,
1632
+ provider_request_id=provider_request_id,
1633
+ usage=provider_usage(failed_body.get("usage"), wire_api="responses"),
1634
+ )
1635
+ assert failure is not None
1636
+ raise failure
1637
+ completed_tool = tool_items.accept(event)
1638
+ text, reasoning = content_items.accept(event)
1639
+ event_type = str(event.get("type") or "")
1640
+ public_summary = summary_buffer.drain(content_items.public_summary(), final=(
1641
+ event_type in {"response.completed", "response.incomplete"}
1642
+ ))
1643
+ if public_summary and model.parameters.public_thinking_summary:
1644
+ yield ModelStreamChunk(public_summary=public_summary,
1645
+ provider_request_id=provider_request_id)
1646
+ refused = refused or content_items.refused
1647
+ usage_value: Usage | None = None
1648
+ if event_type in {"response.completed", "response.incomplete"}:
1649
+ response_payload = event.get("response") or {}
1650
+ usage_value = provider_usage(
1651
+ response_payload.get("usage"), wire_api="responses",
1652
+ )
1653
+ elif event_type in {
1654
+ "error", "response.failed", "response.cancelled", "response.canceled",
1655
+ }:
1656
+ raise StudioError(
1657
+ "MODEL_REQUEST_FAILED",
1658
+ "模型 Responses 流式响应未完成",
1659
+ status_code=502,
1660
+ details={"eventType": event_type},
1661
+ )
1662
+ if text or reasoning:
1663
+ raw_text += text
1664
+ raw_reasoning += reasoning
1665
+ if not first_delta_logged:
1666
+ first_delta_logged = True
1667
+ _LOGGER.info(
1668
+ "model Responses stream first delta: model=%s ttfb_ms=%d",
1669
+ model.model,
1670
+ int((time.monotonic() - request_started) * 1000),
1671
+ )
1672
+ yield ModelStreamChunk(
1673
+ text=text,
1674
+ reasoning=reasoning,
1675
+ provider_request_id=provider_request_id,
1676
+ )
1677
+ if completed_tool is not None and not refused:
1678
+ yield ModelStreamChunk(
1679
+ tool_calls=(ToolCall(
1680
+ id=completed_tool.call_id, name=completed_tool.name,
1681
+ arguments=completed_tool.arguments or "{}",
1682
+ ),),
1683
+ provider_request_id=provider_request_id,
1684
+ )
1685
+ if event_type in {"response.completed", "response.incomplete"}:
1686
+ try:
1687
+ snapshot = reconcile_responses_snapshot(
1688
+ response_payload,
1689
+ status="completed" if event_type == "response.completed"
1690
+ else "incomplete",
1691
+ tools=tool_items, text=raw_text, reasoning=raw_reasoning,
1692
+ content=content_items,
1693
+ )
1694
+ except StudioError as error:
1695
+ raise ModelResponseError(error, usage_value) from None
1696
+ if snapshot is not None:
1697
+ refused = refused or snapshot.projection.finish == "content_filter"
1698
+ if not refused:
1699
+ if snapshot.text_tail or snapshot.reasoning_tail:
1700
+ yield ModelStreamChunk(
1701
+ text=snapshot.text_tail, reasoning=snapshot.reasoning_tail,
1702
+ provider_request_id=provider_request_id,
1703
+ )
1704
+ for tool in snapshot.completed_calls:
1705
+ yield ModelStreamChunk(tool_calls=(ToolCall(
1706
+ id=tool.call_id, name=tool.name,
1707
+ arguments=tool.arguments or "{}",
1708
+ ),), provider_request_id=provider_request_id)
1709
+ if refused:
1710
+ yield ModelStreamChunk(
1711
+ usage=usage_value, done=True, finish_reason="content_filter",
1712
+ provider_request_id=provider_request_id,
1713
+ )
1714
+ return
1715
+ if event_type == "response.completed":
1716
+ try:
1717
+ tool_items.finish()
1718
+ content_items.finish()
1719
+ except StudioError as error:
1720
+ raise ModelResponseError(error, usage_value) from None
1721
+ finish_reason = "tool_calls" if tool_items.calls else "stop"
1722
+ else:
1723
+ details = response_payload.get("incomplete_details")
1724
+ if details is not None and not isinstance(details, dict):
1725
+ raise invalid_model_frame()
1726
+ reason = (details or {}).get("reason")
1727
+ if reason is not None and type(reason) is not str:
1728
+ raise invalid_model_frame()
1729
+ if reason == "max_output_tokens":
1730
+ try:
1731
+ content_items.finish(require_closed=False)
1732
+ except StudioError as error:
1733
+ raise ModelResponseError(error, usage_value) from None
1734
+ # An unclosed input must not grant bounded-text commit
1735
+ # authority even when output exhaustion is confirmed.
1736
+ open_input = any(not tool.closed for tool in tool_items.calls.values())
1737
+ finish_reason = (
1738
+ "content_filter" if reason == "content_filter"
1739
+ else "length" if reason == "max_output_tokens" and not open_input
1740
+ else "incomplete"
1741
+ )
1742
+ if snapshot is not None and snapshot.projection.calls:
1743
+ finish_reason = snapshot.projection.finish
1744
+ yield ModelStreamChunk(
1745
+ usage=usage_value, done=True, finish_reason=finish_reason,
1746
+ provider_request_id=provider_request_id,
1747
+ )
1748
+ return
1749
+
1750
+ if refused:
1751
+ yield ModelStreamChunk(
1752
+ done=True, finish_reason="content_filter",
1753
+ provider_request_id=provider_request_id,
1754
+ )
1755
+
1756
+ async def _stream_anthropic(
1757
+ self,
1758
+ model: ResolvedModel,
1759
+ *,
1760
+ messages: list[dict[str, Any]],
1761
+ network_policy: NetworkPolicy,
1762
+ timeout_seconds: float,
1763
+ tools: list[dict[str, Any]] | None = None,
1764
+ max_output_tokens: int | None = None,
1765
+ reasoning_effort: str | None = None,
1766
+ compatibility_profile: provider_compat.CompatProfile | None = None,
1767
+ thinking_mode: Literal["disabled"] | None = None,
1768
+ tool_choice: str | dict[str, Any] | None = None,
1769
+ parallel_tool_calls: bool | None = None,
1770
+ output_token_parameter: Literal["max_tokens", "max_completion_tokens"] | None = None,
1771
+ ) -> AsyncIterator[ModelStreamChunk]:
1772
+ """Adapt the native Anthropic Messages SSE event stream to Harness chunks."""
1773
+
1774
+ await self.network_guard.check(model.endpoint_url, network_policy)
1775
+ credential = self.credential_resolver.resolve(model.credential_ref)
1776
+ profile = compatibility_profile or provider_compat.compat_profile(
1777
+ base_url=model.endpoint_url, model=model.model,
1778
+ )
1779
+ payload = self._anthropic_payload(model, messages, tools, compatibility_profile=profile)
1780
+ self._apply_tool_choice(payload, "anthropic", tool_choice)
1781
+ self._apply_parallel_tool_policy(payload, "anthropic", parallel_tool_calls)
1782
+ self._apply_output_token_parameter(payload, "anthropic", output_token_parameter)
1783
+ payload["stream"] = True
1784
+ if max_output_tokens is not None:
1785
+ payload["max_tokens"] = max_output_tokens
1786
+ effective_effort = _effective_reasoning_effort(model, tools, reasoning_effort, profile)
1787
+ self._apply_reasoning_effort(
1788
+ payload, "anthropic", effective_effort,
1789
+ )
1790
+ self._apply_thinking_policy(payload, "anthropic", profile, tools, effective_effort,
1791
+ thinking_mode=thinking_mode)
1792
+ headers = self._anthropic_headers(credential, payload)
1793
+ headers["Accept"] = "text/event-stream"
1794
+ timeout = httpx.Timeout(timeout_seconds, connect=min(10, timeout_seconds))
1795
+ client = self._http_client()
1796
+ request_started = time.monotonic()
1797
+ first_delta_logged = False
1798
+ # content_block index → tool_use block 累积(id/name 来自 block_start,参数来自分片)。
1799
+ tool_blocks = AnthropicToolBlocks()
1800
+ content_stream = AnthropicContent(tool_blocks)
1801
+ raw_text = ""
1802
+ stop_reason = ""
1803
+ refused = False
1804
+ usage_accumulator = AnthropicUsageAccumulator()
1805
+ wire_identity = ModelWireIdentity()
1806
+ response_context = client.stream(
1807
+ "POST", model.endpoint_url, headers=headers, json=payload, timeout=timeout
1808
+ )
1809
+ async with owning_async_context(
1810
+ response_context, owner="studio.anthropic_http"
1811
+ ) as response:
1812
+ if response.is_redirect or response.status_code >= 400:
1813
+ raise await _http_model_error(response, wire_api="anthropic")
1814
+ async with _model_frames(
1815
+ response, idle_timeout_seconds=timeout_seconds,
1816
+ known_policy_abort=lambda: refused or stop_reason == "refusal",
1817
+ ) as frames:
1818
+ async for frame in frames:
1819
+ raw = frame.data.strip()
1820
+ if not raw or raw == "[DONE]":
1821
+ continue
1822
+ event = decode_model_frame(raw)
1823
+ wire_identity.accept(event, wire_api="anthropic")
1824
+ if event.get("type") == "error":
1825
+ failure = provider_error(
1826
+ event, wire_api="anthropic", headers=response.headers,
1827
+ provider_request_id=_provider_request_id(response.headers),
1828
+ usage=provider_usage(event.get("usage"), wire_api="anthropic"),
1829
+ )
1830
+ if failure is None:
1831
+ raise invalid_model_frame()
1832
+ raise failure
1833
+ tool_blocks.accept(event)
1834
+ text, reasoning = content_stream.accept(event)
1835
+ event_type = str(event.get("type") or "")
1836
+ usage_update: Usage | None = None
1837
+ if event_type == "message_start":
1838
+ message_usage = (event.get("message") or {}).get("usage")
1839
+ usage_update = usage_accumulator.update(message_usage)
1840
+ elif event_type == "message_delta":
1841
+ delta = event.get("delta") or {}
1842
+ incoming_stop = delta.get("stop_reason")
1843
+ if incoming_stop:
1844
+ if stop_reason and incoming_stop != stop_reason:
1845
+ raise invalid_model_frame()
1846
+ stop_reason = incoming_stop
1847
+ content_stream.finished = True
1848
+ usage_update = usage_accumulator.update(event.get("usage"))
1849
+ try:
1850
+ refused = wire_refusal(
1851
+ delta.get("stop_details"), wire_api="anthropic",
1852
+ ) or refused
1853
+ except StudioError as error:
1854
+ raise ModelResponseError(error, usage_accumulator.report()) from None
1855
+ elif event_type == "error":
1856
+ raise StudioError(
1857
+ "MODEL_REQUEST_FAILED",
1858
+ "模型 Anthropic 流式响应未完成",
1859
+ status_code=502,
1860
+ details={
1861
+ "eventType": str(
1862
+ (event.get("error") or {}).get("type") or event_type
1863
+ )
1864
+ },
1865
+ )
1866
+ if event_type in {"message_start", "message_delta"}:
1867
+ fields = event["message"] if event_type == "message_start" else event
1868
+ try:
1869
+ observed = input_observation(fields, payload, event_type)
1870
+ except (ValueError, TypeError):
1871
+ raise ModelResponseError(
1872
+ invalid_model_frame(), usage_accumulator.report(),
1873
+ ) from None
1874
+ if observed is not None:
1875
+ # Final fallback metadata replaces the start array, including [].
1876
+ yield ModelStreamChunk(provider_input=observed,
1877
+ provider_request_id=_provider_request_id(
1878
+ response.headers,
1879
+ ))
1880
+ raw_text += text
1881
+ if text or reasoning or usage_update is not None:
1882
+ if not first_delta_logged and (text or reasoning):
1883
+ first_delta_logged = True
1884
+ _LOGGER.info(
1885
+ "model Anthropic stream first delta: model=%s ttfb_ms=%d",
1886
+ model.model,
1887
+ int((time.monotonic() - request_started) * 1000),
1888
+ )
1889
+ yield ModelStreamChunk(text=text, reasoning=reasoning, usage=usage_update)
1890
+ if event_type == "message_stop":
1891
+ if refused or stop_reason == "refusal":
1892
+ yield ModelStreamChunk(
1893
+ usage=usage_accumulator.report(), done=True,
1894
+ finish_reason="content_filter",
1895
+ )
1896
+ return
1897
+ try:
1898
+ text_tail, reasoning_tail = content_stream.finish()
1899
+ except StudioError as error:
1900
+ raise ModelResponseError(error, usage_accumulator.report()) from None
1901
+ if text_tail or reasoning_tail:
1902
+ raw_text += text_tail
1903
+ yield ModelStreamChunk(text=text_tail, reasoning=reasoning_tail)
1904
+ calls = tuple(
1905
+ ToolCall(
1906
+ id=value.call_id, name=value.name,
1907
+ arguments=value.arguments or "{}",
1908
+ )
1909
+ for value in tool_blocks.finish()
1910
+ )
1911
+ calls = _normalize_tool_call_arguments(calls, tools)
1912
+ finish_reason = {
1913
+ "end_turn": "stop",
1914
+ "stop_sequence": "stop",
1915
+ "max_tokens": "length",
1916
+ "tool_use": "tool_calls",
1917
+ "refusal": "content_filter",
1918
+ }.get(stop_reason, stop_reason)
1919
+ if refused:
1920
+ finish_reason, calls = "content_filter", ()
1921
+ if not raw_text.strip() and not calls and finish_reason != "content_filter":
1922
+ raise StudioError(
1923
+ "MODEL_EMPTY_RESPONSE",
1924
+ "模型未返回可用内容",
1925
+ status_code=502,
1926
+ details={"finishReason": finish_reason},
1927
+ )
1928
+ if not calls and raw_text.strip():
1929
+ _raise_for_repetitive_output(raw_text)
1930
+ usage_value = usage_accumulator.report()
1931
+ yield ModelStreamChunk(
1932
+ tool_calls=calls, usage=usage_value, done=True,
1933
+ finish_reason=finish_reason,
1934
+ provider_content=content_stream.replay_content(),
1935
+ )
1936
+ return
1937
+
1938
+ if refused or stop_reason == "refusal":
1939
+ yield ModelStreamChunk(
1940
+ usage=usage_accumulator.report(), done=True, finish_reason="content_filter",
1941
+ )
1942
+
469
1943
  @staticmethod
470
1944
  def _is_length_truncation(exc: StudioError) -> bool:
471
1945
  if exc.code != "MODEL_EMPTY_RESPONSE":
@@ -474,11 +1948,145 @@ class OpenAICompatibleModelClient:
474
1948
 
475
1949
  @staticmethod
476
1950
  def _expand_length_budget(payload: dict[str, Any]) -> dict[str, Any]:
477
- key = "max_output_tokens" if "max_output_tokens" in payload else "max_tokens"
1951
+ if "max_output_tokens" in payload:
1952
+ key = "max_output_tokens"
1953
+ elif "max_completion_tokens" in payload:
1954
+ key = "max_completion_tokens"
1955
+ else:
1956
+ key = "max_tokens"
478
1957
  current = int(payload.get(key) or 0)
479
1958
  payload[key] = max(current, _LENGTH_RETRY_TARGET_TOKENS)
480
1959
  return payload
481
1960
 
1961
+ @staticmethod
1962
+ def _apply_tool_choice(
1963
+ payload: dict[str, Any], wire_api: str, choice: str | dict[str, Any] | None,
1964
+ ) -> None:
1965
+ """Project explicit host admission without silently downgrading it.
1966
+
1967
+ Omitted choice preserves each wire's existing default. Unsupported
1968
+ provider policies are reported by the ordinary error path; this layer
1969
+ never retries with auto or changes a reasoning profile to force tools.
1970
+ """
1971
+ if choice is None:
1972
+ return
1973
+ if isinstance(choice, dict):
1974
+ function = choice.get("function")
1975
+ if (set(choice) != {"type", "function"} or choice.get("type") != "function"
1976
+ or not isinstance(function, dict) or set(function) != {"name"}
1977
+ or not isinstance(function["name"], str) or not function["name"].strip()):
1978
+ raise ValueError("invalid named tool choice")
1979
+ name = function["name"]
1980
+ available = {
1981
+ tool.get("name") or (tool.get("function") or {}).get("name")
1982
+ for tool in payload.get("tools", ())
1983
+ }
1984
+ if name not in available:
1985
+ raise ValueError("named tool choice needs an advertised tool")
1986
+ payload["tool_choice"] = (
1987
+ {"type": "tool", "name": name} if wire_api == "anthropic"
1988
+ else {"type": "function", "name": name} if wire_api == "responses"
1989
+ else {"type": "function", "function": {"name": name}}
1990
+ )
1991
+ return
1992
+ if choice == "required" and not payload.get("tools"):
1993
+ raise ValueError("required tool choice needs an admitted tool")
1994
+ if wire_api == "anthropic":
1995
+ kinds = {"auto": "auto", "required": "any", "none": "none"}
1996
+ if choice not in kinds:
1997
+ raise ValueError("unsupported Anthropic tool choice")
1998
+ payload["tool_choice"] = {"type": kinds[choice]}
1999
+ else:
2000
+ payload["tool_choice"] = choice
2001
+
2002
+ @staticmethod
2003
+ def _apply_parallel_tool_policy(
2004
+ payload: dict[str, Any], wire_api: str, parallel: bool | None,
2005
+ ) -> None:
2006
+ """Preserve explicit generation policy independently of tool execution.
2007
+
2008
+ Omission keeps the existing wire defaults. This controls model output;
2009
+ it grants no permission to execute effects concurrently.
2010
+ """
2011
+ if parallel is None or not payload.get("tools"):
2012
+ return
2013
+ if wire_api == "anthropic":
2014
+ choice = dict(payload.get("tool_choice") or {"type": "auto"})
2015
+ if choice.get("type") != "none":
2016
+ choice["disable_parallel_tool_use"] = not parallel
2017
+ payload["tool_choice"] = choice
2018
+ else:
2019
+ payload["parallel_tool_calls"] = parallel
2020
+
2021
+ @staticmethod
2022
+ def _apply_output_token_parameter(
2023
+ payload: dict[str, Any], wire_api: str,
2024
+ parameter: Literal["max_tokens", "max_completion_tokens"] | None,
2025
+ ) -> None:
2026
+ """Rename the Chat limit after budget projection; preserve its value.
2027
+
2028
+ Responses and Anthropic keep their native output-limit fields. No
2029
+ model-name inference, second cap, or adaptive request is introduced.
2030
+ """
2031
+ if wire_api not in {"responses", "anthropic"} and parameter == "max_completion_tokens":
2032
+ if "max_tokens" in payload:
2033
+ payload["max_completion_tokens"] = payload.pop("max_tokens")
2034
+
2035
+ @staticmethod
2036
+ def _apply_thinking_policy(
2037
+ payload: dict[str, Any],
2038
+ wire_api: str,
2039
+ profile: provider_compat.CompatProfile,
2040
+ tools: list[dict[str, Any]] | None,
2041
+ effort: str | None,
2042
+ *, thinking_mode: Literal["disabled"] | None = None,
2043
+ ) -> None:
2044
+ if wire_api not in {"chat", "anthropic"} or not profile.supports_thinking_field:
2045
+ return
2046
+ if thinking_mode == "disabled":
2047
+ payload.update(provider_compat.with_thinking_disabled(
2048
+ payload, profile, reasoning_effort=effort,
2049
+ ))
2050
+ elif provider_compat.should_disable_thinking(
2051
+ profile, tools_present=bool(tools) and payload.get("tool_choice") not in (
2052
+ "none", {"type": "none"},
2053
+ ), max_output_tokens=payload.get("max_tokens"),
2054
+ reasoning_effort=effort,
2055
+ ):
2056
+ payload["thinking"] = {"type": "disabled"}
2057
+ elif wire_api == "anthropic" and profile.anthropic_block_binding == "drop_block":
2058
+ thinking = dict(payload.get("thinking") or {"type": "adaptive"})
2059
+ if thinking.get("type") in {"adaptive", "enabled"}:
2060
+ thinking["block_binding"] = {"prefix_mismatch_behavior": "drop_block"}
2061
+ payload["thinking"] = thinking
2062
+
2063
+ @staticmethod
2064
+ def _anthropic_headers(credential: str, payload: dict[str, Any]) -> dict[str, str]:
2065
+ headers = {
2066
+ "x-api-key": credential,
2067
+ "anthropic-version": ANTHROPIC_VERSION,
2068
+ "Content-Type": "application/json",
2069
+ }
2070
+ if "block_binding" in (payload.get("thinking") or {}):
2071
+ headers["anthropic-beta"] = "thinking-binding-controls-2026-08-01"
2072
+ return headers
2073
+
2074
+ @staticmethod
2075
+ def _apply_reasoning_effort(
2076
+ payload: dict[str, Any], wire_api: str, effort: str | None,
2077
+ ) -> None:
2078
+ """Project an already resolved effort without changing thinking mode or caps."""
2079
+ if effort is None:
2080
+ return
2081
+ if wire_api in {"responses", "anthropic"}:
2082
+ key = "reasoning" if wire_api == "responses" else "output_config"
2083
+ config = payload.get(key)
2084
+ config = dict(config) if isinstance(config, dict) else {}
2085
+ config["effort"] = effort
2086
+ payload[key] = config
2087
+ else:
2088
+ payload["reasoning_effort"] = effort
2089
+
482
2090
  @staticmethod
483
2091
  def _responses_payload(
484
2092
  model: ResolvedModel,
@@ -486,144 +2094,130 @@ class OpenAICompatibleModelClient:
486
2094
  tools: list[dict[str, Any]] | None,
487
2095
  ) -> dict[str, Any]:
488
2096
  """把 chat 语义消息映射到 Responses API 最小可用载荷。"""
489
- if tools:
490
- raise StudioError(
491
- "MODEL_REQUEST_FAILED",
492
- "Responses 端点暂不支持 tools 参数",
493
- status_code=422,
494
- )
495
- instructions: list[str] = []
496
- items: list[dict[str, Any]] = []
497
- for message in messages:
498
- role = str(message.get("role") or "user")
499
- text = str(message.get("content") or "")
500
- if role == "system":
501
- instructions.append(text)
502
- continue
503
- part_type = "output_text" if role == "assistant" else "input_text"
504
- items.append({"role": role, "content": [{"type": part_type, "text": text}]})
2097
+ # Responses tools are flat objects, unlike Chat Completions' nested
2098
+ # ``function`` envelope. Accept both shapes because Harness supplies
2099
+ # Chat-style schemas internally.
2100
+ responses_tools = []
2101
+ for tool in tools or []:
2102
+ function = tool.get("function") if isinstance(tool, dict) else None
2103
+ if isinstance(function, dict):
2104
+ responses_tools.append({"type": "function", **function})
2105
+ elif isinstance(tool, dict):
2106
+ responses_tools.append(dict(tool))
2107
+ instructions, items = responses_messages(messages)
505
2108
  payload: dict[str, Any] = {
506
2109
  "model": model.model,
507
2110
  "input": items,
508
2111
  }
2112
+ if responses_tools:
2113
+ payload["tools"] = responses_tools
2114
+ payload["tool_choice"] = "auto"
2115
+ payload["parallel_tool_calls"] = True
509
2116
  if model.parameters.max_tokens is not None:
510
2117
  payload["max_output_tokens"] = model.parameters.max_tokens
2118
+ if model.parameters.temperature is not None:
2119
+ payload["temperature"] = model.parameters.temperature
2120
+ if model.parameters.top_p is not None:
2121
+ payload["top_p"] = model.parameters.top_p
511
2122
  if instructions:
512
2123
  payload["instructions"] = "\n\n".join(instructions)
513
2124
  return payload
514
2125
 
515
2126
  @staticmethod
516
- def _parse_responses_response(
517
- response: httpx.Response, allow_empty: bool = False
2127
+ def _parse_buffered_response(
2128
+ response: httpx.Response, *, wire_api: str, allow_empty: bool = False,
518
2129
  ) -> ModelResponse:
519
2130
  if len(response.content) > 16 * 1024 * 1024:
520
2131
  raise StudioError(
521
- "MODEL_RESPONSE_TOO_LARGE",
522
- "模型响应超过 16 MiB 限制",
523
- status_code=502,
2132
+ "MODEL_RESPONSE_TOO_LARGE", "模型响应超过 16 MiB 限制", status_code=502,
524
2133
  )
2134
+ payload = decode_model_frame(response.content)
2135
+ validate_buffered_identity(payload, wire_api=wire_api)
2136
+ usage = provider_usage(payload.get("usage"), wire_api=wire_api)
525
2137
  try:
526
- payload = response.json()
527
- except ValueError as exc:
528
- raise StudioError(
529
- "MODEL_RESPONSE_INVALID",
530
- "模型响应不符合 Responses 协议",
531
- status_code=502,
532
- ) from exc
533
- content = payload.get("output_text")
534
- if not isinstance(content, str) or not content:
535
- parts: list[str] = []
536
- for item in payload.get("output") or []:
537
- if not isinstance(item, dict):
538
- continue
539
- for part in item.get("content") or []:
540
- if isinstance(part, dict) and part.get("type") in {"output_text", "text"}:
541
- parts.append(str(part.get("text") or ""))
542
- content = "".join(parts)
543
- status = str(payload.get("status") or "")
544
- if not content and not allow_empty:
545
- raise StudioError(
546
- "MODEL_EMPTY_RESPONSE",
547
- "模型未返回可用内容",
548
- status_code=502,
549
- details={"status": status, "finishReason": status},
2138
+ failure = provider_error(
2139
+ payload, wire_api=wire_api, usage=usage, headers=response.headers,
2140
+ provider_request_id=_provider_request_id(response.headers),
550
2141
  )
551
- raw_usage = payload.get("usage") or {}
552
- usage = Usage(
553
- input_tokens=int(raw_usage.get("input_tokens") or 0),
554
- output_tokens=int(raw_usage.get("output_tokens") or 0),
555
- total_tokens=int(raw_usage.get("total_tokens") or 0),
556
- cached_input_tokens=int(
557
- (raw_usage.get("input_tokens_details") or {}).get("cached_tokens") or 0
558
- ),
559
- reasoning_output_tokens=int(
560
- (raw_usage.get("output_tokens_details") or {}).get("reasoning_tokens") or 0
561
- ),
562
- )
2142
+ if failure is not None:
2143
+ raise failure
2144
+ parsed = buffered_projection(payload, wire_api=wire_api)
2145
+ except StudioError as error:
2146
+ raise ModelResponseError(error, usage) from None
2147
+ calls = [ToolCall(id=call.call_id, name=call.name, arguments=call.arguments, raw=call.raw)
2148
+ for call in parsed.calls]
2149
+ if (not parsed.content and not calls and not allow_empty
2150
+ and parsed.finish not in {"content_filter", "canceled", "cancelled"}):
2151
+ raise ModelResponseError(StudioError(
2152
+ "MODEL_EMPTY_RESPONSE", "模型未返回可用内容", status_code=502,
2153
+ details={"finishReason": parsed.finish},
2154
+ ), usage)
563
2155
  return ModelResponse(
564
- content=content,
565
- finish_reason="stop" if status == "completed" else status,
566
- usage=usage,
567
- tool_calls=[],
568
- raw_message={"output": payload.get("output")},
2156
+ content=parsed.content, finish_reason=parsed.finish, usage=usage or Usage(),
2157
+ tool_calls=calls,
2158
+ raw_message=parsed.raw_message, reasoning=parsed.reasoning,
2159
+ provider_request_id=_provider_request_id(response.headers),
569
2160
  )
570
2161
 
571
2162
  @staticmethod
572
- def _parse_response(response: httpx.Response, allow_empty: bool = False) -> ModelResponse:
573
- if len(response.content) > 16 * 1024 * 1024:
574
- raise StudioError(
575
- "MODEL_RESPONSE_TOO_LARGE",
576
- "模型响应超过 16 MiB 限制",
577
- status_code=502,
578
- )
579
- try:
580
- payload = response.json()
581
- choice = payload["choices"][0]
582
- message = choice["message"]
583
- except (ValueError, KeyError, IndexError, TypeError) as exc:
584
- raise StudioError(
585
- "MODEL_RESPONSE_INVALID",
586
- "模型响应不符合 OpenAI-compatible 协议",
587
- status_code=502,
588
- ) from exc
589
- calls = []
590
- for raw in message.get("tool_calls") or []:
591
- function = raw.get("function") or {}
592
- calls.append(
593
- ToolCall(
594
- id=str(raw.get("id") or ""),
595
- name=str(function.get("name") or ""),
596
- arguments=str(function.get("arguments") or "{}"),
597
- raw=raw,
598
- )
599
- )
600
- content = str(message.get("content") or "")
601
- finish_reason = str(choice.get("finish_reason") or "")
602
- if not content and not calls and not allow_empty:
603
- raise StudioError(
604
- "MODEL_EMPTY_RESPONSE",
605
- "模型未返回可用内容",
606
- status_code=502,
607
- details={"finishReason": finish_reason},
608
- )
609
- raw_usage = payload.get("usage") or {}
610
- usage = Usage(
611
- input_tokens=int(raw_usage.get("prompt_tokens") or 0),
612
- output_tokens=int(raw_usage.get("completion_tokens") or 0),
613
- total_tokens=int(raw_usage.get("total_tokens") or 0),
614
- cached_input_tokens=int(
615
- (raw_usage.get("prompt_tokens_details") or {}).get("cached_tokens") or 0
616
- ),
617
- reasoning_output_tokens=int(
618
- (raw_usage.get("completion_tokens_details") or {}).get("reasoning_tokens") or 0
619
- ),
620
- reported=bool(raw_usage),
621
- source="model-provider" if raw_usage else None,
2163
+ def _parse_responses_response(
2164
+ response: httpx.Response, allow_empty: bool = False,
2165
+ ) -> ModelResponse:
2166
+ return OpenAICompatibleModelClient._parse_buffered_response(
2167
+ response, wire_api="responses", allow_empty=allow_empty,
622
2168
  )
623
- return ModelResponse(
624
- content=content,
625
- finish_reason=finish_reason,
626
- usage=usage,
627
- tool_calls=calls,
628
- raw_message=message,
2169
+
2170
+ @staticmethod
2171
+ def _anthropic_payload(
2172
+ model: ResolvedModel,
2173
+ messages: list[dict[str, Any]],
2174
+ tools: list[dict[str, Any]] | None,
2175
+ *,
2176
+ compatibility_profile: provider_compat.CompatProfile | None = None,
2177
+ ) -> dict[str, Any]:
2178
+ """把 chat 语义消息映射到 Anthropic Messages API 最小可用载荷。"""
2179
+ profile = compatibility_profile or provider_compat.compat_profile(
2180
+ base_url=model.endpoint_url, model=model.model,
2181
+ )
2182
+ system_parts, merged = anthropic_messages(
2183
+ messages, mid_system=profile.anthropic_mid_system is True,
2184
+ )
2185
+ payload: dict[str, Any] = {
2186
+ "model": model.model,
2187
+ "messages": merged,
2188
+ # Anthropic Messages wire 必填 max_tokens;未配置 profile 参数时给安全默认。
2189
+ "max_tokens": model.parameters.max_tokens or _ANTHROPIC_DEFAULT_MAX_TOKENS,
2190
+ }
2191
+ if model.parameters.temperature is not None:
2192
+ payload["temperature"] = model.parameters.temperature
2193
+ if model.parameters.top_p is not None:
2194
+ payload["top_p"] = model.parameters.top_p
2195
+ if system_parts:
2196
+ payload["system"] = "\n\n".join(system_parts)
2197
+ anthropic_tools = []
2198
+ for tool in tools or []:
2199
+ function = tool.get("function") if isinstance(tool, dict) else None
2200
+ if not isinstance(function, dict):
2201
+ continue
2202
+ anthropic_tools.append({
2203
+ "name": str(function.get("name") or ""),
2204
+ "description": str(function.get("description") or ""),
2205
+ "input_schema": function.get("parameters") or {"type": "object", "properties": {}},
2206
+ })
2207
+ if anthropic_tools:
2208
+ payload["tools"] = anthropic_tools
2209
+ return payload
2210
+
2211
+ @staticmethod
2212
+ def _parse_anthropic_response(
2213
+ response: httpx.Response, allow_empty: bool = False,
2214
+ ) -> ModelResponse:
2215
+ return OpenAICompatibleModelClient._parse_buffered_response(
2216
+ response, wire_api="anthropic", allow_empty=allow_empty,
2217
+ )
2218
+
2219
+ @staticmethod
2220
+ def _parse_response(response: httpx.Response, allow_empty: bool = False) -> ModelResponse:
2221
+ return OpenAICompatibleModelClient._parse_buffered_response(
2222
+ response, wire_api="chat", allow_empty=allow_empty,
629
2223
  )