pythinker-code 2.0.0__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 (731) hide show
  1. pythinker_code/CHANGELOG.md +16 -0
  2. pythinker_code/__init__.py +0 -0
  3. pythinker_code/__main__.py +92 -0
  4. pythinker_code/acp/AGENTS.md +93 -0
  5. pythinker_code/acp/__init__.py +13 -0
  6. pythinker_code/acp/convert.py +128 -0
  7. pythinker_code/acp/host.py +298 -0
  8. pythinker_code/acp/mcp.py +46 -0
  9. pythinker_code/acp/server.py +497 -0
  10. pythinker_code/acp/session.py +496 -0
  11. pythinker_code/acp/tools.py +167 -0
  12. pythinker_code/acp/types.py +13 -0
  13. pythinker_code/acp/version.py +45 -0
  14. pythinker_code/agents/default/agent.yaml +36 -0
  15. pythinker_code/agents/default/coder.yaml +25 -0
  16. pythinker_code/agents/default/explore.yaml +46 -0
  17. pythinker_code/agents/default/plan.yaml +30 -0
  18. pythinker_code/agents/default/system.md +164 -0
  19. pythinker_code/agents/okabe/agent.yaml +22 -0
  20. pythinker_code/agentspec.py +163 -0
  21. pythinker_code/app.py +820 -0
  22. pythinker_code/approval_runtime/__init__.py +29 -0
  23. pythinker_code/approval_runtime/models.py +42 -0
  24. pythinker_code/approval_runtime/runtime.py +235 -0
  25. pythinker_code/auth/__init__.py +25 -0
  26. pythinker_code/auth/anthropic_direct.py +207 -0
  27. pythinker_code/auth/deepseek.py +192 -0
  28. pythinker_code/auth/lm_studio.py +418 -0
  29. pythinker_code/auth/minimax.py +203 -0
  30. pythinker_code/auth/oauth.py +1122 -0
  31. pythinker_code/auth/ollama.py +293 -0
  32. pythinker_code/auth/openai.py +771 -0
  33. pythinker_code/auth/opencode_go.py +203 -0
  34. pythinker_code/auth/openrouter.py +225 -0
  35. pythinker_code/auth/platforms.py +466 -0
  36. pythinker_code/background/__init__.py +36 -0
  37. pythinker_code/background/agent_runner.py +231 -0
  38. pythinker_code/background/ids.py +19 -0
  39. pythinker_code/background/manager.py +650 -0
  40. pythinker_code/background/models.py +105 -0
  41. pythinker_code/background/store.py +237 -0
  42. pythinker_code/background/summary.py +66 -0
  43. pythinker_code/background/worker.py +209 -0
  44. pythinker_code/cli/__init__.py +1326 -0
  45. pythinker_code/cli/__main__.py +19 -0
  46. pythinker_code/cli/_lazy_group.py +238 -0
  47. pythinker_code/cli/export.py +322 -0
  48. pythinker_code/cli/info.py +62 -0
  49. pythinker_code/cli/mcp.py +349 -0
  50. pythinker_code/cli/plugin.py +351 -0
  51. pythinker_code/cli/toad.py +74 -0
  52. pythinker_code/cli/vis.py +38 -0
  53. pythinker_code/cli/web.py +80 -0
  54. pythinker_code/config.py +453 -0
  55. pythinker_code/constant.py +33 -0
  56. pythinker_code/exception.py +43 -0
  57. pythinker_code/hooks/__init__.py +4 -0
  58. pythinker_code/hooks/config.py +34 -0
  59. pythinker_code/hooks/engine.py +371 -0
  60. pythinker_code/hooks/events.py +190 -0
  61. pythinker_code/hooks/runner.py +89 -0
  62. pythinker_code/llm.py +412 -0
  63. pythinker_code/metadata.py +79 -0
  64. pythinker_code/notifications/__init__.py +33 -0
  65. pythinker_code/notifications/llm.py +77 -0
  66. pythinker_code/notifications/manager.py +145 -0
  67. pythinker_code/notifications/models.py +50 -0
  68. pythinker_code/notifications/notifier.py +41 -0
  69. pythinker_code/notifications/store.py +118 -0
  70. pythinker_code/notifications/wire.py +21 -0
  71. pythinker_code/plugin/__init__.py +124 -0
  72. pythinker_code/plugin/manager.py +153 -0
  73. pythinker_code/plugin/tool.py +173 -0
  74. pythinker_code/prompts/__init__.py +6 -0
  75. pythinker_code/prompts/compact.md +73 -0
  76. pythinker_code/prompts/init.md +21 -0
  77. pythinker_code/py.typed +0 -0
  78. pythinker_code/session.py +319 -0
  79. pythinker_code/session_fork.py +325 -0
  80. pythinker_code/session_state.py +132 -0
  81. pythinker_code/share.py +14 -0
  82. pythinker_code/skill/__init__.py +727 -0
  83. pythinker_code/skill/flow/__init__.py +99 -0
  84. pythinker_code/skill/flow/d2.py +482 -0
  85. pythinker_code/skill/flow/mermaid.py +266 -0
  86. pythinker_code/skills/pythinker-code-help/SKILL.md +54 -0
  87. pythinker_code/skills/skill-creator/SKILL.md +367 -0
  88. pythinker_code/soul/__init__.py +304 -0
  89. pythinker_code/soul/agent.py +520 -0
  90. pythinker_code/soul/approval.py +267 -0
  91. pythinker_code/soul/btw.py +214 -0
  92. pythinker_code/soul/compaction.py +189 -0
  93. pythinker_code/soul/context.py +339 -0
  94. pythinker_code/soul/denwarenji.py +39 -0
  95. pythinker_code/soul/dynamic_injection.py +84 -0
  96. pythinker_code/soul/dynamic_injections/__init__.py +0 -0
  97. pythinker_code/soul/dynamic_injections/auto_mode.py +72 -0
  98. pythinker_code/soul/dynamic_injections/plan_mode.py +239 -0
  99. pythinker_code/soul/message.py +92 -0
  100. pythinker_code/soul/pythinkersoul.py +1613 -0
  101. pythinker_code/soul/slash.py +340 -0
  102. pythinker_code/soul/toolset.py +788 -0
  103. pythinker_code/subagents/__init__.py +21 -0
  104. pythinker_code/subagents/builder.py +42 -0
  105. pythinker_code/subagents/core.py +86 -0
  106. pythinker_code/subagents/git_context.py +172 -0
  107. pythinker_code/subagents/models.py +54 -0
  108. pythinker_code/subagents/output.py +71 -0
  109. pythinker_code/subagents/registry.py +28 -0
  110. pythinker_code/subagents/runner.py +428 -0
  111. pythinker_code/subagents/store.py +196 -0
  112. pythinker_code/telemetry/__init__.py +211 -0
  113. pythinker_code/telemetry/config.py +54 -0
  114. pythinker_code/telemetry/crash.py +157 -0
  115. pythinker_code/telemetry/metrics.py +208 -0
  116. pythinker_code/telemetry/otel.py +240 -0
  117. pythinker_code/telemetry/sentry.py +167 -0
  118. pythinker_code/telemetry/sink.py +189 -0
  119. pythinker_code/tools/AGENTS.md +6 -0
  120. pythinker_code/tools/__init__.py +105 -0
  121. pythinker_code/tools/agent/__init__.py +277 -0
  122. pythinker_code/tools/agent/description.md +41 -0
  123. pythinker_code/tools/ask_user/__init__.py +159 -0
  124. pythinker_code/tools/ask_user/description.md +19 -0
  125. pythinker_code/tools/background/__init__.py +318 -0
  126. pythinker_code/tools/background/list.md +10 -0
  127. pythinker_code/tools/background/output.md +11 -0
  128. pythinker_code/tools/background/stop.md +8 -0
  129. pythinker_code/tools/display.py +46 -0
  130. pythinker_code/tools/dmail/__init__.py +38 -0
  131. pythinker_code/tools/dmail/dmail.md +17 -0
  132. pythinker_code/tools/file/__init__.py +30 -0
  133. pythinker_code/tools/file/glob.md +17 -0
  134. pythinker_code/tools/file/glob.py +160 -0
  135. pythinker_code/tools/file/grep.md +6 -0
  136. pythinker_code/tools/file/grep_local.py +589 -0
  137. pythinker_code/tools/file/plan_mode.py +45 -0
  138. pythinker_code/tools/file/read.md +16 -0
  139. pythinker_code/tools/file/read.py +300 -0
  140. pythinker_code/tools/file/read_media.md +24 -0
  141. pythinker_code/tools/file/read_media.py +217 -0
  142. pythinker_code/tools/file/replace.md +7 -0
  143. pythinker_code/tools/file/replace.py +195 -0
  144. pythinker_code/tools/file/utils.py +257 -0
  145. pythinker_code/tools/file/write.md +5 -0
  146. pythinker_code/tools/file/write.py +177 -0
  147. pythinker_code/tools/plan/__init__.py +327 -0
  148. pythinker_code/tools/plan/description.md +29 -0
  149. pythinker_code/tools/plan/enter.py +190 -0
  150. pythinker_code/tools/plan/enter_description.md +35 -0
  151. pythinker_code/tools/plan/heroes.py +277 -0
  152. pythinker_code/tools/shell/__init__.py +253 -0
  153. pythinker_code/tools/shell/bash.md +35 -0
  154. pythinker_code/tools/shell/powershell.md +30 -0
  155. pythinker_code/tools/test.py +55 -0
  156. pythinker_code/tools/think/__init__.py +21 -0
  157. pythinker_code/tools/think/think.md +1 -0
  158. pythinker_code/tools/todo/__init__.py +168 -0
  159. pythinker_code/tools/todo/set_todo_list.md +23 -0
  160. pythinker_code/tools/utils.py +199 -0
  161. pythinker_code/tools/web/__init__.py +4 -0
  162. pythinker_code/tools/web/fetch.md +1 -0
  163. pythinker_code/tools/web/fetch.py +189 -0
  164. pythinker_code/tools/web/search.md +1 -0
  165. pythinker_code/tools/web/search.py +163 -0
  166. pythinker_code/ui/__init__.py +0 -0
  167. pythinker_code/ui/acp/__init__.py +99 -0
  168. pythinker_code/ui/print/__init__.py +474 -0
  169. pythinker_code/ui/print/visualize.py +185 -0
  170. pythinker_code/ui/shell/__init__.py +1696 -0
  171. pythinker_code/ui/shell/console.py +109 -0
  172. pythinker_code/ui/shell/debug.py +190 -0
  173. pythinker_code/ui/shell/echo.py +17 -0
  174. pythinker_code/ui/shell/export_import.py +117 -0
  175. pythinker_code/ui/shell/keyboard.py +300 -0
  176. pythinker_code/ui/shell/mcp_status.py +113 -0
  177. pythinker_code/ui/shell/model_picker.py +318 -0
  178. pythinker_code/ui/shell/oauth.py +272 -0
  179. pythinker_code/ui/shell/placeholders.py +531 -0
  180. pythinker_code/ui/shell/prompt.py +2278 -0
  181. pythinker_code/ui/shell/replay.py +215 -0
  182. pythinker_code/ui/shell/session_picker.py +227 -0
  183. pythinker_code/ui/shell/setup.py +212 -0
  184. pythinker_code/ui/shell/slash.py +898 -0
  185. pythinker_code/ui/shell/startup.py +32 -0
  186. pythinker_code/ui/shell/task_browser.py +486 -0
  187. pythinker_code/ui/shell/update.py +350 -0
  188. pythinker_code/ui/shell/usage.py +291 -0
  189. pythinker_code/ui/shell/usage_adapters/__init__.py +50 -0
  190. pythinker_code/ui/shell/usage_adapters/anthropic_admin.py +233 -0
  191. pythinker_code/ui/shell/usage_adapters/base.py +72 -0
  192. pythinker_code/ui/shell/usage_adapters/deepseek.py +137 -0
  193. pythinker_code/ui/shell/usage_adapters/minimax.py +236 -0
  194. pythinker_code/ui/shell/usage_adapters/openai_admin.py +225 -0
  195. pythinker_code/ui/shell/usage_adapters/openai_chatgpt.py +241 -0
  196. pythinker_code/ui/shell/usage_adapters/opencode_go.py +232 -0
  197. pythinker_code/ui/shell/usage_adapters/openrouter.py +105 -0
  198. pythinker_code/ui/shell/usage_adapters/pythinker.py +189 -0
  199. pythinker_code/ui/shell/usage_adapters/pythinker_ai.py +50 -0
  200. pythinker_code/ui/shell/usage_render.py +150 -0
  201. pythinker_code/ui/shell/visualize/__init__.py +165 -0
  202. pythinker_code/ui/shell/visualize/_approval_panel.py +505 -0
  203. pythinker_code/ui/shell/visualize/_blocks.py +629 -0
  204. pythinker_code/ui/shell/visualize/_btw_panel.py +224 -0
  205. pythinker_code/ui/shell/visualize/_input_router.py +48 -0
  206. pythinker_code/ui/shell/visualize/_interactive.py +523 -0
  207. pythinker_code/ui/shell/visualize/_live_view.py +826 -0
  208. pythinker_code/ui/shell/visualize/_question_panel.py +586 -0
  209. pythinker_code/ui/theme.py +241 -0
  210. pythinker_code/usage_ratelimit_cache.py +175 -0
  211. pythinker_code/utils/__init__.py +0 -0
  212. pythinker_code/utils/aiohttp.py +24 -0
  213. pythinker_code/utils/aioqueue.py +72 -0
  214. pythinker_code/utils/broadcast.py +37 -0
  215. pythinker_code/utils/changelog.py +108 -0
  216. pythinker_code/utils/clipboard.py +246 -0
  217. pythinker_code/utils/datetime.py +64 -0
  218. pythinker_code/utils/diff.py +135 -0
  219. pythinker_code/utils/editor.py +91 -0
  220. pythinker_code/utils/environment.py +73 -0
  221. pythinker_code/utils/envvar.py +22 -0
  222. pythinker_code/utils/export.py +696 -0
  223. pythinker_code/utils/file_filter.py +375 -0
  224. pythinker_code/utils/frontmatter.py +70 -0
  225. pythinker_code/utils/io.py +27 -0
  226. pythinker_code/utils/logging.py +146 -0
  227. pythinker_code/utils/media_tags.py +29 -0
  228. pythinker_code/utils/message.py +24 -0
  229. pythinker_code/utils/path.py +199 -0
  230. pythinker_code/utils/proctitle.py +33 -0
  231. pythinker_code/utils/proxy.py +31 -0
  232. pythinker_code/utils/pyinstaller.py +45 -0
  233. pythinker_code/utils/rich/__init__.py +33 -0
  234. pythinker_code/utils/rich/columns.py +99 -0
  235. pythinker_code/utils/rich/diff_render.py +481 -0
  236. pythinker_code/utils/rich/markdown.py +900 -0
  237. pythinker_code/utils/rich/markdown_sample.md +108 -0
  238. pythinker_code/utils/rich/markdown_sample_short.md +2 -0
  239. pythinker_code/utils/rich/syntax.py +114 -0
  240. pythinker_code/utils/sensitive.py +54 -0
  241. pythinker_code/utils/server.py +121 -0
  242. pythinker_code/utils/signals.py +43 -0
  243. pythinker_code/utils/slashcmd.py +124 -0
  244. pythinker_code/utils/string.py +41 -0
  245. pythinker_code/utils/subprocess_env.py +73 -0
  246. pythinker_code/utils/term.py +168 -0
  247. pythinker_code/utils/typing.py +20 -0
  248. pythinker_code/vis/__init__.py +0 -0
  249. pythinker_code/vis/api/__init__.py +5 -0
  250. pythinker_code/vis/api/sessions.py +687 -0
  251. pythinker_code/vis/api/statistics.py +209 -0
  252. pythinker_code/vis/api/system.py +19 -0
  253. pythinker_code/vis/app.py +175 -0
  254. pythinker_code/vis/static/assets/highlighted-body-B3W2YXNL-D2MTYyJz.js +1 -0
  255. pythinker_code/vis/static/assets/index-CezafTt_.js +185 -0
  256. pythinker_code/vis/static/assets/index-DSRInNnm.css +1 -0
  257. pythinker_code/vis/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  258. pythinker_code/vis/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  259. pythinker_code/vis/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  260. pythinker_code/vis/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  261. pythinker_code/vis/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  262. pythinker_code/vis/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  263. pythinker_code/vis/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  264. pythinker_code/vis/static/index.html +17 -0
  265. pythinker_code/web/__init__.py +5 -0
  266. pythinker_code/web/api/__init__.py +15 -0
  267. pythinker_code/web/api/config.py +208 -0
  268. pythinker_code/web/api/open_in.py +199 -0
  269. pythinker_code/web/api/sessions.py +1225 -0
  270. pythinker_code/web/app.py +451 -0
  271. pythinker_code/web/auth.py +191 -0
  272. pythinker_code/web/models.py +98 -0
  273. pythinker_code/web/runner/__init__.py +5 -0
  274. pythinker_code/web/runner/messages.py +57 -0
  275. pythinker_code/web/runner/process.py +754 -0
  276. pythinker_code/web/runner/worker.py +97 -0
  277. pythinker_code/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  278. pythinker_code/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  279. pythinker_code/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  280. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  281. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  282. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  283. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  284. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  285. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  286. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  287. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  288. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  289. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  290. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  291. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  292. pythinker_code/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  293. pythinker_code/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  294. pythinker_code/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  295. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  296. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  297. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  298. pythinker_code/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  299. pythinker_code/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  300. pythinker_code/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  301. pythinker_code/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  302. pythinker_code/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  303. pythinker_code/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  304. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  305. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  306. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  307. pythinker_code/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  308. pythinker_code/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  309. pythinker_code/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  310. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  311. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  312. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  313. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  314. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  315. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  316. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  317. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  318. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  319. pythinker_code/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  320. pythinker_code/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  321. pythinker_code/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  322. pythinker_code/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  323. pythinker_code/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  324. pythinker_code/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  325. pythinker_code/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  326. pythinker_code/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  327. pythinker_code/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  328. pythinker_code/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  329. pythinker_code/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  330. pythinker_code/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  331. pythinker_code/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  332. pythinker_code/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  333. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  334. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  335. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  336. pythinker_code/web/static/assets/_baseUniq--dyU3g5v.js +1 -0
  337. pythinker_code/web/static/assets/abap-BdImnpbu.js +1 -0
  338. pythinker_code/web/static/assets/actionscript-3-CfeIJUat.js +1 -0
  339. pythinker_code/web/static/assets/ada-bCR0ucgS.js +1 -0
  340. pythinker_code/web/static/assets/andromeeda-C-Jbm3Hp.js +1 -0
  341. pythinker_code/web/static/assets/angular-html-CU67Zn6k.js +1 -0
  342. pythinker_code/web/static/assets/angular-ts-BwZT4LLn.js +1 -0
  343. pythinker_code/web/static/assets/apache-Pmp26Uib.js +1 -0
  344. pythinker_code/web/static/assets/apex-D8_7TLub.js +1 -0
  345. pythinker_code/web/static/assets/apl-dKokRX4l.js +1 -0
  346. pythinker_code/web/static/assets/applescript-Co6uUVPk.js +1 -0
  347. pythinker_code/web/static/assets/ara-BRHolxvo.js +1 -0
  348. pythinker_code/web/static/assets/arc-DkMjLpYa.js +1 -0
  349. pythinker_code/web/static/assets/architectureDiagram-VXUJARFQ-CHWVaGo9.js +36 -0
  350. pythinker_code/web/static/assets/asciidoc-Dv7Oe6Be.js +1 -0
  351. pythinker_code/web/static/assets/asm-D_Q5rh1f.js +1 -0
  352. pythinker_code/web/static/assets/astro-CbQHKStN.js +1 -0
  353. pythinker_code/web/static/assets/aurora-x-D-2ljcwZ.js +1 -0
  354. pythinker_code/web/static/assets/awk-DMzUqQB5.js +1 -0
  355. pythinker_code/web/static/assets/ayu-dark-CmMr59Fi.js +1 -0
  356. pythinker_code/web/static/assets/ballerina-BFfxhgS-.js +1 -0
  357. pythinker_code/web/static/assets/bat-BkioyH1T.js +1 -0
  358. pythinker_code/web/static/assets/beancount-k_qm7-4y.js +1 -0
  359. pythinker_code/web/static/assets/berry-uYugtg8r.js +1 -0
  360. pythinker_code/web/static/assets/bibtex-CHM0blh-.js +1 -0
  361. pythinker_code/web/static/assets/bicep-Bmn6On1c.js +1 -0
  362. pythinker_code/web/static/assets/blade-D4QpJJKB.js +1 -0
  363. pythinker_code/web/static/assets/blockDiagram-VD42YOAC-DzdKe497.js +122 -0
  364. pythinker_code/web/static/assets/bsl-BO_Y6i37.js +1 -0
  365. pythinker_code/web/static/assets/c-BIGW1oBm.js +1 -0
  366. pythinker_code/web/static/assets/c3-VCDPK7BO.js +1 -0
  367. pythinker_code/web/static/assets/c4Diagram-YG6GDRKO-D84Blotg.js +10 -0
  368. pythinker_code/web/static/assets/cadence-Bv_4Rxtq.js +1 -0
  369. pythinker_code/web/static/assets/cairo-KRGpt6FW.js +1 -0
  370. pythinker_code/web/static/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  371. pythinker_code/web/static/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  372. pythinker_code/web/static/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  373. pythinker_code/web/static/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  374. pythinker_code/web/static/assets/channel-CllSjjdl.js +1 -0
  375. pythinker_code/web/static/assets/chunk-4BX2VUAB-C9w8wleE.js +1 -0
  376. pythinker_code/web/static/assets/chunk-55IACEB6-YlYJ8HnF.js +1 -0
  377. pythinker_code/web/static/assets/chunk-B4BG7PRW-Bwtz_AHU.js +165 -0
  378. pythinker_code/web/static/assets/chunk-DI55MBZ5-BbEHkl8h.js +220 -0
  379. pythinker_code/web/static/assets/chunk-FMBD7UC4-BKPbvjLC.js +15 -0
  380. pythinker_code/web/static/assets/chunk-QN33PNHL-D73dQvKf.js +1 -0
  381. pythinker_code/web/static/assets/chunk-QZHKN3VN-zGiLKes_.js +1 -0
  382. pythinker_code/web/static/assets/chunk-TZMSLE5B-LHJCi2fy.js +1 -0
  383. pythinker_code/web/static/assets/clarity-D53aC0YG.js +1 -0
  384. pythinker_code/web/static/assets/classDiagram-2ON5EDUG-vX27iZwa.js +1 -0
  385. pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-vX27iZwa.js +1 -0
  386. pythinker_code/web/static/assets/clojure-P80f7IUj.js +1 -0
  387. pythinker_code/web/static/assets/clone-DYBkaPm2.js +1 -0
  388. pythinker_code/web/static/assets/cmake-D1j8_8rp.js +1 -0
  389. pythinker_code/web/static/assets/cobol-nwyudZeR.js +1 -0
  390. pythinker_code/web/static/assets/code-block-IT6T5CEO-NtKViZGl.js +2 -0
  391. pythinker_code/web/static/assets/codeowners-Bp6g37R7.js +1 -0
  392. pythinker_code/web/static/assets/codeql-DsOJ9woJ.js +1 -0
  393. pythinker_code/web/static/assets/coffee-Ch7k5sss.js +1 -0
  394. pythinker_code/web/static/assets/common-lisp-Cg-RD9OK.js +1 -0
  395. pythinker_code/web/static/assets/coq-DkFqJrB1.js +1 -0
  396. pythinker_code/web/static/assets/cose-bilkent-S5V4N54A-DialjZpd.js +1 -0
  397. pythinker_code/web/static/assets/cpp-CofmeUqb.js +1 -0
  398. pythinker_code/web/static/assets/crystal-tKQVLTB8.js +1 -0
  399. pythinker_code/web/static/assets/csharp-K5feNrxe.js +1 -0
  400. pythinker_code/web/static/assets/css-DPfMkruS.js +1 -0
  401. pythinker_code/web/static/assets/csv-fuZLfV_i.js +1 -0
  402. pythinker_code/web/static/assets/cue-D82EKSYY.js +1 -0
  403. pythinker_code/web/static/assets/cypher-COkxafJQ.js +1 -0
  404. pythinker_code/web/static/assets/cytoscape.esm-C_Fzpdck.js +321 -0
  405. pythinker_code/web/static/assets/d-85-TOEBH.js +1 -0
  406. pythinker_code/web/static/assets/dagre-6UL2VRFP-DfuvkZZ7.js +4 -0
  407. pythinker_code/web/static/assets/dark-plus-C3mMm8J8.js +1 -0
  408. pythinker_code/web/static/assets/dart-CF10PKvl.js +1 -0
  409. pythinker_code/web/static/assets/dax-CEL-wOlO.js +1 -0
  410. pythinker_code/web/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  411. pythinker_code/web/static/assets/desktop-BmXAJ9_W.js +1 -0
  412. pythinker_code/web/static/assets/diagram-PSM6KHXK-DLGctX3v.js +24 -0
  413. pythinker_code/web/static/assets/diagram-QEK2KX5R-DnxN6S0F.js +43 -0
  414. pythinker_code/web/static/assets/diagram-S2PKOQOG-Caq_Set9.js +24 -0
  415. pythinker_code/web/static/assets/diff-D97Zzqfu.js +1 -0
  416. pythinker_code/web/static/assets/docker-BcOcwvcX.js +1 -0
  417. pythinker_code/web/static/assets/dotenv-Da5cRb03.js +1 -0
  418. pythinker_code/web/static/assets/dracula-BzJJZx-M.js +1 -0
  419. pythinker_code/web/static/assets/dracula-soft-BXkSAIEj.js +1 -0
  420. pythinker_code/web/static/assets/dream-maker-BtqSS_iP.js +1 -0
  421. pythinker_code/web/static/assets/edge-BkV0erSs.js +1 -0
  422. pythinker_code/web/static/assets/elixir-CDX3lj18.js +1 -0
  423. pythinker_code/web/static/assets/elm-DbKCFpqz.js +1 -0
  424. pythinker_code/web/static/assets/emacs-lisp-C9XAeP06.js +1 -0
  425. pythinker_code/web/static/assets/erDiagram-Q2GNP2WA-BgTfALoK.js +60 -0
  426. pythinker_code/web/static/assets/erb-BOJIQeun.js +1 -0
  427. pythinker_code/web/static/assets/erlang-DsQrWhSR.js +1 -0
  428. pythinker_code/web/static/assets/everforest-dark-BgDCqdQA.js +1 -0
  429. pythinker_code/web/static/assets/everforest-light-C8M2exoo.js +1 -0
  430. pythinker_code/web/static/assets/fennel-BYunw83y.js +1 -0
  431. pythinker_code/web/static/assets/fish-BvzEVeQv.js +1 -0
  432. pythinker_code/web/static/assets/flowDiagram-NV44I4VS-QjW_fnGi.js +162 -0
  433. pythinker_code/web/static/assets/fluent-C4IJs8-o.js +1 -0
  434. pythinker_code/web/static/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  435. pythinker_code/web/static/assets/fortran-free-form-BxgE0vQu.js +1 -0
  436. pythinker_code/web/static/assets/fsharp-CXgrBDvD.js +1 -0
  437. pythinker_code/web/static/assets/ganttDiagram-JELNMOA3-fqi8JFof.js +267 -0
  438. pythinker_code/web/static/assets/gdresource-B7Tvp0Sc.js +1 -0
  439. pythinker_code/web/static/assets/gdscript-DTMYz4Jt.js +1 -0
  440. pythinker_code/web/static/assets/gdshader-DkwncUOv.js +1 -0
  441. pythinker_code/web/static/assets/genie-D0YGMca9.js +1 -0
  442. pythinker_code/web/static/assets/gherkin-DyxjwDmM.js +1 -0
  443. pythinker_code/web/static/assets/git-commit-F4YmCXRG.js +1 -0
  444. pythinker_code/web/static/assets/git-rebase-r7XF79zn.js +1 -0
  445. pythinker_code/web/static/assets/gitGraphDiagram-NY62KEGX-i7o6VQ8x.js +65 -0
  446. pythinker_code/web/static/assets/github-dark-DHJKELXO.js +1 -0
  447. pythinker_code/web/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
  448. pythinker_code/web/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  449. pythinker_code/web/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  450. pythinker_code/web/static/assets/github-light-DAi9KRSo.js +1 -0
  451. pythinker_code/web/static/assets/github-light-default-D7oLnXFd.js +1 -0
  452. pythinker_code/web/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  453. pythinker_code/web/static/assets/gleam-BspZqrRM.js +1 -0
  454. pythinker_code/web/static/assets/glimmer-js-Rg0-pVw9.js +1 -0
  455. pythinker_code/web/static/assets/glimmer-ts-U6CK756n.js +1 -0
  456. pythinker_code/web/static/assets/glsl-DplSGwfg.js +1 -0
  457. pythinker_code/web/static/assets/gn-n2N0HUVH.js +1 -0
  458. pythinker_code/web/static/assets/gnuplot-DdkO51Og.js +1 -0
  459. pythinker_code/web/static/assets/go-Dn2_MT6a.js +1 -0
  460. pythinker_code/web/static/assets/graph-C0vZK2pT.js +1 -0
  461. pythinker_code/web/static/assets/graphql-ChdNCCLP.js +1 -0
  462. pythinker_code/web/static/assets/groovy-gcz8RCvz.js +1 -0
  463. pythinker_code/web/static/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  464. pythinker_code/web/static/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  465. pythinker_code/web/static/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  466. pythinker_code/web/static/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  467. pythinker_code/web/static/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  468. pythinker_code/web/static/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  469. pythinker_code/web/static/assets/hack-CaT9iCJl.js +1 -0
  470. pythinker_code/web/static/assets/haml-B8DHNrY2.js +1 -0
  471. pythinker_code/web/static/assets/handlebars-BL8al0AC.js +1 -0
  472. pythinker_code/web/static/assets/haskell-Df6bDoY_.js +1 -0
  473. pythinker_code/web/static/assets/haxe-CzTSHFRz.js +1 -0
  474. pythinker_code/web/static/assets/hcl-BWvSN4gD.js +1 -0
  475. pythinker_code/web/static/assets/hjson-D5-asLiD.js +1 -0
  476. pythinker_code/web/static/assets/hlsl-D3lLCCz7.js +1 -0
  477. pythinker_code/web/static/assets/houston-DnULxvSX.js +1 -0
  478. pythinker_code/web/static/assets/html-GMplVEZG.js +1 -0
  479. pythinker_code/web/static/assets/html-derivative-BFtXZ54Q.js +1 -0
  480. pythinker_code/web/static/assets/http-jrhK8wxY.js +1 -0
  481. pythinker_code/web/static/assets/hurl-irOxFIW8.js +1 -0
  482. pythinker_code/web/static/assets/hxml-Bvhsp5Yf.js +1 -0
  483. pythinker_code/web/static/assets/hy-DFXneXwc.js +1 -0
  484. pythinker_code/web/static/assets/imba-DGztddWO.js +1 -0
  485. pythinker_code/web/static/assets/index-BYCCk6-K.js +153 -0
  486. pythinker_code/web/static/assets/index-BpoLgcEt.js +1 -0
  487. pythinker_code/web/static/assets/index-Cpy4G3uJ.js +2 -0
  488. pythinker_code/web/static/assets/index-CzV_vCfu.css +1 -0
  489. pythinker_code/web/static/assets/index-DI2oedCt.js +19 -0
  490. pythinker_code/web/static/assets/index-DdIkp80K.js +5 -0
  491. pythinker_code/web/static/assets/infoDiagram-WHAUD3N6-BMPpeZSM.js +2 -0
  492. pythinker_code/web/static/assets/ini-BEwlwnbL.js +1 -0
  493. pythinker_code/web/static/assets/init-Gi6I4Gst.js +1 -0
  494. pythinker_code/web/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  495. pythinker_code/web/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  496. pythinker_code/web/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  497. pythinker_code/web/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  498. pythinker_code/web/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  499. pythinker_code/web/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  500. pythinker_code/web/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  501. pythinker_code/web/static/assets/java-CylS5w8V.js +1 -0
  502. pythinker_code/web/static/assets/javascript-wDzz0qaB.js +1 -0
  503. pythinker_code/web/static/assets/jinja-4LBKfQ-Z.js +1 -0
  504. pythinker_code/web/static/assets/jison-wvAkD_A8.js +1 -0
  505. pythinker_code/web/static/assets/journeyDiagram-XKPGCS4Q-DAM7gngo.js +139 -0
  506. pythinker_code/web/static/assets/json-Cp-IABpG.js +1 -0
  507. pythinker_code/web/static/assets/json5-C9tS-k6U.js +1 -0
  508. pythinker_code/web/static/assets/jsonc-Des-eS-w.js +1 -0
  509. pythinker_code/web/static/assets/jsonl-DcaNXYhu.js +1 -0
  510. pythinker_code/web/static/assets/jsonnet-DFQXde-d.js +1 -0
  511. pythinker_code/web/static/assets/jssm-C2t-YnRu.js +1 -0
  512. pythinker_code/web/static/assets/jsx-g9-lgVsj.js +1 -0
  513. pythinker_code/web/static/assets/julia-CxzCAyBv.js +1 -0
  514. pythinker_code/web/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  515. pythinker_code/web/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  516. pythinker_code/web/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
  517. pythinker_code/web/static/assets/kanban-definition-3W4ZIXB7-ChpBpV0k.js +89 -0
  518. pythinker_code/web/static/assets/katex-D2lIc1rk.css +1 -0
  519. pythinker_code/web/static/assets/kdl-DV7GczEv.js +1 -0
  520. pythinker_code/web/static/assets/kotlin-BdnUsdx6.js +1 -0
  521. pythinker_code/web/static/assets/kusto-DZf3V79B.js +1 -0
  522. pythinker_code/web/static/assets/laserwave-DUszq2jm.js +1 -0
  523. pythinker_code/web/static/assets/latex-B4uzh10-.js +1 -0
  524. pythinker_code/web/static/assets/layout-C3Jp1gKO.js +1 -0
  525. pythinker_code/web/static/assets/lean-BZvkOJ9d.js +1 -0
  526. pythinker_code/web/static/assets/less-B1dDrJ26.js +1 -0
  527. pythinker_code/web/static/assets/light-plus-B7mTdjB0.js +1 -0
  528. pythinker_code/web/static/assets/linear-BGHtL1N4.js +1 -0
  529. pythinker_code/web/static/assets/liquid-DYVedYrR.js +1 -0
  530. pythinker_code/web/static/assets/llvm-BtvRca6l.js +1 -0
  531. pythinker_code/web/static/assets/log-2UxHyX5q.js +1 -0
  532. pythinker_code/web/static/assets/logo-BtOb2qkB.js +1 -0
  533. pythinker_code/web/static/assets/lua-BbnMAYS6.js +1 -0
  534. pythinker_code/web/static/assets/luau-C-HG3fhB.js +1 -0
  535. pythinker_code/web/static/assets/make-CHLpvVh8.js +1 -0
  536. pythinker_code/web/static/assets/markdown-Cvjx9yec.js +1 -0
  537. pythinker_code/web/static/assets/marko-DZsq8hO1.js +1 -0
  538. pythinker_code/web/static/assets/material-theme-D5KoaKCx.js +1 -0
  539. pythinker_code/web/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
  540. pythinker_code/web/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  541. pythinker_code/web/static/assets/material-theme-ocean-CyktbL80.js +1 -0
  542. pythinker_code/web/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  543. pythinker_code/web/static/assets/matlab-D7o27uSR.js +1 -0
  544. pythinker_code/web/static/assets/mdc-DUICxH0z.js +1 -0
  545. pythinker_code/web/static/assets/mdx-Cmh6b_Ma.js +1 -0
  546. pythinker_code/web/static/assets/mermaid-VLURNSYL-B2P5VJ9v.css +1 -0
  547. pythinker_code/web/static/assets/mermaid-VLURNSYL-C_HW6koB.js +465 -0
  548. pythinker_code/web/static/assets/mermaid-mWjccvbQ.js +1 -0
  549. pythinker_code/web/static/assets/mermaid.core-CnT4VrPC.js +191 -0
  550. pythinker_code/web/static/assets/min-Dn5VRVmX.js +1 -0
  551. pythinker_code/web/static/assets/min-dark-CafNBF8u.js +1 -0
  552. pythinker_code/web/static/assets/min-light-CTRr51gU.js +1 -0
  553. pythinker_code/web/static/assets/mindmap-definition-VGOIOE7T-x8EwhfIt.js +68 -0
  554. pythinker_code/web/static/assets/mipsasm-CKIfxQSi.js +1 -0
  555. pythinker_code/web/static/assets/mojo-B93PlW-d.js +1 -0
  556. pythinker_code/web/static/assets/monokai-D4h5O-jR.js +1 -0
  557. pythinker_code/web/static/assets/moonbit-Ba13S78F.js +1 -0
  558. pythinker_code/web/static/assets/move-Bu9oaDYs.js +1 -0
  559. pythinker_code/web/static/assets/narrat-DRg8JJMk.js +1 -0
  560. pythinker_code/web/static/assets/nextflow-BrzmwbiE.js +1 -0
  561. pythinker_code/web/static/assets/nginx-DknmC5AR.js +1 -0
  562. pythinker_code/web/static/assets/night-owl-C39BiMTA.js +1 -0
  563. pythinker_code/web/static/assets/nim-CVrawwO9.js +1 -0
  564. pythinker_code/web/static/assets/nix-CwoSXNpI.js +1 -0
  565. pythinker_code/web/static/assets/nord-Ddv68eIx.js +1 -0
  566. pythinker_code/web/static/assets/nushell-C-sUppwS.js +1 -0
  567. pythinker_code/web/static/assets/objective-c-DXmwc3jG.js +1 -0
  568. pythinker_code/web/static/assets/objective-cpp-CLxacb5B.js +1 -0
  569. pythinker_code/web/static/assets/ocaml-C0hk2d4L.js +1 -0
  570. pythinker_code/web/static/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  571. pythinker_code/web/static/assets/one-light-PoHY5YXO.js +1 -0
  572. pythinker_code/web/static/assets/openscad-C4EeE6gA.js +1 -0
  573. pythinker_code/web/static/assets/ordinal-Cboi1Yqb.js +1 -0
  574. pythinker_code/web/static/assets/pascal-D93ZcfNL.js +1 -0
  575. pythinker_code/web/static/assets/perl-C0TMdlhV.js +1 -0
  576. pythinker_code/web/static/assets/php-CDn_0X-4.js +1 -0
  577. pythinker_code/web/static/assets/pieDiagram-ADFJNKIX-DgxBKGz2.js +30 -0
  578. pythinker_code/web/static/assets/pkl-u5AG7uiY.js +1 -0
  579. pythinker_code/web/static/assets/plastic-3e1v2bzS.js +1 -0
  580. pythinker_code/web/static/assets/plsql-ChMvpjG-.js +1 -0
  581. pythinker_code/web/static/assets/po-BTJTHyun.js +1 -0
  582. pythinker_code/web/static/assets/poimandres-CS3Unz2-.js +1 -0
  583. pythinker_code/web/static/assets/polar-C0HS_06l.js +1 -0
  584. pythinker_code/web/static/assets/postcss-CXtECtnM.js +1 -0
  585. pythinker_code/web/static/assets/powerquery-CEu0bR-o.js +1 -0
  586. pythinker_code/web/static/assets/powershell-Dpen1YoG.js +1 -0
  587. pythinker_code/web/static/assets/prisma-Dd19v3D-.js +1 -0
  588. pythinker_code/web/static/assets/prolog-CbFg5uaA.js +1 -0
  589. pythinker_code/web/static/assets/proto-C7zT0LnQ.js +1 -0
  590. pythinker_code/web/static/assets/pug-CGlum2m_.js +1 -0
  591. pythinker_code/web/static/assets/puppet-BMWR74SV.js +1 -0
  592. pythinker_code/web/static/assets/purescript-CklMAg4u.js +1 -0
  593. pythinker_code/web/static/assets/python-B6aJPvgy.js +1 -0
  594. pythinker_code/web/static/assets/qml-3beO22l8.js +1 -0
  595. pythinker_code/web/static/assets/qmldir-C8lEn-DE.js +1 -0
  596. pythinker_code/web/static/assets/qss-IeuSbFQv.js +1 -0
  597. pythinker_code/web/static/assets/quadrantDiagram-AYHSOK5B-DSpe_dqk.js +7 -0
  598. pythinker_code/web/static/assets/r-Dspwwk_N.js +1 -0
  599. pythinker_code/web/static/assets/racket-BqYA7rlc.js +1 -0
  600. pythinker_code/web/static/assets/raku-DXvB9xmW.js +1 -0
  601. pythinker_code/web/static/assets/razor-C1TweQQi.js +1 -0
  602. pythinker_code/web/static/assets/red-bN70gL4F.js +1 -0
  603. pythinker_code/web/static/assets/reg-C-SQnVFl.js +1 -0
  604. pythinker_code/web/static/assets/regexp-CDVJQ6XC.js +1 -0
  605. pythinker_code/web/static/assets/rel-C3B-1QV4.js +1 -0
  606. pythinker_code/web/static/assets/requirementDiagram-UZGBJVZJ-8o9hozL-.js +64 -0
  607. pythinker_code/web/static/assets/riscv-BM1_JUlF.js +1 -0
  608. pythinker_code/web/static/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  609. pythinker_code/web/static/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  610. pythinker_code/web/static/assets/rose-pine-qdsjHGoJ.js +1 -0
  611. pythinker_code/web/static/assets/rosmsg-BJDFO7_C.js +1 -0
  612. pythinker_code/web/static/assets/rst-B0xPkSld.js +1 -0
  613. pythinker_code/web/static/assets/ruby-BvKwtOVI.js +1 -0
  614. pythinker_code/web/static/assets/rust-B1yitclQ.js +1 -0
  615. pythinker_code/web/static/assets/sankeyDiagram-TZEHDZUN-BLOSUixH.js +10 -0
  616. pythinker_code/web/static/assets/sas-cz2c8ADy.js +1 -0
  617. pythinker_code/web/static/assets/sass-Cj5Yp3dK.js +1 -0
  618. pythinker_code/web/static/assets/scala-C151Ov-r.js +1 -0
  619. pythinker_code/web/static/assets/scheme-C98Dy4si.js +1 -0
  620. pythinker_code/web/static/assets/scss-OYdSNvt2.js +1 -0
  621. pythinker_code/web/static/assets/sdbl-DVxCFoDh.js +1 -0
  622. pythinker_code/web/static/assets/sequenceDiagram-WL72ISMW-6F2G8JTU.js +145 -0
  623. pythinker_code/web/static/assets/shaderlab-Dg9Lc6iA.js +1 -0
  624. pythinker_code/web/static/assets/shellscript-Yzrsuije.js +1 -0
  625. pythinker_code/web/static/assets/shellsession-BADoaaVG.js +1 -0
  626. pythinker_code/web/static/assets/slack-dark-BthQWCQV.js +1 -0
  627. pythinker_code/web/static/assets/slack-ochin-DqwNpetd.js +1 -0
  628. pythinker_code/web/static/assets/smalltalk-BERRCDM3.js +1 -0
  629. pythinker_code/web/static/assets/snazzy-light-Bw305WKR.js +1 -0
  630. pythinker_code/web/static/assets/solarized-dark-DXbdFlpD.js +1 -0
  631. pythinker_code/web/static/assets/solarized-light-L9t79GZl.js +1 -0
  632. pythinker_code/web/static/assets/solidity-rGO070M0.js +1 -0
  633. pythinker_code/web/static/assets/soy-Brmx7dQM.js +1 -0
  634. pythinker_code/web/static/assets/sparql-rVzFXLq3.js +1 -0
  635. pythinker_code/web/static/assets/splunk-BtCnVYZw.js +1 -0
  636. pythinker_code/web/static/assets/sql-BLtJtn59.js +1 -0
  637. pythinker_code/web/static/assets/ssh-config-_ykCGR6B.js +1 -0
  638. pythinker_code/web/static/assets/stata-BH5u7GGu.js +1 -0
  639. pythinker_code/web/static/assets/stateDiagram-FKZM4ZOC-DP8xP0iJ.js +1 -0
  640. pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-1l6-EZNX.js +1 -0
  641. pythinker_code/web/static/assets/stylus-BEDo0Tqx.js +1 -0
  642. pythinker_code/web/static/assets/svelte-zxCyuUbr.js +1 -0
  643. pythinker_code/web/static/assets/swift-Dg5xB15N.js +1 -0
  644. pythinker_code/web/static/assets/synthwave-84-CbfX1IO0.js +1 -0
  645. pythinker_code/web/static/assets/system-verilog-CnnmHF94.js +1 -0
  646. pythinker_code/web/static/assets/systemd-4A_iFExJ.js +1 -0
  647. pythinker_code/web/static/assets/talonscript-CkByrt1z.js +1 -0
  648. pythinker_code/web/static/assets/tasl-QIJgUcNo.js +1 -0
  649. pythinker_code/web/static/assets/tcl-dwOrl1Do.js +1 -0
  650. pythinker_code/web/static/assets/templ-W15q3VgB.js +1 -0
  651. pythinker_code/web/static/assets/terraform-BETggiCN.js +1 -0
  652. pythinker_code/web/static/assets/tex-CvyZ59Mk.js +1 -0
  653. pythinker_code/web/static/assets/timeline-definition-IT6M3QCI-DMgruDfK.js +61 -0
  654. pythinker_code/web/static/assets/tokyo-night-hegEt444.js +1 -0
  655. pythinker_code/web/static/assets/toml-vGWfd6FD.js +1 -0
  656. pythinker_code/web/static/assets/treemap-KMMF4GRG-Bo9ZkrAK.js +128 -0
  657. pythinker_code/web/static/assets/ts-tags-zn1MmPIZ.js +1 -0
  658. pythinker_code/web/static/assets/tsv-B_m7g4N7.js +1 -0
  659. pythinker_code/web/static/assets/tsx-COt5Ahok.js +1 -0
  660. pythinker_code/web/static/assets/turtle-BsS91CYL.js +1 -0
  661. pythinker_code/web/static/assets/twig-CO9l9SDP.js +1 -0
  662. pythinker_code/web/static/assets/typescript-BPQ3VLAy.js +1 -0
  663. pythinker_code/web/static/assets/typespec-BGHnOYBU.js +1 -0
  664. pythinker_code/web/static/assets/typst-DHCkPAjA.js +1 -0
  665. pythinker_code/web/static/assets/v-BcVCzyr7.js +1 -0
  666. pythinker_code/web/static/assets/vala-CsfeWuGM.js +1 -0
  667. pythinker_code/web/static/assets/vb-D17OF-Vu.js +1 -0
  668. pythinker_code/web/static/assets/verilog-BQ8w6xss.js +1 -0
  669. pythinker_code/web/static/assets/vesper-DU1UobuO.js +1 -0
  670. pythinker_code/web/static/assets/vhdl-CeAyd5Ju.js +1 -0
  671. pythinker_code/web/static/assets/viml-CJc9bBzg.js +1 -0
  672. pythinker_code/web/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
  673. pythinker_code/web/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
  674. pythinker_code/web/static/assets/vitesse-light-CVO1_9PV.js +1 -0
  675. pythinker_code/web/static/assets/vue-DN_0RTcg.js +1 -0
  676. pythinker_code/web/static/assets/vue-html-AaS7Mt5G.js +1 -0
  677. pythinker_code/web/static/assets/vue-vine-CQOfvN7w.js +1 -0
  678. pythinker_code/web/static/assets/vyper-CDx5xZoG.js +1 -0
  679. pythinker_code/web/static/assets/wasm-CG6Dc4jp.js +1 -0
  680. pythinker_code/web/static/assets/wasm-MzD3tlZU.js +1 -0
  681. pythinker_code/web/static/assets/wenyan-BV7otONQ.js +1 -0
  682. pythinker_code/web/static/assets/wgsl-Dx-B1_4e.js +1 -0
  683. pythinker_code/web/static/assets/wikitext-BhOHFoWU.js +1 -0
  684. pythinker_code/web/static/assets/wit-5i3qLPDT.js +1 -0
  685. pythinker_code/web/static/assets/wolfram-lXgVvXCa.js +1 -0
  686. pythinker_code/web/static/assets/xml-sdJ4AIDG.js +1 -0
  687. pythinker_code/web/static/assets/xsl-CtQFsRM5.js +1 -0
  688. pythinker_code/web/static/assets/xychartDiagram-PRI3JC2R-GeF2johi.js +7 -0
  689. pythinker_code/web/static/assets/yaml-Buea-lGh.js +1 -0
  690. pythinker_code/web/static/assets/zenscript-DVFEvuxE.js +1 -0
  691. pythinker_code/web/static/assets/zig-VOosw3JB.js +1 -0
  692. pythinker_code/web/static/brand/apple-touch-icon.png +0 -0
  693. pythinker_code/web/static/brand/arctecture.webp +0 -0
  694. pythinker_code/web/static/brand/bimi-logo.svg +46 -0
  695. pythinker_code/web/static/brand/favicon.ico +0 -0
  696. pythinker_code/web/static/brand/fonts/dm-sans-latin-ext.woff2 +0 -0
  697. pythinker_code/web/static/brand/fonts/dm-sans-latin.woff2 +0 -0
  698. pythinker_code/web/static/brand/fonts/instrument-sans-latin-ext.woff2 +0 -0
  699. pythinker_code/web/static/brand/fonts/instrument-sans-latin.woff2 +0 -0
  700. pythinker_code/web/static/brand/fonts/instrument-serif-latin-ext.woff2 +0 -0
  701. pythinker_code/web/static/brand/fonts/instrument-serif-latin.woff2 +0 -0
  702. pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin-ext.woff2 +0 -0
  703. pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin.woff2 +0 -0
  704. pythinker_code/web/static/brand/fonts/libre-baskerville-latin-ext.woff2 +0 -0
  705. pythinker_code/web/static/brand/fonts/libre-baskerville-latin.woff2 +0 -0
  706. pythinker_code/web/static/brand/fonts/roboto-latin-ext.woff2 +0 -0
  707. pythinker_code/web/static/brand/fonts/roboto-latin.woff2 +0 -0
  708. pythinker_code/web/static/brand/icon-192.png +0 -0
  709. pythinker_code/web/static/brand/icon-512.png +0 -0
  710. pythinker_code/web/static/brand/icon.svg +1 -0
  711. pythinker_code/web/static/brand/logo.png +0 -0
  712. pythinker_code/web/static/brand/pythinker_animated.svg +79 -0
  713. pythinker_code/web/static/brand/robots.txt +4 -0
  714. pythinker_code/web/static/index.html +15 -0
  715. pythinker_code/web/static/logo.png +0 -0
  716. pythinker_code/web/store/__init__.py +1 -0
  717. pythinker_code/web/store/sessions.py +432 -0
  718. pythinker_code/wire/__init__.py +148 -0
  719. pythinker_code/wire/file.py +151 -0
  720. pythinker_code/wire/jsonrpc.py +263 -0
  721. pythinker_code/wire/protocol.py +2 -0
  722. pythinker_code/wire/root_hub.py +27 -0
  723. pythinker_code/wire/serde.py +26 -0
  724. pythinker_code/wire/server.py +1069 -0
  725. pythinker_code/wire/types.py +698 -0
  726. pythinker_code-2.0.0.dist-info/METADATA +660 -0
  727. pythinker_code-2.0.0.dist-info/RECORD +731 -0
  728. pythinker_code-2.0.0.dist-info/WHEEL +4 -0
  729. pythinker_code-2.0.0.dist-info/entry_points.txt +4 -0
  730. pythinker_code-2.0.0.dist-info/licenses/LICENSE +202 -0
  731. pythinker_code-2.0.0.dist-info/licenses/NOTICE +14 -0
@@ -0,0 +1,1069 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import json
6
+ from typing import Any, Literal, cast
7
+
8
+ import acp # type: ignore[reportMissingTypeStubs]
9
+ import pydantic
10
+ from pythinker_core.chat_provider import APIStatusError, ChatProviderError
11
+ from pythinker_core.tooling import ToolError, ToolResult
12
+ from pythinker_core.utils.typing import JsonType
13
+
14
+ from pythinker_code.approval_runtime import ApprovalRuntime
15
+ from pythinker_code.constant import USER_AGENT
16
+ from pythinker_code.soul import (
17
+ LLMNotSet,
18
+ LLMNotSupported,
19
+ MaxStepsReached,
20
+ RunCancelled,
21
+ Soul,
22
+ run_soul,
23
+ )
24
+ from pythinker_code.soul.pythinkersoul import PythinkerSoul
25
+ from pythinker_code.soul.toolset import PythinkerToolset, WireExternalTool
26
+ from pythinker_code.utils.aioqueue import Queue, QueueShutDown
27
+ from pythinker_code.utils.logging import logger
28
+ from pythinker_code.utils.signals import install_sigint_handler
29
+ from pythinker_code.wire import Wire
30
+ from pythinker_code.wire.types import (
31
+ ApprovalRequest,
32
+ ApprovalResponse,
33
+ HookRequest,
34
+ HookResponse,
35
+ QuestionNotSupported,
36
+ QuestionRequest,
37
+ QuestionResponse,
38
+ Request,
39
+ StatusUpdate,
40
+ ToolCallRequest,
41
+ is_event,
42
+ is_request,
43
+ )
44
+
45
+ from .jsonrpc import (
46
+ ClientInfo,
47
+ ErrorCodes,
48
+ JSONRPCCancelMessage,
49
+ JSONRPCErrorObject,
50
+ JSONRPCErrorResponse,
51
+ JSONRPCErrorResponseNullableID,
52
+ JSONRPCEventMessage,
53
+ JSONRPCInitializeMessage,
54
+ JSONRPCInMessage,
55
+ JSONRPCInMessageAdapter,
56
+ JSONRPCMessage,
57
+ JSONRPCOutMessage,
58
+ JSONRPCPromptMessage,
59
+ JSONRPCReplayMessage,
60
+ JSONRPCRequestMessage,
61
+ JSONRPCSetPlanModeMessage,
62
+ JSONRPCSteerMessage,
63
+ JSONRPCSuccessResponse,
64
+ Statuses,
65
+ )
66
+
67
+ # Maximum buffer size for the asyncio StreamReader used for stdio.
68
+ # Passed as the `limit` argument to `acp.stdio_streams`, this caps how much
69
+ # data can be buffered when reading from stdin (e.g., large tool or model
70
+ # outputs sent over JSON-RPC). A 100MB limit is large enough for typical
71
+ # interactive use while still protecting the process from unbounded memory
72
+ # growth or buffer-overrun errors when peers send unexpectedly large payloads.
73
+ STDIO_BUFFER_LIMIT = 100 * 1024 * 1024
74
+
75
+
76
+ def _is_oauth_session(runtime: Any) -> bool:
77
+ """Return True if the current session uses OAuth-based authentication."""
78
+ if runtime is None:
79
+ return False
80
+ llm = getattr(runtime, "llm", None)
81
+ if llm is None:
82
+ return False
83
+ provider_config = getattr(llm, "provider_config", None)
84
+ if provider_config is None:
85
+ return False
86
+ return getattr(provider_config, "oauth", None) is not None
87
+
88
+
89
+ class WireServer:
90
+ def __init__(self, soul: Soul):
91
+ self._reader: asyncio.StreamReader | None = None
92
+ self._writer: asyncio.StreamWriter | None = None
93
+
94
+ # outward
95
+ self._write_task: asyncio.Task[None] | None = None
96
+ self._write_queue: Queue[JSONRPCOutMessage] = Queue()
97
+
98
+ # inward
99
+ self._dispatch_tasks: set[asyncio.Task[None]] = set()
100
+
101
+ # soul running stuffs
102
+ self._soul = soul
103
+ self._cancel_event: asyncio.Event | None = None
104
+ self._pending_requests: dict[str, Request] = {}
105
+ """Maps JSON RPC message IDs to pending `Request`s."""
106
+ self._client_supports_question: bool = False
107
+ """Whether the Wire client supports QuestionRequest."""
108
+ self._client_supports_plan_mode: bool = False
109
+ """Whether the Wire client supports plan mode."""
110
+ self._initialized: bool = False
111
+ self._root_hub_queue: Queue[Any] | None = None
112
+ self._root_hub_task: asyncio.Task[None] | None = None
113
+
114
+ @property
115
+ def _approval_runtime(self) -> ApprovalRuntime | None:
116
+ if isinstance(self._soul, PythinkerSoul):
117
+ return self._soul.runtime.approval_runtime
118
+ return None
119
+
120
+ async def serve(self) -> None:
121
+ logger.info("Starting Wire server on stdio")
122
+
123
+ self._reader, self._writer = await acp.stdio_streams(limit=STDIO_BUFFER_LIMIT)
124
+ self._write_task = asyncio.create_task(self._write_loop())
125
+ if isinstance(self._soul, PythinkerSoul) and self._soul.runtime.root_wire_hub is not None:
126
+ self._root_hub_queue = self._soul.runtime.root_wire_hub.subscribe()
127
+ self._root_hub_task = asyncio.create_task(self._root_hub_loop())
128
+ stop_event = asyncio.Event()
129
+ loop = asyncio.get_running_loop()
130
+ remove_sigint = install_sigint_handler(loop, stop_event.set)
131
+ read_task = asyncio.create_task(self._read_loop())
132
+ stop_task = asyncio.create_task(stop_event.wait())
133
+ tasks: set[asyncio.Task[Any]] = {read_task, stop_task}
134
+ pending = tasks
135
+ try:
136
+ done, pending = await asyncio.wait(
137
+ tasks,
138
+ return_when=asyncio.FIRST_COMPLETED,
139
+ )
140
+ if stop_event.is_set():
141
+ logger.info("Wire server interrupted, shutting down")
142
+ if self._cancel_event is not None:
143
+ self._cancel_event.set()
144
+ if not read_task.done():
145
+ read_task.cancel()
146
+ with contextlib.suppress(asyncio.CancelledError):
147
+ await read_task
148
+ elif read_task in done:
149
+ read_task.result()
150
+ except KeyboardInterrupt:
151
+ logger.info("Wire server interrupted, shutting down")
152
+ if self._cancel_event is not None:
153
+ self._cancel_event.set()
154
+ finally:
155
+ remove_sigint()
156
+ for task in pending:
157
+ task.cancel()
158
+ with contextlib.suppress(asyncio.CancelledError):
159
+ await task
160
+ await self._shutdown()
161
+
162
+ async def _root_hub_loop(self) -> None:
163
+ assert self._root_hub_queue is not None
164
+ while True:
165
+ try:
166
+ msg = await self._root_hub_queue.get()
167
+ except QueueShutDown:
168
+ return
169
+ try:
170
+ if not self._initialized:
171
+ continue
172
+ if isinstance(msg, ApprovalRequest):
173
+ await self._request_approval(msg)
174
+ elif isinstance(msg, ApprovalResponse):
175
+ self._pending_requests.pop(msg.request_id, None)
176
+ await self._send_msg(JSONRPCEventMessage(method="event", params=msg))
177
+ elif is_event(msg):
178
+ await self._send_msg(JSONRPCEventMessage(method="event", params=msg))
179
+ except Exception:
180
+ logger.exception("Root hub message handling failed")
181
+
182
+ async def _write_loop(self) -> None:
183
+ assert self._writer is not None
184
+
185
+ try:
186
+ while True:
187
+ try:
188
+ msg = await self._write_queue.get()
189
+ except QueueShutDown:
190
+ logger.debug("Send queue shut down, stopping Wire server write loop")
191
+ break
192
+ self._writer.write(msg.model_dump_json().encode("utf-8") + b"\n")
193
+ await self._writer.drain()
194
+ except asyncio.CancelledError:
195
+ raise
196
+ except Exception:
197
+ logger.exception("Wire server write loop error:")
198
+ raise
199
+
200
+ async def _read_loop(self) -> None:
201
+ assert self._reader is not None
202
+
203
+ while True:
204
+ raw_line = await self._reader.readline()
205
+ if not raw_line:
206
+ logger.info("stdin closed, Wire server exiting")
207
+ break
208
+ line = raw_line.decode("utf-8", errors="replace").strip()
209
+
210
+ try:
211
+ msg_json = json.loads(line)
212
+ except ValueError:
213
+ logger.error("Invalid JSON line: {line}", line=line)
214
+ await self._send_msg(
215
+ JSONRPCErrorResponseNullableID(
216
+ id=None,
217
+ error=JSONRPCErrorObject(
218
+ code=ErrorCodes.PARSE_ERROR,
219
+ message="Invalid JSON format",
220
+ ),
221
+ )
222
+ )
223
+ continue
224
+
225
+ try:
226
+ generic_msg = JSONRPCMessage.model_validate(msg_json)
227
+ except pydantic.ValidationError as e:
228
+ logger.error("Invalid JSON-RPC message: {error}", error=e)
229
+ await self._send_msg(
230
+ JSONRPCErrorResponseNullableID(
231
+ id=None,
232
+ error=JSONRPCErrorObject(
233
+ code=ErrorCodes.INVALID_REQUEST,
234
+ message="Invalid request",
235
+ ),
236
+ )
237
+ )
238
+ continue
239
+
240
+ if generic_msg.is_response():
241
+ # for responses, we skip the method check
242
+ try:
243
+ msg = JSONRPCInMessageAdapter.validate_python(msg_json)
244
+ except pydantic.ValidationError as e:
245
+ logger.error("Invalid JSON-RPC response: {error}", error=e)
246
+ await self._send_msg(
247
+ JSONRPCErrorResponseNullableID(
248
+ id=None,
249
+ error=JSONRPCErrorObject(
250
+ code=ErrorCodes.INVALID_REQUEST,
251
+ message="Invalid response",
252
+ ),
253
+ )
254
+ )
255
+ continue # ignore invalid json-rpc responses
256
+
257
+ if not isinstance(msg, (JSONRPCSuccessResponse, JSONRPCErrorResponse)):
258
+ logger.error(
259
+ "Invalid JSON-RPC response message: {msg}",
260
+ msg=msg_json,
261
+ )
262
+ continue # ignore invalid response messages
263
+
264
+ task = asyncio.create_task(self._dispatch_msg(msg))
265
+ task.add_done_callback(self._dispatch_tasks.discard)
266
+ self._dispatch_tasks.add(task)
267
+ continue
268
+
269
+ if not generic_msg.method_is_inbound():
270
+ logger.error(
271
+ "Unexpected JSON-RPC method received: {method}",
272
+ method=generic_msg.method,
273
+ )
274
+ if generic_msg.id is not None:
275
+ resp = JSONRPCErrorResponse(
276
+ id=generic_msg.id,
277
+ error=JSONRPCErrorObject(
278
+ code=ErrorCodes.METHOD_NOT_FOUND,
279
+ message=f"Unexpected method received: {generic_msg.method}",
280
+ ),
281
+ )
282
+ await self._send_msg(resp)
283
+ continue # ignore unexpected outbound methods
284
+
285
+ try:
286
+ msg = JSONRPCInMessageAdapter.validate_python(msg_json)
287
+ except pydantic.ValidationError as e:
288
+ logger.error("Invalid JSON-RPC inbound message: {error}", error=e)
289
+ if generic_msg.id is not None:
290
+ resp = JSONRPCErrorResponse(
291
+ id=generic_msg.id,
292
+ error=JSONRPCErrorObject(
293
+ code=ErrorCodes.INVALID_PARAMS,
294
+ message=f"Invalid parameters for method `{generic_msg.method}`",
295
+ ),
296
+ )
297
+ await self._send_msg(resp)
298
+ continue # ignore invalid inbound messages
299
+
300
+ task = asyncio.create_task(self._dispatch_msg(msg))
301
+ task.add_done_callback(self._dispatch_tasks.discard)
302
+ self._dispatch_tasks.add(task)
303
+
304
+ async def _shutdown(self) -> None:
305
+ for request in self._pending_requests.values():
306
+ if request.resolved:
307
+ continue
308
+ match request:
309
+ case ApprovalRequest():
310
+ if request.source_kind == "foreground_turn":
311
+ request.resolve("reject")
312
+ if self._approval_runtime is not None:
313
+ self._approval_runtime.resolve(request.id, "reject")
314
+ case ToolCallRequest():
315
+ request.resolve(
316
+ ToolError(
317
+ message="Wire connection closed before tool result was received.",
318
+ brief="Wire closed",
319
+ )
320
+ )
321
+ case QuestionRequest():
322
+ request.resolve({})
323
+ case HookRequest():
324
+ request.resolve("allow")
325
+ self._pending_requests.clear()
326
+
327
+ if self._cancel_event is not None:
328
+ self._cancel_event.set()
329
+ self._cancel_event = None
330
+
331
+ self._write_queue.shutdown()
332
+ if self._write_task is not None:
333
+ with contextlib.suppress(asyncio.CancelledError):
334
+ await self._write_task
335
+
336
+ if self._root_hub_task is not None:
337
+ self._root_hub_task.cancel()
338
+ with contextlib.suppress(asyncio.CancelledError):
339
+ await self._root_hub_task
340
+ self._root_hub_task = None
341
+ if (
342
+ isinstance(self._soul, PythinkerSoul)
343
+ and self._root_hub_queue is not None
344
+ and self._soul.runtime.root_wire_hub is not None
345
+ ):
346
+ self._soul.runtime.root_wire_hub.unsubscribe(self._root_hub_queue)
347
+ self._root_hub_queue = None
348
+
349
+ await asyncio.gather(*self._dispatch_tasks, return_exceptions=True)
350
+ self._dispatch_tasks.clear()
351
+
352
+ if self._writer is not None:
353
+ self._writer.close()
354
+ with contextlib.suppress(Exception):
355
+ await self._writer.wait_closed()
356
+ self._writer = None
357
+
358
+ self._reader = None
359
+ self._initialized = False
360
+
361
+ async def _dispatch_msg(self, msg: JSONRPCInMessage) -> None:
362
+ resp: JSONRPCSuccessResponse | JSONRPCErrorResponse | None = None
363
+ try:
364
+ match msg:
365
+ case JSONRPCInitializeMessage():
366
+ resp = await self._handle_initialize(msg)
367
+ case JSONRPCPromptMessage():
368
+ resp = await self._handle_prompt(msg)
369
+ case JSONRPCReplayMessage():
370
+ resp = await self._handle_replay(msg)
371
+ case JSONRPCSteerMessage():
372
+ resp = await self._handle_steer(msg)
373
+ case JSONRPCSetPlanModeMessage():
374
+ resp = await self._handle_set_plan_mode(msg)
375
+ case JSONRPCCancelMessage():
376
+ resp = await self._handle_cancel(msg)
377
+ case JSONRPCSuccessResponse() | JSONRPCErrorResponse():
378
+ await self._handle_response(msg)
379
+
380
+ if resp is not None:
381
+ await self._send_msg(resp)
382
+ except Exception:
383
+ logger.exception("Unexpected error dispatching JSONRPC message:")
384
+ raise
385
+
386
+ async def _send_msg(self, msg: JSONRPCOutMessage) -> None:
387
+ try:
388
+ await self._write_queue.put(msg)
389
+ except QueueShutDown:
390
+ logger.error("Send queue shut down; dropping message: {msg}", msg=msg)
391
+
392
+ @property
393
+ def _is_streaming(self) -> bool:
394
+ return self._cancel_event is not None
395
+
396
+ async def _handle_initialize(
397
+ self, msg: JSONRPCInitializeMessage
398
+ ) -> JSONRPCSuccessResponse | JSONRPCErrorResponse:
399
+ if self._is_streaming:
400
+ return JSONRPCErrorResponse(
401
+ id=msg.id,
402
+ error=JSONRPCErrorObject(
403
+ code=ErrorCodes.INVALID_STATE,
404
+ message="An agent turn is already in progress",
405
+ ),
406
+ )
407
+
408
+ accepted: list[str] = []
409
+ rejected: list[dict[str, str]] = []
410
+ toolset = None
411
+ if isinstance(self._soul, PythinkerSoul) and isinstance(
412
+ self._soul.agent.toolset, PythinkerToolset
413
+ ):
414
+ toolset = self._soul.agent.toolset
415
+
416
+ if toolset and msg.params.external_tools:
417
+ for tool in msg.params.external_tools:
418
+ existing = toolset.find(tool.name)
419
+ if existing is not None and not isinstance(existing, WireExternalTool):
420
+ rejected.append({"name": tool.name, "reason": "conflicts with builtin tool"})
421
+ continue
422
+ ok, reason = toolset.register_external_tool(
423
+ tool.name,
424
+ tool.description,
425
+ tool.parameters,
426
+ )
427
+ if ok:
428
+ accepted.append(tool.name)
429
+ else:
430
+ rejected.append({"name": tool.name, "reason": reason or "invalid schema"})
431
+
432
+ slash_commands: list[JsonType] = []
433
+ for cmd in self._soul.available_slash_commands:
434
+ slash_commands.append(
435
+ cast(
436
+ JsonType,
437
+ {"name": cmd.name, "description": cmd.description, "aliases": cmd.aliases},
438
+ )
439
+ )
440
+
441
+ from pythinker_code.constant import NAME, VERSION
442
+ from pythinker_code.hooks.config import HOOK_EVENT_TYPES
443
+ from pythinker_code.hooks.engine import WireHookHandle, WireHookSubscription
444
+ from pythinker_code.soul import wire_send
445
+ from pythinker_code.wire.protocol import WIRE_PROTOCOL_VERSION
446
+ from pythinker_code.wire.types import HookResolved, HookTriggered
447
+
448
+ # Hook engine setup — register wire subscriptions and callbacks
449
+
450
+ hook_engine = self._soul.hook_engine
451
+
452
+ if msg.params.hooks:
453
+ wire_subs: list[WireHookSubscription] = []
454
+ for wh in msg.params.hooks:
455
+ if wh.event not in HOOK_EVENT_TYPES:
456
+ logger.warning("Ignoring unknown hook event from client: {}", wh.event)
457
+ continue
458
+ wire_subs.append(
459
+ WireHookSubscription(
460
+ id=wh.id,
461
+ event=wh.event,
462
+ matcher=wh.matcher,
463
+ timeout=wh.timeout,
464
+ )
465
+ )
466
+ if wire_subs:
467
+ hook_engine.add_wire_subscriptions(wire_subs)
468
+ logger.info("Registered {} wire hook subscriptions from client", len(wire_subs))
469
+
470
+ def _on_triggered(event: str, target: str, count: int) -> None:
471
+ wire_send(HookTriggered(event=event, target=target, hook_count=count))
472
+
473
+ def _on_resolved(
474
+ event: str,
475
+ target: str,
476
+ action: str,
477
+ reason: str,
478
+ duration_ms: int,
479
+ ) -> None:
480
+ wire_send(
481
+ HookResolved(
482
+ event=event,
483
+ target=target,
484
+ action=cast(Literal["allow", "block"], action),
485
+ reason=reason,
486
+ duration_ms=duration_ms,
487
+ )
488
+ )
489
+
490
+ async def _on_wire_hook(handle: WireHookHandle) -> None:
491
+ """Send HookRequest to client, wire response back to handle."""
492
+ request = HookRequest(
493
+ id=handle.id,
494
+ subscription_id=handle.subscription_id,
495
+ event=handle.event,
496
+ target=handle.target,
497
+ input_data=handle.input_data,
498
+ )
499
+ self._pending_requests[handle.id] = request
500
+ await self._send_msg(JSONRPCRequestMessage(id=handle.id, params=request))
501
+ # Wait for client response (resolved via _handle_response)
502
+ action, reason = await request.wait()
503
+ handle.resolve(action, reason)
504
+
505
+ hook_engine.set_callbacks(
506
+ on_triggered=_on_triggered,
507
+ on_resolved=_on_resolved,
508
+ on_wire_hook=_on_wire_hook,
509
+ )
510
+
511
+ hooks_info: dict[str, JsonType] = cast(
512
+ dict[str, JsonType],
513
+ {
514
+ "supported_events": HOOK_EVENT_TYPES,
515
+ "configured": hook_engine.summary,
516
+ },
517
+ )
518
+
519
+ result: dict[str, JsonType] = {
520
+ "protocol_version": WIRE_PROTOCOL_VERSION,
521
+ "server": cast(JsonType, {"name": NAME, "version": VERSION}),
522
+ "slash_commands": cast(JsonType, slash_commands),
523
+ }
524
+ if accepted or rejected:
525
+ result["external_tools"] = cast(
526
+ JsonType,
527
+ {
528
+ "accepted": accepted,
529
+ "rejected": rejected,
530
+ },
531
+ )
532
+
533
+ if hooks_info:
534
+ result["hooks"] = cast(JsonType, hooks_info)
535
+
536
+ self._apply_wire_client_info(msg.params.client)
537
+ self._track_session_started(msg.params.client)
538
+
539
+ if msg.params.capabilities is not None:
540
+ self._client_supports_question = msg.params.capabilities.supports_question
541
+ self._client_supports_plan_mode = msg.params.capabilities.supports_plan_mode
542
+
543
+ if toolset is not None:
544
+ self._sync_ask_user_tool_visibility(toolset)
545
+ self._sync_plan_mode_tool_visibility(toolset)
546
+
547
+ self._initialized = True
548
+ if self._approval_runtime is not None:
549
+ for request in self._approval_runtime.list_pending():
550
+ await self._request_approval(
551
+ ApprovalRequest(
552
+ id=request.id,
553
+ tool_call_id=request.tool_call_id,
554
+ sender=request.sender,
555
+ action=request.action,
556
+ description=request.description,
557
+ display=request.display,
558
+ source_kind=request.source.kind,
559
+ source_id=request.source.id,
560
+ agent_id=request.source.agent_id,
561
+ subagent_type=request.source.subagent_type,
562
+ )
563
+ )
564
+
565
+ result["capabilities"] = cast(
566
+ JsonType,
567
+ {"supports_question": True},
568
+ )
569
+
570
+ return JSONRPCSuccessResponse(
571
+ id=msg.id,
572
+ result=result,
573
+ )
574
+
575
+ def _sync_ask_user_tool_visibility(self, toolset: PythinkerToolset) -> None:
576
+ """Hide or unhide the AskUserQuestion tool based on client capabilities."""
577
+ from pythinker_code.tools.ask_user import NAME as ASK_USER_TOOL_NAME
578
+
579
+ all_toolsets = [toolset]
580
+
581
+ if self._client_supports_question:
582
+ for ts in all_toolsets:
583
+ ts.unhide(ASK_USER_TOOL_NAME)
584
+ else:
585
+ for ts in all_toolsets:
586
+ ts.hide(ASK_USER_TOOL_NAME)
587
+ logger.info(
588
+ "Hid {tool} tool: client does not support questions",
589
+ tool=ASK_USER_TOOL_NAME,
590
+ )
591
+
592
+ def _sync_plan_mode_tool_visibility(self, toolset: PythinkerToolset) -> None:
593
+ """Hide or unhide plan mode tools based on client capabilities."""
594
+ from pythinker_code.tools.plan import NAME as EXIT_PLAN_MODE_TOOL_NAME
595
+ from pythinker_code.tools.plan.enter import NAME as ENTER_PLAN_MODE_TOOL_NAME
596
+
597
+ plan_tool_names = [ENTER_PLAN_MODE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME]
598
+
599
+ all_toolsets = [toolset]
600
+
601
+ if self._client_supports_plan_mode:
602
+ for ts in all_toolsets:
603
+ for name in plan_tool_names:
604
+ ts.unhide(name)
605
+ else:
606
+ for ts in all_toolsets:
607
+ for name in plan_tool_names:
608
+ ts.hide(name)
609
+ logger.info(
610
+ "Hide plan mode tools: client does not support plan mode",
611
+ )
612
+
613
+ def _apply_wire_client_info(self, client: ClientInfo | None) -> None:
614
+ if client is not None:
615
+ from pythinker_code.telemetry import set_client_info
616
+
617
+ set_client_info(name=client.name, version=client.version)
618
+
619
+ if not isinstance(self._soul, PythinkerSoul):
620
+ return
621
+ llm = self._soul.runtime.llm
622
+ if llm is None:
623
+ return
624
+
625
+ ua_suffix = ""
626
+ if client is not None:
627
+ ua_suffix = client.name
628
+ if client.version:
629
+ ua_suffix += f" {client.version}"
630
+ ua_suffix = f" ({ua_suffix.strip()})"
631
+
632
+ from pythinker_core.chat_provider.pythinker import Pythinker
633
+
634
+ if isinstance(llm.chat_provider, Pythinker):
635
+ pythinker_codeent = llm.chat_provider.client
636
+ headers = dict(pythinker_codeent._custom_headers) # pyright: ignore[reportPrivateUsage]
637
+ headers["User-Agent"] = f"{USER_AGENT}{ua_suffix}"
638
+ pythinker_codeent._custom_headers = headers # pyright: ignore[reportPrivateUsage]
639
+
640
+ def _track_session_started(self, client: ClientInfo | None) -> None:
641
+ if not isinstance(self._soul, PythinkerSoul):
642
+ return
643
+
644
+ from pythinker_code.telemetry import track_session_started_once
645
+
646
+ track_session_started_once(
647
+ ui_mode="wire",
648
+ resumed=self._soul.runtime.resumed,
649
+ client_name=client.name if client is not None else None,
650
+ client_version=client.version if client is not None else None,
651
+ )
652
+
653
+ async def _handle_prompt(
654
+ self, msg: JSONRPCPromptMessage
655
+ ) -> JSONRPCSuccessResponse | JSONRPCErrorResponse:
656
+ if self._is_streaming:
657
+ # TODO: support queueing multiple inputs
658
+ return JSONRPCErrorResponse(
659
+ id=msg.id,
660
+ error=JSONRPCErrorObject(
661
+ code=ErrorCodes.INVALID_STATE, message="An agent turn is already in progress"
662
+ ),
663
+ )
664
+
665
+ if not self._initialized:
666
+ self._track_session_started(None)
667
+
668
+ self._cancel_event = asyncio.Event()
669
+ runtime = self._soul.runtime if isinstance(self._soul, PythinkerSoul) else None
670
+ try:
671
+ await run_soul(
672
+ self._soul,
673
+ msg.params.user_input,
674
+ self._stream_wire_messages,
675
+ self._cancel_event,
676
+ runtime.session.wire_file if runtime else None,
677
+ runtime,
678
+ )
679
+ return JSONRPCSuccessResponse(
680
+ id=msg.id,
681
+ result={"status": Statuses.FINISHED},
682
+ )
683
+ except LLMNotSet:
684
+ return JSONRPCErrorResponse(
685
+ id=msg.id,
686
+ error=JSONRPCErrorObject(code=ErrorCodes.LLM_NOT_SET, message="LLM is not set"),
687
+ )
688
+ except LLMNotSupported as e:
689
+ return JSONRPCErrorResponse(
690
+ id=msg.id,
691
+ error=JSONRPCErrorObject(code=ErrorCodes.LLM_NOT_SUPPORTED, message=str(e)),
692
+ )
693
+ except APIStatusError as e:
694
+ if e.status_code == 401 and _is_oauth_session(runtime):
695
+ return JSONRPCErrorResponse(
696
+ id=msg.id,
697
+ error=JSONRPCErrorObject(
698
+ code=ErrorCodes.AUTH_EXPIRED,
699
+ message=(
700
+ "Authentication failed. Your login session may have expired. "
701
+ 'Please run "/login" to sign in again.'
702
+ ),
703
+ ),
704
+ )
705
+ return JSONRPCErrorResponse(
706
+ id=msg.id,
707
+ error=JSONRPCErrorObject(code=ErrorCodes.CHAT_PROVIDER_ERROR, message=str(e)),
708
+ )
709
+ except ChatProviderError as e:
710
+ return JSONRPCErrorResponse(
711
+ id=msg.id,
712
+ error=JSONRPCErrorObject(code=ErrorCodes.CHAT_PROVIDER_ERROR, message=str(e)),
713
+ )
714
+ except MaxStepsReached as e:
715
+ return JSONRPCSuccessResponse(
716
+ id=msg.id,
717
+ result={"status": Statuses.MAX_STEPS_REACHED, "steps": e.n_steps},
718
+ )
719
+ except RunCancelled:
720
+ return JSONRPCSuccessResponse(
721
+ id=msg.id,
722
+ result={"status": Statuses.CANCELLED},
723
+ )
724
+ except Exception as e:
725
+ logger.exception("Unexpected error in prompt handler")
726
+ return JSONRPCErrorResponse(
727
+ id=msg.id,
728
+ error=JSONRPCErrorObject(
729
+ code=ErrorCodes.INTERNAL_ERROR,
730
+ message=f"{type(e).__name__}: {e}",
731
+ ),
732
+ )
733
+ finally:
734
+ # Clean up any remaining pending requests from this turn.
735
+ # After run_soul() returns, the soul and all subagents are done,
736
+ # so any unresolved requests are stale.
737
+ stale_ids = [k for k, v in self._pending_requests.items() if not v.resolved]
738
+ for msg_id in stale_ids:
739
+ request = self._pending_requests[msg_id]
740
+ match request:
741
+ case ApprovalRequest():
742
+ if request.source_kind == "foreground_turn":
743
+ self._pending_requests.pop(msg_id, None)
744
+ request.resolve("reject")
745
+ if self._approval_runtime is not None:
746
+ self._approval_runtime.resolve(request.id, "reject")
747
+ case ToolCallRequest():
748
+ self._pending_requests.pop(msg_id, None)
749
+ request.resolve(
750
+ ToolError(
751
+ message="Agent turn ended before tool result was received.",
752
+ brief="Turn ended",
753
+ )
754
+ )
755
+ case QuestionRequest():
756
+ self._pending_requests.pop(msg_id, None)
757
+ request.resolve({})
758
+ case HookRequest():
759
+ self._pending_requests.pop(msg_id, None)
760
+ request.resolve("allow")
761
+ case _:
762
+ pass
763
+ self._cancel_event = None
764
+
765
+ async def _handle_steer(
766
+ self, msg: JSONRPCSteerMessage
767
+ ) -> JSONRPCSuccessResponse | JSONRPCErrorResponse:
768
+ if not isinstance(self._soul, PythinkerSoul) or not self._is_streaming:
769
+ return JSONRPCErrorResponse(
770
+ id=msg.id,
771
+ error=JSONRPCErrorObject(
772
+ code=ErrorCodes.INVALID_STATE,
773
+ message="No agent turn is in progress",
774
+ ),
775
+ )
776
+
777
+ self._soul.steer(msg.params.user_input)
778
+ return JSONRPCSuccessResponse(
779
+ id=msg.id,
780
+ result={"status": Statuses.STEERED},
781
+ )
782
+
783
+ async def _handle_set_plan_mode(
784
+ self, msg: JSONRPCSetPlanModeMessage
785
+ ) -> JSONRPCSuccessResponse | JSONRPCErrorResponse:
786
+ if not isinstance(self._soul, PythinkerSoul):
787
+ return JSONRPCErrorResponse(
788
+ id=msg.id,
789
+ error=JSONRPCErrorObject(
790
+ code=ErrorCodes.INVALID_STATE,
791
+ message="Plan mode is not supported",
792
+ ),
793
+ )
794
+
795
+ new_state = await self._soul.set_plan_mode_from_manual(msg.params.enabled)
796
+
797
+ status = StatusUpdate(plan_mode=new_state)
798
+ await self._send_msg(JSONRPCEventMessage(params=status))
799
+ # Persist to wire file so replay reconstructs plan mode state
800
+ await self._soul.wire_file.append_message(status)
801
+ return JSONRPCSuccessResponse(
802
+ id=msg.id,
803
+ result={"status": "ok", "plan_mode": new_state},
804
+ )
805
+
806
+ async def _handle_replay(
807
+ self, msg: JSONRPCReplayMessage
808
+ ) -> JSONRPCSuccessResponse | JSONRPCErrorResponse:
809
+ if self._is_streaming:
810
+ return JSONRPCErrorResponse(
811
+ id=msg.id,
812
+ error=JSONRPCErrorObject(
813
+ code=ErrorCodes.INVALID_STATE, message="An agent turn is already in progress"
814
+ ),
815
+ )
816
+
817
+ wire_file = self._soul.wire_file if isinstance(self._soul, PythinkerSoul) else None
818
+
819
+ self._cancel_event = asyncio.Event()
820
+ events = 0
821
+ requests = 0
822
+ try:
823
+ if wire_file is None or not wire_file.path.exists():
824
+ return JSONRPCSuccessResponse(
825
+ id=msg.id,
826
+ result={"status": Statuses.FINISHED, "events": 0, "requests": 0},
827
+ )
828
+
829
+ async for record in wire_file.iter_records():
830
+ if self._cancel_event.is_set():
831
+ return JSONRPCSuccessResponse(
832
+ id=msg.id,
833
+ result={
834
+ "status": Statuses.CANCELLED,
835
+ "events": events,
836
+ "requests": requests,
837
+ },
838
+ )
839
+
840
+ try:
841
+ wire_msg = record.to_wire_message()
842
+ except Exception:
843
+ logger.exception(
844
+ "Failed to deserialize wire record for replay: {file}",
845
+ file=wire_file.path,
846
+ )
847
+ continue
848
+
849
+ if is_request(wire_msg):
850
+ await self._send_msg(JSONRPCRequestMessage(id=wire_msg.id, params=wire_msg))
851
+ requests += 1
852
+ elif is_event(wire_msg):
853
+ await self._send_msg(JSONRPCEventMessage(params=wire_msg))
854
+ events += 1
855
+ else:
856
+ # Not reachable for valid WireMessage, but keep a guard for corrupted data.
857
+ logger.warning(
858
+ "Skipping non-wire message during replay: {msg}",
859
+ msg=wire_msg,
860
+ )
861
+
862
+ await asyncio.sleep(0) # yield control for cancel handling
863
+
864
+ if self._cancel_event.is_set():
865
+ return JSONRPCSuccessResponse(
866
+ id=msg.id,
867
+ result={
868
+ "status": Statuses.CANCELLED,
869
+ "events": events,
870
+ "requests": requests,
871
+ },
872
+ )
873
+
874
+ return JSONRPCSuccessResponse(
875
+ id=msg.id,
876
+ result={"status": Statuses.FINISHED, "events": events, "requests": requests},
877
+ )
878
+ except Exception:
879
+ logger.exception("Replay failed:")
880
+ return JSONRPCErrorResponse(
881
+ id=msg.id,
882
+ error=JSONRPCErrorObject(
883
+ code=ErrorCodes.INTERNAL_ERROR,
884
+ message="Replay failed",
885
+ ),
886
+ )
887
+ finally:
888
+ self._cancel_event = None
889
+
890
+ async def _handle_cancel(
891
+ self, msg: JSONRPCCancelMessage
892
+ ) -> JSONRPCSuccessResponse | JSONRPCErrorResponse:
893
+ if not self._is_streaming:
894
+ return JSONRPCErrorResponse(
895
+ id=msg.id,
896
+ error=JSONRPCErrorObject(
897
+ code=ErrorCodes.INVALID_STATE, message="No agent turn is in progress"
898
+ ),
899
+ )
900
+
901
+ assert self._cancel_event is not None
902
+ self._cancel_event.set()
903
+ return JSONRPCSuccessResponse(
904
+ id=msg.id,
905
+ result={},
906
+ )
907
+
908
+ async def _handle_response(self, msg: JSONRPCSuccessResponse | JSONRPCErrorResponse) -> None:
909
+ request = self._pending_requests.pop(msg.id, None)
910
+ if request is None:
911
+ logger.error("No pending request for response id={id}", id=msg.id)
912
+ return
913
+
914
+ match request:
915
+ case ApprovalRequest():
916
+ if isinstance(msg, JSONRPCErrorResponse):
917
+ request.resolve("reject")
918
+ if self._approval_runtime is not None:
919
+ self._approval_runtime.resolve(request.id, "reject")
920
+ return
921
+
922
+ try:
923
+ result = ApprovalResponse.model_validate(msg.result)
924
+ except pydantic.ValidationError as e:
925
+ logger.error(
926
+ "Invalid response result for request id={id}: {error}",
927
+ id=msg.id,
928
+ error=e,
929
+ )
930
+ request.resolve("reject")
931
+ if self._approval_runtime is not None:
932
+ self._approval_runtime.resolve(request.id, "reject")
933
+ return
934
+
935
+ if result.request_id != request.id:
936
+ logger.warning(
937
+ "Approval response id mismatch: request={request_id}, "
938
+ "response={response_id}",
939
+ request_id=request.id,
940
+ response_id=result.request_id,
941
+ )
942
+ request.resolve(result.response)
943
+ if self._approval_runtime is not None:
944
+ self._approval_runtime.resolve(
945
+ request.id, result.response, feedback=result.feedback
946
+ )
947
+ case ToolCallRequest():
948
+ if isinstance(msg, JSONRPCErrorResponse):
949
+ error = msg.error.message
950
+ request.resolve(
951
+ ToolError(
952
+ message=error,
953
+ brief="External tool error",
954
+ )
955
+ )
956
+ return
957
+
958
+ try:
959
+ tool_result = ToolResult.model_validate(msg.result)
960
+ except pydantic.ValidationError as e:
961
+ logger.error(
962
+ "Invalid tool result for request id={id}: {error}",
963
+ id=msg.id,
964
+ error=e,
965
+ )
966
+ request.resolve(
967
+ ToolError(
968
+ message="Invalid tool result payload from client.",
969
+ brief="Invalid tool result",
970
+ )
971
+ )
972
+ return
973
+ if tool_result.tool_call_id != request.id:
974
+ logger.warning(
975
+ "Tool result id mismatch: request={request_id}, result={result_id}",
976
+ request_id=request.id,
977
+ result_id=tool_result.tool_call_id,
978
+ )
979
+ request.resolve(tool_result.return_value)
980
+ case QuestionRequest():
981
+ if isinstance(msg, JSONRPCErrorResponse):
982
+ request.resolve({})
983
+ return
984
+
985
+ try:
986
+ result = QuestionResponse.model_validate(msg.result)
987
+ except pydantic.ValidationError as e:
988
+ logger.error(
989
+ "Invalid question response for request id={id}: {error}",
990
+ id=msg.id,
991
+ error=e,
992
+ )
993
+ request.resolve({})
994
+ return
995
+
996
+ if result.request_id != request.id:
997
+ logger.warning(
998
+ "Question response id mismatch: request={request_id}, "
999
+ "response={response_id}",
1000
+ request_id=request.id,
1001
+ response_id=result.request_id,
1002
+ )
1003
+ request.resolve(result.answers)
1004
+ case HookRequest():
1005
+ if isinstance(msg, JSONRPCErrorResponse):
1006
+ request.resolve("allow")
1007
+ return
1008
+
1009
+ try:
1010
+ result = HookResponse.model_validate(msg.result)
1011
+ except pydantic.ValidationError as e:
1012
+ logger.error(
1013
+ "Invalid hook response for request id={id}: {error}",
1014
+ id=msg.id,
1015
+ error=e,
1016
+ )
1017
+ request.resolve("allow")
1018
+ return
1019
+
1020
+ if result.request_id != request.id:
1021
+ logger.warning(
1022
+ "Hook response id mismatch: request={request_id}, response={response_id}",
1023
+ request_id=request.id,
1024
+ response_id=result.request_id,
1025
+ )
1026
+ request.resolve(result.action, result.reason)
1027
+
1028
+ async def _stream_wire_messages(self, wire: Wire) -> None:
1029
+ wire_ui = wire.ui_side(merge=False)
1030
+ while True:
1031
+ msg = await wire_ui.receive()
1032
+ match msg:
1033
+ case ApprovalRequest():
1034
+ await self._request_approval(msg)
1035
+ case ToolCallRequest():
1036
+ await self._request_external_tool(msg)
1037
+ case QuestionRequest():
1038
+ await self._request_question(msg)
1039
+ case HookRequest():
1040
+ pass # handled via hook engine callbacks
1041
+ case _:
1042
+ await self._send_msg(JSONRPCEventMessage(method="event", params=msg))
1043
+
1044
+ async def _request_approval(self, request: ApprovalRequest) -> None:
1045
+ msg_id = request.id # just use the approval request id as message id
1046
+ self._pending_requests[msg_id] = request
1047
+ await self._send_msg(JSONRPCRequestMessage(id=msg_id, params=request))
1048
+ # Do NOT await request.wait() here. The approval future is awaited by
1049
+ # the tool that created the request (inside the soul task). Blocking the
1050
+ # UI loop would prevent ALL subsequent Wire messages — from every
1051
+ # concurrent subagent — from reaching stdout, causing a cascade deadlock
1052
+ # when the approval response is lost (e.g. no WebSocket connected).
1053
+
1054
+ async def _request_external_tool(self, request: ToolCallRequest) -> None:
1055
+ msg_id = request.id
1056
+ self._pending_requests[msg_id] = request
1057
+ await self._send_msg(JSONRPCRequestMessage(id=msg_id, params=request))
1058
+ # Same rationale as _request_approval: do not block the UI loop.
1059
+
1060
+ async def _request_question(self, request: QuestionRequest) -> None:
1061
+ if not self._client_supports_question:
1062
+ # Client does not support interactive questions; signal the tool
1063
+ # so it can tell the LLM to use an alternative approach.
1064
+ request.set_exception(QuestionNotSupported())
1065
+ return
1066
+ msg_id = request.id
1067
+ self._pending_requests[msg_id] = request
1068
+ await self._send_msg(JSONRPCRequestMessage(id=msg_id, params=request))
1069
+ # Same rationale as _request_approval: do not block the UI loop.