feat: show model-written tool intent in the working indicator - #57
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe change adds optional tool-intent injection to the agent loop, propagates intent through protocol events, and displays sanitized live intent in the TUI. An experimental flag and environment variable control the feature. ChangesTool intent flow
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant AgentTurn
participant RunTurn
participant ExecuteLoopStep
participant ToolCallPreflight
participant TUI
AgentTurn->>RunTurn: pass toolIntentEnabled
RunTurn->>ExecuteLoopStep: forward toolIntentEnabled
ExecuteLoopStep->>ToolCallPreflight: inject and preflight tool intent
ToolCallPreflight->>TUI: publish tool-call intent event
TUI->>TUI: render or clear live intent
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
commit: |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/agent-core/src/loop/tool-call.ts (1)
267-289: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winStrip intent from hook-visible
ToolCallobjects.Preflight replaces
call.args, butrunPrepareToolExecutionHookstill passes rawcall.toolCall.argumentsand rawstep.toolCallsto hooks. An enabled call can therefore expose the original unbounded and unsanitizedivalue to hook code.
packages/agent-core/src/loop/tool-call.ts#L267-L289: serialize the extracted arguments into the preflightedToolCall, and construct the batch hook list from preflighted calls.packages/agent-core/test/loop/tool-call.e2e.test.ts#L122-L146: assert thatcontext.toolCall.argumentsandcontext.toolCallsprovided to hooks do not containi.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/agent-core/src/loop/tool-call.ts` around lines 267 - 289, Update the preflight result construction in tool-call.ts so runnable and rejected ToolCall objects serialize extracted.args into toolCall.arguments, removing the injected i field before hooks observe them; ensure batch hook inputs use these preflighted calls rather than raw step.toolCalls. In packages/agent-core/src/loop/tool-call.ts lines 267-289, update the relevant result construction and hook-list flow; in packages/agent-core/test/loop/tool-call.e2e.test.ts lines 122-146, add assertions that both context.toolCall.arguments and context.toolCalls omit i.
🧹 Nitpick comments (3)
packages/agent-core/test/loop/tool-intent.test.ts (2)
15-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCreate the default schema for each helper call.
Oxlint reports
unicorn(no-object-as-default-parameter)here. Return a fresh default schema from a helper so a future fixture mutation cannot leak into another test.As per coding guidelines, use oxlint for linting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/agent-core/test/loop/tool-intent.test.ts` around lines 15 - 19, Replace the mutable default object parameter near the helper call with a helper that creates and returns a fresh schema object on each invocation. Update the relevant helper signature to use that factory, preserving the existing schema contents and run oxlint to verify the lint issue is resolved.Sources: Coding guidelines, Linters/SAST tools
107-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the conditional from this Vitest assertion.
Oxlint reports
vitest(no-conditional-in-test)forsanitized ?? ''. Compare directly with'🙂'.repeat(INTENT_MAX_LENGTH)to verify both the cap and the exact result without a conditional.As per coding guidelines, use oxlint for linting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/agent-core/test/loop/tool-intent.test.ts` around lines 107 - 110, Update the test case “caps the result by code points” to assert sanitized directly against '🙂'.repeat(INTENT_MAX_LENGTH), removing the sanitized ?? '' conditional while still verifying the capped length and exact result. Use oxlint to confirm the test no longer triggers vitest(no-conditional-in-test).Sources: Coding guidelines, Linters/SAST tools
packages/agent-core/test/loop/tool-call.e2e.test.ts (1)
71-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the provider schema in this integration helper.
The fake provider always returns an
iargument. The enabled test can pass ifexecuteLoopStepstops sending the injected schema to the provider. Returnllmfrom this helper and assert that its first call contains an eligible tool schema with requiredi.As per path instructions,
packages/**/*.tsrequires new behavior to have Vitest coverage.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/agent-core/test/loop/tool-call.e2e.test.ts` around lines 71 - 104, Update runToolIntentTurn to return the FakeLLM instance alongside strict, sink, and context. Add Vitest coverage in the enabled tool-intent test that inspects the first LLM call and verifies an eligible tool schema is present with i listed as required, ensuring the injected schema reaches the provider.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/pythinker-code/src/tui/controllers/session-event-handler.ts`:
- Line 649: Update
apps/pythinker-code/src/tui/controllers/session-event-handler.ts at lines 649
and 684-685: always call setLiveIntent with event.intent, and pass undefined
when preview?.args['i'] is not a string so prior labels are cleared. Add a
regression test covering an intent-bearing tool event followed by one without
intent.
In `@docs/configuration/config-files.md`:
- Line 195: Update the tool-intent documentation to describe the feature as
applying only to eligible tool calls: change the wording in
docs/configuration/config-files.md at lines 195-195,
docs/configuration/env-vars.md at lines 138-138, and
.changeset/tool-intent-indicator.md at lines 5-5, preserving the existing
descriptions while scoping each statement to eligibility.
In `@packages/agent-core/src/loop/tool-intent.ts`:
- Line 9: Update the ANSI_ESCAPE OSC branch to terminate at either BEL or the
standard ST sequence (ESC followed by backslash), so it does not consume
following visible text. Add coverage for an ST-terminated OSC hyperlink followed
by text and verify the text is preserved.
- Around line 19-39: The required-field merge in injectIntentIntoTools can emit
duplicate INTENT_FIELD entries when the schema already requires it without
defining the property. Treat INTENT_FIELD as already present when it appears in
schema.required, or deduplicate the merged required array before returning the
injected schema; preserve existing required fields and add coverage for a
required INTENT_FIELD missing from properties.
---
Outside diff comments:
In `@packages/agent-core/src/loop/tool-call.ts`:
- Around line 267-289: Update the preflight result construction in tool-call.ts
so runnable and rejected ToolCall objects serialize extracted.args into
toolCall.arguments, removing the injected i field before hooks observe them;
ensure batch hook inputs use these preflighted calls rather than raw
step.toolCalls. In packages/agent-core/src/loop/tool-call.ts lines 267-289,
update the relevant result construction and hook-list flow; in
packages/agent-core/test/loop/tool-call.e2e.test.ts lines 122-146, add
assertions that both context.toolCall.arguments and context.toolCalls omit i.
---
Nitpick comments:
In `@packages/agent-core/test/loop/tool-call.e2e.test.ts`:
- Around line 71-104: Update runToolIntentTurn to return the FakeLLM instance
alongside strict, sink, and context. Add Vitest coverage in the enabled
tool-intent test that inspects the first LLM call and verifies an eligible tool
schema is present with i listed as required, ensuring the injected schema
reaches the provider.
In `@packages/agent-core/test/loop/tool-intent.test.ts`:
- Around line 15-19: Replace the mutable default object parameter near the
helper call with a helper that creates and returns a fresh schema object on each
invocation. Update the relevant helper signature to use that factory, preserving
the existing schema contents and run oxlint to verify the lint issue is
resolved.
- Around line 107-110: Update the test case “caps the result by code points” to
assert sanitized directly against '🙂'.repeat(INTENT_MAX_LENGTH), removing the
sanitized ?? '' conditional while still verifying the capped length and exact
result. Use oxlint to confirm the test no longer triggers
vitest(no-conditional-in-test).
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fce28038-8932-482a-bf7a-4dff91378d93
📒 Files selected for processing (18)
.changeset/tool-intent-indicator.mdapps/pythinker-code/src/tui/constant/rendering.tsapps/pythinker-code/src/tui/controllers/session-event-handler.tsapps/pythinker-code/src/tui/controllers/streaming-ui.tsapps/pythinker-code/test/tui/tool-intent-label.test.tsdocs/configuration/config-files.mddocs/configuration/env-vars.mdpackages/agent-core/src/agent/turn/index.tspackages/agent-core/src/flags/registry.tspackages/agent-core/src/loop/events.tspackages/agent-core/src/loop/run-turn.tspackages/agent-core/src/loop/tool-call.tspackages/agent-core/src/loop/tool-intent.tspackages/agent-core/src/loop/turn-step.tspackages/agent-core/test/loop/tool-call.e2e.test.tspackages/agent-core/test/loop/tool-intent.test.tspackages/protocol/src/__tests__/events.test.tspackages/protocol/src/events.ts
582ff5d to
9114c46
Compare
34230c4 to
e8fa4d0
Compare
e8fa4d0 to
3f9b0c4
Compare
57fd00d to
388d3ee
Compare
Inject a required first field "i" (concise intent) into tool schemas sent to providers, gated by the tool_intent experimental flag (default on). The runtime strips the field before validation, hooks, execution, and persistence, and carries it on the tool.call.started event. The TUI shows the intent live in the spinner label, streamed from partial tool-call arguments, and falls back to the rotating verbs without it.
StructuredOutput is mechanical and its consumers treat the advertised schema as an exact contract.
A stale intent label survived into the retried step. Also guard the deliberately duplicated ANSI sanitizer with mirrored test fixtures on both sides so future drift breaks a test.
388d3ee to
e5ff2c3
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
apps/pythinker-code/src/tui/constant/streaming.ts (1)
4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd the
uflag to the regular expression.Oxlint reports
require-unicode-regexpfor this changed expression. Change the flags fromgtogu.Proposed fix
export const STREAMING_ARGS_FIELD_RE = - /"(i|path|file_path|command|pattern|query|url|description|title|name)"\s*:\s*"((?:\\.|[^"\\])*)"/g; + /"(i|path|file_path|command|pattern|query|url|description|title|name)"\s*:\s*"((?:\\.|[^"\\])*)"/gu;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/pythinker-code/src/tui/constant/streaming.ts` at line 4, Update the regular expression in the streaming constant to use Unicode-aware matching by changing its flags from g to gu, resolving the require-unicode-regexp lint violation while preserving the existing global matching behavior.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/agent-core/test/loop/tool-intent.test.ts`:
- Around line 115-118: Extend the test for extractIntentFromArgs with malformed
i-field input such as an object containing numeric i and value fields, and
assert that the returned args omit i while intent remains undefined. Preserve
the existing no-i coverage and verify removal occurs before validation for
non-string i values.
- Around line 22-26: Update makeTool so parameters is optional rather than an
object-literal default parameter, then create the existing default schema inside
the function body when no parameters are provided. Preserve the current schema
and behavior for explicitly supplied parameters, and verify the change with
oxlint.
- Line 128: Update the test assertion around sanitized to first assert that
sanitized is defined, then measure its code-point length directly with
Array.from(sanitized). Remove the nullish fallback while preserving the
INTENT_MAX_LENGTH expectation.
---
Nitpick comments:
In `@apps/pythinker-code/src/tui/constant/streaming.ts`:
- Line 4: Update the regular expression in the streaming constant to use
Unicode-aware matching by changing its flags from g to gu, resolving the
require-unicode-regexp lint violation while preserving the existing global
matching behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1e3f4ece-ab5c-4a33-ae1e-022148000fa2
📒 Files selected for processing (10)
.changeset/tool-intent-indicator.mdapps/pythinker-code/src/tui/constant/rendering.tsapps/pythinker-code/src/tui/constant/streaming.tsapps/pythinker-code/src/tui/controllers/session-event-handler.tsapps/pythinker-code/test/tui/tool-intent-label.test.tsapps/pythinker-code/test/tui/utils/event-payload.test.tsdocs/configuration/config-files.mddocs/configuration/env-vars.mdpackages/agent-core/src/loop/tool-intent.tspackages/agent-core/test/loop/tool-intent.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- apps/pythinker-code/test/tui/tool-intent-label.test.ts
- docs/configuration/env-vars.md
- packages/agent-core/src/loop/tool-intent.ts
- apps/pythinker-code/src/tui/constant/rendering.ts
- apps/pythinker-code/src/tui/controllers/session-event-handler.ts
- .changeset/tool-intent-indicator.md
The injected intent field is required and string-typed, so a model that emitted a non-string value left it in the arguments and the call died in schema validation. The field is now removed whenever present, and an intent is produced only from a usable string.
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @pythoughts/pythinker-code@0.16.0 ### Minor Changes - [#59](#59) [`6999b68`](6999b68) - Add an opt-in advisor: a second model reviews the conversation after a completed user turn unless another review is already running, and its notes appear as an `<advisory>` block in the agent's next turn; enable with `[advisor] enabled = true` plus an advisor model (the `advisor` model role or `[advisor] model`), and it runs only when the advisor shares the session model's provider. - [#56](#56) [`b71f094`](b71f094) - Add model roles: lock a model alias to the small, implementer, or advisor slot with `/model <role>`, list assignments with `/model roles`, and reference roles as `@small`, `@implementer`, or `@advisor` wherever a subagent model can be set; an assigned implementer role becomes the default model for subagents. - [#57](#57) [`99c427c`](99c427c) - Show what the agent is doing in the working indicator: eligible tool calls whose input schema accepts the injected field now carry a short model-written intent, streamed live into the spinner label (for example "check failing test…") instead of a rotating placeholder; disable with `PYTHINKER_CODE_EXPERIMENTAL_TOOL_INTENT=0`. - [#58](#58) [`065bf2e`](065bf2e) - Redesign core TUI surfaces: tool cards get state-tinted backgrounds with three new theme tokens, a status bar with a per-session accent color appears between the input box and footer, and the prompt box uses a neutral border while permission mode appears in the status bar. The working-label shimmer uses a calmer constant-velocity sweep with alternating mission-control highlights. ### Patch Changes - [#62](#62) [`7fc36fd`](7fc36fd) - Repair invalid escape sequences and unescaped quotes in model-written tool arguments instead of failing the tool call. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Related Issue
No linked issue — directly requested feature; problem explained below.
Problem
While the agent works, the spinner shows a random rotating verb ("pythinking…", "marinating…") that carries no information about what the agent is actually doing. Users watching a long turn cannot tell whether the agent is reading, editing, or running tests without expanding tool cards.
What changed
Each tool call now carries a short, model-written intent that the working indicator shows live.
i("concise intent"). Injection happens on request-only schema clones — registered tools and their validation schemas are never mutated. Tools with an exact schema contract (StructuredOutput), an existingiproperty, or a non-object schema root are skipped.iafter JSON parse and before validation, so hooks, permission prompts, execution, and the persisted transcript all see clean arguments. The sanitized intent (control characters stripped, 120-char cap) rides thetool.call.startedevent as a new optionalintentfield.tool.call.startedintent, and clears on every turn/step/result boundary. Without an intent, the rotating labels behave exactly as before.tool_intentexperimental flag, default on; disable withPYTHINKER_CODE_EXPERIMENTAL_TOOL_INTENT=0or[experimental] tool_intent = false.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.Summary by CodeRabbit
New Features
Documentation
Tests