Commit 5e50508
authored
feat: agent harness enhancements (#122)
* fix(tools): wrap FetchURL output as untrusted after truncation
FetchURL wrote pre-rendered <untrusted_data> envelopes into the
ToolResultBuilder, so when a page exceeded the builder's character
limit, truncation cut off the closing </untrusted_data> tag. The model
then received an unterminated untrusted-data envelope (the exact
failure mode the wrapper exists to prevent), and
strip_untrusted_envelope could not strip the torn envelope, so the raw
envelope leaked into display/UI paths.
Write the raw text and call builder.mark_untrusted() instead (the same
idiom SearchWeb uses), so wrapping happens after truncation in ok()
and the closing tag can never be cut. Applied to all three write
sites: the verbatim text/plain+markdown path, the trafilatura
extraction path, and the fetch-service path. The spill file now holds
raw unwrapped output, matching the documented spill contract.
Regression tests cover all three paths with >50k-char pages that
trigger builder truncation, asserting the envelope stays well-formed
and strip_untrusted_envelope round-trips.
* refactor(soul): expose public turn() contract for external drivers
External drivers (FlowRunner._flow_turn, the /goal and /learn slash
handlers) were calling the private PythinkerSoul._turn directly, each
carrying a pyright reportPrivateUsage suppression.
Add a public turn() method that documents the single-turn contract: one
user message in, one full agent turn out (model steps plus tool calls
until the model stops), with TurnOutcome conveying stop_reason
(no_tool_calls / tool_rejected / stuck), the final assistant message,
and step count. turn() does not emit TurnBegin/TurnEnd wire framing;
callers frame the turn themselves, as run() does.
turn() is a thin delegate to _turn on purpose: many tests monkeypatch
soul._turn to stub turn execution, so _turn stays the single
implementation/patch point and those patches keep intercepting turns
started through turn(). The three external call sites now use turn()
with the suppressions removed; internal self._turn calls are unchanged,
and the runtime_checkable Soul protocol is deliberately untouched.
* refactor(soul): replace string-matched tool gates with declarative flags
Two gates identified special tool classes by mechanisms that break
invisibly on rename/move:
- check_tool_call_allowed (permission.py) and
_is_external_side_effect_tool (toolset.py) matched external adapters
(MCPTool, WireExternalTool, PluginTool) by module/qualname strings.
This is a permission gate that FAILS OPEN: moving or renaming any of
these classes silently drops them out of external-tool permission
gating with no test failure. A planned refactor moves MCPTool out of
toolset.py, so the trap is defused first.
- _tool_defers_execution_started duck-typed on the private _approval
attribute to decide whether ToolExecutionStarted is deferred until
after approval.
Both gates now read explicit class-level flags instead:
- external_side_effect_tool (ClassVar) on MCPTool, WireExternalTool,
and PluginTool, documented as a security contract and pinned by tests
so a future move/rename that loses the flag fails CI instead of
failing open.
- emits_tool_execution_started_after_approval on every approval-gated
tool class (Shell, WriteFile, StrReplaceFile, TaskInput, TaskStop,
Terminal, PluginTool), matching the existing RunAgentsTool precedent;
the hasattr(_approval) fallback is removed.
Routing is behavior-preserving: the same tool classes pass through the
same gates before and after, and the Shell/network branches keep
priority in check_tool_call_allowed.
* docs(tasks): record design adoption blueprint and branch task log
Add the verified design-adoption blueprint (ranked, review-caveated
refactor plan for cleaner agent/runtime layering) and update the task
log with this branch's three landed tasks, their review outcomes, and
the deferred follow-ups, including the known machine-local PTY
shell-cancel test failure verified pre-existing on main.
* feat: sharpen orchestration guidance and genericize design comments
Add a root-only OrchestrationInjectionProvider that nudges substantial
normal-mode tasks toward the lightest effective work shape (direct tools,
SetTodoList, foreground RunAgents, verification), throttled via a
history-scanned reminder marker and suppressed under plan/auto/goal/subagent
modes. Sharpen the matching system-prompt guidance, refresh a feature tip
to promote /goal, and rephrase design-source comment attributions as
generic agent-enhancement notes.
* feat(acp): stop advertising the question tool to ACP clients
ACP sessions cannot present interactive questions (the session loop
signals QuestionNotSupported), so advertising AskUserQuestion invites a
wasted model step per question. replace_tools now hides the tool from
the model-facing list while keeping it registered, so a stray call still
resolves through the graceful textual fallback. Harmonize the task log
after merging refactor/agent-contract-and-tool-metadata.
* fix: apply review-deferred fixes from the contract/metadata work
- FetchURL: await spill_to_disk() at the trafilatura and fetch-service
sites so large pages spill off the event loop instead of falling back
to the synchronous spill inside ok().
- MCPTool: declare emits_tool_execution_started_after_approval so the
ToolExecutionStarted event defers until approval resolves, matching
every other approval-gated tool; the old _approval duck-typing missed
this class because it requests via runtime.approval. Pinned in
test_toolset.py.
- spinner_words: genericize remaining external credit wording.
- tasks/todo.md: record the fixes; document why structural flag
enforcement for future adapters is deferred (no shared adapter base
until the toolset split lands).
* docs(tasks): add verified agent-harness adoption plan (124 ranked items)
Synthesized from a 14-cluster map+adversarial-verify workflow comparing
the local reference agent harness against src/pythinker_code. Each item
records current state, verifier evidence, an adoption sketch fitted to
pythinker's design, effort/value, and target files; three refuted claims
are pinned so they are not re-implemented. Includes execution discipline
for the multi-writer branch (checkpoint = TDD + clean-code-guard + green
gates, hot-file serialization).
* feat(soul): repair tool call/result pairing at context restore
A crash between persisting an assistant tool-call message and its tool
results leaves context.jsonl with a dangling call or an orphaned result;
after resume every provider request then fails with a pairing error.
restore() and revert_to() now run a pure repair pass that synthesizes an
explicit lost-result message for unpaired calls and drops orphaned or
duplicate tool results, logging each repair. The file is never rewritten,
so re-repair on each restore stays idempotent.
Plan item: context-mgmt/history-invariant-repair (Tier 1).
* feat(soul): decision-complete planning protocol in plan-mode reminders
Plan mode now teaches a two-kinds-of-unknowns rule (explore
repo-discoverable facts yourself; surface preference/scope decisions
early via AskUserQuestion with a recommended default), records
unanswered defaults under an Assumptions section, gates ExitPlanMode on
a decision-complete plan (no decisions left to the implementer), and
adds a plan-file brevity rubric (3-5 short sections, subsystem-grouped
bullets). Phrase pins lock the new clauses into all three reminder
variants.
Plan item: prompts-instructions/decision-complete-plan-mode (Tier 1).
* feat(print): strict stdout/stderr channel discipline on failure paths
Headless failure diagnostics (provider errors, max-steps + handoff,
interrupt, unknown errors) printed plain text to stdout, corrupting the
stream-json channel for machine parsers. All diagnostics now route to
the pre-redirect stderr fd (falling back to sys.stderr), and stream-json
mode additionally emits one structured error record — a Notification
with category=run, type=error, the failure class, and the exit code —
written via raw stdout so rich cannot soft-wrap the JSON line.
Plan item: protocol-headless/channel-discipline (Tier 1).
* feat(subagents): inject merge-base-scoped git context into review agents
Review-class subagents (review, code_reviewer, security_reviewer)
received scope purely via the parent's prompt text and burned their
first turns rediscovering branch, dirty files, and the merge base. The
git-context prefix now also resolves the merge base against the first
existing base ref (origin/main, main, master), names the exact review
scope (git diff <sha>...HEAD), omits it when HEAD is the base, and is
injected for reviewer-class agents alongside explore.
Plan item: review-mode/deterministic-review-target-resolution (Tier 1,
agent-dispatch slice).
* feat(soul): same-step concurrency policy for parallel tool calls
Provider-emitted parallel tool calls all executed concurrently — two
mutating tools (WriteFile + Shell from one assistant message) could race
with no ordering guarantee. Tool dispatch now runs through a
reader-writer gate: tools declaring supports_parallel (read-only
builtins: ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, Think,
Recall, ListMcpResources, ReadMcpResource, SearchWeb, FetchURL) overlap
freely, while everything else — including unflagged plugin/MCP tools,
the safe default — executes exclusively in dispatch order. Writers
drain in-flight readers and cannot be starved.
Plan item: tools-registry-codemode/concurrency-policy (Tier 1).
* feat(soul): reactive context-overflow recovery (compact-and-retry)
A provider context-length 400 was telemetry-classified but treated as
fatal: the step raised and the turn died, even though the proactive
prune/compact thresholds run on heuristic counts that can undercount.
Two recovery layers, both bounded:
- Agent loop: on a context_overflow-classified step error, prune
(best-effort), force a full compaction, and retry the step — once per
turn; telemetry records recovered vs failed.
- SimpleCompaction: the compaction request itself carries the whole
to-compact slice and can overflow too; on a context-length rejection
it drops the oldest half and retries, terminally falling back to the
preserved tail plus an explicit dropped-context note.
classify_api_error moves to soul/api_errors.py (re-exported from
pythinkersoul) so compaction can classify without a circular import.
Plan items: core-loop/reactive-overflow-recovery +
context-mgmt/context-overflow-recovery (Tier 1).
* docs(tasks): record Tier-1 adoption progress and next M-item queue
* feat(config): gate project-scope hooks behind durable per-project trust
A cloned repository's .pythinker/config.toml merged unconditionally,
so its [[hooks]] shell commands auto-executed at session start —
arbitrary code execution from cloning a repo. Project/local-scope hooks
now load only after the user records trust:
- New project_trust store (user-scope trusted_projects.json, atomic
writes, fail-closed on corruption) keyed by the resolved repo root,
so the repo itself can never grant its own trust.
- _load_scoped strips hooks from untrusted project/local scopes with a
warning naming /trust as the fix; broken TOML in an untrusted project
degrades to an empty scope instead of blocking startup (trusted
projects keep the loud error).
- /trust on|off persists the per-project decision alongside the
session flags and points at /reload for hook activation.
- find_project_root promoted to public API (the /trust path needs it).
Plan item: config-features/per-project-trust-gating (Tier 1).
Out of scope (own plan item): sanitize-and-warn for scope-locked keys
in untrusted scopes — they keep the existing loud ConfigError.
* feat(config): warn on unknown config keys with source-located diagnostics
Config models ignore extra keys, so a typo'd key silently vanished and
changed behavior with no signal. After merge, the raw dict is now
diffed against the model field tree (aliases and AliasChoices honored;
recursion follows provable shapes only — nested models, dict-of-model
maps, lists of models — so unmodellable values can never false-
positive). Each finding warns with the dotted path and the scope file
it came from via the existing provenance map; PYTHINKER_STRICT_CONFIG=1
escalates to ConfigError for CI use.
Plan item: config-features/unknown-config-key-detection (Tier 1).
* docs(tasks): record trust-gating and unknown-key checkpoints
* feat(shell): carry a conversation summary across /model switches
/model discarded the entire conversation by starting a fresh session.
The switch now summarizes the outgoing session with the OUTGOING model
— only plain text crosses the model boundary, so the incoming provider
never sees foreign thinking blocks or tool-call schemas — and seeds the
new session's context with it before Reload. Best-effort with a
start-fresh fallback on empty history, summarization failure, or
model_switch_carryover=false. SimpleCompaction gains summarize_all()
(no preserved tail) atop the extracted overflow-halving summarizer.
Plan item: core-loop/model-switch-context-continuity (Tier 1).
* docs(tasks): record model-switch carry-over checkpoint
* feat(shell): elide approval prompts for provably read-only commands
The first ls or git status of a session always interrupted the user
with an approval dialog. soul/permission.py gains
is_known_safe_command(): a positive allowlist, fail closed — the
mutation guard's hidden-command/substitution/newline, write-redirection,
and network/mutation rejections run first, then every ;/&&/||/| segment
must start with an allowlisted read-only binary or read-only git
subcommand (--output rejected). Wrappers (sudo/env/time) are never
unwrapped, and absolute command paths must live in a system bin dir so
a workspace-local fake git cannot ride its basename onto the allowlist.
Shell consults it only in the root agent (subagent approval requests
stay — they are part of the unattended-denial defense surface) and only
after the deny gate, so elision can never override a deny. Elisions are
tracked in telemetry; the started event fires at the elision point.
Plan item: exec-safety/known-safe-command-auto-approval (Tier 1).
* docs(tasks): record safe-command elision checkpoint
* fix(security): allowlist inline env prefixes on the elision path
The safe-command elision accepted any KEY=VALUE prefix, so
PATH=/tmp/evil ls would resolve ls from the attacker directory —
defeating the system-bin pinning — and LD_PRELOAD/DYLD_*/GIT_PAGER
prefixes could inject code into otherwise read-only commands. Only
harmless locale/timezone assignments (LANG/LC_*/TZ) may now prefix an
elidable command; every other assignment fails closed to the normal
approval prompt. Flagged by automated security review.
* test(e2e): pin shell approval round-trip with non-elidable commands
The approval-protocol e2e tests drove Shell with 'echo ok', which the
new known-safe elision now runs without a prompt — the round-trip these
tests exist to pin never started. 'env echo ok' keeps stdout identical
while the wrapper prefix disqualifies elision, so the approval exchange
still exercises request/approve/reject. Fallout from 5dc87aa (caught
by the full tests_e2e scope).
* feat(mcp): per-server startup timeout with actionable failure diagnostics
A hung MCP connect left the server in 'connecting' forever and blocked
every agent turn (the loop awaits MCP loading with no bound). Connect +
inventory is now wrapped in asyncio.wait_for governed by a new
mcp.client.startup_timeout_ms (default 30s), and every connect failure
is classified into one short actionable line — timeout names the config
knob, 401/unauthorized names the exact 'pythinker mcp auth' command,
ENOENT names the missing binary — carried on MCPServerInfo and
MCPServerSnapshot and rendered by /mcp instead of a bare 'failed'.
Plan item: mcp/per-server-startup-timeout-diagnostics (Tier 1).
* test: refresh wire snapshots for the MCP server error field
The serde and e2e snapshots pin wire-model dumps; the new optional
MCPServerSnapshot.error field appears as null in them. Applied via
--inline-snapshot=fix (deliberate, follows 05f8642).
* docs(tasks): record elision and MCP-timeout checkpoints
* feat(mcp): per-server tool allow/deny filtering (enabledTools/disabledTools)
A server listing 30 tools floods the model tool list with all of them.
mcp.json server entries now accept optional enabledTools (exclusive
allowlist) and disabledTools (denylist, wins on conflict): filtered
tools are skipped at connect time — never registered in the toolset or
runtime.mcp_tools — and MCPTool re-checks membership at call time as
defense in depth for tool maps shared with subagents and future live
tool-list updates. No filter fields keeps today's permissive behavior.
Plan item: mcp/per-server-tool-allow-deny-filtering (Tier 1).
* docs(tasks): record MCP tool-filtering checkpoint
* feat(subagents): spawn-time context fork for foreground agents
New children started blank, relying on the orchestrator hand-writing a
context packet into every prompt. Agent(fork_context=true) now seeds a
new foreground child with the parent's conversational spine — user
requests and assistant text, with tool traffic (whose call/result
pairing would dangle), thinking parts, injected reminders/notifications,
and checkpoint markers all filtered out. The fork reads the persisted
parent context (inheriting the restore-time pairing repair) and seeds
the child's own context file, so resume keeps working unchanged.
Invalid with resume or run_in_background (background fork is a tracked
follow-up); read failures degrade to a blank child rather than failing
the spawn. Tool/agent schema snapshots refreshed.
Plan item: multi-agent/spawn-time-context-fork (Tier 1, foreground
slice).
* docs(tasks): record context-fork checkpoint
* docs(tasks): worktree-isolation design note (re-sized M→L, phased seam plan)
The audit found ~94 work-dir consumer sites and shared session/
builtin_args across child runtimes; honoring isolation=worktree without
a single work-dir seam first would yield false isolation. Phases: P1
mechanical Runtime.work_dir seam, P2 worktree lifecycle in the
background runner (create/redirect/report/cleanup, non-git rejection),
P3 RunAgents batch reuse.
* refactor(soul): Runtime.work_dir seam for worktree isolation (P1)
Operational cwd/path-resolution sites (26 across tools, soul,
permission, app, UI) now read runtime.work_dir — work_dir_override or
the session's — instead of reaching through runtime.session.work_dir.
copy_for_subagent accepts work_dir_override (re-rendering the child's
PYTHINKER_WORK_DIR/_LS prompt args) and propagates it to grandchildren;
the shared session keeps owning persistence paths. Behavior-preserving
with no override set; full suite green (5381 local + e2e, the two
TimeoutError wire tests verified pre-existing/machine-local on the
stashed tree).
Phase P1 of tasks/worktree-isolation-design.md; P2 wires the worktree
lifecycle into the background runner.
* docs(tasks): record work-dir seam (P1) checkpoint
* feat(subagents): enforce worktree isolation for background write agents (P2)
isolation='worktree' only recorded intent; parallel coder/implementer
children shared one working tree and could clobber each other. The
background runner now creates a detached git worktree of HEAD per
write-profile child under <session>/worktrees/<agent_id>, points the
child runtime at it through the P1 work_dir seam (prompt work-dir args
re-rendered), and on completion appends the worktree path plus a diff
summary to the final report so the orchestrator merges deliberately.
Clean worktrees are removed; changed or failed ones are retained.
Non-git roots fail before any model spend with an actionable error;
read-profile children log and ignore the request; resume reuses the
existing worktree. Local subprocesses are safe here — the manager
enforces a local backend for agent tasks.
Phase P2 of tasks/worktree-isolation-design.md; P3 (RunAgents batch)
remains.
* docs(tasks): record worktree-isolation P2 checkpoint
* feat(subagents): document enforced isolation on RunAgents batches (P3)
The RunAgents → Agent → create_agent_task → BackgroundAgentRunner chain
already threads isolation per child, so P2 enforcement covers batch
fan-outs; the parameter description now states the enforced semantics
(per-child worktrees, diff-summary reports, deliberate merging) instead
of 'records an intent'. Closes tasks/worktree-isolation-design.md.
* docs(tasks): close worktree-isolation item (P1-P3 complete)
* feat(tools): graduated fuzzy-matching ladder for edit-location recovery
Whitespace drift or smart-punctuation mismatch in StrReplaceFile's old
string hard-failed with 'not found', burning a re-read + retry turn —
the drift is invisible in numbered ReadFile output. After the exact
match and CRLF fallback miss, a line-window seek now retries with
graduated relaxations (trailing-whitespace -> indentation ->
unicode-punctuation); the first firing tier replaces the ACTUAL matched
file slice — never the needle text — adopting the slice's CRLF style
and trailing newline, and the tool message names the relaxation.
Ambiguity contract preserved: multiple fuzzy hits without replace_all
error with the tier named. Deferred (low value): opt-in final-newline
normalization for whole-file writes.
Plan item: patch-file-tools/graduated-fuzzy-matching-ladder (Tier 1).
* docs(tasks): record fuzzy edit-ladder checkpoint
* feat(soul): live permissions-state injection (posture-fingerprinted)
Enforcement was rich (profiles, safe mode, yolo/auto, session approvals,
shlex-based command classification) but invisible prompt-side — the
model discovered policy through denied tool calls. A new
PermissionsInjectionProvider renders the enforced profile, posture
flags, mutation/network allowances, session-approved actions, and the
command-shaping rules the gate can actually classify. Fingerprinted on
(profile, yolo, auto, safe_mode, approvals): re-emits exactly on
posture changes, after compaction, and on auto toggles; root-only
(subagent overlays already document their constraints). Approval gains
read accessors is_safe_mode/session_approved_actions. History-shape
test pins scoped to their subject; wire-session e2e snapshots
refreshed.
Plan item: prompts-instructions/dynamic-permissions-state (Tier 1).
* docs(tasks): record permissions-state checkpoint
* fix: apply external review findings across recent checkpoints
- CRITICAL: GIT_CONTEXT_AGENT_TYPES used underscored reviewer names while
registered type names are dashed (code-reviewer/security-reviewer), so
reviewer agents silently missed the git-context injection; names fixed
and a pin added asserting every gate name is a real profile key.
- Foreground isolation requests now fail fast on Agent AND RunAgents
instead of warning-and-proceeding unisolated (degraded behavior was
presented as authoritative); warning pin updated to the new contract.
- Unknown-config-key diagnostics now also run for explicit loads
(--config-file / --config text) via single-source provenance.
- Failure/timeout/cancel paths name the retained isolation worktree in
the task output (retention is deliberate for resume, never silent).
- Best-effort prune in overflow recovery logs its failure instead of
contextlib.suppress.
- supports_parallel flags annotated (: bool); test helpers cleaned
(fail-fast _git asserts, unused _ListingClient params dropped).
* fix: harden isolation, elision, and concurrency per adversarial arc review
A 16-agent adversarial review (4 dimensions, every finding refuted-or-
confirmed against live code) confirmed 11 findings; all fixed except one
deliberate deferral (exclusive gate held across approval waits — needs
the approval-split refactor; recorded in tasks/todo.md).
- DATA LOSS (high): a child that committed its work left a clean
worktree, so cleanup removed it and orphaned the commits. Creation now
records a base-SHA sidecar (next to the worktree, never inside it);
commits ahead of base count as changes and force retention, with
unknown provenance failing closed to retention.
- FALSE ISOLATION (high): foreground shell inherited the process cwd and
relative file-tool paths resolved against it, so isolated children
mutated the original repo. Host exec (protocol, local, ssh, ACP
fallback) gained a cwd argument; foreground shell passes the runtime
work dir, and write/replace/read resolve relative paths against it
while preserving the relative-escape error contract.
- REGRESSION (high): safe mode now disables the read-only-command prompt
elision — users who disabled auto-approval keep every checkpoint.
- REGRESSION (high): untrusted-project hook stripping now publishes a
session notification (web/ACP visible), not just a shell log line.
- MCP readOnlyHint annotations enable supports_parallel via property;
worktree add/remove serializes per repo; CHANGELOG documents the
same-step serialization and stderr-diagnostics behavior changes.
* fix: resolve PR #122 review findings (CodeRabbit, CodeQL, typos)
Address all bot review feedback on the agent-harness branch:
Security / CI gates:
- project_trust: store SHA-256 digests instead of clear-text paths
(CodeQL clear-text-storage) and serialize read-modify-write behind a
cross-process file lock; read legacy clear-text stores for compat.
- typos: rename intentional config-key fixtures to validly-spelled
unknown keys; fix `unparsable`/`default_yolo_typo` prose in planning doc.
- api_errors: drop redundant `400 <= status < 500` guard (always true
after the >=500 early return).
Correctness:
- ssh: resolve relative cwd against the host's tracked cwd, not the SSH
login dir.
- config: read untrusted project/local scopes independently so one bad
file no longer discards the other.
- context: re-repair the post-usage slice so token accounting reflects
the repaired history; keep tool messages with no call id.
- soul/agent + subagents/builder: recompute AGENTS.md payload for a child
worktree override instead of inheriting the parent's.
- subagents/core: import TextPart/ThinkPart from pythinker_core.message;
collect git context from the effective child work dir.
- subagents/runner: fail an explicit context fork loudly instead of
silently degrading to a blank child.
- subagents/worktree: validate a pre-existing dest is a registered
worktree before reusing it.
- background/agent_runner: report retained isolation worktrees on the
early failure/empty-output exits too.
- slash: persist the carry-over summary as a system turn, not a user turn.
Safety hardening:
- permission: refuse prompt elision for read-only commands with
path-bearing operands (cat /etc/shadow, git -C /other, ../secret).
- toolset: keep MCP tools exclusive in the same-step gate (ignore
untrusted remote readOnlyHint); stage MCP inventory locally until
connect succeeds.
- read_media: keep ReadMediaFile serialized (large in-memory payloads).
- permissions_state: include agent_execution_profile in the injection
fingerprint so profile switches reinject.
plan_mode: complete the truncated exit-rule sentence and wrap multi-line
reminder literals in parentheses (fixes the implicit-concat warning
without splitting sentences across rendered lines).
Tests: cover the trust-store hashing/legacy path, the work_dir and
runtime.work_dir seams by behavior not identity, the MCP exclusive
default, legacy StatusUpdate deserialization, and the new unsafe
path-operand commands; stop pinning full reminder text in loop tests.
* fix(tui): hide thinking shimmer while a foreground tool runs
The verb spinner (shimmering "Working…/Thinking…") was gated only on
`_active_turn_depth > 0`, i.e. the whole turn. When the agent started a
long-running foreground command — a dev server via npm/docker, a watch
task — the agent coroutine just awaits the subprocess, but the shimmer
kept animating for the full turn, falsely signalling active agent
cognition. The tool card already shows an animated running marker plus
streaming output, so the shimmer was redundant and misleading.
Suppress the working indicator while any foreground tool is mid-execution
(execution started, no result yet, not a detached background agent) on
both render surfaces — the non-interactive Rich Live path and the
interactive pinned status tail. The shimmer now means "the agent is
thinking" and reappears the moment the command returns. Platform-agnostic:
the root cause was turn-level gating, not Windows-specific.
Adds `_ToolCallBlock.is_executing` and `_LiveView._foreground_tool_executing()`,
and a test pinning that the pinned tail is empty mid-execution and returns
once the tool finishes.
* fix(api-errors): keep 4xx lower bound; drop redundant 500 check
The earlier `if status < 500` simplification was behaviour-changing: after
the `status >= 500` early return the upper bound is always true (CodeQL
"redundant comparison" + "unreachable code" on the `return "api"` fallback),
but dropping the `>= 400` lower bound also routed sub-400 statuses (e.g. the
`status=0` default for non-HTTP-ish errors) into `4xx_client` instead of the
generic `api` bucket. Use `if status >= 400` — equivalent to the original
`400 <= status < 500` given the preceding return, with the `api` fallback
reachable again for status < 400.
Also parenthesize the remaining sparse plan-mode reminder concatenation so
CodeQL's implicit-string-concatenation check stays quiet without splitting
the line across the rendered output.
* fix: address CodeRabbit re-review on harness changes
- soul/agent: when overriding a child's work dir, only replace
PYTHINKER_AGENTS_MD when an explicit value is provided; None now keeps
the parent payload instead of silently clearing inherited context.
- soul/context: drop tool results with no tool_call_id during pairing
repair. Keeping them left malformed history that re-broke the next
provider request — the exact failure the repair exists to prevent.
- subagents/worktree: a failed `git worktree list` no longer collapses to
"not a registered worktree" (which could send the operator to delete a
path holding the child's only work); raise WorktreeError with the git
stderr instead.
- tests/memory: annotate the `_runtime` helper return type (ANN202).
* test(context): use well-formed tool pairs in pending-token fixtures
The drop of id-less tool results during pairing repair (previous commit)
correctly removes malformed history, but three pending-token tests fed bare
`tool` messages (no tool_call_id, no opening assistant tool call) as token
ballast through the restore/repair path, so they now under-counted.
Production tool results always carry the originating tool_call_id, so model
the fixtures realistically: an assistant message that opens a tool call plus
a paired tool result, both after the last `_usage`. They survive pairing
repair and keep the pending estimate intact — exercising the post-`_usage`
slice accounting without depending on malformed history.1 parent 199cbe9 commit 5e50508
120 files changed
Lines changed: 6916 additions & 322 deletions
File tree
- packages/pythinker-host/src/pythinker_host
- src/pythinker_code
- acp
- agents/default
- background
- plugin
- soul
- dynamic_injections
- subagents
- tools
- agent
- background
- file
- mcp_resource
- memory
- recall
- shell
- think
- todo
- web
- ui
- print
- shell
- components
- visualize
- wire
- tasks
- tests_e2e
- tests
- acp
- core
- subagents
- tools
- ui_and_conv
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
18 | 29 | | |
19 | 30 | | |
20 | 31 | | |
21 | 32 | | |
| 33 | + | |
22 | 34 | | |
23 | 35 | | |
24 | 36 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
562 | 562 | | |
563 | 563 | | |
564 | 564 | | |
565 | | - | |
| 565 | + | |
566 | 566 | | |
567 | 567 | | |
568 | 568 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
220 | 220 | | |
221 | 221 | | |
222 | 222 | | |
223 | | - | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
224 | 226 | | |
225 | 227 | | |
226 | 228 | | |
227 | 229 | | |
228 | 230 | | |
229 | 231 | | |
230 | 232 | | |
| 233 | + | |
| 234 | + | |
231 | 235 | | |
232 | 236 | | |
233 | 237 | | |
| |||
347 | 351 | | |
348 | 352 | | |
349 | 353 | | |
350 | | - | |
351 | | - | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
352 | 358 | | |
353 | 359 | | |
354 | 360 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
193 | | - | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
194 | 196 | | |
195 | 197 | | |
196 | 198 | | |
| |||
208 | 210 | | |
209 | 211 | | |
210 | 212 | | |
| 213 | + | |
211 | 214 | | |
212 | 215 | | |
213 | 216 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
303 | 303 | | |
304 | 304 | | |
305 | 305 | | |
306 | | - | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
307 | 309 | | |
308 | 310 | | |
309 | 311 | | |
| |||
313 | 315 | | |
314 | 316 | | |
315 | 317 | | |
316 | | - | |
317 | | - | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
318 | 327 | | |
319 | 328 | | |
320 | 329 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
296 | 296 | | |
297 | 297 | | |
298 | 298 | | |
299 | | - | |
300 | | - | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
301 | 303 | | |
302 | 304 | | |
303 | 305 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
31 | 37 | | |
32 | 38 | | |
33 | 39 | | |
| |||
48 | 54 | | |
49 | 55 | | |
50 | 56 | | |
| 57 | + | |
| 58 | + | |
51 | 59 | | |
52 | 60 | | |
53 | 61 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | | - | |
| 108 | + | |
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
388 | 388 | | |
389 | 389 | | |
390 | 390 | | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
391 | 415 | | |
392 | 416 | | |
393 | 417 | | |
| |||
603 | 627 | | |
604 | 628 | | |
605 | 629 | | |
606 | | - | |
| 630 | + | |
607 | 631 | | |
608 | 632 | | |
609 | 633 | | |
| |||
782 | 806 | | |
783 | 807 | | |
784 | 808 | | |
785 | | - | |
| 809 | + | |
786 | 810 | | |
787 | 811 | | |
788 | 812 | | |
| |||
0 commit comments