-
Notifications
You must be signed in to change notification settings - Fork 13.5k
feat(cli): show acknowledgment when user steering hint is processed #26498
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
c4d6009
9e83545
0351b52
e81028a
698fcc8
1470105
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| {"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"Starting a long task. First, I'll list the files."},{"functionCall":{"name":"list_directory","args":{"dir_path":"."}}}]},"finishReason":"STOP"}]}]} | ||
| {"method":"generateContent","response":{"candidates":[{"content":{"role":"model","parts":[{"text":"Understood. I'll focus on .txt files."}]},"finishReason":"STOP"}]}} | ||
| {"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I see the files. Since you want me to focus on .txt files, I will read file1.txt."},{"functionCall":{"name":"read_file","args":{"file_path":"file1.txt"}}}]},"finishReason":"STOP"}]}]} | ||
| {"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I have read file1.txt. Task complete."}]},"finishReason":"STOP"}]}]} |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -57,11 +57,23 @@ export const USER_STEERING_INSTRUCTION = | |||||||||||||||||||||||||||||||||||||||||||||||||
| 'Do not cancel/skip tasks unless the user explicitly cancels them. ' + | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Acknowledge the steering briefly and state the course correction.'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Wraps user input in XML-like tags to mitigate prompt injection. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const XML_CLOSING_TAG_RE = /<\/([^>]+)>/gi; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const CONTEXT_BREAKER_RE = /\]/g; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const NEWLINE_RE = /\r?\n/g; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| function sanitizeForWrapper(input: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return input | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(XML_CLOSING_TAG_RE, '<\\/$1>') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(CONTEXT_BREAKER_RE, '\\]') | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The implementation escapes the ] character instead of removing it. According to the repository rules, context-breaking characters like ] should be removed when sanitizing data from LLM-driven tools to prevent prompt injection. Escaping may not be sufficient depending on how the model parses the input.
Suggested change
References
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(NEWLINE_RE, '\\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+64
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The sanitization logic for XML closing tags and context breakers is currently ineffective. In JavaScript string literals, the backslash is an escape character. For characters that do not have a special escape sequence (like As a result:
To ensure a literal backslash is included in the replacement string (to escape the character for the LLM), you must use double backslashes in the string literal (e.g., '\\' to represent a single '\' in the string). Note that '\\n' is already correctly handled because '\n' is a special escape sequence for a newline, so '\\n' correctly produces a literal backslash followed by an 'n'.
Suggested change
References
Comment on lines
+64
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
References
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| function wrapInput(input: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return `<user_input>\n${input}\n</user_input>`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return `<user_input>\n${sanitizeForWrapper(input)}\n</user_input>`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| function wrapBackgroundOutput(input: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return `<background_output>\n${sanitizeForWrapper(input)}\n</background_output>`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export function buildUserSteeringHintPrompt(hintText: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -88,7 +100,7 @@ const BACKGROUND_COMPLETION_INSTRUCTION = | |||||||||||||||||||||||||||||||||||||||||||||||||
| * Wraps untrusted output in XML tags with inline instructions to treat it as data. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export function formatBackgroundCompletionForModel(output: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return `Background execution update:\n<background_output>\n${output}\n</background_output>\n\n${BACKGROUND_COMPLETION_INSTRUCTION}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return `Background execution update:\n${wrapBackgroundOutput(output)}\n\n${BACKGROUND_COMPLETION_INSTRUCTION}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const STEERING_ACK_INSTRUCTION = | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -113,15 +125,23 @@ function buildSteeringFallbackMessage(hintText: string): string { | |||||||||||||||||||||||||||||||||||||||||||||||||
| export async function generateSteeringAckMessage( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| llmClient: BaseLlmClient, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| hintText: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| options?: { signal?: AbortSignal }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<string> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const fallbackText = buildSteeringFallbackMessage(hintText); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (options?.signal?.aborted) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return fallbackText; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const abortController = new AbortController(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const timeout = setTimeout( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| () => abortController.abort(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| STEERING_ACK_TIMEOUT_MS, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const onParentAbort = () => abortController.abort(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| options?.signal?.addEventListener('abort', onParentAbort, { once: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return await generateFastAckText(llmClient, { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| instruction: STEERING_ACK_INSTRUCTION, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -134,6 +154,7 @@ export async function generateSteeringAckMessage( | |||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| clearTimeout(timeout); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| options?.signal?.removeEventListener('abort', onParentAbort); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The steering acknowledgment is scheduled via
queueMicrotaskbefore the check formodelSwitchedFromQuotaError. If a quota error occurs and the model is switched,submitQueryis skipped (line 2049), but the acknowledgment will still be rendered in the UI. This provides misleading feedback to the user as the hint was never actually sent to the model. Consider moving this logic after the quota error check or using a flag to ensure the acknowledgment only fires if the continuation turn actually proceeds.