Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
254002c
docs(tasks): add verified reference-adoption catalog
elkaix Jun 14, 2026
06e0aa4
feat(cli): add read-only `system-prompt` command to inspect the assem…
elkaix Jun 14, 2026
d42431b
fix(shell): interpolate timeout caps into the tool description from t…
elkaix Jun 14, 2026
3ad07e0
feat(memory): warn that recalled notes are a point-in-time snapshot
elkaix Jun 14, 2026
9677071
feat(toolset): bound parallel-safe tool fan-out with a concurrency cap
elkaix Jun 14, 2026
49eb86b
feat(loop): enforce an optional per-session USD spend ceiling
elkaix Jun 14, 2026
395758e
feat(approval): always re-confirm edits to sensitive host files
elkaix Jun 14, 2026
68c60be
feat(approval): add accept-edits auto-approve tier for reversible in-…
elkaix Jun 14, 2026
fcdc99b
feat(loop): add terminal-quality predicate (observable degenerate-sto…
elkaix Jun 14, 2026
74ce744
feat(subagents): gate spawn on declarative required MCP servers
elkaix Jun 14, 2026
ef03ebf
feat(hooks): inject UserPromptSubmit additionalContext into the user …
elkaix Jun 14, 2026
84c2519
docs(tasks): record adoption-arc status + Wave 4 execution notes
elkaix Jun 14, 2026
38eb98d
feat(file): stale-overwrite guard via a per-agent file read cache
elkaix Jun 14, 2026
9546a2b
docs(tasks): record executable plan for the remaining Wave 4 items (1…
elkaix Jun 14, 2026
67fca31
feat(core): surface a truncation signal on GenerateResult
elkaix Jun 14, 2026
1af5eec
feat(loop): recover from output-token truncation with bounded continu…
elkaix Jun 14, 2026
e1be8b5
polish(subagents): harden required-MCP gate diagnostics
elkaix Jun 14, 2026
eb9773f
feat(file): extend the stale-overwrite guard to StrReplaceFile
elkaix Jun 14, 2026
7cc0f7b
docs(tasks): record Wave 4 outcomes (#11/#13 done, #12 architectural …
elkaix Jun 14, 2026
ae00162
feat(prompt): deliver merged AGENTS.md as a session-start system-remi…
elkaix Jun 14, 2026
ce4a9fe
fix(core): make output-token truncation a required StreamedMessage si…
elkaix Jun 14, 2026
7b4cf32
fix(file,mcp): harden stale-overwrite detection and the required-MCP …
elkaix Jun 14, 2026
414f8a5
feat(auth,providers): add Kimi provider and GLM-5.2 defaults, fix Min…
elkaix Jun 14, 2026
b9908ed
fix(core,file): satisfy the finish_reason contract in test doubles; s…
elkaix Jun 14, 2026
af531ea
feat(file): reject a full overwrite of a partially-read file
elkaix Jun 14, 2026
c658722
Revert "feat(file): reject a full overwrite of a partially-read file"
elkaix Jun 14, 2026
43bb3dd
feat(tui): review findings table, lighter borders, and RunAgents polish
elkaix Jun 14, 2026
1e017ec
feat(tui): standardize welcome banner colors and diff sign spacing
elkaix Jun 14, 2026
d59bedc
fix(core): make Gemini finish_reason sticky once 'length' is captured
elkaix Jun 14, 2026
82640b3
test(core): add streaming + reverse-order sticky-truncation test cove…
elkaix Jun 14, 2026
d3b808d
fix(soul): wire budget-exhausted and stuck-loop messages to shell
elkaix Jun 14, 2026
36b7655
fix(ui): sync dark-theme ptk hex values to TUI token constants
elkaix Jun 14, 2026
53a098c
fix(ui): restore preview line for non-review successful agents
elkaix Jun 14, 2026
0259c61
fix(subagents): drop non-string values from required_mcp_servers
elkaix Jun 14, 2026
8d9841c
docs(changelog): add Unreleased entries for deep-scan fixes
elkaix Jun 14, 2026
a0b8d2f
Merge remote-tracking branch 'origin/main' into major-fixes-and-enhan…
elkaix Jun 14, 2026
f6fdfc0
style(memory,tests): satisfy CodeQL implicit-concat and mixed-import …
elkaix Jun 14, 2026
459ff11
fix(config): resolve scoped config for read-only system-prompt dump
elkaix Jun 14, 2026
f538708
fix(soul): re-check spend ceiling after billable compaction
elkaix Jun 14, 2026
e97123f
fix(file): re-check staleness after approval to close TOCTOU window
elkaix Jun 14, 2026
ad278cd
style(soul,tests): address CodeRabbit type/hygiene nits
elkaix Jun 14, 2026
c408206
fix(typo): toggleable, edge_diagnostic
elkaix Jun 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,33 @@ validated, never trusted.
abstraction, custom logic where native features or existing helpers suffice, and changes a junior
maintainer would struggle to follow.

## Guardrails: pythinker-guard Skill

**REQUIRED BACKGROUND:** Before committing any changes to Pythinker code, **use the
`pythinker-guard` skill** to enforce the non-negotiable rules above against time pressure and
sunk-cost rationalization.

**When to use:** Invoke `pythinker-guard` BEFORE:
- Committing changes to Pythinker codebase
- Opening a PR
- Declaring a feature complete

**What it prevents:** The skill enforces:
- Surgical changes (no drive-by refactors, reformatting, or cleanup)
- Explicit error contracts (no bare `except`, no silent failures)
- Type safety (all new functions typed; `make check` passes)
- Test-driven development (tests written first, gate passed locally)
- Fail-closed behavior (errors distinguished and logged, never swallowed)

The skill specifically guards against 5 pressure vectors that trigger violations:
1. Time scarcity → shortcuts in testing, typing, error handling
2. Sunk-cost fallacy → skipping types/tests because "we've already built most of it"
3. Confidence illusion → "it's obvious this works" → silent errors
4. Proximity heuristic → "we're in the file anyway" → unrelated cleanup
5. Inversion of priorities → "tests slow us down" → untestable design

See the skill itself for verification checkpoints, hard stops, and escalation triggers.

## Quick commands

Use these first; they encode the supported local workflow.
Expand Down
84 changes: 84 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,90 @@ GitHub Releases page; `0.8.0` is the new starting line.
## Unreleased

- **Agent-tracing dashboard.** Added `pythinker dashboard` — a local web UI for inspecting sessions, wire events, context messages, tool statistics, and usage over time. It is also reachable from the interactive shell via the `/reports` slash command (aliased `/dashboard`).
- **Gemini finish_reason is now sticky once truncation is detected.** A second candidate with any non-`MAX_TOKENS` finish reason no longer overwrites a previously captured `"length"` signal at either the streaming or non-streaming path in the Google GenAI provider.
- **Budget-exhausted and stuck-loop messages are now visible in the shell.** Both handoff messages were appended to context but never sent to the wire, so the interactive shell showed no feedback when a spend ceiling or stuck-loop exit fired. Both now emit a `TextPart` wire event so the message appears in the shell.
- **Dark-theme prompt_toolkit border colors now match the TUI token constants.** Six stale slate hex values in `_PROMPT_STYLE_DARK` diverged from the current `border` and `border_muted` token values; they are now in sync and a parity test binds them going forward.
- **Non-review successful agents now show a summary preview in the agent tree.** Completed subagents with a `summary_preview` now display a dim preview line in the RunAgents tree renderer (review runs continue to use the findings table instead).
- **Non-string values in `required_mcp_servers` YAML are silently dropped.** Integers, booleans, and `null` entries were previously coerced to strings (`"1"`, `"False"`, `"None"`), creating permanently unsatisfiable MCP server names. Only actual string entries are now retained.

- **Fixed MiniMax token-plan `/usage` accuracy.** The token-plan response now meters by
percentage (the count fields are 0) and reports reset times in milliseconds; the adapter was
reading the zero counts (showing "0 requests used") and treating milliseconds as seconds
(showing resets like "171d" for a 5-hour window). It now reads `*_remaining_percent` and
converts reset times correctly, so e.g. a week at 82% remaining shows "18% used" resetting in
hours, not days.
- **GLM-5.2 (1M context) is now the default Z.AI model.** Logging in with Z.AI selects
`z-ai/glm-5.2` with its full 1,000,000-token context window. GLM-5.2 is absent from z.ai's
model-listing API, so it is pinned into the catalog and offered even when discovery omits it;
if z.ai later lists it, the API definition wins and it is shown once (no duplicate). Earlier
GLM models (5.1, 5, 5-turbo, 4.7, 4.5-air) remain available.
- **Kimi K2.7 Code added, and a new Kimi Coding Plan provider.** `kimi-k2.7-code` is now the
default model on the Moonshot plan, and a separate "Kimi Coding Plan" provider
(`/login kimi`) targets Moonshot's Anthropic-compatible coding endpoint with `kimi-k2.7-code`.
- **Verb spinner stays visible while a foreground tool or subagent runs.** The shimmering
"Working…/Thinking…" activity indicator now persists for the whole active turn — including
while a foreground tool (such as a shell-started server) or a subagent is executing — instead
of disappearing until the tool finished. This keeps long tool/subagent waits feeling alive
rather than frozen. An in-progress todo still swaps the verb for the todo title as before.
- **Project instructions delivered as a separate authoritative message.** The merged
`AGENTS.md` is no longer baked into the system prompt; it is delivered as a session-start,
user-role `<system-reminder>` preamble, assembled fresh on every request from session
state. This keeps the project rules immune to two regressions a system-prompt move would
otherwise risk — context compaction can no longer summarize them away (they never enter the
persisted history) and the dynamic-injection token budget can no longer truncate them —
while keeping the system prompt free of per-project content. `pythinker system-prompt` shows
the reminder below a labeled divider so the dump stays faithful.
- **Inspect the assembled system prompt.** New `pythinker system-prompt` command
renders and prints the fully-assembled system prompt for an agent
(`--agent <name>`, `--agent-file <path>`, `--work-dir <dir>`) — substituting the
live work directory, OS/shell, merged `AGENTS.md`, and discovered skills. It is
read-only: no session is created, no provider auth is required, and no MCP
servers are loaded.
- **Bound parallel tool fan-out.** Parallel-safe tool calls in one turn still
overlap, but now up to a fixed concurrency cap (10) instead of without bound, so
a turn that fans out many readers (e.g. dozens of web fetches) can no longer open
an unbounded number of sockets/file handles at once. Mutating-tool ordering and
writer exclusivity are unchanged.
- **Optional per-session spend ceiling.** New `loop_control.max_session_cost_usd`
config option (off by default). When set, a turn stops with a clear
budget-exhausted handoff message once the session's accumulated estimated cost
reaches the ceiling, instead of running to the step limit — and goal
auto-continuations and agent flows halt too. Best-effort: cost is `0` for models
with unknown pricing, so the ceiling never blocks when spend cannot be estimated.
- **Sensitive host files always re-confirm.** Writes to shell startup files
(`.zshrc`, `.bash_profile`, …), `.git` internals/hooks, the custom `.githooks`
hooks directory, `.ssh`, `.vscode`, and git
credentials (`.gitconfig`, `.netrc`, `.git-credentials`) are now classified as a
distinct edit action that re-confirms every time — even under yolo/auto — and is
never recorded as session-approved, exactly like edits to pythinker's own config.
This closes an auto-approve gap where a `.git/hooks` write inside the workspace was
treated as an ordinary edit.
- **Accept-edits mode.** New `/accept-edits` toggle auto-approves reversible
in-workspace ordinary file edits while still prompting for shell, destructive,
outside-workspace, config-surface, and sensitive host-file edits. It is
session-local (not persisted) and suppressed by safe mode. Pairs with the deny-set
above so a `.git/hooks` or shell-rc write is never swept into the auto-approve tier.
- **Agents can declare required MCP servers.** A markdown agent's frontmatter may set
`required_mcp_servers: [..]`; spawning that agent (via `Agent` or `RunAgents`) is
rejected with a clear message when those servers are configured-and-absent, instead of
wasting a turn on an agent that cannot reach its tools. While MCP is still loading the
spawn is allowed (the servers may yet connect).
- **UserPromptSubmit hooks can add context.** A non-blocking `UserPromptSubmit` hook's
`additionalContext` is now injected into the user turn as a system reminder, so the
model sees it as context for the prompt (previously only a hook *block* was honored).
Slash-command parsing still reads only the user's text, never the appended context.
- **Stale-overwrite guard for file edits.** If you read a file and it then changes on
disk (edited by you in another window or by another tool), overwriting it with WriteFile
or editing it with StrReplaceFile is now rejected with "File has been modified since you
last read it" so external changes are not silently clobbered — read it again first. This
catches external edits that StrReplaceFile's exact-string matching alone would miss.
First-contact writes (a file you never read) are unaffected, and a tool's own write
refreshes the read-state so consecutive edits are never falsely flagged.
- **Recover from output-token truncation.** When a response is cut off by the
output-token limit and makes no tool call, the turn no longer ends with a half-finished
answer treated as complete — the model is nudged to continue from where it stopped, up
to `loop_control.max_truncation_recoveries` times per turn (default 3; `0` disables).
pythinker-core now surfaces the provider's truncation signal so the loop can detect it.

## 0.44.0 (2026-06-13)

Expand Down
84 changes: 84 additions & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,90 @@ GitHub Releases page; `0.8.0` is the new starting line.
## Unreleased

- **Agent-tracing dashboard.** Added `pythinker dashboard` — a local web UI for inspecting sessions, wire events, context messages, tool statistics, and usage over time. It is also reachable from the interactive shell via the `/reports` slash command (aliased `/dashboard`).
- **Gemini finish_reason is now sticky once truncation is detected.** A second candidate with any non-`MAX_TOKENS` finish reason no longer overwrites a previously captured `"length"` signal at either the streaming or non-streaming path in the Google GenAI provider.
- **Budget-exhausted and stuck-loop messages are now visible in the shell.** Both handoff messages were appended to context but never sent to the wire, so the interactive shell showed no feedback when a spend ceiling or stuck-loop exit fired. Both now emit a `TextPart` wire event so the message appears in the shell.
- **Dark-theme prompt_toolkit border colors now match the TUI token constants.** Six stale slate hex values in `_PROMPT_STYLE_DARK` diverged from the current `border` and `border_muted` token values; they are now in sync and a parity test binds them going forward.
- **Non-review successful agents now show a summary preview in the agent tree.** Completed subagents with a `summary_preview` now display a dim preview line in the RunAgents tree renderer (review runs continue to use the findings table instead).
- **Non-string values in `required_mcp_servers` YAML are silently dropped.** Integers, booleans, and `null` entries were previously coerced to strings (`"1"`, `"False"`, `"None"`), creating permanently unsatisfiable MCP server names. Only actual string entries are now retained.

- **Fixed MiniMax token-plan `/usage` accuracy.** The token-plan response now meters by
percentage (the count fields are 0) and reports reset times in milliseconds; the adapter was
reading the zero counts (showing "0 requests used") and treating milliseconds as seconds
(showing resets like "171d" for a 5-hour window). It now reads `*_remaining_percent` and
converts reset times correctly, so e.g. a week at 82% remaining shows "18% used" resetting in
hours, not days.
- **GLM-5.2 (1M context) is now the default Z.AI model.** Logging in with Z.AI selects
`z-ai/glm-5.2` with its full 1,000,000-token context window. GLM-5.2 is absent from z.ai's
model-listing API, so it is pinned into the catalog and offered even when discovery omits it;
if z.ai later lists it, the API definition wins and it is shown once (no duplicate). Earlier
GLM models (5.1, 5, 5-turbo, 4.7, 4.5-air) remain available.
- **Kimi K2.7 Code added, and a new Kimi Coding Plan provider.** `kimi-k2.7-code` is now the
default model on the Moonshot plan, and a separate "Kimi Coding Plan" provider
(`/login kimi`) targets Moonshot's Anthropic-compatible coding endpoint with `kimi-k2.7-code`.
- **Verb spinner stays visible while a foreground tool or subagent runs.** The shimmering
"Working…/Thinking…" activity indicator now persists for the whole active turn — including
while a foreground tool (such as a shell-started server) or a subagent is executing — instead
of disappearing until the tool finished. This keeps long tool/subagent waits feeling alive
rather than frozen. An in-progress todo still swaps the verb for the todo title as before.
- **Project instructions delivered as a separate authoritative message.** The merged
`AGENTS.md` is no longer baked into the system prompt; it is delivered as a session-start,
user-role `<system-reminder>` preamble, assembled fresh on every request from session
state. This keeps the project rules immune to two regressions a system-prompt move would
otherwise risk — context compaction can no longer summarize them away (they never enter the
persisted history) and the dynamic-injection token budget can no longer truncate them —
while keeping the system prompt free of per-project content. `pythinker system-prompt` shows
the reminder below a labeled divider so the dump stays faithful.
- **Inspect the assembled system prompt.** New `pythinker system-prompt` command
renders and prints the fully-assembled system prompt for an agent
(`--agent <name>`, `--agent-file <path>`, `--work-dir <dir>`) — substituting the
live work directory, OS/shell, merged `AGENTS.md`, and discovered skills. It is
read-only: no session is created, no provider auth is required, and no MCP
servers are loaded.
- **Bound parallel tool fan-out.** Parallel-safe tool calls in one turn still
overlap, but now up to a fixed concurrency cap (10) instead of without bound, so
a turn that fans out many readers (e.g. dozens of web fetches) can no longer open
an unbounded number of sockets/file handles at once. Mutating-tool ordering and
writer exclusivity are unchanged.
- **Optional per-session spend ceiling.** New `loop_control.max_session_cost_usd`
config option (off by default). When set, a turn stops with a clear
budget-exhausted handoff message once the session's accumulated estimated cost
reaches the ceiling, instead of running to the step limit — and goal
auto-continuations and agent flows halt too. Best-effort: cost is `0` for models
with unknown pricing, so the ceiling never blocks when spend cannot be estimated.
- **Sensitive host files always re-confirm.** Writes to shell startup files
(`.zshrc`, `.bash_profile`, …), `.git` internals/hooks, the custom `.githooks`
hooks directory, `.ssh`, `.vscode`, and git
credentials (`.gitconfig`, `.netrc`, `.git-credentials`) are now classified as a
distinct edit action that re-confirms every time — even under yolo/auto — and is
never recorded as session-approved, exactly like edits to pythinker's own config.
This closes an auto-approve gap where a `.git/hooks` write inside the workspace was
treated as an ordinary edit.
- **Accept-edits mode.** New `/accept-edits` toggle auto-approves reversible
in-workspace ordinary file edits while still prompting for shell, destructive,
outside-workspace, config-surface, and sensitive host-file edits. It is
session-local (not persisted) and suppressed by safe mode. Pairs with the deny-set
above so a `.git/hooks` or shell-rc write is never swept into the auto-approve tier.
- **Agents can declare required MCP servers.** A markdown agent's frontmatter may set
`required_mcp_servers: [..]`; spawning that agent (via `Agent` or `RunAgents`) is
rejected with a clear message when those servers are configured-and-absent, instead of
wasting a turn on an agent that cannot reach its tools. While MCP is still loading the
spawn is allowed (the servers may yet connect).
- **UserPromptSubmit hooks can add context.** A non-blocking `UserPromptSubmit` hook's
`additionalContext` is now injected into the user turn as a system reminder, so the
model sees it as context for the prompt (previously only a hook *block* was honored).
Slash-command parsing still reads only the user's text, never the appended context.
- **Stale-overwrite guard for file edits.** If you read a file and it then changes on
disk (edited by you in another window or by another tool), overwriting it with WriteFile
or editing it with StrReplaceFile is now rejected with "File has been modified since you
last read it" so external changes are not silently clobbered — read it again first. This
catches external edits that StrReplaceFile's exact-string matching alone would miss.
First-contact writes (a file you never read) are unaffected, and a tool's own write
refreshes the read-state so consecutive edits are never falsely flagged.
- **Recover from output-token truncation.** When a response is cut off by the
output-token limit and makes no tool call, the turn no longer ends with a half-finished
answer treated as complete — the model is nudged to continue from where it stopped, up
to `loop_control.max_truncation_recoveries` times per turn (default 3; `0` disables).
pythinker-core now surfaces the provider's truncation signal so the loop can detect it.

## 0.44.0 (2026-06-13)

Expand Down
4 changes: 4 additions & 0 deletions packages/pythinker-core/src/pythinker_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ async def on_tool_call(tool_call: ToolCall):
result.usage,
tool_calls,
tool_result_futures,
truncated=result.truncated,
)


Expand All @@ -139,6 +140,9 @@ class StepResult:
_tool_result_futures: dict[str, ToolResultFuture]
"""@private The futures of the results of the spawned tool calls."""

truncated: bool = False
"""True when the model's response was cut off by the output-token limit."""

async def tool_results(self) -> list[ToolResult]:
"""All the tool results returned by corresponding tool calls."""
if not self._tool_result_futures:
Expand Down
7 changes: 7 additions & 0 deletions packages/pythinker-core/src/pythinker_core/_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ async def generate(
id=stream.id,
message=message,
usage=stream.usage,
# finish_reason 'length' means the output-token limit cut the response off. It is a
# required member of the StreamedMessage contract (every provider maps its own signal
# onto it), so the agent loop can always detect and recover from truncation instead of
# treating a cut-off response as a clean completion.
truncated=stream.finish_reason == "length",
)


Expand All @@ -105,6 +110,8 @@ class GenerateResult:
"""The generated message."""
usage: TokenUsage | None
"""The token usage of the generated message."""
truncated: bool = False
"""True when the response was cut off by the output-token limit (finish_reason 'length')."""


def _message_append(message: Message, part: StreamedMessagePart) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ def usage(self) -> TokenUsage | None:
"""The token usage of the streamed message."""
...

@property
def finish_reason(self) -> str | None:
"""The OpenAI-compatible finish reason of the streamed message.

``'length'`` when the output-token limit cut the response off (the signal the agent
loop uses to recover from truncation); ``None`` when the provider reports none. A
required member of the contract so no provider can silently omit the truncation
signal — see :mod:`pythinker_core._generate`.
"""
...
Comment thread
elkaix marked this conversation as resolved.


class TokenUsage(BaseModel):
"""Token usage statistics."""
Expand Down
Loading
Loading