Skip to content

Commit d22ecb0

Browse files
committed
fix: address review feedback on tool intent
1 parent 3102d2e commit d22ecb0

7 files changed

Lines changed: 53 additions & 7 deletions

File tree

.changeset/tool-intent-indicator.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@pythoughts/pythinker-code": minor
33
---
44

5-
Show what the agent is doing in the working indicator: each tool call now carries a short model-written intent, streamed live into the spinner label (for example "check failing test…") instead of a random verb; disable with `PYTHINKER_CODE_EXPERIMENTAL_TOOL_INTENT=0`.
5+
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 random verb; disable with `PYTHINKER_CODE_EXPERIMENTAL_TOOL_INTENT=0`.

apps/pythinker-code/src/tui/controllers/session-event-handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,7 @@ export class SessionEventHandler {
646646
) {
647647
return;
648648
}
649-
if (event.intent !== undefined) setLiveIntent(event.intent);
649+
setLiveIntent(event.intent);
650650
const { streamingUI } = this.host;
651651
streamingUI.flushNow();
652652
const { turnId, step } = streamingUI.getTurnContext();
@@ -682,7 +682,7 @@ export class SessionEventHandler {
682682
streamingUI.accumulateToolCallDelta(event.toolCallId, event.name, event.argumentsPart);
683683
const preview = streamingUI.getStreamingToolCallPreview(event.toolCallId);
684684
const intent = preview?.args['i'];
685-
if (typeof intent === 'string') setLiveIntent(intent);
685+
setLiveIntent(typeof intent === 'string' ? intent : undefined);
686686
if (
687687
preview !== undefined &&
688688
preview.name === 'DynamicWorkflow'

apps/pythinker-code/test/tui/tool-intent-label.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,32 @@ describe('tool intent thinking label', () => {
8080
});
8181
expect(formatThinkingSpinnerLabel(0)).toBe('thinking…');
8282
});
83+
84+
it('clears a stale intent when the next tool call has no intent', () => {
85+
const driver = new PythinkerTUI({} as never, makeStartupInput());
86+
const dispatch = (event: Event): void =>
87+
driver.sessionEventHandler.handleEvent(event, vi.fn());
88+
89+
dispatch({
90+
type: 'tool.call.started',
91+
agentId: 'main',
92+
sessionId: 'session-1',
93+
turnId: 1,
94+
toolCallId: 'call-1',
95+
name: 'echo',
96+
args: {},
97+
intent: 'check failing test',
98+
});
99+
dispatch({
100+
type: 'tool.call.started',
101+
agentId: 'main',
102+
sessionId: 'session-1',
103+
turnId: 1,
104+
toolCallId: 'call-2',
105+
name: 'StructuredOutput',
106+
args: {},
107+
});
108+
109+
expect(formatThinkingSpinnerLabel(0)).toBe('thinking…');
110+
});
83111
});

docs/configuration/config-files.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ advisor = "reviewer-model"
234234
| Field | Type | Default | Description |
235235
| --- | --- | --- | --- |
236236
| `micro_compaction` | `boolean` | `true` | Trim older large tool results from context while preserving recent conversation |
237-
| `tool_intent` | `boolean` | `true` | Ask the model to state a concise intent with each tool call and show it live in the working indicator; set `false` to return to the rotating label |
237+
| `tool_intent` | `boolean` | `true` | Ask the model to state a concise intent with eligible tool calls whose input schema accepts the injected field and show it live in the working indicator; set `false` to return to the rotating label |
238238

239239
## `services`
240240

docs/configuration/env-vars.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ Switches that control the behavior of subsystems such as telemetry, background t
135135
| `PYTHINKER_CODE_WORKFLOW_SIZE_GUIDELINE` | Override the advisory Dynamic Workflow size guideline injected into the tool guidance; takes higher priority than `config.toml` | `small`, `medium`, `large`, `unrestricted` |
136136
| `PYTHINKER_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process; `micro_compaction` is already enabled by default | `1`, `true`, `yes`, `on` |
137137
| `PYTHINKER_CODE_EXPERIMENTAL_MICRO_COMPACTION` | Override [`[experimental].micro_compaction`](./config-files.md#experimental) for this process | Truthy or falsy |
138-
| `PYTHINKER_CODE_EXPERIMENTAL_TOOL_INTENT` | Override [`[experimental].tool_intent`](./config-files.md#experimental) for this process. When on (the default), each tool call carries a short model-written intent that the working indicator shows live; set a falsy value to turn it off | Truthy or falsy |
138+
| `PYTHINKER_CODE_EXPERIMENTAL_TOOL_INTENT` | Override [`[experimental].tool_intent`](./config-files.md#experimental) for this process. When on (the default), eligible tool calls whose input schema accepts the injected field carry a short model-written intent that the working indicator shows live; set a falsy value to turn it off | Truthy or falsy |
139139
| `PYTHINKER_SHELL_PATH` | Override the Git Bash path on Windows (used when auto-detection fails) | Absolute path |
140140
| `PYTHINKER_MODEL_MAX_COMPLETION_TOKENS` | Hard cap on `max_completion_tokens` per LLM step; applies to the `pythinker` provider only | Positive integer; `0` or negative disables clamping |
141141
| `PYTHINKER_MODEL_TEMPERATURE` | Sampling temperature for every request; applies to the `pythinker` provider only (global — independent of `PYTHINKER_MODEL_NAME`) | Number, e.g. `0.3` |

packages/agent-core/src/loop/tool-intent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const INTENT_MAX_LENGTH = 120;
66
export const INTENT_OMIT_TOOLS: ReadonlySet<string> = new Set(['StructuredOutput']);
77

88
// oxlint-disable-next-line no-control-regex -- model-authored terminal text must not retain escape sequences.
9-
const ANSI_ESCAPE = /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|$))/gu;
9+
const ANSI_ESCAPE = /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007\u001B]*(?:\u0007|\u001B\\|$))/gu;
1010
const CONTROL_CHARACTER = /\p{Cc}/gu;
1111

1212
function isPlainRecord(value: unknown): value is Record<string, unknown> {
@@ -35,7 +35,7 @@ export function injectIntentIntoTools(tools: readonly ExecutableTool[]): Executa
3535
[INTENT_FIELD]: { type: 'string', description: 'concise intent' },
3636
...properties,
3737
},
38-
required: [INTENT_FIELD, ...required],
38+
required: [INTENT_FIELD, ...required.filter((entry) => entry !== INTENT_FIELD)],
3939
},
4040
resolveExecution: tool.resolveExecution.bind(tool),
4141
};

packages/agent-core/test/loop/tool-intent.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ describe('tool intent schema injection', () => {
5050
expect(injected?.parameters['required']).toEqual([INTENT_FIELD, 'value']);
5151
});
5252

53+
it('does not duplicate intent in required', () => {
54+
const tool = makeTool('test', {
55+
type: 'object',
56+
properties: { value: { type: 'string' } },
57+
required: [INTENT_FIELD],
58+
});
59+
60+
const [injected] = injectIntentIntoTools([tool]);
61+
62+
expect(injected?.parameters['required']).toEqual([INTENT_FIELD]);
63+
});
64+
5365
it('returns omitted tools unchanged', () => {
5466
const omittedTools = INTENT_OMIT_TOOLS as Set<string>;
5567
omittedTools.add('omitted');
@@ -104,6 +116,12 @@ describe('tool intent sanitization', () => {
104116
expect(sanitizeIntent('\u001B[31mcheck\n\u0007 failing\u001B[0m')).toBe('check failing');
105117
});
106118

119+
it('preserves visible text after an ST-terminated OSC hyperlink', () => {
120+
expect(sanitizeIntent('\u001B]8;;https://example.com\u001B\\click\u001B]8;;\u001B\\ done')).toBe(
121+
'click done',
122+
);
123+
});
124+
107125
it('caps the result by code points', () => {
108126
const sanitized = sanitizeIntent('🙂'.repeat(INTENT_MAX_LENGTH + 1));
109127
expect(Array.from(sanitized ?? '')).toHaveLength(INTENT_MAX_LENGTH);

0 commit comments

Comments
 (0)