pythinker-code 2.4.0__py3-none-any.whl → 2.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. pythinker_code/CHANGELOG.md +80 -0
  2. pythinker_code/acp/tools.py +7 -0
  3. pythinker_code/agents/default/agent.yaml +1 -0
  4. pythinker_code/agents/default/coder.yaml +1 -0
  5. pythinker_code/agents/default/explore.yaml +1 -0
  6. pythinker_code/agents/default/implementer.yaml +1 -0
  7. pythinker_code/agents/default/plan.yaml +1 -0
  8. pythinker_code/agents/default/review.yaml +1 -0
  9. pythinker_code/agents/default/verifier.yaml +1 -0
  10. pythinker_code/app.py +18 -10
  11. pythinker_code/background/manager.py +22 -4
  12. pythinker_code/background/models.py +14 -1
  13. pythinker_code/background/store.py +7 -1
  14. pythinker_code/config.py +30 -2
  15. pythinker_code/llm.py +3 -1
  16. pythinker_code/plugin/manager.py +19 -6
  17. pythinker_code/soul/agent.py +1 -1
  18. pythinker_code/soul/permission.py +341 -0
  19. pythinker_code/soul/pythinkersoul.py +104 -65
  20. pythinker_code/soul/toolset.py +35 -31
  21. pythinker_code/subagents/builder.py +1 -0
  22. pythinker_code/subagents/models.py +2 -0
  23. pythinker_code/subagents/runner.py +16 -8
  24. pythinker_code/subagents/store.py +4 -0
  25. pythinker_code/telemetry/config.py +27 -4
  26. pythinker_code/telemetry/crash.py +15 -2
  27. pythinker_code/telemetry/otel.py +2 -1
  28. pythinker_code/telemetry/sentry.py +46 -1
  29. pythinker_code/tools/agent/__init__.py +47 -1
  30. pythinker_code/tools/file/__init__.py +2 -1
  31. pythinker_code/tools/file/grep_local.py +103 -1
  32. pythinker_code/tools/file/replace.py +6 -0
  33. pythinker_code/tools/file/write.py +6 -0
  34. pythinker_code/tools/plan/__init__.py +32 -0
  35. pythinker_code/tools/plan/handoff.py +69 -0
  36. pythinker_code/tools/shell/__init__.py +4 -0
  37. pythinker_code/tools/web/fetch.py +74 -2
  38. pythinker_code/ui/shell/__init__.py +8 -0
  39. pythinker_code/ui/shell/prompt.py +27 -0
  40. pythinker_code/ui/shell/slash.py +49 -32
  41. pythinker_code/ui/shell/update.py +116 -2
  42. pythinker_code/vis/api/sessions.py +9 -6
  43. pythinker_code/vis/app.py +26 -2
  44. pythinker_code/vis/static/assets/{highlighted-body-B3W2YXNL-D2MTYyJz.js → highlighted-body-B3W2YXNL-CY1rtwrX.js} +1 -1
  45. pythinker_code/vis/static/assets/{index-CezafTt_.js → index-DgmTI2M_.js} +70 -70
  46. pythinker_code/vis/static/index.html +1 -1
  47. pythinker_code/web/api/open_in.py +34 -19
  48. pythinker_code/web/api/sessions.py +18 -13
  49. pythinker_code/web/app.py +2 -4
  50. pythinker_code/web/static/assets/{_baseUniq-CnjLtNBK.js → _baseUniq-xopHfKWX.js} +1 -1
  51. pythinker_code/web/static/assets/{arc-p8Zl45yf.js → arc-BBJ7V9Gp.js} +1 -1
  52. pythinker_code/web/static/assets/{architectureDiagram-VXUJARFQ-Lxm5mR82.js → architectureDiagram-VXUJARFQ-DwGo6qSm.js} +1 -1
  53. pythinker_code/web/static/assets/{blockDiagram-VD42YOAC-B7qw0bmu.js → blockDiagram-VD42YOAC-B96wqtrk.js} +1 -1
  54. pythinker_code/web/static/assets/{c4Diagram-YG6GDRKO-CdgJaayE.js → c4Diagram-YG6GDRKO-BAv4oGOt.js} +1 -1
  55. pythinker_code/web/static/assets/channel-BMhsY12h.js +1 -0
  56. pythinker_code/web/static/assets/{chunk-4BX2VUAB-DNr_hwHQ.js → chunk-4BX2VUAB-xvC1-2wn.js} +1 -1
  57. pythinker_code/web/static/assets/{chunk-55IACEB6-BWRJeLuP.js → chunk-55IACEB6-Ci3UbC9h.js} +1 -1
  58. pythinker_code/web/static/assets/{chunk-B4BG7PRW-BRveHO02.js → chunk-B4BG7PRW-YlIqOoyf.js} +1 -1
  59. pythinker_code/web/static/assets/{chunk-DI55MBZ5-CC8092Ai.js → chunk-DI55MBZ5-CiKvkLSV.js} +1 -1
  60. pythinker_code/web/static/assets/{chunk-FMBD7UC4-BWlTTg4C.js → chunk-FMBD7UC4-6L3DZcgX.js} +1 -1
  61. pythinker_code/web/static/assets/{chunk-QN33PNHL-CQB7XXqV.js → chunk-QN33PNHL--uAxWJyr.js} +1 -1
  62. pythinker_code/web/static/assets/{chunk-QZHKN3VN-DR05TXzx.js → chunk-QZHKN3VN-BgsfDfGl.js} +1 -1
  63. pythinker_code/web/static/assets/{chunk-TZMSLE5B-BooWpSCF.js → chunk-TZMSLE5B-CmVX5blB.js} +1 -1
  64. pythinker_code/web/static/assets/classDiagram-2ON5EDUG-xLNcUMaH.js +1 -0
  65. pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-xLNcUMaH.js +1 -0
  66. pythinker_code/web/static/assets/clone-D4ji5nla.js +1 -0
  67. pythinker_code/web/static/assets/{code-block-IT6T5CEO-C0nbBxoU.js → code-block-IT6T5CEO-DOvUTd9H.js} +1 -1
  68. pythinker_code/web/static/assets/{cose-bilkent-S5V4N54A---Tl6BDU.js → cose-bilkent-S5V4N54A-CnxamR9p.js} +1 -1
  69. pythinker_code/web/static/assets/{cytoscape.esm-Dlvswyl5.js → cytoscape.esm-JliMJb2r.js} +1 -1
  70. pythinker_code/web/static/assets/{dagre-6UL2VRFP-CXURVXMQ.js → dagre-6UL2VRFP-CZPQL3_X.js} +1 -1
  71. pythinker_code/web/static/assets/{diagram-PSM6KHXK-DqPWQvWf.js → diagram-PSM6KHXK-BdkuXkbh.js} +1 -1
  72. pythinker_code/web/static/assets/{diagram-QEK2KX5R-XG4wk_zx.js → diagram-QEK2KX5R-B_gzBcDN.js} +1 -1
  73. pythinker_code/web/static/assets/{diagram-S2PKOQOG-sNvTPY2M.js → diagram-S2PKOQOG-C9KMyyaT.js} +1 -1
  74. pythinker_code/web/static/assets/{erDiagram-Q2GNP2WA-BOJaQQQU.js → erDiagram-Q2GNP2WA-BMG5oLO5.js} +1 -1
  75. pythinker_code/web/static/assets/{flowDiagram-NV44I4VS-CbfuToSV.js → flowDiagram-NV44I4VS-ByMcd6Qs.js} +1 -1
  76. pythinker_code/web/static/assets/{ganttDiagram-JELNMOA3-ulVOoUco.js → ganttDiagram-JELNMOA3-uIly3nqs.js} +1 -1
  77. pythinker_code/web/static/assets/{gitGraphDiagram-NY62KEGX-D2qIB9v4.js → gitGraphDiagram-NY62KEGX-DMlBqiTw.js} +1 -1
  78. pythinker_code/web/static/assets/{graph-CMrFXUW3.js → graph-B2e5iBNo.js} +1 -1
  79. pythinker_code/web/static/assets/{index-nZJqxMTn.js → index-ChRG_K1c.js} +1 -1
  80. pythinker_code/web/static/assets/{index-BORZhTVE.js → index-DgWvtkAz.js} +2 -2
  81. pythinker_code/web/static/assets/{index-Cw0e9z0j.js → index-XVss4lOI.js} +1 -1
  82. pythinker_code/web/static/assets/{infoDiagram-WHAUD3N6-B-DtK8KA.js → infoDiagram-WHAUD3N6-CihjcIPb.js} +1 -1
  83. pythinker_code/web/static/assets/{journeyDiagram-XKPGCS4Q-CVpxG_1t.js → journeyDiagram-XKPGCS4Q-D8UHxaOf.js} +1 -1
  84. pythinker_code/web/static/assets/{kanban-definition-3W4ZIXB7-DsoNxPLk.js → kanban-definition-3W4ZIXB7-DMJJ5np4.js} +1 -1
  85. pythinker_code/web/static/assets/{layout-C-IPsObI.js → layout-DCD-kyE5.js} +1 -1
  86. pythinker_code/web/static/assets/{linear-BoIapCU_.js → linear-D32cHhqF.js} +1 -1
  87. pythinker_code/web/static/assets/{mermaid-VLURNSYL-BNd1nBm2.js → mermaid-VLURNSYL-BdQ-I_9q.js} +7 -7
  88. pythinker_code/web/static/assets/{mermaid.core-CiwyVvHW.js → mermaid.core-TDrBZ18i.js} +5 -5
  89. pythinker_code/web/static/assets/{min-BCUh9ALv.js → min-BLYTe2ei.js} +1 -1
  90. pythinker_code/web/static/assets/{mindmap-definition-VGOIOE7T-CjwMDCnL.js → mindmap-definition-VGOIOE7T-D4dVU6Wl.js} +1 -1
  91. pythinker_code/web/static/assets/{pieDiagram-ADFJNKIX-fDnq6EaG.js → pieDiagram-ADFJNKIX-BPhhXGkK.js} +1 -1
  92. pythinker_code/web/static/assets/{quadrantDiagram-AYHSOK5B-DyHz9xJE.js → quadrantDiagram-AYHSOK5B-Bw33P7le.js} +1 -1
  93. pythinker_code/web/static/assets/{requirementDiagram-UZGBJVZJ-CtnhMmz_.js → requirementDiagram-UZGBJVZJ-BJhppvmZ.js} +1 -1
  94. pythinker_code/web/static/assets/{sankeyDiagram-TZEHDZUN-DBjlbTUH.js → sankeyDiagram-TZEHDZUN-B2QsrhB7.js} +1 -1
  95. pythinker_code/web/static/assets/{sequenceDiagram-WL72ISMW-B8vAgUxw.js → sequenceDiagram-WL72ISMW-D-zE9VAU.js} +1 -1
  96. pythinker_code/web/static/assets/{stateDiagram-FKZM4ZOC-CKckm--Z.js → stateDiagram-FKZM4ZOC-fXBn8fR5.js} +1 -1
  97. pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-Uh1qibgy.js +1 -0
  98. pythinker_code/web/static/assets/{timeline-definition-IT6M3QCI-ByteGYaR.js → timeline-definition-IT6M3QCI-QJcB1GYA.js} +1 -1
  99. pythinker_code/web/static/assets/{treemap-KMMF4GRG-CBb0li3c.js → treemap-KMMF4GRG-BIsZeCJI.js} +1 -1
  100. pythinker_code/web/static/assets/{xychartDiagram-PRI3JC2R-E_ri2DuP.js → xychartDiagram-PRI3JC2R-D23m9ArK.js} +1 -1
  101. pythinker_code/web/static/index.html +1 -1
  102. pythinker_code/wire/server.py +16 -13
  103. {pythinker_code-2.4.0.dist-info → pythinker_code-2.6.0.dist-info}/METADATA +38 -2
  104. {pythinker_code-2.4.0.dist-info → pythinker_code-2.6.0.dist-info}/RECORD +108 -106
  105. pythinker_code/web/static/assets/channel-BZBK5lN6.js +0 -1
  106. pythinker_code/web/static/assets/classDiagram-2ON5EDUG-BpVZZpbi.js +0 -1
  107. pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-BpVZZpbi.js +0 -1
  108. pythinker_code/web/static/assets/clone-C-R24ClB.js +0 -1
  109. pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-DFvB4bvk.js +0 -1
  110. {pythinker_code-2.4.0.dist-info → pythinker_code-2.6.0.dist-info}/WHEEL +0 -0
  111. {pythinker_code-2.4.0.dist-info → pythinker_code-2.6.0.dist-info}/entry_points.txt +0 -0
  112. {pythinker_code-2.4.0.dist-info → pythinker_code-2.6.0.dist-info}/licenses/LICENSE +0 -0
  113. {pythinker_code-2.4.0.dist-info → pythinker_code-2.6.0.dist-info}/licenses/NOTICE +0 -0
@@ -2,6 +2,86 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.6.0 (2026-05-13)
6
+
7
+ Packaging fix: pin `pythinker-core[contrib]==1.1.0` so the Kimi K2.x / DeepSeek strict-interleaved reasoning-replay fix reaches PyPI installs.
8
+
9
+ ### Why 2.6.0 lands the same day as 2.5.0
10
+
11
+ The runtime fix for Moonshot's `thinking is enabled but reasoning_content is missing in assistant tool call message at index N` rejection landed in the `pythinker-core` source tree on 2026-05-11 (released in `pythinker-code` 2.4.0 source), but **the published `pythinker-core==1.0.0` on PyPI predates that change** (uploaded 2026-05-07). `pythinker-code` 2.4.0 and 2.5.0 both pinned `pythinker-core[contrib]==1.0.0`, so PyPI users on Kimi K2.5 / K2.6 / DeepSeek through OpenCode Go (and elsewhere) kept hitting the bug even on the latest CLI. Reported in [#37](https://github.com/mohamed-elkholy95/Pythinker-Code/issues/37).
12
+
13
+ ### The fix
14
+
15
+ - **`pythinker-core` bumped to 1.1.0** (published independently on PyPI) — carries the strict-interleaved replay logic that emits `reasoning_content` on every assistant turn for `kimi-k2*` / `deepseek*` models, falling back to `extract_text()` then `"[reasoning unavailable]"` when no `ThinkPart` was captured.
16
+ - **Root pin updated to `pythinker-core[contrib]==1.1.0`** in `pyproject.toml`. New installs of `pip install --upgrade pythinker-code==2.6.0` (and `uv tool install`/`upgrade`) get the fix automatically.
17
+ - See `packages/pythinker-core/CHANGELOG.md` for the full pythinker-core 1.1.0 entry.
18
+
19
+ ### No app-level behavior changes vs 2.5.0
20
+
21
+ Everything that landed in 2.5.0 is still there. The 2.6.0 diff is exclusively the version bump and the upstream-dep pin; existing 2.5.0 users **must** upgrade to 2.6.0 if they use Kimi K2.x, DeepSeek, or any other strict-interleaved-thinking model through an OpenAI-compatible provider.
22
+
23
+ Upgrade with `pythinker update` or `pip install --upgrade pythinker-code==2.6.0`.
24
+
25
+ ## 2.5.0 (2026-05-13)
26
+
27
+ bk_box_main coding-agent runtime port, Windows self-upgrade fix, FetchURL SSRF hardening, and a broad reliability/security pass.
28
+
29
+ ### Subagent runtime & permissions
30
+
31
+ - Runtime-enforced permission profiles for every built-in role: **read-only**, **plan**, **ask**, **implement**, **review**, **verify**. Profiles are snapshot per LLM step in the new `src/pythinker_code/soul/permission.py` so a mid-step model switch can't escalate. Plan mode now **hard-denies** non-plan writes and dangerous shell mutations instead of relying on prompt-deny.
32
+ - New plan-handoff workflow in `src/pythinker_code/tools/plan/handoff.py` with dynamic injection through `soul/dynamic_injections/plan_mode.py`. Smooth handoff from `plan` → `implement` without re-priming the context.
33
+ - New smart-search grep variant; new subagent metadata plumbing (`subagents/models.py`, `subagents/store.py`, `subagents/builder.py`, `subagents/runner.py`).
34
+
35
+ ### Background tasks
36
+
37
+ - Recovery distinguishes **`recoverable`** (resumable via a stored `agent_id`) from **`lost`** (worker is gone with no resume target). Agent instances are parked as `idle` rather than failed when the underlying task is recoverable.
38
+ - Guards against overwriting terminal task states; subagent races on instance transitions closed.
39
+ - `pythinker-host`: subprocess teardown now kills the **entire child process tree** and creates a new session group, so background workers can no longer survive their parent on Linux/macOS.
40
+
41
+ ### FetchURL — SSRF + resource-exhaustion hardening
42
+
43
+ - `pythinker_code.tools.web.fetch._validate_fetch_url` blocks **private, loopback, link-local, multicast, and reserved** IPv4/IPv6 ranges; rejects non-`http`/`https` schemes and host-less URLs up front.
44
+ - Responses are streamed with a hard **5 MB** ceiling (`_read_limited`) honoring `Content-Length`. Both the direct path and the configured fetch-service path enforce the same caps.
45
+
46
+ ### Web / vis surface
47
+
48
+ - Upload limits, open-in path escaping, and vis auth all hardened (`src/pythinker_code/web/`, `src/pythinker_code/vis/`, `vis/src/lib/api.ts`).
49
+
50
+ ### Plugin
51
+
52
+ - Plugin definitions no longer persist host credentials. Plugin **name validation** tightened to reject path-traversal and shell-meta characters.
53
+
54
+ ### Telemetry & observability
55
+
56
+ - OTel `service.name` normalized to a stable value, decoupled from the configured display name, so SigNoz dashboards keep working across rebrands.
57
+ - Sentry filters drop test-process noise and benign shutdown errors; `pythinker_code/telemetry/config.py` and `pythinker_code/telemetry/crash.py` updated accordingly.
58
+ - New `tests/telemetry/test_otel_resource.py` asserts the resource identity used by the dashboards.
59
+
60
+ ### Windows
61
+
62
+ - `pythinker update` on Windows now spawns the upgrade in a **detached console** and exits the parent process before `uv tool upgrade` runs, releasing the lock on the running `pythinker.exe`. Fixes the `os error 32: The process cannot access the file because it is being used by another process` error that blocked self-upgrade.
63
+ - New CI matrix entry on **`windows-2025-vs2026`** (experimental, non-blocking) for the pythinker-host and pythinker-cli build, validating Visual Studio 2026 / MSVC v144 forward-compat before GitHub eventually deprecates `windows-2022`.
64
+
65
+ ### Feedback
66
+
67
+ - New `feedback` config block: `endpoint_url`, `api_key`, `custom_headers`. The `/feedback` slash command now routes user submissions to a user-configured HTTP endpoint instead of being a no-op.
68
+
69
+ ### UI
70
+
71
+ - Pythinker version is shown on the welcome screen.
72
+
73
+ ### CI
74
+
75
+ - Pre-push hooks mirror CI's `check` target (`ruff format --check`, `ruff check`, `pyright`) so local pushes catch the same regressions CI does.
76
+ - README + CHANGELOG release-validate gate hardened; the GitHub Release publish step is now resilient to transient upstream failures.
77
+ - Spell-check vocabulary fix in `soul/permission.py` for an internal error string the typos crate flagged; experimental `windows-2025-vs2026` build no longer collides with `windows-2022` on the shared `pythinker-x86_64-pc-windows-msvc` artifact name.
78
+
79
+ ### Compatibility
80
+
81
+ - `pythinker_core.contrib.chat_provider.anthropic`: handle the six new tool-result block types added by anthropic SDK 0.101 (`web_fetch_tool_result`, `code_execution_tool_result`, `bash_code_execution_tool_result`, `text_editor_code_execution_tool_result`, `tool_search_tool_result`, `container_upload`). pyright is exhaustive again.
82
+
83
+ Upgrade with `pythinker update` or `pip install --upgrade pythinker-code==2.5.0`.
84
+
5
85
  ## 2.4.0 (2026-05-11)
6
86
 
7
87
  Subagent roles overhaul, Moonshot/Kimi K2 provider support, and a ripgrep-free Grep fallback.
@@ -8,6 +8,7 @@ from pythinker_host.local import local_host
8
8
 
9
9
  from pythinker_code.soul.agent import Runtime
10
10
  from pythinker_code.soul.approval import Approval
11
+ from pythinker_code.soul.permission import check_shell_command_allowed
11
12
  from pythinker_code.soul.toolset import PythinkerToolset
12
13
  from pythinker_code.tools.shell import Params as ShellParams
13
14
  from pythinker_code.tools.shell import Shell
@@ -35,6 +36,7 @@ def replace_tools(
35
36
  acp_conn,
36
37
  acp_session_id,
37
38
  runtime.approval,
39
+ runtime,
38
40
  )
39
41
  )
40
42
 
@@ -52,6 +54,7 @@ class Terminal(CallableTool2[ShellParams]):
52
54
  acp_conn: acp.Client,
53
55
  acp_session_id: str,
54
56
  approval: Approval,
57
+ runtime: Runtime,
55
58
  ) -> None:
56
59
  # Use the `name`, `description`, and `params` from the existing Shell tool,
57
60
  # so that when this is added to the toolset, it replaces the original Shell tool.
@@ -59,6 +62,7 @@ class Terminal(CallableTool2[ShellParams]):
59
62
  self._acp_conn = acp_conn
60
63
  self._acp_session_id = acp_session_id
61
64
  self._approval = approval
65
+ self._runtime = runtime
62
66
 
63
67
  async def __call__(self, params: ShellParams) -> ToolReturnValue:
64
68
  from pythinker_code.acp.session import get_current_acp_tool_call_id_or_none
@@ -71,6 +75,9 @@ class Terminal(CallableTool2[ShellParams]):
71
75
  if not params.command:
72
76
  return builder.error("Command cannot be empty.", brief="Empty command")
73
77
 
78
+ if err := check_shell_command_allowed(self._runtime, params.command):
79
+ return err
80
+
74
81
  approval_result = await self._approval.request(
75
82
  self.name,
76
83
  "run shell command",
@@ -18,6 +18,7 @@ agent:
18
18
  - "pythinker_code.tools.file:ReadMediaFile"
19
19
  - "pythinker_code.tools.file:Glob"
20
20
  - "pythinker_code.tools.file:Grep"
21
+ - "pythinker_code.tools.file:SmartSearch"
21
22
  - "pythinker_code.tools.file:WriteFile"
22
23
  - "pythinker_code.tools.file:StrReplaceFile"
23
24
  - "pythinker_code.tools.web:SearchWeb"
@@ -28,6 +28,7 @@ agent:
28
28
  - "pythinker_code.tools.file:ReadMediaFile"
29
29
  - "pythinker_code.tools.file:Glob"
30
30
  - "pythinker_code.tools.file:Grep"
31
+ - "pythinker_code.tools.file:SmartSearch"
31
32
  - "pythinker_code.tools.file:WriteFile"
32
33
  - "pythinker_code.tools.file:StrReplaceFile"
33
34
  - "pythinker_code.tools.web:SearchWeb"
@@ -45,6 +45,7 @@ agent:
45
45
  - "pythinker_code.tools.file:ReadMediaFile"
46
46
  - "pythinker_code.tools.file:Glob"
47
47
  - "pythinker_code.tools.file:Grep"
48
+ - "pythinker_code.tools.file:SmartSearch"
48
49
  - "pythinker_code.tools.web:SearchWeb"
49
50
  - "pythinker_code.tools.web:FetchURL"
50
51
  exclude_tools:
@@ -32,6 +32,7 @@ agent:
32
32
  - "pythinker_code.tools.file:ReadMediaFile"
33
33
  - "pythinker_code.tools.file:Glob"
34
34
  - "pythinker_code.tools.file:Grep"
35
+ - "pythinker_code.tools.file:SmartSearch"
35
36
  - "pythinker_code.tools.file:WriteFile"
36
37
  - "pythinker_code.tools.file:StrReplaceFile"
37
38
  - "pythinker_code.tools.web:SearchWeb"
@@ -27,6 +27,7 @@ agent:
27
27
  - "pythinker_code.tools.file:ReadMediaFile"
28
28
  - "pythinker_code.tools.file:Glob"
29
29
  - "pythinker_code.tools.file:Grep"
30
+ - "pythinker_code.tools.file:SmartSearch"
30
31
  - "pythinker_code.tools.web:SearchWeb"
31
32
  - "pythinker_code.tools.web:FetchURL"
32
33
  exclude_tools:
@@ -33,6 +33,7 @@ agent:
33
33
  - "pythinker_code.tools.file:ReadMediaFile"
34
34
  - "pythinker_code.tools.file:Glob"
35
35
  - "pythinker_code.tools.file:Grep"
36
+ - "pythinker_code.tools.file:SmartSearch"
36
37
  - "pythinker_code.tools.web:SearchWeb"
37
38
  - "pythinker_code.tools.web:FetchURL"
38
39
  exclude_tools:
@@ -32,6 +32,7 @@ agent:
32
32
  - "pythinker_code.tools.file:ReadMediaFile"
33
33
  - "pythinker_code.tools.file:Glob"
34
34
  - "pythinker_code.tools.file:Grep"
35
+ - "pythinker_code.tools.file:SmartSearch"
35
36
  exclude_tools:
36
37
  - "pythinker_code.tools.agent:Agent"
37
38
  - "pythinker_code.tools.ask_user:AskUserQuestion"
pythinker_code/app.py CHANGED
@@ -38,6 +38,9 @@ if TYPE_CHECKING:
38
38
  from fastmcp.mcp_config import MCPConfig
39
39
 
40
40
 
41
+ _CWD_LOCK = asyncio.Lock()
42
+
43
+
41
44
  def _patch_session_id(record: dict[str, Any]) -> None:
42
45
  """Inject the current session ID (from ContextVar) into log records."""
43
46
  try:
@@ -522,15 +525,16 @@ class PythinkerCLI:
522
525
 
523
526
  @contextlib.asynccontextmanager
524
527
  async def _env(self) -> AsyncGenerator[None]:
525
- original_cwd = HostPath.cwd()
526
- await pythinker_host.chdir(self._runtime.session.work_dir)
527
- try:
528
- # to ignore possible warnings from dateparser
529
- warnings.filterwarnings("ignore", category=DeprecationWarning)
530
- async with self._runtime.oauth.refreshing(self._runtime):
531
- yield
532
- finally:
533
- await pythinker_host.chdir(original_cwd)
528
+ async with _CWD_LOCK:
529
+ original_cwd = HostPath.cwd()
530
+ await pythinker_host.chdir(self._runtime.session.work_dir)
531
+ try:
532
+ # to ignore possible warnings from dateparser
533
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
534
+ async with self._runtime.oauth.refreshing(self._runtime):
535
+ yield
536
+ finally:
537
+ await pythinker_host.chdir(original_cwd)
534
538
 
535
539
  async def run(
536
540
  self,
@@ -703,9 +707,13 @@ class PythinkerCLI:
703
707
  from pythinker_code.ui.shell import Shell, WelcomeInfoItem
704
708
 
705
709
  if command is None:
706
- from pythinker_code.ui.shell.update import print_update_banner
710
+ from pythinker_code.ui.shell.update import (
711
+ print_update_banner,
712
+ schedule_auto_update_check,
713
+ )
707
714
 
708
715
  print_update_banner()
716
+ schedule_auto_update_check()
709
717
 
710
718
  welcome_info = [
711
719
  WelcomeInfoItem(
@@ -215,6 +215,9 @@ class BackgroundTaskManager:
215
215
  model_override: str | None,
216
216
  timeout_s: int | None = None,
217
217
  resumed: bool = False,
218
+ dependencies: list[str] | None = None,
219
+ budget_seconds: int | None = None,
220
+ isolation: str | None = None,
218
221
  ) -> TaskView:
219
222
  from .agent_runner import BackgroundAgentRunner
220
223
 
@@ -244,12 +247,19 @@ class BackgroundTaskManager:
244
247
  # an explicit per-agent timeout instead of always falling back to
245
248
  # ``config.background.agent_task_timeout_s``.
246
249
  timeout_s=effective_timeout,
250
+ dependencies=list(dependencies or ()),
251
+ budget_seconds=budget_seconds,
252
+ synthesis_state="pending",
253
+ isolation=isolation,
247
254
  kind_payload={
248
255
  "agent_id": agent_id,
249
256
  "subagent_type": subagent_type,
250
257
  "prompt": prompt,
251
258
  "model_override": model_override,
252
259
  "launch_mode": "background",
260
+ "dependencies": list(dependencies or ()),
261
+ "budget_seconds": budget_seconds,
262
+ "isolation": isolation,
253
263
  },
254
264
  )
255
265
  self._store.create_task(spec)
@@ -427,10 +437,15 @@ class BackgroundTaskManager:
427
437
  runtime = view.runtime.model_copy()
428
438
  runtime.finished_at = now
429
439
  runtime.updated_at = now
430
- runtime.status = "lost"
431
- runtime.failure_reason = "In-process background agent is no longer running"
432
- self._store.write_runtime(view.spec.id, runtime)
433
440
  agent_id = (view.spec.kind_payload or {}).get("agent_id")
441
+ runtime.status = "recoverable" if isinstance(agent_id, str) else "lost"
442
+ runtime.failure_reason = (
443
+ "In-process background agent is no longer running; resume the stored agent "
444
+ f"instance {agent_id} to continue."
445
+ if isinstance(agent_id, str)
446
+ else "In-process background agent is no longer running"
447
+ )
448
+ self._store.write_runtime(view.spec.id, runtime)
434
449
  if (
435
450
  isinstance(agent_id, str)
436
451
  and self._runtime is not None
@@ -438,7 +453,7 @@ class BackgroundTaskManager:
438
453
  ):
439
454
  record = self._runtime.subagent_store.get_instance(agent_id)
440
455
  if record is not None and record.status == "running_background":
441
- self._runtime.subagent_store.update_instance(agent_id, status="failed")
456
+ self._runtime.subagent_store.update_instance(agent_id, status="idle")
442
457
  continue
443
458
  last_progress_at = (
444
459
  view.runtime.heartbeat_at
@@ -506,6 +521,9 @@ class BackgroundTaskManager:
506
521
  case "lost":
507
522
  severity = "warning"
508
523
  title = f"Background task lost: {view.spec.description}"
524
+ case "recoverable":
525
+ severity = "warning"
526
+ title = f"Background task recoverable: {view.spec.description}"
509
527
  case _:
510
528
  severity = "info"
511
529
  title = f"Background task updated: {view.spec.description}"
@@ -15,10 +15,17 @@ type TaskStatus = Literal[
15
15
  "failed",
16
16
  "killed",
17
17
  "lost",
18
+ "recoverable",
18
19
  ]
19
20
  type TaskOwnerRole = Literal["root", "subagent"]
20
21
 
21
- TERMINAL_TASK_STATUSES: tuple[TaskStatus, ...] = ("completed", "failed", "killed", "lost")
22
+ TERMINAL_TASK_STATUSES: tuple[TaskStatus, ...] = (
23
+ "completed",
24
+ "failed",
25
+ "killed",
26
+ "lost",
27
+ "recoverable",
28
+ )
22
29
 
23
30
 
24
31
  def is_terminal_status(status: TaskStatus) -> bool:
@@ -50,6 +57,12 @@ class TaskSpec(BaseModel):
50
57
  shell_path: str | None = None
51
58
  cwd: str | None = None
52
59
  timeout_s: int | None = None
60
+ parent_task_id: str | None = None
61
+ child_task_ids: list[str] = Field(default_factory=list)
62
+ dependencies: list[str] = Field(default_factory=list)
63
+ budget_seconds: int | None = None
64
+ synthesis_state: str | None = None
65
+ isolation: str | None = None
53
66
  kind_payload: dict[str, Any] | None = None
54
67
 
55
68
 
@@ -17,6 +17,7 @@ from .models import (
17
17
  TaskSpec,
18
18
  TaskStatus,
19
19
  TaskView,
20
+ is_terminal_status,
20
21
  )
21
22
 
22
23
  _VALID_TASK_ID = re.compile(r"^[a-z0-9][a-z0-9\-]{1,24}$")
@@ -101,7 +102,12 @@ class BackgroundTaskStore:
101
102
  return TaskSpec.model_validate_json(self.spec_path(task_id).read_text(encoding="utf-8"))
102
103
 
103
104
  def write_runtime(self, task_id: str, runtime: TaskRuntime) -> None:
104
- atomic_json_write(runtime.model_dump(mode="json"), self.runtime_path(task_id))
105
+ path = self.runtime_path(task_id)
106
+ if path.exists():
107
+ current = self.read_runtime(task_id)
108
+ if is_terminal_status(current.status) and not is_terminal_status(runtime.status):
109
+ return
110
+ atomic_json_write(runtime.model_dump(mode="json"), path)
105
111
 
106
112
  def read_runtime(self, task_id: str) -> TaskRuntime:
107
113
  path = self.runtime_path(task_id)
pythinker_code/config.py CHANGED
@@ -168,6 +168,30 @@ class Services(BaseModel):
168
168
  """Pythinker AI Fetch configuration."""
169
169
 
170
170
 
171
+ class FeedbackConfig(BaseModel):
172
+ """User-submitted feedback endpoint configuration."""
173
+
174
+ endpoint_url: str = Field(
175
+ default="",
176
+ description=(
177
+ "Full URL for the /feedback slash command. Overrides the built-in "
178
+ "Pythinker platform endpoint when set."
179
+ ),
180
+ )
181
+ api_key: SecretStr | None = Field(
182
+ default=None,
183
+ description="Optional bearer token for the feedback endpoint.",
184
+ )
185
+ custom_headers: dict[str, str] | None = Field(
186
+ default=None,
187
+ description="Optional extra headers for feedback endpoint requests.",
188
+ )
189
+
190
+ @field_serializer("api_key", when_used="json")
191
+ def dump_secret(self, v: SecretStr | None):
192
+ return v.get_secret_value() if v is not None else None
193
+
194
+
171
195
  class MCPClientConfig(BaseModel):
172
196
  """MCP client configuration."""
173
197
 
@@ -251,6 +275,10 @@ class Config(BaseModel):
251
275
  default_factory=NotificationConfig, description="Notification configuration"
252
276
  )
253
277
  services: Services = Field(default_factory=Services, description="Services configuration")
278
+ feedback: FeedbackConfig = Field(
279
+ default_factory=FeedbackConfig,
280
+ description="User-submitted feedback endpoint configuration",
281
+ )
254
282
  mcp: MCPConfig = Field(default_factory=MCPConfig, description="MCP configuration")
255
283
  tui: TUIConfig = Field(default_factory=TUIConfig, description="TUI rendering configuration")
256
284
  hooks: list[HookDef] = Field(default_factory=list, description="Hook definitions") # pyright: ignore[reportUnknownVariableType]
@@ -274,9 +302,9 @@ class Config(BaseModel):
274
302
  ),
275
303
  )
276
304
  telemetry: bool = Field(
277
- default=False,
305
+ default=True,
278
306
  description=(
279
- "Enable anonymous telemetry to help improve pythinker-code. Set to true to enable."
307
+ "Enable anonymous telemetry to help improve pythinker-code. Set to false to opt out."
280
308
  ),
281
309
  )
282
310
 
pythinker_code/llm.py CHANGED
@@ -371,6 +371,7 @@ def clone_llm_with_model_alias(
371
371
  *,
372
372
  session_id: str,
373
373
  oauth: OAuthManager | None,
374
+ thinking: bool | None = None,
374
375
  ) -> LLM | None:
375
376
  if model_alias is None:
376
377
  return llm
@@ -378,7 +379,8 @@ def clone_llm_with_model_alias(
378
379
  raise KeyError(f"Unknown model alias: {model_alias}")
379
380
  model = config.models[model_alias]
380
381
  provider = config.providers[model.provider]
381
- thinking: bool | None = llm.thinking if llm is not None else None
382
+ if thinking is None and llm is not None:
383
+ thinking = llm.thinking
382
384
  if thinking is None and llm is not None:
383
385
  effort = getattr(llm.chat_provider, "thinking_effort", None)
384
386
  if effort is not None:
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  import shutil
6
7
  import tempfile
7
8
  from pathlib import Path
@@ -12,7 +13,6 @@ from pythinker_code.plugin import (
12
13
  PluginError,
13
14
  PluginRuntime,
14
15
  PluginSpec,
15
- inject_config,
16
16
  parse_plugin_json,
17
17
  write_runtime,
18
18
  )
@@ -51,14 +51,26 @@ def collect_host_values(config: Config, oauth: OAuthManager) -> dict[str, str]:
51
51
  return values
52
52
 
53
53
 
54
+ _PLUGIN_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
55
+
56
+
54
57
  def _validate_name(name: str, plugins_dir: Path) -> Path:
55
58
  """Resolve and validate plugin name, returning the safe destination path."""
59
+ if not _PLUGIN_NAME_RE.fullmatch(name):
60
+ raise PluginError(f"Invalid plugin name: {name}")
56
61
  dest = (plugins_dir / name).resolve()
57
- if not dest.is_relative_to(plugins_dir.resolve()):
62
+ plugins_root = plugins_dir.resolve()
63
+ if dest == plugins_root or not dest.is_relative_to(plugins_root):
58
64
  raise PluginError(f"Invalid plugin name: {name}")
59
65
  return dest
60
66
 
61
67
 
68
+ def _validate_inject_values(spec: PluginSpec, host_values: dict[str, str]) -> None:
69
+ for source_key in spec.inject.values():
70
+ if source_key not in host_values:
71
+ raise PluginError(f"Host does not provide required inject key '{source_key}'")
72
+
73
+
62
74
  def install_plugin(
63
75
  *,
64
76
  source: Path,
@@ -87,8 +99,9 @@ def install_plugin(
87
99
  staging_plugin = staging / spec.name
88
100
  shutil.copytree(source, staging_plugin)
89
101
 
90
- # Apply inject + runtime on the staged copy
91
- inject_config(staging_plugin, spec, host_values)
102
+ # Validate required host values, but do not persist credentials into plugin files.
103
+ # Plugin tools receive fresh credentials through environment variables at runtime.
104
+ _validate_inject_values(spec, host_values)
92
105
  runtime = PluginRuntime(host=host_name, host_version=host_version)
93
106
  write_runtime(staging_plugin, runtime)
94
107
 
@@ -123,8 +136,8 @@ def refresh_plugin_configs(plugins_dir: Path, host_values: dict[str, str]) -> No
123
136
  continue
124
137
  try:
125
138
  spec = parse_plugin_json(plugin_json)
126
- if spec.inject and spec.config_file:
127
- inject_config(child, spec, host_values)
139
+ if spec.inject:
140
+ _validate_inject_values(spec, host_values)
128
141
  except Exception:
129
142
  continue
130
143
 
@@ -439,7 +439,7 @@ async def load_agent(
439
439
  )
440
440
  )
441
441
 
442
- toolset = PythinkerToolset()
442
+ toolset = PythinkerToolset(runtime)
443
443
  tool_deps = {
444
444
  PythinkerToolset: toolset,
445
445
  Runtime: runtime,