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