pythinker-code 0.8.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 (790) hide show
  1. pythinker_code/CHANGELOG.md +60 -0
  2. pythinker_code/__init__.py +0 -0
  3. pythinker_code/__main__.py +97 -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 +301 -0
  8. pythinker_code/acp/mcp.py +46 -0
  9. pythinker_code/acp/server.py +497 -0
  10. pythinker_code/acp/session.py +502 -0
  11. pythinker_code/acp/tools.py +174 -0
  12. pythinker_code/acp/types.py +13 -0
  13. pythinker_code/acp/version.py +45 -0
  14. pythinker_code/agents/default/agent.yaml +55 -0
  15. pythinker_code/agents/default/code_reviewer.yaml +47 -0
  16. pythinker_code/agents/default/coder.yaml +42 -0
  17. pythinker_code/agents/default/debugger.yaml +35 -0
  18. pythinker_code/agents/default/explore.yaml +59 -0
  19. pythinker_code/agents/default/implementer.yaml +46 -0
  20. pythinker_code/agents/default/plan.yaml +42 -0
  21. pythinker_code/agents/default/review.yaml +47 -0
  22. pythinker_code/agents/default/security_reviewer.yaml +37 -0
  23. pythinker_code/agents/default/system.md +192 -0
  24. pythinker_code/agents/default/verifier.yaml +46 -0
  25. pythinker_code/agents/okabe/agent.yaml +22 -0
  26. pythinker_code/agentspec.py +163 -0
  27. pythinker_code/app.py +847 -0
  28. pythinker_code/approval_runtime/__init__.py +29 -0
  29. pythinker_code/approval_runtime/models.py +42 -0
  30. pythinker_code/approval_runtime/runtime.py +235 -0
  31. pythinker_code/auth/__init__.py +25 -0
  32. pythinker_code/auth/anthropic_direct.py +207 -0
  33. pythinker_code/auth/deepseek.py +192 -0
  34. pythinker_code/auth/github_feedback.py +228 -0
  35. pythinker_code/auth/lm_studio.py +418 -0
  36. pythinker_code/auth/minimax.py +203 -0
  37. pythinker_code/auth/oauth.py +1145 -0
  38. pythinker_code/auth/ollama.py +293 -0
  39. pythinker_code/auth/openai.py +783 -0
  40. pythinker_code/auth/opencode_go.py +203 -0
  41. pythinker_code/auth/openrouter.py +225 -0
  42. pythinker_code/auth/platforms.py +475 -0
  43. pythinker_code/background/__init__.py +36 -0
  44. pythinker_code/background/agent_runner.py +231 -0
  45. pythinker_code/background/ids.py +19 -0
  46. pythinker_code/background/manager.py +668 -0
  47. pythinker_code/background/models.py +118 -0
  48. pythinker_code/background/store.py +243 -0
  49. pythinker_code/background/summary.py +66 -0
  50. pythinker_code/background/worker.py +209 -0
  51. pythinker_code/cli/__init__.py +1326 -0
  52. pythinker_code/cli/__main__.py +19 -0
  53. pythinker_code/cli/_lazy_group.py +268 -0
  54. pythinker_code/cli/debug.py +11 -0
  55. pythinker_code/cli/export.py +322 -0
  56. pythinker_code/cli/info.py +62 -0
  57. pythinker_code/cli/mcp.py +362 -0
  58. pythinker_code/cli/plugin.py +351 -0
  59. pythinker_code/cli/review.py +74 -0
  60. pythinker_code/cli/secscan.py +11 -0
  61. pythinker_code/cli/security_scan.py +35 -0
  62. pythinker_code/cli/toad.py +74 -0
  63. pythinker_code/cli/update.py +26 -0
  64. pythinker_code/cli/vis.py +38 -0
  65. pythinker_code/cli/web.py +80 -0
  66. pythinker_code/config.py +511 -0
  67. pythinker_code/constant.py +33 -0
  68. pythinker_code/events.py +106 -0
  69. pythinker_code/exception.py +43 -0
  70. pythinker_code/extensions.py +151 -0
  71. pythinker_code/hooks/__init__.py +4 -0
  72. pythinker_code/hooks/config.py +34 -0
  73. pythinker_code/hooks/engine.py +383 -0
  74. pythinker_code/hooks/events.py +190 -0
  75. pythinker_code/hooks/runner.py +92 -0
  76. pythinker_code/llm.py +441 -0
  77. pythinker_code/metadata.py +79 -0
  78. pythinker_code/notifications/__init__.py +33 -0
  79. pythinker_code/notifications/llm.py +77 -0
  80. pythinker_code/notifications/manager.py +145 -0
  81. pythinker_code/notifications/models.py +50 -0
  82. pythinker_code/notifications/notifier.py +41 -0
  83. pythinker_code/notifications/store.py +118 -0
  84. pythinker_code/notifications/wire.py +21 -0
  85. pythinker_code/plugin/__init__.py +124 -0
  86. pythinker_code/plugin/manager.py +166 -0
  87. pythinker_code/plugin/tool.py +173 -0
  88. pythinker_code/prompt_templates.py +181 -0
  89. pythinker_code/prompts/__init__.py +6 -0
  90. pythinker_code/prompts/compact.md +73 -0
  91. pythinker_code/prompts/init.md +21 -0
  92. pythinker_code/py.typed +0 -0
  93. pythinker_code/session.py +319 -0
  94. pythinker_code/session_fork.py +325 -0
  95. pythinker_code/session_state.py +132 -0
  96. pythinker_code/share.py +14 -0
  97. pythinker_code/skill/__init__.py +727 -0
  98. pythinker_code/skill/flow/__init__.py +99 -0
  99. pythinker_code/skill/flow/d2.py +482 -0
  100. pythinker_code/skill/flow/mermaid.py +266 -0
  101. pythinker_code/skills/pythinker-code-help/SKILL.md +54 -0
  102. pythinker_code/skills/skill-creator/SKILL.md +367 -0
  103. pythinker_code/soul/__init__.py +304 -0
  104. pythinker_code/soul/agent.py +552 -0
  105. pythinker_code/soul/approval.py +267 -0
  106. pythinker_code/soul/btw.py +220 -0
  107. pythinker_code/soul/compaction.py +189 -0
  108. pythinker_code/soul/context.py +339 -0
  109. pythinker_code/soul/denwarenji.py +39 -0
  110. pythinker_code/soul/dynamic_injection.py +84 -0
  111. pythinker_code/soul/dynamic_injections/__init__.py +0 -0
  112. pythinker_code/soul/dynamic_injections/auto_mode.py +72 -0
  113. pythinker_code/soul/dynamic_injections/plan_mode.py +239 -0
  114. pythinker_code/soul/message.py +92 -0
  115. pythinker_code/soul/permission.py +368 -0
  116. pythinker_code/soul/pythinkersoul.py +1763 -0
  117. pythinker_code/soul/slash.py +340 -0
  118. pythinker_code/soul/toolset.py +826 -0
  119. pythinker_code/subagents/__init__.py +21 -0
  120. pythinker_code/subagents/builder.py +43 -0
  121. pythinker_code/subagents/core.py +86 -0
  122. pythinker_code/subagents/discovery.py +234 -0
  123. pythinker_code/subagents/git_context.py +172 -0
  124. pythinker_code/subagents/models.py +56 -0
  125. pythinker_code/subagents/output.py +71 -0
  126. pythinker_code/subagents/registry.py +28 -0
  127. pythinker_code/subagents/runner.py +442 -0
  128. pythinker_code/subagents/store.py +200 -0
  129. pythinker_code/telemetry/__init__.py +217 -0
  130. pythinker_code/telemetry/config.py +113 -0
  131. pythinker_code/telemetry/crash.py +191 -0
  132. pythinker_code/telemetry/errors.py +113 -0
  133. pythinker_code/telemetry/metrics.py +208 -0
  134. pythinker_code/telemetry/otel.py +303 -0
  135. pythinker_code/telemetry/sentry.py +212 -0
  136. pythinker_code/telemetry/sink.py +189 -0
  137. pythinker_code/tools/AGENTS.md +6 -0
  138. pythinker_code/tools/__init__.py +105 -0
  139. pythinker_code/tools/agent/__init__.py +326 -0
  140. pythinker_code/tools/agent/description.md +65 -0
  141. pythinker_code/tools/ask_user/__init__.py +162 -0
  142. pythinker_code/tools/ask_user/description.md +19 -0
  143. pythinker_code/tools/background/__init__.py +318 -0
  144. pythinker_code/tools/background/list.md +10 -0
  145. pythinker_code/tools/background/output.md +11 -0
  146. pythinker_code/tools/background/stop.md +8 -0
  147. pythinker_code/tools/display.py +46 -0
  148. pythinker_code/tools/dmail/__init__.py +38 -0
  149. pythinker_code/tools/dmail/dmail.md +17 -0
  150. pythinker_code/tools/file/__init__.py +31 -0
  151. pythinker_code/tools/file/glob.md +17 -0
  152. pythinker_code/tools/file/glob.py +163 -0
  153. pythinker_code/tools/file/grep.md +6 -0
  154. pythinker_code/tools/file/grep_local.py +904 -0
  155. pythinker_code/tools/file/plan_mode.py +45 -0
  156. pythinker_code/tools/file/read.md +16 -0
  157. pythinker_code/tools/file/read.py +303 -0
  158. pythinker_code/tools/file/read_media.md +24 -0
  159. pythinker_code/tools/file/read_media.py +220 -0
  160. pythinker_code/tools/file/replace.md +7 -0
  161. pythinker_code/tools/file/replace.py +204 -0
  162. pythinker_code/tools/file/utils.py +257 -0
  163. pythinker_code/tools/file/write.md +5 -0
  164. pythinker_code/tools/file/write.py +186 -0
  165. pythinker_code/tools/plan/__init__.py +362 -0
  166. pythinker_code/tools/plan/description.md +29 -0
  167. pythinker_code/tools/plan/enter.py +193 -0
  168. pythinker_code/tools/plan/enter_description.md +35 -0
  169. pythinker_code/tools/plan/handoff.py +69 -0
  170. pythinker_code/tools/plan/heroes.py +277 -0
  171. pythinker_code/tools/shell/__init__.py +263 -0
  172. pythinker_code/tools/shell/bash.md +35 -0
  173. pythinker_code/tools/shell/powershell.md +30 -0
  174. pythinker_code/tools/test.py +55 -0
  175. pythinker_code/tools/think/__init__.py +21 -0
  176. pythinker_code/tools/think/think.md +1 -0
  177. pythinker_code/tools/todo/__init__.py +168 -0
  178. pythinker_code/tools/todo/set_todo_list.md +23 -0
  179. pythinker_code/tools/utils.py +200 -0
  180. pythinker_code/tools/web/__init__.py +4 -0
  181. pythinker_code/tools/web/fetch.md +1 -0
  182. pythinker_code/tools/web/fetch.py +261 -0
  183. pythinker_code/tools/web/search.md +1 -0
  184. pythinker_code/tools/web/search.py +163 -0
  185. pythinker_code/ui/__init__.py +0 -0
  186. pythinker_code/ui/acp/__init__.py +99 -0
  187. pythinker_code/ui/print/__init__.py +474 -0
  188. pythinker_code/ui/print/visualize.py +185 -0
  189. pythinker_code/ui/shell/__init__.py +1806 -0
  190. pythinker_code/ui/shell/components/__init__.py +110 -0
  191. pythinker_code/ui/shell/components/base.py +25 -0
  192. pythinker_code/ui/shell/components/bash_execution.py +249 -0
  193. pythinker_code/ui/shell/components/bordered_loader.py +62 -0
  194. pythinker_code/ui/shell/components/diff.py +308 -0
  195. pythinker_code/ui/shell/components/footer.py +231 -0
  196. pythinker_code/ui/shell/components/key_hints.py +27 -0
  197. pythinker_code/ui/shell/components/messages.py +152 -0
  198. pythinker_code/ui/shell/components/render_utils.py +198 -0
  199. pythinker_code/ui/shell/components/settings_list.py +369 -0
  200. pythinker_code/ui/shell/components/special_messages.py +125 -0
  201. pythinker_code/ui/shell/components/tool_execution.py +261 -0
  202. pythinker_code/ui/shell/console.py +109 -0
  203. pythinker_code/ui/shell/debug.py +190 -0
  204. pythinker_code/ui/shell/echo.py +30 -0
  205. pythinker_code/ui/shell/export_import.py +117 -0
  206. pythinker_code/ui/shell/keyboard.py +300 -0
  207. pythinker_code/ui/shell/keymap.py +84 -0
  208. pythinker_code/ui/shell/mcp_status.py +112 -0
  209. pythinker_code/ui/shell/model_picker.py +318 -0
  210. pythinker_code/ui/shell/oauth.py +273 -0
  211. pythinker_code/ui/shell/placeholders.py +578 -0
  212. pythinker_code/ui/shell/prompt.py +2888 -0
  213. pythinker_code/ui/shell/replay.py +215 -0
  214. pythinker_code/ui/shell/selector.py +364 -0
  215. pythinker_code/ui/shell/selectors/__init__.py +38 -0
  216. pythinker_code/ui/shell/selectors/extension.py +37 -0
  217. pythinker_code/ui/shell/selectors/oauth.py +63 -0
  218. pythinker_code/ui/shell/selectors/settings.py +349 -0
  219. pythinker_code/ui/shell/selectors/show_images.py +29 -0
  220. pythinker_code/ui/shell/selectors/theme.py +28 -0
  221. pythinker_code/ui/shell/selectors/thinking.py +42 -0
  222. pythinker_code/ui/shell/session_picker.py +227 -0
  223. pythinker_code/ui/shell/setup.py +212 -0
  224. pythinker_code/ui/shell/slash.py +1433 -0
  225. pythinker_code/ui/shell/spinner_words.py +222 -0
  226. pythinker_code/ui/shell/startup.py +32 -0
  227. pythinker_code/ui/shell/task_browser.py +486 -0
  228. pythinker_code/ui/shell/tool_renderers/__init__.py +197 -0
  229. pythinker_code/ui/shell/tool_renderers/_render_utils.py +168 -0
  230. pythinker_code/ui/shell/tool_renderers/agent.py +140 -0
  231. pythinker_code/ui/shell/tool_renderers/ask_user.py +93 -0
  232. pythinker_code/ui/shell/tool_renderers/background.py +144 -0
  233. pythinker_code/ui/shell/tool_renderers/bash.py +103 -0
  234. pythinker_code/ui/shell/tool_renderers/edit.py +163 -0
  235. pythinker_code/ui/shell/tool_renderers/find.py +81 -0
  236. pythinker_code/ui/shell/tool_renderers/generic.py +60 -0
  237. pythinker_code/ui/shell/tool_renderers/grep.py +98 -0
  238. pythinker_code/ui/shell/tool_renderers/plan.py +98 -0
  239. pythinker_code/ui/shell/tool_renderers/read.py +103 -0
  240. pythinker_code/ui/shell/tool_renderers/think.py +66 -0
  241. pythinker_code/ui/shell/tool_renderers/todo.py +164 -0
  242. pythinker_code/ui/shell/tool_renderers/web.py +128 -0
  243. pythinker_code/ui/shell/tool_renderers/write.py +102 -0
  244. pythinker_code/ui/shell/update.py +352 -0
  245. pythinker_code/ui/shell/usage.py +291 -0
  246. pythinker_code/ui/shell/usage_adapters/__init__.py +50 -0
  247. pythinker_code/ui/shell/usage_adapters/anthropic_admin.py +233 -0
  248. pythinker_code/ui/shell/usage_adapters/base.py +72 -0
  249. pythinker_code/ui/shell/usage_adapters/deepseek.py +137 -0
  250. pythinker_code/ui/shell/usage_adapters/minimax.py +236 -0
  251. pythinker_code/ui/shell/usage_adapters/openai_admin.py +225 -0
  252. pythinker_code/ui/shell/usage_adapters/openai_chatgpt.py +241 -0
  253. pythinker_code/ui/shell/usage_adapters/opencode_go.py +232 -0
  254. pythinker_code/ui/shell/usage_adapters/openrouter.py +105 -0
  255. pythinker_code/ui/shell/usage_adapters/pythinker.py +189 -0
  256. pythinker_code/ui/shell/usage_adapters/pythinker_ai.py +50 -0
  257. pythinker_code/ui/shell/usage_render.py +150 -0
  258. pythinker_code/ui/shell/visualize/__init__.py +165 -0
  259. pythinker_code/ui/shell/visualize/_approval_panel.py +539 -0
  260. pythinker_code/ui/shell/visualize/_blocks.py +802 -0
  261. pythinker_code/ui/shell/visualize/_btw_panel.py +227 -0
  262. pythinker_code/ui/shell/visualize/_input_router.py +48 -0
  263. pythinker_code/ui/shell/visualize/_interactive.py +531 -0
  264. pythinker_code/ui/shell/visualize/_live_view.py +891 -0
  265. pythinker_code/ui/shell/visualize/_question_panel.py +586 -0
  266. pythinker_code/ui/shell/visualize/_worklog.py +245 -0
  267. pythinker_code/ui/theme.py +395 -0
  268. pythinker_code/ui/tui_config.py +82 -0
  269. pythinker_code/usage_ratelimit_cache.py +175 -0
  270. pythinker_code/utils/__init__.py +0 -0
  271. pythinker_code/utils/aiohttp.py +24 -0
  272. pythinker_code/utils/aioqueue.py +72 -0
  273. pythinker_code/utils/broadcast.py +38 -0
  274. pythinker_code/utils/changelog.py +108 -0
  275. pythinker_code/utils/clipboard.py +246 -0
  276. pythinker_code/utils/datetime.py +64 -0
  277. pythinker_code/utils/diff.py +135 -0
  278. pythinker_code/utils/editor.py +91 -0
  279. pythinker_code/utils/environment.py +73 -0
  280. pythinker_code/utils/envvar.py +22 -0
  281. pythinker_code/utils/export.py +696 -0
  282. pythinker_code/utils/file_filter.py +375 -0
  283. pythinker_code/utils/frontmatter.py +70 -0
  284. pythinker_code/utils/io.py +27 -0
  285. pythinker_code/utils/logging.py +146 -0
  286. pythinker_code/utils/media_tags.py +29 -0
  287. pythinker_code/utils/message.py +24 -0
  288. pythinker_code/utils/path.py +199 -0
  289. pythinker_code/utils/proctitle.py +33 -0
  290. pythinker_code/utils/proxy.py +31 -0
  291. pythinker_code/utils/pyinstaller.py +45 -0
  292. pythinker_code/utils/rich/__init__.py +33 -0
  293. pythinker_code/utils/rich/columns.py +99 -0
  294. pythinker_code/utils/rich/diff_render.py +481 -0
  295. pythinker_code/utils/rich/markdown.py +935 -0
  296. pythinker_code/utils/rich/markdown_sample.md +108 -0
  297. pythinker_code/utils/rich/markdown_sample_short.md +2 -0
  298. pythinker_code/utils/rich/syntax.py +114 -0
  299. pythinker_code/utils/sensitive.py +54 -0
  300. pythinker_code/utils/server.py +121 -0
  301. pythinker_code/utils/signals.py +43 -0
  302. pythinker_code/utils/slashcmd.py +124 -0
  303. pythinker_code/utils/string.py +41 -0
  304. pythinker_code/utils/subprocess_env.py +83 -0
  305. pythinker_code/utils/term.py +168 -0
  306. pythinker_code/utils/typing.py +20 -0
  307. pythinker_code/vis/__init__.py +0 -0
  308. pythinker_code/vis/api/__init__.py +5 -0
  309. pythinker_code/vis/api/sessions.py +714 -0
  310. pythinker_code/vis/api/statistics.py +209 -0
  311. pythinker_code/vis/api/system.py +19 -0
  312. pythinker_code/vis/app.py +199 -0
  313. pythinker_code/vis/static/assets/highlighted-body-B3W2YXNL-CY1rtwrX.js +1 -0
  314. pythinker_code/vis/static/assets/index-DSRInNnm.css +1 -0
  315. pythinker_code/vis/static/assets/index-DgmTI2M_.js +185 -0
  316. pythinker_code/vis/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  317. pythinker_code/vis/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  318. pythinker_code/vis/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  319. pythinker_code/vis/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  320. pythinker_code/vis/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  321. pythinker_code/vis/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  322. pythinker_code/vis/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  323. pythinker_code/vis/static/index.html +17 -0
  324. pythinker_code/web/__init__.py +5 -0
  325. pythinker_code/web/api/__init__.py +15 -0
  326. pythinker_code/web/api/config.py +217 -0
  327. pythinker_code/web/api/open_in.py +233 -0
  328. pythinker_code/web/api/sessions.py +1256 -0
  329. pythinker_code/web/app.py +449 -0
  330. pythinker_code/web/auth.py +191 -0
  331. pythinker_code/web/models.py +98 -0
  332. pythinker_code/web/runner/__init__.py +5 -0
  333. pythinker_code/web/runner/messages.py +57 -0
  334. pythinker_code/web/runner/process.py +754 -0
  335. pythinker_code/web/runner/worker.py +97 -0
  336. pythinker_code/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  337. pythinker_code/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  338. pythinker_code/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  339. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  340. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  341. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  342. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  343. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  344. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  345. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  346. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  347. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  348. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  349. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  350. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  351. pythinker_code/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  352. pythinker_code/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  353. pythinker_code/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  354. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  355. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  356. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  357. pythinker_code/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  358. pythinker_code/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  359. pythinker_code/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  360. pythinker_code/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  361. pythinker_code/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  362. pythinker_code/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  363. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  364. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  365. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  366. pythinker_code/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  367. pythinker_code/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  368. pythinker_code/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  369. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  370. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  371. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  372. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  373. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  374. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  375. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  376. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  377. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  378. pythinker_code/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  379. pythinker_code/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  380. pythinker_code/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  381. pythinker_code/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  382. pythinker_code/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  383. pythinker_code/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  384. pythinker_code/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  385. pythinker_code/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  386. pythinker_code/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  387. pythinker_code/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  388. pythinker_code/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  389. pythinker_code/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  390. pythinker_code/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  391. pythinker_code/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  392. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  393. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  394. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  395. pythinker_code/web/static/assets/_baseUniq-DpSMr1jx.js +1 -0
  396. pythinker_code/web/static/assets/abap-BdImnpbu.js +1 -0
  397. pythinker_code/web/static/assets/actionscript-3-CfeIJUat.js +1 -0
  398. pythinker_code/web/static/assets/ada-bCR0ucgS.js +1 -0
  399. pythinker_code/web/static/assets/andromeeda-C-Jbm3Hp.js +1 -0
  400. pythinker_code/web/static/assets/angular-html-CU67Zn6k.js +1 -0
  401. pythinker_code/web/static/assets/angular-ts-BwZT4LLn.js +1 -0
  402. pythinker_code/web/static/assets/apache-Pmp26Uib.js +1 -0
  403. pythinker_code/web/static/assets/apex-D8_7TLub.js +1 -0
  404. pythinker_code/web/static/assets/apl-dKokRX4l.js +1 -0
  405. pythinker_code/web/static/assets/applescript-Co6uUVPk.js +1 -0
  406. pythinker_code/web/static/assets/ara-BRHolxvo.js +1 -0
  407. pythinker_code/web/static/assets/arc-DpsahJyV.js +1 -0
  408. pythinker_code/web/static/assets/architectureDiagram-VXUJARFQ-DqiRv9Eg.js +36 -0
  409. pythinker_code/web/static/assets/asciidoc-Dv7Oe6Be.js +1 -0
  410. pythinker_code/web/static/assets/asm-D_Q5rh1f.js +1 -0
  411. pythinker_code/web/static/assets/astro-CbQHKStN.js +1 -0
  412. pythinker_code/web/static/assets/aurora-x-D-2ljcwZ.js +1 -0
  413. pythinker_code/web/static/assets/awk-DMzUqQB5.js +1 -0
  414. pythinker_code/web/static/assets/ayu-dark-CmMr59Fi.js +1 -0
  415. pythinker_code/web/static/assets/ballerina-BFfxhgS-.js +1 -0
  416. pythinker_code/web/static/assets/bat-BkioyH1T.js +1 -0
  417. pythinker_code/web/static/assets/beancount-k_qm7-4y.js +1 -0
  418. pythinker_code/web/static/assets/berry-uYugtg8r.js +1 -0
  419. pythinker_code/web/static/assets/bibtex-CHM0blh-.js +1 -0
  420. pythinker_code/web/static/assets/bicep-Bmn6On1c.js +1 -0
  421. pythinker_code/web/static/assets/blade-D4QpJJKB.js +1 -0
  422. pythinker_code/web/static/assets/blockDiagram-VD42YOAC-WgtUvqbp.js +122 -0
  423. pythinker_code/web/static/assets/bsl-BO_Y6i37.js +1 -0
  424. pythinker_code/web/static/assets/c-BIGW1oBm.js +1 -0
  425. pythinker_code/web/static/assets/c3-VCDPK7BO.js +1 -0
  426. pythinker_code/web/static/assets/c4Diagram-YG6GDRKO-rK0RPuZd.js +10 -0
  427. pythinker_code/web/static/assets/cadence-Bv_4Rxtq.js +1 -0
  428. pythinker_code/web/static/assets/cairo-KRGpt6FW.js +1 -0
  429. pythinker_code/web/static/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  430. pythinker_code/web/static/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  431. pythinker_code/web/static/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  432. pythinker_code/web/static/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  433. pythinker_code/web/static/assets/channel-B0rlvkH-.js +1 -0
  434. pythinker_code/web/static/assets/chunk-4BX2VUAB-DIkMuLV-.js +1 -0
  435. pythinker_code/web/static/assets/chunk-55IACEB6-CORdm4k4.js +1 -0
  436. pythinker_code/web/static/assets/chunk-B4BG7PRW-D9xDhwHO.js +165 -0
  437. pythinker_code/web/static/assets/chunk-DI55MBZ5-BDmF9Bh-.js +220 -0
  438. pythinker_code/web/static/assets/chunk-FMBD7UC4-BCse_HmM.js +15 -0
  439. pythinker_code/web/static/assets/chunk-QN33PNHL-DCpBmTzA.js +1 -0
  440. pythinker_code/web/static/assets/chunk-QZHKN3VN-BqLuqobw.js +1 -0
  441. pythinker_code/web/static/assets/chunk-TZMSLE5B-8K2ogOKS.js +1 -0
  442. pythinker_code/web/static/assets/clarity-D53aC0YG.js +1 -0
  443. pythinker_code/web/static/assets/classDiagram-2ON5EDUG-D_ZHSii2.js +1 -0
  444. pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-D_ZHSii2.js +1 -0
  445. pythinker_code/web/static/assets/clojure-P80f7IUj.js +1 -0
  446. pythinker_code/web/static/assets/clone-GSXejyY1.js +1 -0
  447. pythinker_code/web/static/assets/cmake-D1j8_8rp.js +1 -0
  448. pythinker_code/web/static/assets/cobol-nwyudZeR.js +1 -0
  449. pythinker_code/web/static/assets/code-block-IT6T5CEO-DWTFYA28.js +2 -0
  450. pythinker_code/web/static/assets/codeowners-Bp6g37R7.js +1 -0
  451. pythinker_code/web/static/assets/codeql-DsOJ9woJ.js +1 -0
  452. pythinker_code/web/static/assets/coffee-Ch7k5sss.js +1 -0
  453. pythinker_code/web/static/assets/common-lisp-Cg-RD9OK.js +1 -0
  454. pythinker_code/web/static/assets/coq-DkFqJrB1.js +1 -0
  455. pythinker_code/web/static/assets/cose-bilkent-S5V4N54A-BRI7ES-N.js +1 -0
  456. pythinker_code/web/static/assets/cpp-CofmeUqb.js +1 -0
  457. pythinker_code/web/static/assets/crystal-tKQVLTB8.js +1 -0
  458. pythinker_code/web/static/assets/csharp-K5feNrxe.js +1 -0
  459. pythinker_code/web/static/assets/css-DPfMkruS.js +1 -0
  460. pythinker_code/web/static/assets/csv-fuZLfV_i.js +1 -0
  461. pythinker_code/web/static/assets/cue-D82EKSYY.js +1 -0
  462. pythinker_code/web/static/assets/cypher-COkxafJQ.js +1 -0
  463. pythinker_code/web/static/assets/cytoscape.esm-B6BxUuKW.js +321 -0
  464. pythinker_code/web/static/assets/d-85-TOEBH.js +1 -0
  465. pythinker_code/web/static/assets/dagre-6UL2VRFP-Ci5GdWfi.js +4 -0
  466. pythinker_code/web/static/assets/dark-plus-C3mMm8J8.js +1 -0
  467. pythinker_code/web/static/assets/dart-CF10PKvl.js +1 -0
  468. pythinker_code/web/static/assets/dax-CEL-wOlO.js +1 -0
  469. pythinker_code/web/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  470. pythinker_code/web/static/assets/desktop-BmXAJ9_W.js +1 -0
  471. pythinker_code/web/static/assets/diagram-PSM6KHXK-0hhAylV4.js +24 -0
  472. pythinker_code/web/static/assets/diagram-QEK2KX5R-8fxgaW6d.js +43 -0
  473. pythinker_code/web/static/assets/diagram-S2PKOQOG-FRr0_atE.js +24 -0
  474. pythinker_code/web/static/assets/diff-D97Zzqfu.js +1 -0
  475. pythinker_code/web/static/assets/docker-BcOcwvcX.js +1 -0
  476. pythinker_code/web/static/assets/dotenv-Da5cRb03.js +1 -0
  477. pythinker_code/web/static/assets/dracula-BzJJZx-M.js +1 -0
  478. pythinker_code/web/static/assets/dracula-soft-BXkSAIEj.js +1 -0
  479. pythinker_code/web/static/assets/dream-maker-BtqSS_iP.js +1 -0
  480. pythinker_code/web/static/assets/edge-BkV0erSs.js +1 -0
  481. pythinker_code/web/static/assets/elixir-CDX3lj18.js +1 -0
  482. pythinker_code/web/static/assets/elm-DbKCFpqz.js +1 -0
  483. pythinker_code/web/static/assets/emacs-lisp-C9XAeP06.js +1 -0
  484. pythinker_code/web/static/assets/erDiagram-Q2GNP2WA-B3T-hJUM.js +60 -0
  485. pythinker_code/web/static/assets/erb-BOJIQeun.js +1 -0
  486. pythinker_code/web/static/assets/erlang-DsQrWhSR.js +1 -0
  487. pythinker_code/web/static/assets/everforest-dark-BgDCqdQA.js +1 -0
  488. pythinker_code/web/static/assets/everforest-light-C8M2exoo.js +1 -0
  489. pythinker_code/web/static/assets/fennel-BYunw83y.js +1 -0
  490. pythinker_code/web/static/assets/fish-BvzEVeQv.js +1 -0
  491. pythinker_code/web/static/assets/flowDiagram-NV44I4VS-D0S3u7ot.js +162 -0
  492. pythinker_code/web/static/assets/fluent-C4IJs8-o.js +1 -0
  493. pythinker_code/web/static/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  494. pythinker_code/web/static/assets/fortran-free-form-BxgE0vQu.js +1 -0
  495. pythinker_code/web/static/assets/fsharp-CXgrBDvD.js +1 -0
  496. pythinker_code/web/static/assets/ganttDiagram-JELNMOA3-CHrN2a23.js +267 -0
  497. pythinker_code/web/static/assets/gdresource-B7Tvp0Sc.js +1 -0
  498. pythinker_code/web/static/assets/gdscript-DTMYz4Jt.js +1 -0
  499. pythinker_code/web/static/assets/gdshader-DkwncUOv.js +1 -0
  500. pythinker_code/web/static/assets/genie-D0YGMca9.js +1 -0
  501. pythinker_code/web/static/assets/gherkin-DyxjwDmM.js +1 -0
  502. pythinker_code/web/static/assets/git-commit-F4YmCXRG.js +1 -0
  503. pythinker_code/web/static/assets/git-rebase-r7XF79zn.js +1 -0
  504. pythinker_code/web/static/assets/gitGraphDiagram-NY62KEGX-CfcXZWg0.js +65 -0
  505. pythinker_code/web/static/assets/github-dark-DHJKELXO.js +1 -0
  506. pythinker_code/web/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
  507. pythinker_code/web/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  508. pythinker_code/web/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  509. pythinker_code/web/static/assets/github-light-DAi9KRSo.js +1 -0
  510. pythinker_code/web/static/assets/github-light-default-D7oLnXFd.js +1 -0
  511. pythinker_code/web/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  512. pythinker_code/web/static/assets/gleam-BspZqrRM.js +1 -0
  513. pythinker_code/web/static/assets/glimmer-js-Rg0-pVw9.js +1 -0
  514. pythinker_code/web/static/assets/glimmer-ts-U6CK756n.js +1 -0
  515. pythinker_code/web/static/assets/glsl-DplSGwfg.js +1 -0
  516. pythinker_code/web/static/assets/gn-n2N0HUVH.js +1 -0
  517. pythinker_code/web/static/assets/gnuplot-DdkO51Og.js +1 -0
  518. pythinker_code/web/static/assets/go-Dn2_MT6a.js +1 -0
  519. pythinker_code/web/static/assets/graph-8jMJwCqE.js +1 -0
  520. pythinker_code/web/static/assets/graphql-ChdNCCLP.js +1 -0
  521. pythinker_code/web/static/assets/groovy-gcz8RCvz.js +1 -0
  522. pythinker_code/web/static/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  523. pythinker_code/web/static/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  524. pythinker_code/web/static/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  525. pythinker_code/web/static/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  526. pythinker_code/web/static/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  527. pythinker_code/web/static/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  528. pythinker_code/web/static/assets/hack-CaT9iCJl.js +1 -0
  529. pythinker_code/web/static/assets/haml-B8DHNrY2.js +1 -0
  530. pythinker_code/web/static/assets/handlebars-BL8al0AC.js +1 -0
  531. pythinker_code/web/static/assets/haskell-Df6bDoY_.js +1 -0
  532. pythinker_code/web/static/assets/haxe-CzTSHFRz.js +1 -0
  533. pythinker_code/web/static/assets/hcl-BWvSN4gD.js +1 -0
  534. pythinker_code/web/static/assets/hjson-D5-asLiD.js +1 -0
  535. pythinker_code/web/static/assets/hlsl-D3lLCCz7.js +1 -0
  536. pythinker_code/web/static/assets/houston-DnULxvSX.js +1 -0
  537. pythinker_code/web/static/assets/html-GMplVEZG.js +1 -0
  538. pythinker_code/web/static/assets/html-derivative-BFtXZ54Q.js +1 -0
  539. pythinker_code/web/static/assets/http-jrhK8wxY.js +1 -0
  540. pythinker_code/web/static/assets/hurl-irOxFIW8.js +1 -0
  541. pythinker_code/web/static/assets/hxml-Bvhsp5Yf.js +1 -0
  542. pythinker_code/web/static/assets/hy-DFXneXwc.js +1 -0
  543. pythinker_code/web/static/assets/imba-DGztddWO.js +1 -0
  544. pythinker_code/web/static/assets/index-BXrFnzMy.js +153 -0
  545. pythinker_code/web/static/assets/index-BpoLgcEt.js +1 -0
  546. pythinker_code/web/static/assets/index-BrfQJnRD.js +5 -0
  547. pythinker_code/web/static/assets/index-C4gFzubz.js +2 -0
  548. pythinker_code/web/static/assets/index-CzV_vCfu.css +1 -0
  549. pythinker_code/web/static/assets/index-DI2oedCt.js +19 -0
  550. pythinker_code/web/static/assets/infoDiagram-WHAUD3N6-DdxonBf3.js +2 -0
  551. pythinker_code/web/static/assets/ini-BEwlwnbL.js +1 -0
  552. pythinker_code/web/static/assets/init-Gi6I4Gst.js +1 -0
  553. pythinker_code/web/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  554. pythinker_code/web/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  555. pythinker_code/web/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  556. pythinker_code/web/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  557. pythinker_code/web/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  558. pythinker_code/web/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  559. pythinker_code/web/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  560. pythinker_code/web/static/assets/java-CylS5w8V.js +1 -0
  561. pythinker_code/web/static/assets/javascript-wDzz0qaB.js +1 -0
  562. pythinker_code/web/static/assets/jinja-4LBKfQ-Z.js +1 -0
  563. pythinker_code/web/static/assets/jison-wvAkD_A8.js +1 -0
  564. pythinker_code/web/static/assets/journeyDiagram-XKPGCS4Q-BXf4aQei.js +139 -0
  565. pythinker_code/web/static/assets/json-Cp-IABpG.js +1 -0
  566. pythinker_code/web/static/assets/json5-C9tS-k6U.js +1 -0
  567. pythinker_code/web/static/assets/jsonc-Des-eS-w.js +1 -0
  568. pythinker_code/web/static/assets/jsonl-DcaNXYhu.js +1 -0
  569. pythinker_code/web/static/assets/jsonnet-DFQXde-d.js +1 -0
  570. pythinker_code/web/static/assets/jssm-C2t-YnRu.js +1 -0
  571. pythinker_code/web/static/assets/jsx-g9-lgVsj.js +1 -0
  572. pythinker_code/web/static/assets/julia-CxzCAyBv.js +1 -0
  573. pythinker_code/web/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  574. pythinker_code/web/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  575. pythinker_code/web/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
  576. pythinker_code/web/static/assets/kanban-definition-3W4ZIXB7-DLpPPOu8.js +89 -0
  577. pythinker_code/web/static/assets/katex-D2lIc1rk.css +1 -0
  578. pythinker_code/web/static/assets/kdl-DV7GczEv.js +1 -0
  579. pythinker_code/web/static/assets/kotlin-BdnUsdx6.js +1 -0
  580. pythinker_code/web/static/assets/kusto-DZf3V79B.js +1 -0
  581. pythinker_code/web/static/assets/laserwave-DUszq2jm.js +1 -0
  582. pythinker_code/web/static/assets/latex-B4uzh10-.js +1 -0
  583. pythinker_code/web/static/assets/layout-DH73UoAH.js +1 -0
  584. pythinker_code/web/static/assets/lean-BZvkOJ9d.js +1 -0
  585. pythinker_code/web/static/assets/less-B1dDrJ26.js +1 -0
  586. pythinker_code/web/static/assets/light-plus-B7mTdjB0.js +1 -0
  587. pythinker_code/web/static/assets/linear-bAer2-sK.js +1 -0
  588. pythinker_code/web/static/assets/liquid-DYVedYrR.js +1 -0
  589. pythinker_code/web/static/assets/llvm-BtvRca6l.js +1 -0
  590. pythinker_code/web/static/assets/log-2UxHyX5q.js +1 -0
  591. pythinker_code/web/static/assets/logo-BtOb2qkB.js +1 -0
  592. pythinker_code/web/static/assets/lua-BbnMAYS6.js +1 -0
  593. pythinker_code/web/static/assets/luau-C-HG3fhB.js +1 -0
  594. pythinker_code/web/static/assets/make-CHLpvVh8.js +1 -0
  595. pythinker_code/web/static/assets/markdown-Cvjx9yec.js +1 -0
  596. pythinker_code/web/static/assets/marko-DZsq8hO1.js +1 -0
  597. pythinker_code/web/static/assets/material-theme-D5KoaKCx.js +1 -0
  598. pythinker_code/web/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
  599. pythinker_code/web/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  600. pythinker_code/web/static/assets/material-theme-ocean-CyktbL80.js +1 -0
  601. pythinker_code/web/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  602. pythinker_code/web/static/assets/matlab-D7o27uSR.js +1 -0
  603. pythinker_code/web/static/assets/mdc-DUICxH0z.js +1 -0
  604. pythinker_code/web/static/assets/mdx-Cmh6b_Ma.js +1 -0
  605. pythinker_code/web/static/assets/mermaid-VLURNSYL-B2P5VJ9v.css +1 -0
  606. pythinker_code/web/static/assets/mermaid-VLURNSYL-CuqbwKXv.js +465 -0
  607. pythinker_code/web/static/assets/mermaid-mWjccvbQ.js +1 -0
  608. pythinker_code/web/static/assets/mermaid.core-Nx-rTKiV.js +191 -0
  609. pythinker_code/web/static/assets/min-DbfD8Ywu.js +1 -0
  610. pythinker_code/web/static/assets/min-dark-CafNBF8u.js +1 -0
  611. pythinker_code/web/static/assets/min-light-CTRr51gU.js +1 -0
  612. pythinker_code/web/static/assets/mindmap-definition-VGOIOE7T-C6l761Ue.js +68 -0
  613. pythinker_code/web/static/assets/mipsasm-CKIfxQSi.js +1 -0
  614. pythinker_code/web/static/assets/mojo-B93PlW-d.js +1 -0
  615. pythinker_code/web/static/assets/monokai-D4h5O-jR.js +1 -0
  616. pythinker_code/web/static/assets/moonbit-Ba13S78F.js +1 -0
  617. pythinker_code/web/static/assets/move-Bu9oaDYs.js +1 -0
  618. pythinker_code/web/static/assets/narrat-DRg8JJMk.js +1 -0
  619. pythinker_code/web/static/assets/nextflow-BrzmwbiE.js +1 -0
  620. pythinker_code/web/static/assets/nginx-DknmC5AR.js +1 -0
  621. pythinker_code/web/static/assets/night-owl-C39BiMTA.js +1 -0
  622. pythinker_code/web/static/assets/nim-CVrawwO9.js +1 -0
  623. pythinker_code/web/static/assets/nix-CwoSXNpI.js +1 -0
  624. pythinker_code/web/static/assets/nord-Ddv68eIx.js +1 -0
  625. pythinker_code/web/static/assets/nushell-C-sUppwS.js +1 -0
  626. pythinker_code/web/static/assets/objective-c-DXmwc3jG.js +1 -0
  627. pythinker_code/web/static/assets/objective-cpp-CLxacb5B.js +1 -0
  628. pythinker_code/web/static/assets/ocaml-C0hk2d4L.js +1 -0
  629. pythinker_code/web/static/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  630. pythinker_code/web/static/assets/one-light-PoHY5YXO.js +1 -0
  631. pythinker_code/web/static/assets/openscad-C4EeE6gA.js +1 -0
  632. pythinker_code/web/static/assets/ordinal-Cboi1Yqb.js +1 -0
  633. pythinker_code/web/static/assets/pascal-D93ZcfNL.js +1 -0
  634. pythinker_code/web/static/assets/perl-C0TMdlhV.js +1 -0
  635. pythinker_code/web/static/assets/php-CDn_0X-4.js +1 -0
  636. pythinker_code/web/static/assets/pieDiagram-ADFJNKIX-fNg41mT9.js +30 -0
  637. pythinker_code/web/static/assets/pkl-u5AG7uiY.js +1 -0
  638. pythinker_code/web/static/assets/plastic-3e1v2bzS.js +1 -0
  639. pythinker_code/web/static/assets/plsql-ChMvpjG-.js +1 -0
  640. pythinker_code/web/static/assets/po-BTJTHyun.js +1 -0
  641. pythinker_code/web/static/assets/poimandres-CS3Unz2-.js +1 -0
  642. pythinker_code/web/static/assets/polar-C0HS_06l.js +1 -0
  643. pythinker_code/web/static/assets/postcss-CXtECtnM.js +1 -0
  644. pythinker_code/web/static/assets/powerquery-CEu0bR-o.js +1 -0
  645. pythinker_code/web/static/assets/powershell-Dpen1YoG.js +1 -0
  646. pythinker_code/web/static/assets/prisma-Dd19v3D-.js +1 -0
  647. pythinker_code/web/static/assets/prolog-CbFg5uaA.js +1 -0
  648. pythinker_code/web/static/assets/proto-C7zT0LnQ.js +1 -0
  649. pythinker_code/web/static/assets/pug-CGlum2m_.js +1 -0
  650. pythinker_code/web/static/assets/puppet-BMWR74SV.js +1 -0
  651. pythinker_code/web/static/assets/purescript-CklMAg4u.js +1 -0
  652. pythinker_code/web/static/assets/python-B6aJPvgy.js +1 -0
  653. pythinker_code/web/static/assets/qml-3beO22l8.js +1 -0
  654. pythinker_code/web/static/assets/qmldir-C8lEn-DE.js +1 -0
  655. pythinker_code/web/static/assets/qss-IeuSbFQv.js +1 -0
  656. pythinker_code/web/static/assets/quadrantDiagram-AYHSOK5B-DJz3Kx87.js +7 -0
  657. pythinker_code/web/static/assets/r-Dspwwk_N.js +1 -0
  658. pythinker_code/web/static/assets/racket-BqYA7rlc.js +1 -0
  659. pythinker_code/web/static/assets/raku-DXvB9xmW.js +1 -0
  660. pythinker_code/web/static/assets/razor-C1TweQQi.js +1 -0
  661. pythinker_code/web/static/assets/red-bN70gL4F.js +1 -0
  662. pythinker_code/web/static/assets/reg-C-SQnVFl.js +1 -0
  663. pythinker_code/web/static/assets/regexp-CDVJQ6XC.js +1 -0
  664. pythinker_code/web/static/assets/rel-C3B-1QV4.js +1 -0
  665. pythinker_code/web/static/assets/requirementDiagram-UZGBJVZJ-B4SbrfE9.js +64 -0
  666. pythinker_code/web/static/assets/riscv-BM1_JUlF.js +1 -0
  667. pythinker_code/web/static/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  668. pythinker_code/web/static/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  669. pythinker_code/web/static/assets/rose-pine-qdsjHGoJ.js +1 -0
  670. pythinker_code/web/static/assets/rosmsg-BJDFO7_C.js +1 -0
  671. pythinker_code/web/static/assets/rst-B0xPkSld.js +1 -0
  672. pythinker_code/web/static/assets/ruby-BvKwtOVI.js +1 -0
  673. pythinker_code/web/static/assets/rust-B1yitclQ.js +1 -0
  674. pythinker_code/web/static/assets/sankeyDiagram-TZEHDZUN-CoSUjLAG.js +10 -0
  675. pythinker_code/web/static/assets/sas-cz2c8ADy.js +1 -0
  676. pythinker_code/web/static/assets/sass-Cj5Yp3dK.js +1 -0
  677. pythinker_code/web/static/assets/scala-C151Ov-r.js +1 -0
  678. pythinker_code/web/static/assets/scheme-C98Dy4si.js +1 -0
  679. pythinker_code/web/static/assets/scss-OYdSNvt2.js +1 -0
  680. pythinker_code/web/static/assets/sdbl-DVxCFoDh.js +1 -0
  681. pythinker_code/web/static/assets/sequenceDiagram-WL72ISMW-PjhBNHi3.js +145 -0
  682. pythinker_code/web/static/assets/shaderlab-Dg9Lc6iA.js +1 -0
  683. pythinker_code/web/static/assets/shellscript-Yzrsuije.js +1 -0
  684. pythinker_code/web/static/assets/shellsession-BADoaaVG.js +1 -0
  685. pythinker_code/web/static/assets/slack-dark-BthQWCQV.js +1 -0
  686. pythinker_code/web/static/assets/slack-ochin-DqwNpetd.js +1 -0
  687. pythinker_code/web/static/assets/smalltalk-BERRCDM3.js +1 -0
  688. pythinker_code/web/static/assets/snazzy-light-Bw305WKR.js +1 -0
  689. pythinker_code/web/static/assets/solarized-dark-DXbdFlpD.js +1 -0
  690. pythinker_code/web/static/assets/solarized-light-L9t79GZl.js +1 -0
  691. pythinker_code/web/static/assets/solidity-rGO070M0.js +1 -0
  692. pythinker_code/web/static/assets/soy-Brmx7dQM.js +1 -0
  693. pythinker_code/web/static/assets/sparql-rVzFXLq3.js +1 -0
  694. pythinker_code/web/static/assets/splunk-BtCnVYZw.js +1 -0
  695. pythinker_code/web/static/assets/sql-BLtJtn59.js +1 -0
  696. pythinker_code/web/static/assets/ssh-config-_ykCGR6B.js +1 -0
  697. pythinker_code/web/static/assets/stata-BH5u7GGu.js +1 -0
  698. pythinker_code/web/static/assets/stateDiagram-FKZM4ZOC-DOwESt8-.js +1 -0
  699. pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-yl3OHWiP.js +1 -0
  700. pythinker_code/web/static/assets/stylus-BEDo0Tqx.js +1 -0
  701. pythinker_code/web/static/assets/svelte-zxCyuUbr.js +1 -0
  702. pythinker_code/web/static/assets/swift-Dg5xB15N.js +1 -0
  703. pythinker_code/web/static/assets/synthwave-84-CbfX1IO0.js +1 -0
  704. pythinker_code/web/static/assets/system-verilog-CnnmHF94.js +1 -0
  705. pythinker_code/web/static/assets/systemd-4A_iFExJ.js +1 -0
  706. pythinker_code/web/static/assets/talonscript-CkByrt1z.js +1 -0
  707. pythinker_code/web/static/assets/tasl-QIJgUcNo.js +1 -0
  708. pythinker_code/web/static/assets/tcl-dwOrl1Do.js +1 -0
  709. pythinker_code/web/static/assets/templ-W15q3VgB.js +1 -0
  710. pythinker_code/web/static/assets/terraform-BETggiCN.js +1 -0
  711. pythinker_code/web/static/assets/tex-CvyZ59Mk.js +1 -0
  712. pythinker_code/web/static/assets/timeline-definition-IT6M3QCI-CkCLnAgi.js +61 -0
  713. pythinker_code/web/static/assets/tokyo-night-hegEt444.js +1 -0
  714. pythinker_code/web/static/assets/toml-vGWfd6FD.js +1 -0
  715. pythinker_code/web/static/assets/treemap-KMMF4GRG-CZS5XwTf.js +128 -0
  716. pythinker_code/web/static/assets/ts-tags-zn1MmPIZ.js +1 -0
  717. pythinker_code/web/static/assets/tsv-B_m7g4N7.js +1 -0
  718. pythinker_code/web/static/assets/tsx-COt5Ahok.js +1 -0
  719. pythinker_code/web/static/assets/turtle-BsS91CYL.js +1 -0
  720. pythinker_code/web/static/assets/twig-CO9l9SDP.js +1 -0
  721. pythinker_code/web/static/assets/typescript-BPQ3VLAy.js +1 -0
  722. pythinker_code/web/static/assets/typespec-BGHnOYBU.js +1 -0
  723. pythinker_code/web/static/assets/typst-DHCkPAjA.js +1 -0
  724. pythinker_code/web/static/assets/v-BcVCzyr7.js +1 -0
  725. pythinker_code/web/static/assets/vala-CsfeWuGM.js +1 -0
  726. pythinker_code/web/static/assets/vb-D17OF-Vu.js +1 -0
  727. pythinker_code/web/static/assets/verilog-BQ8w6xss.js +1 -0
  728. pythinker_code/web/static/assets/vesper-DU1UobuO.js +1 -0
  729. pythinker_code/web/static/assets/vhdl-CeAyd5Ju.js +1 -0
  730. pythinker_code/web/static/assets/viml-CJc9bBzg.js +1 -0
  731. pythinker_code/web/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
  732. pythinker_code/web/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
  733. pythinker_code/web/static/assets/vitesse-light-CVO1_9PV.js +1 -0
  734. pythinker_code/web/static/assets/vue-DN_0RTcg.js +1 -0
  735. pythinker_code/web/static/assets/vue-html-AaS7Mt5G.js +1 -0
  736. pythinker_code/web/static/assets/vue-vine-CQOfvN7w.js +1 -0
  737. pythinker_code/web/static/assets/vyper-CDx5xZoG.js +1 -0
  738. pythinker_code/web/static/assets/wasm-CG6Dc4jp.js +1 -0
  739. pythinker_code/web/static/assets/wasm-MzD3tlZU.js +1 -0
  740. pythinker_code/web/static/assets/wenyan-BV7otONQ.js +1 -0
  741. pythinker_code/web/static/assets/wgsl-Dx-B1_4e.js +1 -0
  742. pythinker_code/web/static/assets/wikitext-BhOHFoWU.js +1 -0
  743. pythinker_code/web/static/assets/wit-5i3qLPDT.js +1 -0
  744. pythinker_code/web/static/assets/wolfram-lXgVvXCa.js +1 -0
  745. pythinker_code/web/static/assets/xml-sdJ4AIDG.js +1 -0
  746. pythinker_code/web/static/assets/xsl-CtQFsRM5.js +1 -0
  747. pythinker_code/web/static/assets/xychartDiagram-PRI3JC2R-DkqqHNLh.js +7 -0
  748. pythinker_code/web/static/assets/yaml-Buea-lGh.js +1 -0
  749. pythinker_code/web/static/assets/zenscript-DVFEvuxE.js +1 -0
  750. pythinker_code/web/static/assets/zig-VOosw3JB.js +1 -0
  751. pythinker_code/web/static/brand/apple-touch-icon.png +0 -0
  752. pythinker_code/web/static/brand/arctecture.webp +0 -0
  753. pythinker_code/web/static/brand/bimi-logo.svg +46 -0
  754. pythinker_code/web/static/brand/favicon.ico +0 -0
  755. pythinker_code/web/static/brand/fonts/dm-sans-latin-ext.woff2 +0 -0
  756. pythinker_code/web/static/brand/fonts/dm-sans-latin.woff2 +0 -0
  757. pythinker_code/web/static/brand/fonts/instrument-sans-latin-ext.woff2 +0 -0
  758. pythinker_code/web/static/brand/fonts/instrument-sans-latin.woff2 +0 -0
  759. pythinker_code/web/static/brand/fonts/instrument-serif-latin-ext.woff2 +0 -0
  760. pythinker_code/web/static/brand/fonts/instrument-serif-latin.woff2 +0 -0
  761. pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin-ext.woff2 +0 -0
  762. pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin.woff2 +0 -0
  763. pythinker_code/web/static/brand/fonts/libre-baskerville-latin-ext.woff2 +0 -0
  764. pythinker_code/web/static/brand/fonts/libre-baskerville-latin.woff2 +0 -0
  765. pythinker_code/web/static/brand/fonts/roboto-latin-ext.woff2 +0 -0
  766. pythinker_code/web/static/brand/fonts/roboto-latin.woff2 +0 -0
  767. pythinker_code/web/static/brand/icon-192.png +0 -0
  768. pythinker_code/web/static/brand/icon-512.png +0 -0
  769. pythinker_code/web/static/brand/icon.svg +1 -0
  770. pythinker_code/web/static/brand/logo.png +0 -0
  771. pythinker_code/web/static/brand/pythinker_animated.svg +79 -0
  772. pythinker_code/web/static/brand/robots.txt +4 -0
  773. pythinker_code/web/static/index.html +15 -0
  774. pythinker_code/web/static/logo.png +0 -0
  775. pythinker_code/web/store/__init__.py +1 -0
  776. pythinker_code/web/store/sessions.py +433 -0
  777. pythinker_code/wire/__init__.py +148 -0
  778. pythinker_code/wire/file.py +151 -0
  779. pythinker_code/wire/jsonrpc.py +263 -0
  780. pythinker_code/wire/protocol.py +2 -0
  781. pythinker_code/wire/root_hub.py +27 -0
  782. pythinker_code/wire/serde.py +26 -0
  783. pythinker_code/wire/server.py +1072 -0
  784. pythinker_code/wire/types.py +698 -0
  785. pythinker_code-0.8.0.dist-info/METADATA +706 -0
  786. pythinker_code-0.8.0.dist-info/RECORD +790 -0
  787. pythinker_code-0.8.0.dist-info/WHEEL +4 -0
  788. pythinker_code-0.8.0.dist-info/entry_points.txt +4 -0
  789. pythinker_code-0.8.0.dist-info/licenses/LICENSE +202 -0
  790. pythinker_code-0.8.0.dist-info/licenses/NOTICE +14 -0
@@ -0,0 +1,1763 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import time
6
+ import uuid
7
+ from collections.abc import Awaitable, Callable, Sequence
8
+ from dataclasses import dataclass
9
+ from functools import partial
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any, Literal, cast
12
+
13
+ import pythinker_core
14
+ import tenacity
15
+ from pythinker_core import StepResult
16
+ from pythinker_core.chat_provider import (
17
+ APIConnectionError,
18
+ APIEmptyResponseError,
19
+ APIStatusError,
20
+ APITimeoutError,
21
+ RetryableChatProvider,
22
+ )
23
+ from pythinker_core.message import Message
24
+ from tenacity import RetryCallState, retry_if_exception, stop_after_attempt, wait_exponential_jitter
25
+
26
+ from pythinker_code.approval_runtime import (
27
+ ApprovalSource,
28
+ get_current_approval_source_or_none,
29
+ reset_current_approval_source,
30
+ set_current_approval_source,
31
+ )
32
+ from pythinker_code.background import build_active_task_snapshot
33
+ from pythinker_code.hooks.engine import HookEngine
34
+ from pythinker_code.llm import ModelCapability
35
+ from pythinker_code.notifications import (
36
+ NotificationView,
37
+ build_notification_message,
38
+ extract_notification_ids,
39
+ )
40
+ from pythinker_code.prompt_templates import PromptTemplate, expand_prompt_template
41
+ from pythinker_code.skill import Skill, read_skill_text
42
+ from pythinker_code.skill.flow import Flow, FlowEdge, FlowNode, parse_choice
43
+ from pythinker_code.soul import (
44
+ LLMNotSet,
45
+ LLMNotSupported,
46
+ MaxStepsReached,
47
+ Soul,
48
+ StatusSnapshot,
49
+ wire_send,
50
+ )
51
+ from pythinker_code.soul.agent import Agent, Runtime
52
+ from pythinker_code.soul.compaction import (
53
+ CompactionResult,
54
+ SimpleCompaction,
55
+ estimate_text_tokens,
56
+ should_auto_compact,
57
+ )
58
+ from pythinker_code.soul.context import Context
59
+ from pythinker_code.soul.dynamic_injection import (
60
+ DynamicInjection,
61
+ DynamicInjectionProvider,
62
+ normalize_history,
63
+ )
64
+ from pythinker_code.soul.dynamic_injections.auto_mode import AutoModeInjectionProvider
65
+ from pythinker_code.soul.dynamic_injections.plan_mode import PlanModeInjectionProvider
66
+ from pythinker_code.soul.message import (
67
+ check_message,
68
+ system,
69
+ system_reminder,
70
+ tool_result_to_message,
71
+ )
72
+ from pythinker_code.soul.permission import (
73
+ permission_profile_for_runtime,
74
+ reset_step_permission_profile,
75
+ set_step_permission_profile,
76
+ )
77
+ from pythinker_code.soul.slash import registry as soul_slash_registry
78
+ from pythinker_code.soul.toolset import PythinkerToolset
79
+ from pythinker_code.tools.dmail import NAME as SendDMail_NAME
80
+ from pythinker_code.tools.utils import ToolRejectedError
81
+ from pythinker_code.utils.logging import logger
82
+ from pythinker_code.utils.slashcmd import SlashCommand, parse_slash_command_call
83
+ from pythinker_code.wire.file import WireFile
84
+ from pythinker_code.wire.types import (
85
+ CompactionBegin,
86
+ CompactionEnd,
87
+ ContentPart,
88
+ MCPLoadingBegin,
89
+ MCPLoadingEnd,
90
+ StatusUpdate,
91
+ SteerInput,
92
+ StepBegin,
93
+ StepInterrupted,
94
+ TextPart,
95
+ ToolResult,
96
+ TurnBegin,
97
+ TurnEnd,
98
+ )
99
+
100
+ if TYPE_CHECKING:
101
+
102
+ def type_check(soul: PythinkerSoul):
103
+ _: Soul = soul
104
+
105
+
106
+ SKILL_COMMAND_PREFIX = "skill:"
107
+ FLOW_COMMAND_PREFIX = "flow:"
108
+ DEFAULT_MAX_FLOW_MOVES = 1000
109
+
110
+
111
+ def classify_llm_system(chat_provider: object | None) -> str:
112
+ """Classify a chat provider into a stable gen_ai.system telemetry value."""
113
+ try:
114
+ provider_class = type(chat_provider).__name__.lower() if chat_provider is not None else ""
115
+ if "anthropic" in provider_class:
116
+ return "anthropic"
117
+ if "openai" in provider_class:
118
+ return "openai"
119
+ if "google" in provider_class or "gemini" in provider_class:
120
+ return "google"
121
+ return provider_class or "unknown"
122
+ except Exception:
123
+ return "unknown"
124
+
125
+
126
+ def classify_api_error(e: Exception) -> tuple[str, int | None]:
127
+ """Classify an LLM API exception into (error_type, status_code).
128
+
129
+ Exposed at module level so telemetry tests can import the real function
130
+ instead of duplicating the classification table.
131
+
132
+ Returns:
133
+ (error_type, status_code) where status_code is None for non-HTTP errors.
134
+ """
135
+ status_code: int | None = None
136
+ if isinstance(e, APIStatusError):
137
+ status = getattr(e, "status_code", getattr(e, "status", 0))
138
+ status_code = int(status) if status else None
139
+ if status == 429:
140
+ return "rate_limit", status_code
141
+ if status in (401, 403):
142
+ return "auth", status_code
143
+ if status >= 500:
144
+ return "5xx_server", status_code
145
+ if 400 <= status < 500:
146
+ msg_lower = str(e).lower()
147
+ if (
148
+ "context length" in msg_lower
149
+ or "context_length" in msg_lower
150
+ or "max tokens" in msg_lower
151
+ or "maximum context" in msg_lower
152
+ or "too many tokens" in msg_lower
153
+ ):
154
+ return "context_overflow", status_code
155
+ return "4xx_client", status_code
156
+ return "api", status_code
157
+ if isinstance(e, APIConnectionError):
158
+ return "network", None
159
+ if isinstance(e, (APITimeoutError, TimeoutError)):
160
+ return "timeout", None
161
+ if isinstance(e, APIEmptyResponseError):
162
+ return "empty_response", None
163
+ return "other", None
164
+
165
+
166
+ type StepStopReason = Literal["no_tool_calls", "tool_rejected"]
167
+
168
+
169
+ @dataclass(frozen=True, slots=True)
170
+ class StepOutcome:
171
+ stop_reason: StepStopReason
172
+ assistant_message: Message
173
+
174
+
175
+ type TurnStopReason = StepStopReason
176
+
177
+
178
+ @dataclass(frozen=True, slots=True)
179
+ class TurnOutcome:
180
+ stop_reason: TurnStopReason
181
+ final_message: Message | None
182
+ step_count: int
183
+
184
+
185
+ class PythinkerSoul:
186
+ """The soul of Pythinker CLI."""
187
+
188
+ def __init__(
189
+ self,
190
+ agent: Agent,
191
+ *,
192
+ context: Context,
193
+ ):
194
+ """
195
+ Initialize the soul.
196
+
197
+ Args:
198
+ agent (Agent): The agent to run.
199
+ context (Context): The context of the agent.
200
+ """
201
+ self._agent = agent
202
+ self._runtime = agent.runtime
203
+ self._denwa_renji = agent.runtime.denwa_renji
204
+ self._approval = agent.runtime.approval
205
+ self._context = context
206
+ self._loop_control = agent.runtime.config.loop_control
207
+ self._compaction = SimpleCompaction() # TODO: maybe configurable and composable
208
+
209
+ for tool in agent.toolset.tools:
210
+ if tool.name == SendDMail_NAME:
211
+ self._checkpoint_with_user_message = True
212
+ break
213
+ else:
214
+ self._checkpoint_with_user_message = False
215
+
216
+ self._steer_queue: asyncio.Queue[str | list[ContentPart]] = asyncio.Queue()
217
+ self._prompt_queue_lock = asyncio.Lock()
218
+ self._plan_mode: bool = self._runtime.session.state.plan_mode
219
+ self._plan_session_id: str | None = self._runtime.session.state.plan_session_id
220
+ # Pre-warm slug cache so the persisted slug survives process restarts
221
+ if self._plan_session_id is not None and self._runtime.session.state.plan_slug is not None:
222
+ from pythinker_code.tools.plan.heroes import seed_slug_cache
223
+
224
+ seed_slug_cache(self._plan_session_id, self._runtime.session.state.plan_slug)
225
+ self._pending_plan_activation_injection: bool = False
226
+ if self._plan_mode:
227
+ self._ensure_plan_session_id()
228
+ self._injection_providers: list[DynamicInjectionProvider] = [
229
+ PlanModeInjectionProvider(),
230
+ *(
231
+ []
232
+ if self._runtime.config.skip_auto_prompt_injection
233
+ else [AutoModeInjectionProvider()]
234
+ ),
235
+ ]
236
+ self._hook_engine: HookEngine = HookEngine()
237
+ self._stop_hook_active: bool = False
238
+ if self._runtime.role == "root":
239
+ self._runtime.notifications.ack_ids("llm", extract_notification_ids(context.history))
240
+
241
+ # Bind plan mode state to tools that support it
242
+ self._bind_plan_mode_tools()
243
+
244
+ self._slash_commands = self._build_slash_commands()
245
+ self._slash_command_map = self._index_slash_commands(self._slash_commands)
246
+
247
+ @property
248
+ def name(self) -> str:
249
+ return self._agent.name
250
+
251
+ @property
252
+ def model_name(self) -> str:
253
+ return self._runtime.llm.chat_provider.model_name if self._runtime.llm else ""
254
+
255
+ @property
256
+ def model_capabilities(self) -> set[ModelCapability] | None:
257
+ if self._runtime.llm is None:
258
+ return None
259
+ return self._runtime.llm.capabilities
260
+
261
+ @property
262
+ def is_yolo(self) -> bool:
263
+ """Whether explicit yolo mode is active."""
264
+ return self._approval.is_yolo()
265
+
266
+ @property
267
+ def is_auto_approve(self) -> bool:
268
+ """Whether tool approvals are bypassed (explicit yolo, or implied by auto mode)."""
269
+ return self._approval.is_auto_approve()
270
+
271
+ @property
272
+ def is_auto(self) -> bool:
273
+ """Whether no user is present (auto mode)."""
274
+ return self._approval.is_auto()
275
+
276
+ @property
277
+ def is_auto_flag(self) -> bool:
278
+ """Whether persisted auto mode is active."""
279
+ return self._approval.is_auto_flag()
280
+
281
+ @property
282
+ def is_subagent(self) -> bool:
283
+ """Whether this soul is running as a subagent rather than the root session."""
284
+ return self._runtime.role == "subagent"
285
+
286
+ @property
287
+ def plan_mode(self) -> bool:
288
+ """Whether plan mode (read-only research and planning) is active."""
289
+ return self._plan_mode
290
+
291
+ @property
292
+ def hook_engine(self) -> HookEngine:
293
+ return self._hook_engine
294
+
295
+ def set_hook_engine(self, engine: HookEngine) -> None:
296
+ self._hook_engine = engine
297
+ if isinstance(self._agent.toolset, PythinkerToolset):
298
+ self._agent.toolset.set_hook_engine(engine)
299
+
300
+ def add_injection_provider(self, provider: DynamicInjectionProvider) -> None:
301
+ """Register an additional dynamic injection provider."""
302
+ self._injection_providers.append(provider)
303
+
304
+ async def _collect_injections(self) -> list[DynamicInjection]:
305
+ """Collect dynamic injections from all registered providers."""
306
+ injections: list[DynamicInjection] = []
307
+ for provider in self._injection_providers:
308
+ try:
309
+ result = await provider.get_injections(self._context.history, self)
310
+ injections.extend(result)
311
+ except Exception as exc:
312
+ from pythinker_code.telemetry.errors import report_handled_error
313
+
314
+ report_handled_error(
315
+ exc,
316
+ site="soul.injection.get",
317
+ provider=type(provider).__name__,
318
+ )
319
+ logger.warning(
320
+ "injection provider %s failed",
321
+ type(provider).__name__,
322
+ exc_info=True,
323
+ )
324
+ return injections
325
+
326
+ async def _notify_injection_providers_compacted(self) -> None:
327
+ """Notify all injection providers that the context has been compacted.
328
+
329
+ Failures are isolated per-provider so a buggy third-party provider
330
+ cannot abort compaction (which would skip CompactionEnd wire events
331
+ and PostCompact telemetry).
332
+ """
333
+ for provider in self._injection_providers:
334
+ try:
335
+ await provider.on_context_compacted()
336
+ except Exception as exc:
337
+ from pythinker_code.telemetry.errors import report_handled_error
338
+
339
+ report_handled_error(
340
+ exc,
341
+ site="soul.injection.on_context_compacted",
342
+ provider=type(provider).__name__,
343
+ )
344
+ logger.warning(
345
+ "injection provider %s on_context_compacted failed",
346
+ type(provider).__name__,
347
+ exc_info=True,
348
+ )
349
+
350
+ async def notify_auto_changed(self, enabled: bool) -> None:
351
+ """Notify dynamic injection providers that auto mode changed."""
352
+ for provider in self._injection_providers:
353
+ try:
354
+ await provider.on_auto_changed(enabled)
355
+ except Exception as exc:
356
+ from pythinker_code.telemetry.errors import report_handled_error
357
+
358
+ report_handled_error(
359
+ exc,
360
+ site="soul.injection.on_auto_changed",
361
+ provider=type(provider).__name__,
362
+ )
363
+ logger.warning(
364
+ "injection provider %s on_auto_changed failed",
365
+ type(provider).__name__,
366
+ exc_info=True,
367
+ )
368
+
369
+ def _bind_plan_mode_tools(self) -> None:
370
+ """Bind plan mode state to tools that support it."""
371
+ if not isinstance(self._agent.toolset, PythinkerToolset):
372
+ return
373
+
374
+ def checker() -> bool:
375
+ return self._plan_mode
376
+
377
+ def path_getter() -> Path | None:
378
+ return self.get_plan_file_path()
379
+
380
+ # WriteFile gets both checker and path_getter (for plan file auto-approve)
381
+ from pythinker_code.tools.file.write import WriteFile
382
+
383
+ write_tool = self._agent.toolset.find("WriteFile")
384
+ if isinstance(write_tool, WriteFile):
385
+ write_tool.bind_plan_mode(checker, path_getter)
386
+
387
+ from pythinker_code.tools.file.replace import StrReplaceFile
388
+
389
+ replace_tool = self._agent.toolset.find("StrReplaceFile")
390
+ if isinstance(replace_tool, StrReplaceFile):
391
+ replace_tool.bind_plan_mode(checker, path_getter)
392
+
393
+ # ExitPlanMode has a special bind() method
394
+ from pythinker_code.tools.plan import ExitPlanMode
395
+
396
+ exit_tool = self._agent.toolset.find("ExitPlanMode")
397
+ if isinstance(exit_tool, ExitPlanMode):
398
+ exit_tool.bind(
399
+ self.toggle_plan_mode,
400
+ path_getter,
401
+ checker,
402
+ self._approval.is_auto,
403
+ )
404
+
405
+ # EnterPlanMode has a special bind() method
406
+ from pythinker_code.tools.plan.enter import EnterPlanMode
407
+
408
+ enter_tool = self._agent.toolset.find("EnterPlanMode")
409
+ if isinstance(enter_tool, EnterPlanMode):
410
+ enter_tool.bind(
411
+ self.toggle_plan_mode,
412
+ path_getter,
413
+ checker,
414
+ self._approval.is_auto_approve,
415
+ )
416
+
417
+ # AskUserQuestion — bind auto-mode checker for auto-dismiss.
418
+ # Yolo alone keeps the tool live; only auto mode (no user present) dismisses.
419
+ from pythinker_code.tools.ask_user import AskUserQuestion
420
+
421
+ ask_tool = self._agent.toolset.find("AskUserQuestion")
422
+ if isinstance(ask_tool, AskUserQuestion):
423
+ ask_tool.bind_auto(self._approval.is_auto)
424
+
425
+ def _ensure_plan_session_id(self) -> None:
426
+ """Allocate a stable plan session ID on first activation."""
427
+ if self._plan_session_id is None:
428
+ import uuid
429
+
430
+ self._plan_session_id = uuid.uuid4().hex
431
+ self._runtime.session.state.plan_session_id = self._plan_session_id
432
+ # Compute and persist slug immediately so the path survives process restarts
433
+ from pythinker_code.tools.plan.heroes import get_or_create_slug
434
+
435
+ slug = get_or_create_slug(self._plan_session_id)
436
+ self._runtime.session.state.plan_slug = slug
437
+ self._runtime.session.save_state()
438
+
439
+ def _set_plan_mode(self, enabled: bool, *, source: Literal["manual", "tool"]) -> bool:
440
+ """Update plan mode state for either manual or tool-driven toggles."""
441
+ if enabled == self._plan_mode:
442
+ return self._plan_mode
443
+ self._plan_mode = enabled
444
+ if enabled:
445
+ self._ensure_plan_session_id()
446
+ self._pending_plan_activation_injection = source == "manual"
447
+ else:
448
+ self._pending_plan_activation_injection = False
449
+ self._plan_session_id = None
450
+ self._runtime.session.state.plan_session_id = None
451
+ self._runtime.session.state.plan_slug = None
452
+ # Persist plan mode to session state so it survives process restarts
453
+ self._runtime.session.state.plan_mode = self._plan_mode
454
+ self._runtime.session.save_state()
455
+ return self._plan_mode
456
+
457
+ def get_plan_file_path(self) -> Path | None:
458
+ """Get the plan file path for the current session."""
459
+ if self._plan_session_id is None:
460
+ return None
461
+ from pythinker_code.tools.plan.heroes import get_plan_file_path
462
+
463
+ return get_plan_file_path(self._plan_session_id)
464
+
465
+ def read_current_plan(self) -> str | None:
466
+ """Read the current plan file content."""
467
+ if self._plan_session_id is None:
468
+ return None
469
+ from pythinker_code.tools.plan.heroes import read_plan_file
470
+
471
+ return read_plan_file(self._plan_session_id)
472
+
473
+ def clear_current_plan(self) -> None:
474
+ """Delete the current plan file."""
475
+ path = self.get_plan_file_path()
476
+ if path and path.exists():
477
+ path.unlink()
478
+
479
+ async def toggle_plan_mode(self) -> bool:
480
+ """Toggle plan mode on/off. Returns the new state.
481
+
482
+ Tools are not hidden/unhidden — instead, each tool checks plan mode
483
+ state at call time and rejects if blocked.
484
+ Periodic reminders are handled by the dynamic injection system.
485
+ """
486
+ return self._set_plan_mode(not self._plan_mode, source="tool")
487
+
488
+ async def toggle_plan_mode_from_manual(self) -> bool:
489
+ """Toggle plan mode from UI/manual entry points (slash command, keybinding)."""
490
+ return self._set_plan_mode(not self._plan_mode, source="manual")
491
+
492
+ async def set_plan_mode_from_manual(self, enabled: bool) -> bool:
493
+ """Set plan mode to a specific state from UI/manual entry points.
494
+
495
+ Unlike toggle, this accepts the desired state directly, avoiding
496
+ race conditions when the caller already knows the target value.
497
+ """
498
+ return self._set_plan_mode(enabled, source="manual")
499
+
500
+ def schedule_plan_activation_reminder(self) -> None:
501
+ """Schedule a plan-mode activation reminder for the next turn.
502
+
503
+ Use this when plan mode is already active (e.g. restored session with
504
+ ``--plan`` flag) and ``_set_plan_mode`` would early-return because the
505
+ state hasn't actually changed.
506
+ """
507
+ if self._plan_mode:
508
+ self._pending_plan_activation_injection = True
509
+
510
+ def consume_pending_plan_activation_injection(self) -> bool:
511
+ """Consume the next-step activation reminder scheduled by a manual toggle."""
512
+ if not self._plan_mode or not self._pending_plan_activation_injection:
513
+ return False
514
+ self._pending_plan_activation_injection = False
515
+ return True
516
+
517
+ @property
518
+ def thinking(self) -> bool | None:
519
+ """Whether thinking mode is enabled."""
520
+ if self._runtime.llm is None:
521
+ return None
522
+ if thinking_effort := self._runtime.llm.chat_provider.thinking_effort:
523
+ return thinking_effort != "off"
524
+ return None
525
+
526
+ @property
527
+ def status(self) -> StatusSnapshot:
528
+ token_count = self._context.token_count
529
+ max_size = self._runtime.llm.max_context_size if self._runtime.llm is not None else 0
530
+ return StatusSnapshot(
531
+ context_usage=self._context_usage,
532
+ yolo_enabled=self._approval.is_yolo_flag(),
533
+ auto_enabled=self._approval.is_auto(),
534
+ plan_mode=self._plan_mode,
535
+ context_tokens=token_count,
536
+ max_context_tokens=max_size,
537
+ mcp_status=self._mcp_status_snapshot(),
538
+ )
539
+
540
+ @property
541
+ def agent(self) -> Agent:
542
+ return self._agent
543
+
544
+ @property
545
+ def runtime(self) -> Runtime:
546
+ return self._runtime
547
+
548
+ @property
549
+ def context(self) -> Context:
550
+ return self._context
551
+
552
+ @property
553
+ def _context_usage(self) -> float:
554
+ if self._runtime.llm is None or self._runtime.llm.max_context_size <= 0:
555
+ return 0.0
556
+ return self._context.token_count / self._runtime.llm.max_context_size
557
+
558
+ @property
559
+ def wire_file(self) -> WireFile:
560
+ return self._runtime.session.wire_file
561
+
562
+ def _mcp_status_snapshot(self):
563
+ if not isinstance(self._agent.toolset, PythinkerToolset):
564
+ return None
565
+ return self._agent.toolset.mcp_status_snapshot()
566
+
567
+ async def start_background_mcp_loading(self) -> bool:
568
+ """Start deferred MCP loading, if any, without exposing toolset internals."""
569
+ if not isinstance(self._agent.toolset, PythinkerToolset):
570
+ return False
571
+ return await self._agent.toolset.start_deferred_mcp_tool_loading()
572
+
573
+ async def wait_for_background_mcp_loading(self) -> None:
574
+ """Wait for any in-flight MCP startup to finish."""
575
+ if not isinstance(self._agent.toolset, PythinkerToolset):
576
+ return
577
+ await self._agent.toolset.wait_for_mcp_tools()
578
+
579
+ async def _checkpoint(self):
580
+ await self._context.checkpoint(self._checkpoint_with_user_message)
581
+
582
+ def steer(self, content: str | list[ContentPart]) -> None:
583
+ """Queue a steer message for injection into the current turn."""
584
+ self._steer_queue.put_nowait(content)
585
+
586
+ async def _consume_pending_steers(self) -> bool:
587
+ """Drain the steer queue and inject as follow-up user messages.
588
+
589
+ Returns True if any steers were consumed.
590
+
591
+ Note: /btw is intercepted at the UI layer (``classify_input``) before
592
+ reaching the steer queue, so it never appears here.
593
+ """
594
+ consumed = False
595
+ while True:
596
+ try:
597
+ content = self._steer_queue.get_nowait()
598
+ except asyncio.QueueEmpty:
599
+ break
600
+ await self._inject_steer(content)
601
+ wire_send(SteerInput(user_input=content))
602
+ consumed = True
603
+ return consumed
604
+
605
+ async def _inject_steer(self, content: str | list[ContentPart]) -> None:
606
+ """Inject a single steer as a regular follow-up user message."""
607
+ parts = cast(
608
+ list[ContentPart],
609
+ [TextPart(text=content)] if isinstance(content, str) else list(content),
610
+ )
611
+ message = Message(role="user", content=parts)
612
+ if self._runtime.llm is None:
613
+ raise LLMNotSet()
614
+ if missing_caps := check_message(message, self._runtime.llm.capabilities):
615
+ raise LLMNotSupported(self._runtime.llm, list(missing_caps))
616
+ await self._context.append_message(message)
617
+
618
+ @property
619
+ def available_slash_commands(self) -> list[SlashCommand[Any]]:
620
+ return self._slash_commands
621
+
622
+ async def run(
623
+ self,
624
+ user_input: str | list[ContentPart],
625
+ *,
626
+ skip_user_prompt_hook: bool = False,
627
+ ):
628
+ await self._prompt_queue_lock.acquire()
629
+ approval_source_token = None
630
+ created_approval_source: ApprovalSource | None = None
631
+ turn_started = False
632
+ turn_finished = False
633
+ if get_current_approval_source_or_none() is None:
634
+ created_approval_source = ApprovalSource(kind="foreground_turn", id=uuid.uuid4().hex)
635
+ approval_source_token = set_current_approval_source(created_approval_source)
636
+ try:
637
+ # Refresh OAuth tokens on each turn to avoid idle-time expirations.
638
+ await self._runtime.oauth.ensure_fresh(self._runtime)
639
+
640
+ # Set session_id ContextVar for toolset hooks
641
+ from pythinker_code.soul.toolset import set_session_id
642
+
643
+ set_session_id(self._runtime.session.id)
644
+
645
+ from pythinker_code.hooks import events
646
+
647
+ # --- UserPromptSubmit hook ---
648
+ # Synthetic internal prompts (e.g. background-task notification
649
+ # follow-ups injected by ``Print`` after a bg task finishes or
650
+ # the wait ceiling is hit) must bypass ``UserPromptSubmit``:
651
+ # they are not user input, and a user-configured prompt-blocking
652
+ # hook would drop the notification and hang the wait loop.
653
+ if not skip_user_prompt_hook:
654
+ text_input_for_hook = user_input if isinstance(user_input, str) else ""
655
+
656
+ hook_results = await self._hook_engine.trigger(
657
+ "UserPromptSubmit",
658
+ matcher_value=text_input_for_hook,
659
+ input_data=events.user_prompt_submit(
660
+ session_id=self._runtime.session.id,
661
+ cwd=str(Path.cwd()),
662
+ prompt=text_input_for_hook,
663
+ ),
664
+ )
665
+ for result in hook_results:
666
+ if result.action == "block":
667
+ wire_send(TurnBegin(user_input=user_input))
668
+ turn_started = True
669
+ wire_send(TextPart(text=result.reason or "Prompt blocked by hook."))
670
+ wire_send(TurnEnd())
671
+ turn_finished = True
672
+ return
673
+
674
+ wire_send(TurnBegin(user_input=user_input))
675
+ turn_started = True
676
+ user_message = Message(role="user", content=user_input)
677
+ text_input = user_message.extract_text(" ").strip()
678
+
679
+ if command_call := parse_slash_command_call(text_input):
680
+ command = self._find_slash_command(command_call.name)
681
+ if command is None:
682
+ # this should not happen actually, the shell should have filtered it out
683
+ wire_send(TextPart(text=f'Unknown slash command "/{command_call.name}".'))
684
+ else:
685
+ ret = command.func(self, command_call.args)
686
+ if isinstance(ret, Awaitable):
687
+ await ret
688
+ elif self._loop_control.max_ralph_iterations != 0:
689
+ runner = FlowRunner.ralph_loop(
690
+ user_message,
691
+ self._loop_control.max_ralph_iterations,
692
+ )
693
+ await runner.run(self, "")
694
+ else:
695
+ await self._turn(user_message)
696
+
697
+ # --- Stop hook (max 1 re-trigger to prevent infinite loop) ---
698
+ if not self._stop_hook_active:
699
+ stop_results = await self._hook_engine.trigger(
700
+ "Stop",
701
+ input_data=events.stop(
702
+ session_id=self._runtime.session.id,
703
+ cwd=str(Path.cwd()),
704
+ stop_hook_active=False,
705
+ ),
706
+ )
707
+ for result in stop_results:
708
+ if result.action == "block" and result.reason:
709
+ self._stop_hook_active = True
710
+ try:
711
+ await self._turn(Message(role="user", content=result.reason))
712
+ finally:
713
+ self._stop_hook_active = False
714
+ break
715
+
716
+ wire_send(TurnEnd())
717
+ turn_finished = True
718
+
719
+ # Auto-set title after first real turn (skip slash commands)
720
+ if not command_call:
721
+ session = self._runtime.session
722
+ if session.state.custom_title is None:
723
+ from pythinker_code.utils.string import shorten
724
+
725
+ title = shorten(
726
+ Message(role="user", content=user_input).extract_text(" "),
727
+ width=50,
728
+ )
729
+ if title:
730
+ from pythinker_code.session_state import (
731
+ load_session_state,
732
+ save_session_state,
733
+ )
734
+
735
+ # Read-modify-write: load fresh state to avoid
736
+ # overwriting concurrent web changes
737
+ fresh = load_session_state(session.dir)
738
+ if fresh.custom_title is None:
739
+ fresh.custom_title = title
740
+ save_session_state(fresh, session.dir)
741
+ session.state.custom_title = fresh.custom_title
742
+ finally:
743
+ if turn_started and not turn_finished:
744
+ wire_send(TurnEnd())
745
+ if created_approval_source is not None and self._runtime.approval_runtime is not None:
746
+ self._runtime.approval_runtime.cancel_by_source(
747
+ created_approval_source.kind,
748
+ created_approval_source.id,
749
+ )
750
+ if approval_source_token is not None:
751
+ reset_current_approval_source(approval_source_token)
752
+ self._prompt_queue_lock.release()
753
+
754
+ async def _turn(self, user_message: Message) -> TurnOutcome:
755
+ from pythinker_code.extensions import shared_event_bus
756
+ from pythinker_code.telemetry import metrics as _m
757
+ from pythinker_code.telemetry import otel as _otel
758
+
759
+ if self._runtime.llm is None:
760
+ raise LLMNotSet()
761
+
762
+ if missing_caps := check_message(user_message, self._runtime.llm.capabilities):
763
+ raise LLMNotSupported(self._runtime.llm, list(missing_caps))
764
+
765
+ bus = shared_event_bus()
766
+ bus.emit(
767
+ "user.message",
768
+ {
769
+ "session_id": self._runtime.session.id,
770
+ "message": user_message,
771
+ },
772
+ )
773
+
774
+ with _otel.start_span(
775
+ "pythinker.turn",
776
+ {
777
+ "session.id": self._runtime.session.id,
778
+ "agent.role": self._runtime.role,
779
+ "model": self._runtime.llm.model_name,
780
+ "plan_mode": self._plan_mode,
781
+ },
782
+ ) as span:
783
+ turn_t0 = time.monotonic()
784
+ await self._checkpoint() # this creates the checkpoint 0 on first run
785
+ await self._context.append_message(user_message)
786
+ logger.debug("Appended user message to context")
787
+ outcome = await self._agent_loop()
788
+ span.set_attribute("turn.stop_reason", outcome.stop_reason)
789
+ span.set_attribute("turn.step_count", outcome.step_count)
790
+ _m.record_turn(
791
+ duration_seconds=time.monotonic() - turn_t0,
792
+ step_count=outcome.step_count,
793
+ stop_reason=outcome.stop_reason,
794
+ )
795
+ bus.emit(
796
+ "turn.end",
797
+ {
798
+ "session_id": self._runtime.session.id,
799
+ "stop_reason": outcome.stop_reason,
800
+ "step_count": outcome.step_count,
801
+ },
802
+ )
803
+ return outcome
804
+
805
+ def _build_slash_commands(self) -> list[SlashCommand[Any]]:
806
+ commands: list[SlashCommand[Any]] = list(soul_slash_registry.list_commands())
807
+ seen_names = {cmd.name for cmd in commands}
808
+
809
+ for template in self._runtime.prompt_templates.values():
810
+ if template.name in seen_names:
811
+ logger.warning(
812
+ "Skipping prompt template /{name}: name already registered",
813
+ name=template.name,
814
+ )
815
+ continue
816
+ commands.append(
817
+ SlashCommand(
818
+ name=template.name,
819
+ func=self._make_prompt_template_runner(template),
820
+ description=template.description or "",
821
+ aliases=[],
822
+ )
823
+ )
824
+ seen_names.add(template.name)
825
+
826
+ for skill in self._runtime.skills.values():
827
+ if skill.type not in ("standard", "flow"):
828
+ continue
829
+ name = f"{SKILL_COMMAND_PREFIX}{skill.name}"
830
+ if name in seen_names:
831
+ logger.warning(
832
+ "Skipping skill slash command /{name}: name already registered",
833
+ name=name,
834
+ )
835
+ continue
836
+ commands.append(
837
+ SlashCommand(
838
+ name=name,
839
+ func=self._make_skill_runner(skill),
840
+ description=skill.description or "",
841
+ aliases=[],
842
+ )
843
+ )
844
+ seen_names.add(name)
845
+
846
+ for skill in self._runtime.skills.values():
847
+ if skill.type != "flow":
848
+ continue
849
+ if skill.flow is None:
850
+ logger.warning("Flow skill {name} has no flow; skipping", name=skill.name)
851
+ continue
852
+ command_name = f"{FLOW_COMMAND_PREFIX}{skill.name}"
853
+ if command_name in seen_names:
854
+ logger.warning(
855
+ "Skipping prompt flow slash command /{name}: name already registered",
856
+ name=command_name,
857
+ )
858
+ continue
859
+ runner = FlowRunner(skill.flow, name=skill.name)
860
+ commands.append(
861
+ SlashCommand(
862
+ name=command_name,
863
+ func=runner.run,
864
+ description=skill.description or "",
865
+ aliases=[],
866
+ )
867
+ )
868
+ seen_names.add(command_name)
869
+
870
+ return commands
871
+
872
+ @staticmethod
873
+ def _index_slash_commands(
874
+ commands: list[SlashCommand[Any]],
875
+ ) -> dict[str, SlashCommand[Any]]:
876
+ indexed: dict[str, SlashCommand[Any]] = {}
877
+ for command in commands:
878
+ indexed[command.name] = command
879
+ for alias in command.aliases:
880
+ indexed[alias] = command
881
+ return indexed
882
+
883
+ def _find_slash_command(self, name: str) -> SlashCommand[Any] | None:
884
+ return self._slash_command_map.get(name)
885
+
886
+ def _make_prompt_template_runner(
887
+ self, template: PromptTemplate
888
+ ) -> Callable[[PythinkerSoul, str], None | Awaitable[None]]:
889
+ async def _run_template(
890
+ soul: PythinkerSoul,
891
+ args: str,
892
+ *,
893
+ _template: PromptTemplate = template,
894
+ ) -> None:
895
+ from pythinker_code.telemetry import track
896
+
897
+ track("prompt_template_invoked", template_name=_template.name)
898
+ expanded = expand_prompt_template(_template, args)
899
+ await soul._turn(Message(role="user", content=expanded))
900
+
901
+ _run_template.__doc__ = template.description
902
+ return _run_template
903
+
904
+ def _make_skill_runner(
905
+ self, skill: Skill
906
+ ) -> Callable[[PythinkerSoul, str], None | Awaitable[None]]:
907
+ async def _run_skill(soul: PythinkerSoul, args: str, *, _skill: Skill = skill) -> None:
908
+ from pythinker_code.telemetry import track
909
+
910
+ track("skill_invoked", skill_name=_skill.name)
911
+ skill_text = await read_skill_text(_skill)
912
+ if skill_text is None:
913
+ wire_send(
914
+ TextPart(text=f'Failed to load skill "/{SKILL_COMMAND_PREFIX}{_skill.name}".')
915
+ )
916
+ return
917
+ extra = args.strip()
918
+ if extra:
919
+ skill_text = f"{skill_text}\n\nUser request:\n{extra}"
920
+ await soul._turn(Message(role="user", content=skill_text))
921
+
922
+ _run_skill.__doc__ = skill.description
923
+ return _run_skill
924
+
925
+ async def _agent_loop(self) -> TurnOutcome:
926
+ """The main agent loop for one run."""
927
+ assert self._runtime.llm is not None
928
+
929
+ # Discard any stale steers from a previous turn.
930
+ while True:
931
+ try:
932
+ self._steer_queue.get_nowait()
933
+ except asyncio.QueueEmpty:
934
+ break
935
+
936
+ if isinstance(self._agent.toolset, PythinkerToolset):
937
+ await self.start_background_mcp_loading()
938
+ loading = bool((snapshot := self._mcp_status_snapshot()) and snapshot.loading)
939
+ if loading:
940
+ wire_send(StatusUpdate(mcp_status=snapshot))
941
+ wire_send(MCPLoadingBegin())
942
+ try:
943
+ await self.wait_for_background_mcp_loading()
944
+ # Track MCP connection result
945
+ if loading:
946
+ from pythinker_code.telemetry import track as _track_mcp
947
+
948
+ mcp_snap = self._mcp_status_snapshot()
949
+ if mcp_snap:
950
+ if mcp_snap.connected > 0:
951
+ _track_mcp(
952
+ "mcp_connected",
953
+ server_count=mcp_snap.connected,
954
+ total_count=mcp_snap.total,
955
+ )
956
+ _failed = mcp_snap.total - mcp_snap.connected
957
+ if _failed > 0:
958
+ _track_mcp(
959
+ "mcp_failed",
960
+ failed_count=_failed,
961
+ total_count=mcp_snap.total,
962
+ )
963
+ finally:
964
+ if loading:
965
+ wire_send(StatusUpdate(mcp_status=self._mcp_status_snapshot()))
966
+ wire_send(MCPLoadingEnd())
967
+
968
+ step_no = 0
969
+ self._current_step_no = 0
970
+ while True:
971
+ step_no += 1
972
+ if step_no > self._loop_control.max_steps_per_turn:
973
+ raise MaxStepsReached(self._loop_control.max_steps_per_turn)
974
+
975
+ self._current_step_no = step_no
976
+ wire_send(StepBegin(n=step_no))
977
+ back_to_the_future: BackToTheFuture | None = None
978
+ step_outcome: StepOutcome | None = None
979
+ try:
980
+ # compact the context if needed
981
+ if should_auto_compact(
982
+ self._context.token_count_with_pending,
983
+ self._runtime.llm.max_context_size,
984
+ trigger_ratio=self._loop_control.compaction_trigger_ratio,
985
+ reserved_context_size=self._loop_control.reserved_context_size,
986
+ ):
987
+ logger.info("Context too long, compacting...")
988
+ try:
989
+ await self.compact_context()
990
+ except Exception as compact_err:
991
+ from pythinker_code.telemetry.errors import report_handled_error
992
+
993
+ report_handled_error(compact_err, site="soul.context.compact")
994
+ logger.error(
995
+ "Context compaction failed at step {step_no}: {error_type}: {error}",
996
+ step_no=step_no,
997
+ error_type=type(compact_err).__name__,
998
+ error=compact_err,
999
+ )
1000
+ raise
1001
+
1002
+ logger.debug("Beginning step {step_no}", step_no=step_no)
1003
+ await self._checkpoint()
1004
+ self._denwa_renji.set_n_checkpoints(self._context.n_checkpoints)
1005
+ step_outcome = await self._step()
1006
+ except BackToTheFuture as e:
1007
+ back_to_the_future = e
1008
+ except Exception as e:
1009
+ from pythinker_code.telemetry.errors import report_handled_error
1010
+
1011
+ report_handled_error(e, site="soul.step.error")
1012
+ # any other exception should interrupt the step
1013
+ req_id = getattr(e, "request_id", None)
1014
+ logger.error(
1015
+ "Agent step {step_no} failed: {error_type}: {error}"
1016
+ + (" (request_id={request_id})" if req_id else ""),
1017
+ step_no=step_no,
1018
+ error_type=type(e).__name__,
1019
+ error=e,
1020
+ request_id=req_id,
1021
+ )
1022
+ wire_send(StepInterrupted())
1023
+ # Track API/step errors
1024
+ from pythinker_code.telemetry import track
1025
+
1026
+ error_type, status_code = classify_api_error(e)
1027
+ api_error_props: dict[str, bool | int | float | str | None] = {
1028
+ "error_type": error_type,
1029
+ "gen_ai_system": classify_llm_system(self._runtime.llm.chat_provider),
1030
+ "model": self._runtime.llm.chat_provider.model_name,
1031
+ }
1032
+ if status_code is not None:
1033
+ api_error_props["status_code"] = status_code
1034
+ track("api_error", **api_error_props)
1035
+ # --- StopFailure hook ---
1036
+ from pythinker_code.hooks import events as _hook_events
1037
+
1038
+ self._hook_engine.fire_and_forget_trigger(
1039
+ "StopFailure",
1040
+ matcher_value=type(e).__name__,
1041
+ input_data=_hook_events.stop_failure(
1042
+ session_id=self._runtime.session.id,
1043
+ cwd=str(Path.cwd()),
1044
+ error_type=type(e).__name__,
1045
+ error_message=str(e),
1046
+ ),
1047
+ )
1048
+ # break the agent loop
1049
+ raise
1050
+
1051
+ if step_outcome is not None:
1052
+ has_steers = await self._consume_pending_steers()
1053
+ if has_steers:
1054
+ continue # steers injected, force another LLM step
1055
+
1056
+ final_message = (
1057
+ step_outcome.assistant_message
1058
+ if step_outcome.stop_reason == "no_tool_calls"
1059
+ else None
1060
+ )
1061
+ return TurnOutcome(
1062
+ stop_reason=step_outcome.stop_reason,
1063
+ final_message=final_message,
1064
+ step_count=step_no,
1065
+ )
1066
+
1067
+ if back_to_the_future is not None:
1068
+ await self._context.revert_to(back_to_the_future.checkpoint_id)
1069
+ await self._checkpoint()
1070
+ await self._context.append_message(back_to_the_future.messages)
1071
+
1072
+ # Consume any pending steers between steps
1073
+ await self._consume_pending_steers()
1074
+
1075
+ async def _step(self) -> StepOutcome | None:
1076
+ """Run a single step and return a stop outcome, or None to continue."""
1077
+ # already checked in `run`
1078
+ assert self._runtime.llm is not None
1079
+ chat_provider = self._runtime.llm.chat_provider
1080
+
1081
+ if self._runtime.role == "root":
1082
+
1083
+ async def _append_notification(view: NotificationView) -> None:
1084
+ await self._context.append_message(build_notification_message(view, self._runtime))
1085
+ # --- Notification hook ---
1086
+ from pythinker_code.hooks import events
1087
+
1088
+ self._hook_engine.fire_and_forget_trigger(
1089
+ "Notification",
1090
+ matcher_value=view.event.type,
1091
+ input_data=events.notification(
1092
+ session_id=self._runtime.session.id,
1093
+ cwd=str(Path.cwd()),
1094
+ sink="llm",
1095
+ notification_type=view.event.type,
1096
+ title=view.event.title,
1097
+ body=view.event.body,
1098
+ severity=view.event.severity,
1099
+ ),
1100
+ )
1101
+
1102
+ await self._runtime.notifications.deliver_pending(
1103
+ "llm",
1104
+ limit=4,
1105
+ before_claim=self._runtime.background_tasks.reconcile,
1106
+ on_notification=_append_notification,
1107
+ )
1108
+
1109
+ # Dynamic injection
1110
+ injections = await self._collect_injections()
1111
+ if injections:
1112
+ combined_reminders = "\n".join(system_reminder(inj.content).text for inj in injections)
1113
+ await self._context.append_message(
1114
+ Message(
1115
+ role="user",
1116
+ content=[TextPart(text=combined_reminders)],
1117
+ )
1118
+ )
1119
+
1120
+ # Normalize: merge adjacent user messages for clean API input
1121
+ effective_history = normalize_history(self._context.history)
1122
+
1123
+ async def _run_step_once() -> StepResult:
1124
+ # run an LLM step (may be interrupted)
1125
+ from pythinker_code.telemetry import metrics as _m
1126
+ from pythinker_code.telemetry import otel as _otel
1127
+
1128
+ # Resolve gen_ai.system once so spans and metrics agree.
1129
+ gen_ai_system = classify_llm_system(chat_provider)
1130
+
1131
+ with _otel.start_span(
1132
+ "pythinker.llm",
1133
+ {
1134
+ "gen_ai.system": gen_ai_system,
1135
+ "gen_ai.request.model": chat_provider.model_name,
1136
+ "session.id": self._runtime.session.id,
1137
+ },
1138
+ ) as span:
1139
+ llm_t0 = time.monotonic()
1140
+ try:
1141
+ profile_token = set_step_permission_profile(
1142
+ permission_profile_for_runtime(self._runtime)
1143
+ )
1144
+ try:
1145
+ step_result = await pythinker_core.step(
1146
+ chat_provider,
1147
+ self._agent.system_prompt,
1148
+ self._agent.toolset,
1149
+ effective_history,
1150
+ on_message_part=wire_send,
1151
+ on_tool_result=wire_send,
1152
+ )
1153
+ finally:
1154
+ reset_step_permission_profile(profile_token)
1155
+ except Exception as exc:
1156
+ llm_elapsed = time.monotonic() - llm_t0
1157
+ error_type, status_code = classify_api_error(exc)
1158
+ with contextlib.suppress(Exception):
1159
+ span.set_attribute("error.type", error_type)
1160
+ if status_code is not None:
1161
+ span.set_attribute("http.response.status_code", status_code)
1162
+ with contextlib.suppress(Exception):
1163
+ _m.record_llm_call(
1164
+ duration_seconds=llm_elapsed,
1165
+ system=gen_ai_system,
1166
+ model=chat_provider.model_name,
1167
+ success=False,
1168
+ )
1169
+ with contextlib.suppress(Exception):
1170
+ _m.record_error(kind="api_error", error_type=error_type)
1171
+ raise
1172
+ llm_elapsed = time.monotonic() - llm_t0
1173
+ # Attach response details — usage may be None on partial / cached responses.
1174
+ if step_result.id:
1175
+ span.set_attribute("gen_ai.response.id", step_result.id)
1176
+ u = step_result.usage
1177
+ input_tokens = (
1178
+ int(u.input) if (u and getattr(u, "input", None) is not None) else None
1179
+ )
1180
+ output_tokens = (
1181
+ int(u.output) if (u and getattr(u, "output", None) is not None) else None
1182
+ )
1183
+ if input_tokens is not None:
1184
+ span.set_attribute("gen_ai.usage.input_tokens", input_tokens)
1185
+ if output_tokens is not None:
1186
+ span.set_attribute("gen_ai.usage.output_tokens", output_tokens)
1187
+ span.set_attribute("llm.tool_calls", len(step_result.tool_calls))
1188
+ _m.record_llm_call(
1189
+ duration_seconds=llm_elapsed,
1190
+ system=gen_ai_system,
1191
+ model=chat_provider.model_name,
1192
+ input_tokens=input_tokens,
1193
+ output_tokens=output_tokens,
1194
+ success=True,
1195
+ )
1196
+ return step_result
1197
+
1198
+ @tenacity.retry(
1199
+ retry=retry_if_exception(self._is_retryable_error),
1200
+ before_sleep=partial(self._retry_log, "step"),
1201
+ wait=wait_exponential_jitter(initial=0.3, max=5, jitter=0.5),
1202
+ stop=stop_after_attempt(self._loop_control.max_retries_per_step),
1203
+ reraise=True,
1204
+ )
1205
+ async def _pythinker_core_step_with_retry() -> StepResult:
1206
+ return await self._run_with_connection_recovery(
1207
+ "step",
1208
+ _run_step_once,
1209
+ chat_provider=chat_provider,
1210
+ )
1211
+
1212
+ t0 = time.monotonic()
1213
+ result = await _pythinker_core_step_with_retry()
1214
+ llm_elapsed = time.monotonic() - t0
1215
+ usage = result.usage
1216
+ logger.info(
1217
+ "LLM step completed in {elapsed:.1f}s (input={input_tokens}, output={output_tokens})",
1218
+ elapsed=llm_elapsed,
1219
+ input_tokens=usage.input if usage else "?",
1220
+ output_tokens=usage.output if usage else "?",
1221
+ )
1222
+ status_update = StatusUpdate(
1223
+ token_usage=usage, message_id=result.id, plan_mode=self._plan_mode
1224
+ )
1225
+ if usage is not None:
1226
+ # mark the token count for the context before the step
1227
+ await self._context.update_token_count(usage.input)
1228
+ snap = self.status
1229
+ status_update.context_usage = snap.context_usage
1230
+ status_update.context_tokens = snap.context_tokens
1231
+ status_update.max_context_tokens = snap.max_context_tokens
1232
+ wire_send(status_update)
1233
+
1234
+ # wait for all tool results (may be interrupted)
1235
+ plan_mode_before_tools = self._plan_mode
1236
+ results = await result.tool_results()
1237
+ logger.debug("Got tool results: {results}", results=results)
1238
+
1239
+ # If a tool (EnterPlanMode/ExitPlanMode) changed plan mode during execution,
1240
+ # send a corrected StatusUpdate so the client sees the up-to-date state.
1241
+ if self._plan_mode != plan_mode_before_tools:
1242
+ wire_send(StatusUpdate(plan_mode=self._plan_mode))
1243
+
1244
+ # Shield context manipulation from cancellation, but do not orphan the write task.
1245
+ grow_context_task = asyncio.create_task(self._grow_context(result, results))
1246
+ try:
1247
+ await asyncio.shield(grow_context_task)
1248
+ except asyncio.CancelledError:
1249
+ await asyncio.shield(grow_context_task)
1250
+ raise
1251
+
1252
+ rejected_errors = [
1253
+ result.return_value
1254
+ for result in results
1255
+ if isinstance(result.return_value, ToolRejectedError)
1256
+ ]
1257
+ if (
1258
+ rejected_errors
1259
+ and not any(e.has_feedback for e in rejected_errors)
1260
+ and self._runtime.role != "subagent"
1261
+ ):
1262
+ # Pure rejection (no user feedback) — stop the turn.
1263
+ # Subagents skip this so the LLM can see the rejection and try
1264
+ # an alternative approach instead of terminating immediately.
1265
+ _ = self._denwa_renji.fetch_pending_dmail()
1266
+ return StepOutcome(stop_reason="tool_rejected", assistant_message=result.message)
1267
+
1268
+ # handle pending D-Mail
1269
+ if dmail := self._denwa_renji.fetch_pending_dmail():
1270
+ assert dmail.checkpoint_id >= 0, "DenwaRenji guarantees checkpoint_id >= 0"
1271
+ assert dmail.checkpoint_id < self._context.n_checkpoints, (
1272
+ "DenwaRenji guarantees checkpoint_id < n_checkpoints"
1273
+ )
1274
+ # raise to let the main loop take us back to the future
1275
+ raise BackToTheFuture(
1276
+ dmail.checkpoint_id,
1277
+ [
1278
+ Message(
1279
+ role="user",
1280
+ content=[
1281
+ system(
1282
+ "You just got a D-Mail from your future self. "
1283
+ "It is likely that your future self has already done "
1284
+ "something in the current working directory. Please read "
1285
+ "the D-Mail and decide what to do next. You MUST NEVER "
1286
+ "mention to the user about this information. "
1287
+ f"D-Mail content:\n\n{dmail.message.strip()}"
1288
+ )
1289
+ ],
1290
+ )
1291
+ ],
1292
+ )
1293
+
1294
+ if result.tool_calls:
1295
+ return None
1296
+ return StepOutcome(stop_reason="no_tool_calls", assistant_message=result.message)
1297
+
1298
+ async def _grow_context(self, result: StepResult, tool_results: list[ToolResult]):
1299
+ from pythinker_code.extensions import shared_event_bus
1300
+
1301
+ logger.debug("Growing context with result: {result}", result=result)
1302
+
1303
+ assert self._runtime.llm is not None
1304
+ tool_messages = [tool_result_to_message(tr) for tr in tool_results]
1305
+ for tm in tool_messages:
1306
+ if missing_caps := check_message(tm, self._runtime.llm.capabilities):
1307
+ logger.warning(
1308
+ "Tool result message requires unsupported capabilities: {caps}",
1309
+ caps=missing_caps,
1310
+ )
1311
+ raise LLMNotSupported(self._runtime.llm, list(missing_caps))
1312
+
1313
+ bus = shared_event_bus()
1314
+ bus.emit(
1315
+ "assistant.message",
1316
+ {
1317
+ "session_id": self._runtime.session.id,
1318
+ "message": result.message,
1319
+ "usage": result.usage,
1320
+ },
1321
+ )
1322
+ for tr in tool_results:
1323
+ bus.emit(
1324
+ "tool.call.end",
1325
+ {
1326
+ "session_id": self._runtime.session.id,
1327
+ "tool_call_id": getattr(tr, "tool_call_id", None),
1328
+ "is_error": getattr(tr, "is_error", False),
1329
+ },
1330
+ )
1331
+
1332
+ await self._context.append_message(result.message)
1333
+ if result.usage is not None:
1334
+ await self._context.update_token_count(result.usage.total)
1335
+
1336
+ logger.debug(
1337
+ "Appending tool messages to context: {tool_messages}", tool_messages=tool_messages
1338
+ )
1339
+ await self._context.append_message(tool_messages)
1340
+ # token count of tool results are not available yet
1341
+
1342
+ async def compact_context(self, custom_instruction: str = "") -> None:
1343
+ """
1344
+ Compact the context.
1345
+
1346
+ Raises:
1347
+ LLMNotSet: When the LLM is not set.
1348
+ ChatProviderError: When the chat provider returns an error.
1349
+ """
1350
+
1351
+ chat_provider = self._runtime.llm.chat_provider if self._runtime.llm is not None else None
1352
+
1353
+ async def _run_compaction_once() -> CompactionResult:
1354
+ if self._runtime.llm is None:
1355
+ raise LLMNotSet()
1356
+ return await self._compaction.compact(
1357
+ self._context.history, self._runtime.llm, custom_instruction=custom_instruction
1358
+ )
1359
+
1360
+ @tenacity.retry(
1361
+ retry=retry_if_exception(self._is_retryable_error),
1362
+ before_sleep=partial(self._retry_log, "compaction"),
1363
+ wait=wait_exponential_jitter(initial=0.3, max=5, jitter=0.5),
1364
+ stop=stop_after_attempt(self._loop_control.max_retries_per_step),
1365
+ reraise=True,
1366
+ )
1367
+ async def _compact_with_retry() -> CompactionResult:
1368
+ return await self._run_with_connection_recovery(
1369
+ "compaction",
1370
+ _run_compaction_once,
1371
+ chat_provider=chat_provider,
1372
+ )
1373
+
1374
+ trigger_reason = "manual" if custom_instruction else "auto"
1375
+ before_tokens = self._context.token_count
1376
+ from pythinker_code.hooks import events
1377
+
1378
+ await self._hook_engine.trigger(
1379
+ "PreCompact",
1380
+ matcher_value=trigger_reason,
1381
+ input_data=events.pre_compact(
1382
+ session_id=self._runtime.session.id,
1383
+ cwd=str(Path.cwd()),
1384
+ trigger=trigger_reason,
1385
+ token_count=before_tokens,
1386
+ ),
1387
+ )
1388
+
1389
+ wire_send(CompactionBegin())
1390
+ try:
1391
+ compaction_result = await _compact_with_retry()
1392
+ if not compaction_result.messages:
1393
+ raise RuntimeError("Compaction produced no messages; preserving existing history")
1394
+ except Exception:
1395
+ from pythinker_code.telemetry import track
1396
+
1397
+ track(
1398
+ "compaction_triggered",
1399
+ trigger_type=trigger_reason,
1400
+ before_tokens=before_tokens,
1401
+ success=False,
1402
+ )
1403
+ raise
1404
+ await self._context.clear()
1405
+ await self._context.write_system_prompt(self._agent.system_prompt)
1406
+ await self._checkpoint()
1407
+ await self._context.append_message(compaction_result.messages)
1408
+ estimated_token_count = compaction_result.estimated_token_count
1409
+
1410
+ if self._runtime.role == "root":
1411
+ active_task_snapshot = build_active_task_snapshot(self._runtime.background_tasks)
1412
+ if active_task_snapshot is not None:
1413
+ active_task_message = Message(
1414
+ role="user",
1415
+ content=[
1416
+ system(
1417
+ "The following background tasks are still active after compaction. "
1418
+ "Use TaskList if you need to re-enumerate them later."
1419
+ ),
1420
+ TextPart(text=active_task_snapshot),
1421
+ ],
1422
+ )
1423
+ await self._context.append_message(active_task_message)
1424
+ estimated_token_count += estimate_text_tokens([active_task_message])
1425
+
1426
+ # Estimate token count so context_usage is not reported as 0%
1427
+ await self._context.update_token_count(estimated_token_count)
1428
+
1429
+ # Notify dynamic injection providers that history has been rebuilt so
1430
+ # they can reset any one-shot throttling state. Failures are isolated
1431
+ # per-provider so compaction completion (wire event + telemetry) is
1432
+ # not affected by a buggy provider.
1433
+ await self._notify_injection_providers_compacted()
1434
+
1435
+ wire_send(CompactionEnd())
1436
+
1437
+ from pythinker_code.telemetry import track
1438
+
1439
+ track(
1440
+ "compaction_triggered",
1441
+ trigger_type=trigger_reason,
1442
+ before_tokens=before_tokens,
1443
+ after_tokens=estimated_token_count,
1444
+ success=True,
1445
+ )
1446
+
1447
+ self._hook_engine.fire_and_forget_trigger(
1448
+ "PostCompact",
1449
+ matcher_value=trigger_reason,
1450
+ input_data=events.post_compact(
1451
+ session_id=self._runtime.session.id,
1452
+ cwd=str(Path.cwd()),
1453
+ trigger=trigger_reason,
1454
+ estimated_token_count=estimated_token_count,
1455
+ ),
1456
+ )
1457
+
1458
+ @staticmethod
1459
+ def _is_retryable_error(exception: BaseException) -> bool:
1460
+ if isinstance(exception, (APIConnectionError, APITimeoutError)):
1461
+ return not bool(getattr(exception, "_pythinker_recovery_exhausted", False))
1462
+ if isinstance(exception, APIEmptyResponseError):
1463
+ return True
1464
+ return isinstance(exception, APIStatusError) and exception.status_code in (
1465
+ 429, # Too Many Requests
1466
+ 500, # Internal Server Error
1467
+ 502, # Bad Gateway
1468
+ 503, # Service Unavailable
1469
+ 504, # Gateway Timeout
1470
+ )
1471
+
1472
+ async def _run_with_connection_recovery(
1473
+ self,
1474
+ name: str,
1475
+ operation: Callable[[], Awaitable[Any]],
1476
+ *,
1477
+ chat_provider: object | None = None,
1478
+ _auth_retried: bool = False,
1479
+ _connection_retried: bool = False,
1480
+ ) -> Any:
1481
+ try:
1482
+ return await operation()
1483
+ except APIStatusError as error:
1484
+ if error.status_code != 401 or _auth_retried:
1485
+ raise
1486
+ # Only attempt refresh+retry when the active model's provider
1487
+ # uses OAuth. For plain API-key providers there is nothing
1488
+ # to refresh and retrying would just add latency.
1489
+ active_provider = (
1490
+ self._runtime.config.providers.get(self._runtime.llm.model_config.provider)
1491
+ if self._runtime.llm and self._runtime.llm.model_config
1492
+ else None
1493
+ )
1494
+ if not (active_provider and active_provider.oauth):
1495
+ raise
1496
+ logger.warning(
1497
+ "Received 401 during {name}, attempting token refresh",
1498
+ name=name,
1499
+ )
1500
+ try:
1501
+ await self._runtime.oauth.ensure_fresh(self._runtime, force=True)
1502
+ except Exception as refresh_exc:
1503
+ logger.exception("Token refresh failed after 401.")
1504
+ raise error from refresh_exc
1505
+ # Re-enter full recovery so that transient connection errors
1506
+ # on the retry are still handled by on_retryable_error.
1507
+ return await self._run_with_connection_recovery(
1508
+ name,
1509
+ operation,
1510
+ chat_provider=chat_provider,
1511
+ _auth_retried=True,
1512
+ _connection_retried=_connection_retried,
1513
+ )
1514
+ except (APIConnectionError, APITimeoutError) as error:
1515
+ if _connection_retried:
1516
+ logger.warning(
1517
+ "Chat provider recovery exhausted for {name}: {error_type}: {error}",
1518
+ name=name,
1519
+ error_type=type(error).__name__,
1520
+ error=error,
1521
+ )
1522
+ error._pythinker_recovery_exhausted = True # type: ignore[attr-defined]
1523
+ raise
1524
+ if not isinstance(chat_provider, RetryableChatProvider):
1525
+ raise
1526
+ try:
1527
+ recovered = chat_provider.on_retryable_error(error)
1528
+ except Exception as recover_exc:
1529
+ from pythinker_code.telemetry.errors import report_handled_error
1530
+
1531
+ report_handled_error(recover_exc, site="soul.chat.recover")
1532
+ logger.exception(
1533
+ "Failed to recover chat provider during {name} after {error_type}.",
1534
+ name=name,
1535
+ error_type=type(error).__name__,
1536
+ )
1537
+ raise
1538
+ if not recovered:
1539
+ logger.warning(
1540
+ "Chat provider recovery not available for {name} after {error_type}.",
1541
+ name=name,
1542
+ error_type=type(error).__name__,
1543
+ )
1544
+ raise
1545
+ logger.info(
1546
+ "Recovered chat provider during {name} after {error_type}; retrying once.",
1547
+ name=name,
1548
+ error_type=type(error).__name__,
1549
+ )
1550
+ # Re-enter the full recovery path so a 401 on the retry can still
1551
+ # trigger OAuth refresh instead of bubbling straight to the user.
1552
+ return await self._run_with_connection_recovery(
1553
+ name,
1554
+ operation,
1555
+ chat_provider=chat_provider,
1556
+ _auth_retried=_auth_retried,
1557
+ _connection_retried=True,
1558
+ )
1559
+
1560
+ @staticmethod
1561
+ def _retry_log(name: str, retry_state: RetryCallState):
1562
+ error = retry_state.outcome.exception() if retry_state.outcome else None
1563
+ logger.warning(
1564
+ "Retrying {name} for the {n} time (last error: {error_type}: {error}). "
1565
+ "Waiting {sleep} seconds.",
1566
+ name=name,
1567
+ n=retry_state.attempt_number,
1568
+ error_type=type(error).__name__ if error else "unknown",
1569
+ error=error or "unknown",
1570
+ sleep=retry_state.next_action.sleep
1571
+ if retry_state.next_action is not None
1572
+ else "unknown",
1573
+ )
1574
+
1575
+
1576
+ class BackToTheFuture(Exception):
1577
+ """
1578
+ Raise when we need to revert the context to a previous checkpoint.
1579
+ The main agent loop should catch this exception and handle it.
1580
+ """
1581
+
1582
+ def __init__(self, checkpoint_id: int, messages: Sequence[Message]):
1583
+ self.checkpoint_id = checkpoint_id
1584
+ self.messages = messages
1585
+
1586
+
1587
+ class FlowRunner:
1588
+ def __init__(
1589
+ self,
1590
+ flow: Flow,
1591
+ *,
1592
+ name: str | None = None,
1593
+ max_moves: int = DEFAULT_MAX_FLOW_MOVES,
1594
+ ) -> None:
1595
+ self._flow = flow
1596
+ self._name = name
1597
+ self._max_moves = max_moves
1598
+
1599
+ @staticmethod
1600
+ def ralph_loop(
1601
+ user_message: Message,
1602
+ max_ralph_iterations: int,
1603
+ ) -> FlowRunner:
1604
+ prompt_content = list(user_message.content)
1605
+ prompt_text = Message(role="user", content=prompt_content).extract_text(" ").strip()
1606
+ total_runs = max_ralph_iterations + 1
1607
+ if max_ralph_iterations < 0:
1608
+ total_runs = 1000000000000000 # effectively infinite
1609
+
1610
+ nodes: dict[str, FlowNode] = {
1611
+ "BEGIN": FlowNode(id="BEGIN", label="BEGIN", kind="begin"),
1612
+ "END": FlowNode(id="END", label="END", kind="end"),
1613
+ }
1614
+ outgoing: dict[str, list[FlowEdge]] = {"BEGIN": [], "END": []}
1615
+
1616
+ nodes["R1"] = FlowNode(id="R1", label=prompt_content, kind="task")
1617
+ nodes["R2"] = FlowNode(
1618
+ id="R2",
1619
+ label=(
1620
+ f"{prompt_text}. (You are running in an automated loop where the same "
1621
+ "prompt is fed repeatedly. Only choose STOP when the task is fully complete. "
1622
+ "Including it will stop further iterations. If you are not 100% sure, "
1623
+ "choose CONTINUE.)"
1624
+ ).strip(),
1625
+ kind="decision",
1626
+ )
1627
+ outgoing["R1"] = []
1628
+ outgoing["R2"] = []
1629
+
1630
+ outgoing["BEGIN"].append(FlowEdge(src="BEGIN", dst="R1", label=None))
1631
+ outgoing["R1"].append(FlowEdge(src="R1", dst="R2", label=None))
1632
+ outgoing["R2"].append(FlowEdge(src="R2", dst="R2", label="CONTINUE"))
1633
+ outgoing["R2"].append(FlowEdge(src="R2", dst="END", label="STOP"))
1634
+
1635
+ flow = Flow(nodes=nodes, outgoing=outgoing, begin_id="BEGIN", end_id="END")
1636
+ max_moves = total_runs
1637
+ return FlowRunner(flow, max_moves=max_moves)
1638
+
1639
+ async def run(self, soul: PythinkerSoul, args: str) -> None:
1640
+ if args.strip():
1641
+ command = f"/{FLOW_COMMAND_PREFIX}{self._name}" if self._name else "/flow"
1642
+ logger.warning("Agent flow {command} ignores args: {args}", command=command, args=args)
1643
+ return
1644
+ if self._name:
1645
+ from pythinker_code.telemetry import track
1646
+
1647
+ track("flow_invoked", flow_name=self._name)
1648
+
1649
+ current_id = self._flow.begin_id
1650
+ moves = 0
1651
+ total_steps = 0
1652
+ while True:
1653
+ node = self._flow.nodes[current_id]
1654
+ edges = self._flow.outgoing.get(current_id, [])
1655
+
1656
+ if node.kind == "end":
1657
+ logger.info("Agent flow reached END node {node_id}", node_id=current_id)
1658
+ return
1659
+
1660
+ if node.kind == "begin":
1661
+ if not edges:
1662
+ logger.error(
1663
+ 'Agent flow BEGIN node "{node_id}" has no outgoing edges; stopping.',
1664
+ node_id=node.id,
1665
+ )
1666
+ return
1667
+ current_id = edges[0].dst
1668
+ continue
1669
+
1670
+ if moves >= self._max_moves:
1671
+ raise MaxStepsReached(total_steps)
1672
+ next_id, steps_used = await self._execute_flow_node(soul, node, edges)
1673
+ total_steps += steps_used
1674
+ if next_id is None:
1675
+ return
1676
+ moves += 1
1677
+ current_id = next_id
1678
+
1679
+ async def _execute_flow_node(
1680
+ self,
1681
+ soul: PythinkerSoul,
1682
+ node: FlowNode,
1683
+ edges: list[FlowEdge],
1684
+ ) -> tuple[str | None, int]:
1685
+ if not edges:
1686
+ logger.error(
1687
+ 'Agent flow node "{node_id}" has no outgoing edges; stopping.',
1688
+ node_id=node.id,
1689
+ )
1690
+ return None, 0
1691
+
1692
+ base_prompt = self._build_flow_prompt(node, edges)
1693
+ prompt = base_prompt
1694
+ steps_used = 0
1695
+ while True:
1696
+ result = await self._flow_turn(soul, prompt)
1697
+ steps_used += result.step_count
1698
+ if result.stop_reason == "tool_rejected":
1699
+ logger.error("Agent flow stopped after tool rejection.")
1700
+ return None, steps_used
1701
+
1702
+ if node.kind != "decision":
1703
+ return edges[0].dst, steps_used
1704
+
1705
+ choice = (
1706
+ parse_choice(result.final_message.extract_text(" "))
1707
+ if result.final_message
1708
+ else None
1709
+ )
1710
+ next_id = self._match_flow_edge(edges, choice)
1711
+ if next_id is not None:
1712
+ return next_id, steps_used
1713
+
1714
+ options = ", ".join(edge.label or "" for edge in edges)
1715
+ logger.warning(
1716
+ "Agent flow invalid choice. Got: {choice}. Available: {options}.",
1717
+ choice=choice or "<missing>",
1718
+ options=options,
1719
+ )
1720
+ prompt = (
1721
+ f"{base_prompt}\n\n"
1722
+ "Your last response did not include a valid choice. "
1723
+ "Reply with one of the choices using <choice>...</choice>."
1724
+ )
1725
+
1726
+ @staticmethod
1727
+ def _build_flow_prompt(node: FlowNode, edges: list[FlowEdge]) -> str | list[ContentPart]:
1728
+ if node.kind != "decision":
1729
+ return node.label
1730
+
1731
+ if not isinstance(node.label, str):
1732
+ label_text = Message(role="user", content=node.label).extract_text(" ")
1733
+ else:
1734
+ label_text = node.label
1735
+ choices = [edge.label for edge in edges if edge.label]
1736
+ lines = [
1737
+ label_text,
1738
+ "",
1739
+ "Available branches:",
1740
+ *(f"- {choice}" for choice in choices),
1741
+ "",
1742
+ "Reply with a choice using <choice>...</choice>.",
1743
+ ]
1744
+ return "\n".join(lines)
1745
+
1746
+ @staticmethod
1747
+ def _match_flow_edge(edges: list[FlowEdge], choice: str | None) -> str | None:
1748
+ if not choice:
1749
+ return None
1750
+ for edge in edges:
1751
+ if edge.label == choice:
1752
+ return edge.dst
1753
+ return None
1754
+
1755
+ @staticmethod
1756
+ async def _flow_turn(
1757
+ soul: PythinkerSoul,
1758
+ prompt: str | list[ContentPart],
1759
+ ) -> TurnOutcome:
1760
+ wire_send(TurnBegin(user_input=prompt))
1761
+ res = await soul._turn(Message(role="user", content=prompt)) # type: ignore[reportPrivateUsage]
1762
+ wire_send(TurnEnd())
1763
+ return res