Skip to content

Commit 84c5a95

Browse files
tlgimenescursoragentclauderafavalls
authored
feat(decopilot): add user_ask built-in tool (#2419)
* feat(decopilot): add user_ask built-in tool - Add user_ask tool for gathering user input during task execution - Support text, choice, and confirm input types - Integrate built-in tools into decopilot stream endpoint - Simplify base prompt for decopilot Co-authored-by: Cursor <cursoragent@cursor.com> * feat(storage): upsert thread messages for user_ask resolution - Add ON CONFLICT upsert for thread messages (PostgreSQL) - Merge config messages with thread history by id for tool result updates - Deduplicate messages by id before batch insert - Add conversation and threads tests Co-authored-by: Cursor <cursoragent@cursor.com> * feat(chat): add user_ask UI and integration - Add ChatHighlight with UserAskQuestionHighlight for user_ask prompts - Add UserAskQuestionPart for inline placeholder in assistant messages - Support text, choice, and confirm input types with forms - Wire user_ask into chat context, input, and thread cache - Add UserAskToolPart type for tool-user_ask parts Co-authored-by: Cursor <cursoragent@cursor.com> * docs: add user_ask built-in tool API reference Co-authored-by: Cursor <cursoragent@cursor.com> * docs: add implementation plan for cache/db inconsistency fix Create comprehensive plan to fix critical cache consistency issues: - Add error propagation and user notifications - Implement save status tracking endpoint - Add client-side save status polling - Remove immediate cache updates - Reduce stale time from 30s to 5s - Document message transformation contract - Add E2E test stubs Fixes: main-99k Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * docs: remove test writing from cache/db fix plan Remove all test writing and test running steps for faster implementation: - Remove error type tests - Remove save status endpoint tests - Remove useSaveStatus hook tests - Remove E2E test task entirely - Keep only implementation, type check, format, and commit steps - Update task numbering (7 tasks instead of 9) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat(decopilot): enhance conversation processing and model compatibility - Introduced a message ID generator for unique message identification. - Updated DECOPILOT_BASE_PROMPT to return a structured ChatMessage. - Refactored processConversation to accept a memory object and handle message processing more efficiently. - Added ensureModelCompatibility function to validate message compatibility with model capabilities. - Improved error handling for message processing and ensured only one non-system message is present. - Added new model-compat.ts file for extensible model compatibility checks. This update enhances the overall robustness and maintainability of the Decopilot conversation handling. * refactor(chat): extract UserAskQuestionPart into dedicated component and fix types Move the inline UserAskQuestionPart from user-ask-question.tsx into its own file under parts/, handling output-available, output-error, and output-denied states. Update MessageAssistant and MessagePair to use the ChatMessage type for proper UITools type narrowing, removing the need for unsafe casts. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(chat): unify user-ask form with combined schema and improved UX Replace per-question forms with a single unified react-hook-form instance backed by a combined Zod schema. For multiple questions, show a tabbed layout with check-circle progress indicators and a "Next" / "Submit all" button flow. Simplify the highlight component props (disabled boolean instead of status) and clean up unused schema exports. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(decopilot): consolidate Memory interface into exported class Remove the separate Memory interface from types.ts and rename the private ThreadMemory class to Memory, exporting it directly from memory.ts. Moves MemoryConfig to memory.ts as well. This eliminates an unnecessary abstraction layer since there was only ever one implementation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(decopilot): use consistent DEFAULT_THREAD_TITLE for title generation Extract "New chat" as DEFAULT_THREAD_TITLE constant and use it both in thread creation and to determine whether title generation should run. Previously the title check relied on message count which could skip generation on conversation resumption. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(decopilot): add windowSize to Memory.loadHistory with user-role trimming - Add optional windowSize param to loadHistory(); fetch latest N messages via sort desc - Extend listMessages with sort option (asc|desc) in storage port and implementation - Trim window to start with user role; return [] if no user message in window - Remove redundant getPrunedHistory() - Update conversation tests with mockListMessages for desc sort Co-authored-by: Cursor <cursoragent@cursor.com> * chore(decopilot): remove unused ensureUser export Co-authored-by: Cursor <cursoragent@cursor.com> * test(decopilot): align user_ask tests with current description Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(decopilot): simplify user_ask tool description and schema Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(decopilot): update types, schemas, model provider and routes Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(chat): update chat components and MCP config form Co-authored-by: Cursor <cursoragent@cursor.com> * feat(chat): fix last message pair layout for bottom visibility Co-authored-by: Cursor <cursoragent@cursor.com> * docs: add user-ask tool and tool output deduplication plans Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(decopilot): simplify title generation with promise-based API Replace callback-based onTitle pattern with a direct Promise<string | null> return value, making the control flow easier to follow. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(decopilot): preserve assistant/tool context when no user message in window When the history window contains no user message, return the full windowed messages instead of an empty array so that assistant and tool context is retained for follow-up turns. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(decopilot): split system/request messages at validation boundary Extract splitRequestMessages() to separate system messages from the single request message during request validation, simplifying processConversation by removing internal message splitting and the instruction parameter. Also adopts the promise-based title generation in the streaming route. Co-authored-by: Cursor <cursoragent@cursor.com> * update design * refactor(ui): use Tailwind shrink-0 in page header Co-authored-by: Cursor <cursoragent@cursor.com> * feat(chat): improve user_ask UI - remove disabled state, add skip, use isStreaming for loading Co-authored-by: Cursor <cursoragent@cursor.com> * chore: remove refs/chatbot-tool-usage.md and docs/plans/ Co-authored-by: Cursor <cursoragent@cursor.com> * fix(decopilot): ensure unique timestamps for request/response message pairs Use incrementing millisecond offsets when saving request and response messages to avoid identical createdAt/updatedAt timestamps, which could cause ordering ambiguity or deduplication issues downstream. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(chat): remove copy-on-click behavior from user messages Remove unused extractTextFromMessage helper and useCopy hook usage, simplifying the click handler to only focus and scroll to the pair. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): hide user-ask prompt when no pending parts Return null early when pendingParts is empty to avoid rendering an empty prompt container. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: rafavalls <valls@deco.cx>
1 parent 9a21022 commit 84c5a95

50 files changed

Lines changed: 2972 additions & 729 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
---
2+
title: user_ask
3+
description: Interactive user input tool for gathering responses during agent execution
4+
icon: MessageCircle
5+
---
6+
7+
import Callout from "../../../../components/ui/Callout.astro";
8+
9+
The `user_ask` built-in tool allows agents to pause execution and request input from users interactively. It supports three input types: free-form text, multiple-choice selection, and yes/no confirmation.
10+
11+
<Callout type="info">
12+
**Client-side tool**: `user_ask` has no server-side execution. The tool call streams to the client, renders an interactive UI component, and returns the user's response to continue the agent's workflow.
13+
</Callout>
14+
15+
## Input Types
16+
17+
### text
18+
19+
Free-form text input for open-ended questions.
20+
21+
```typescript
22+
{
23+
prompt: "What is your preferred contact email?",
24+
type: "text",
25+
default: "user@example.com" // optional
26+
}
27+
```
28+
29+
**Use cases:**
30+
- Collecting names, emails, or descriptions
31+
- Requesting custom values or parameters
32+
- Gathering user feedback or notes
33+
34+
### choice
35+
36+
Multiple-choice selection from a list of options.
37+
38+
```typescript
39+
{
40+
prompt: "Select your preferred deployment environment",
41+
type: "choice",
42+
options: ["development", "staging", "production"],
43+
default: "staging" // optional
44+
}
45+
```
46+
47+
**Validation:**
48+
- `options` array is required
49+
- Must contain at least 2 items
50+
- `default` must match one of the options if provided
51+
52+
**Use cases:**
53+
- Selecting from predefined categories
54+
- Choosing configuration options
55+
- Picking from a list of available resources
56+
57+
### confirm
58+
59+
Yes/No confirmation for binary decisions.
60+
61+
```typescript
62+
{
63+
prompt: "Do you want to proceed with database migration?",
64+
type: "confirm",
65+
default: "yes" // optional: "yes" or "no"
66+
}
67+
```
68+
69+
**Use cases:**
70+
- Confirming destructive operations
71+
- Approving next steps in a workflow
72+
- Validating assumptions before proceeding
73+
74+
## Output Schema
75+
76+
All input types return the same output structure:
77+
78+
```typescript
79+
{
80+
response: string
81+
}
82+
```
83+
84+
**Response values by type:**
85+
- **text**: User's free-form input as a string
86+
- **choice**: The selected option from the `options` array
87+
- **confirm**: Either `"yes"` or `"no"`
88+
89+
## Examples
90+
91+
### Example 1: Text Input
92+
93+
**Agent request:**
94+
```typescript
95+
{
96+
prompt: "What domain name should we use for the production deployment?",
97+
type: "text",
98+
default: "app.example.com"
99+
}
100+
```
101+
102+
**User response:**
103+
```typescript
104+
{
105+
response: "production.myapp.io"
106+
}
107+
```
108+
109+
### Example 2: Choice Input
110+
111+
**Agent request:**
112+
```typescript
113+
{
114+
prompt: "Which authentication provider should we configure?",
115+
type: "choice",
116+
options: ["GitHub", "Google", "Microsoft", "Email"],
117+
default: "GitHub"
118+
}
119+
```
120+
121+
**User response:**
122+
```typescript
123+
{
124+
response: "Google"
125+
}
126+
```
127+
128+
### Example 3: Confirm Input
129+
130+
**Agent request:**
131+
```typescript
132+
{
133+
prompt: "This will delete all test data. Continue?",
134+
type: "confirm",
135+
default: "no"
136+
}
137+
```
138+
139+
**User response:**
140+
```typescript
141+
{
142+
response: "yes"
143+
}
144+
```
145+
146+
### Example 4: Multi-Step Workflow
147+
148+
Agents can chain multiple `user_ask` calls to build complex workflows:
149+
150+
```typescript
151+
// Step 1: Confirm operation
152+
{
153+
prompt: "Ready to deploy the application?",
154+
type: "confirm"
155+
}
156+
// Response: { response: "yes" }
157+
158+
// Step 2: Select environment
159+
{
160+
prompt: "Select target environment",
161+
type: "choice",
162+
options: ["staging", "production"]
163+
}
164+
// Response: { response: "production" }
165+
166+
// Step 3: Get confirmation details
167+
{
168+
prompt: "Enter your approval ticket number",
169+
type: "text"
170+
}
171+
// Response: { response: "TICKET-12345" }
172+
```
173+
174+
## Behavior
175+
176+
### Execution Flow
177+
178+
1. **Agent invokes tool**: The agent calls `user_ask` with input parameters
179+
2. **Streaming to client**: Tool call streams to the client as it's generated
180+
3. **UI renders prompt**: Client displays an interactive input component
181+
4. **User responds**: User provides input through the UI
182+
5. **Response returns**: User's response is sent back to the agent
183+
6. **Agent continues**: Execution resumes with the user's input
184+
185+
### Blocking Behavior
186+
187+
<Callout type="warning">
188+
The agent's execution **blocks** while waiting for user input. The tool call will not complete until the user provides a response.
189+
</Callout>
190+
191+
This blocking behavior is intentional and allows agents to:
192+
- Request clarification mid-task
193+
- Confirm destructive operations before execution
194+
- Gather missing parameters on-demand
195+
196+
### Streaming
197+
198+
Tool calls stream progressively as the agent generates them:
199+
200+
1. **`input-streaming`**: Tool parameters are being generated
201+
2. **`input-available`**: Complete input received, waiting for user response (this is the default state after input-streaming completes and before output is available)
202+
3. **`output-available`**: User has responded, output ready
203+
204+
The UI updates in real-time as the tool call streams, providing immediate feedback to users. The component checks for explicit states (`input-streaming` and `output-available`) and renders the interactive prompt for any other state where input is complete.
205+
206+
## UI Rendering
207+
208+
The client renders different UI components based on the input type:
209+
210+
### Text Input
211+
212+
Renders a text input field with the default value pre-filled if provided.
213+
214+
**Features:**
215+
- Single-line input field
216+
- Placeholder text: "Type your response..."
217+
- Default value pre-filled in the input field if provided
218+
- Submit button or Enter key to confirm
219+
220+
### Choice Input
221+
222+
Renders all options as a vertical stack of outline buttons.
223+
224+
**Features:**
225+
- Each option appears as a clickable outline button
226+
- Options are displayed in a vertical stack
227+
- Clicking any option immediately submits that choice
228+
- No explicit submit button needed
229+
230+
### Confirm Input
231+
232+
Renders two prominent buttons: Yes and No.
233+
234+
**Features:**
235+
- Clear visual distinction between Yes/No actions
236+
- Default option highlighted if provided
237+
- Keyboard shortcuts (Y/N) for quick response
238+
239+
<Callout type="tip">
240+
The UI automatically focuses the input element when rendered, allowing users to respond immediately without clicking.
241+
</Callout>
242+
243+
## Error Handling
244+
245+
### Validation Rules
246+
247+
**Choice type validation:**
248+
```typescript
249+
// ✅ Valid
250+
{ type: "choice", options: ["A", "B"] }
251+
252+
// ❌ Invalid: missing options
253+
{ type: "choice" }
254+
255+
// ❌ Invalid: less than 2 options
256+
{ type: "choice", options: ["A"] }
257+
```
258+
259+
**Default value validation:**
260+
```typescript
261+
// ✅ Valid: default matches an option
262+
{ type: "choice", options: ["A", "B", "C"], default: "B" }
263+
264+
// ⚠️ Warning: default doesn't match (will be ignored)
265+
{ type: "choice", options: ["A", "B", "C"], default: "D" }
266+
```
267+
268+
### Error Messages
269+
270+
| Error | Cause | Resolution |
271+
|-------|-------|------------|
272+
| `Options array with at least 2 items required for 'choice' type` | `choice` type used without valid `options` | Provide an `options` array with 2+ items |
273+
| `Invalid input type` | Unknown value for `type` field | Use `"text"`, `"choice"`, or `"confirm"` |
274+
| `Prompt is required` | Missing or empty `prompt` field | Provide a non-empty string for `prompt` |
275+
276+
## Best Practices
277+
278+
### 1. Provide Clear Prompts
279+
280+
Write prompts that clearly communicate what input is needed and why.
281+
282+
```typescript
283+
// ✅ Good: specific and actionable
284+
{
285+
prompt: "Enter the API endpoint URL for the production environment",
286+
type: "text"
287+
}
288+
289+
// ❌ Poor: vague and unclear
290+
{
291+
prompt: "URL?",
292+
type: "text"
293+
}
294+
```
295+
296+
### 2. Use Appropriate Input Types
297+
298+
Choose the input type that best matches the expected response.
299+
300+
```typescript
301+
// ✅ Good: binary decision uses confirm
302+
{
303+
prompt: "Enable debug logging?",
304+
type: "confirm"
305+
}
306+
307+
// ❌ Poor: binary decision uses text
308+
{
309+
prompt: "Enable debug logging? (yes/no)",
310+
type: "text"
311+
}
312+
```
313+
314+
### 3. Provide Sensible Defaults
315+
316+
Defaults help users respond quickly and reduce friction.
317+
318+
```typescript
319+
// ✅ Good: common default pre-selected
320+
{
321+
prompt: "Select log level",
322+
type: "choice",
323+
options: ["debug", "info", "warn", "error"],
324+
default: "info"
325+
}
326+
327+
// ⚠️ Acceptable but less helpful: no default
328+
{
329+
prompt: "Select log level",
330+
type: "choice",
331+
options: ["debug", "info", "warn", "error"]
332+
}
333+
```
334+
335+
### 4. Limit Choice Options
336+
337+
Keep the number of options manageable for better UX.
338+
339+
```typescript
340+
// ✅ Good: focused set of options
341+
{
342+
prompt: "Select deployment region",
343+
type: "choice",
344+
options: ["us-east", "us-west", "eu-central", "ap-southeast"]
345+
}
346+
347+
// ⚠️ Acceptable but consider pagination or search for larger sets
348+
{
349+
prompt: "Select deployment region",
350+
type: "choice",
351+
options: [...50 regions...]
352+
}
353+
```
354+
355+
## Technical Details
356+
357+
### Framework
358+
359+
`user_ask` is implemented using the **AI SDK** (`ai` package) rather than MCP's `defineTool()`:
360+
361+
```typescript
362+
import { tool, zodSchema } from "ai";
363+
364+
export const userAskTool = tool({
365+
description: "Ask the user a question and wait for their response...",
366+
inputSchema: zodSchema(UserAskInputSchema),
367+
outputSchema: zodSchema(UserAskOutputSchema),
368+
// NO execute function - client-side only
369+
});
370+
```
371+
372+
This design allows the tool to be handled entirely on the client side without server-side execution logic.
373+
374+
### Protocol
375+
376+
Tool calls follow the standard AI SDK tool call protocol:
377+
378+
1. Agent generates tool call with `toolCallId`
379+
2. Tool call streams to client via SSE
380+
3. Client renders UI based on tool input
381+
4. User provides response
382+
5. Client sends tool result back to agent
383+
6. Agent processes result and continues
384+
385+
### Implementation Location
386+
387+
**Server-side definition:**
388+
- `/apps/mesh/src/api/routes/decopilot/built-in-tools/user-ask.ts`
389+
390+
**Client-side UI:**
391+
- `/apps/mesh/src/web/components/chat/highlight.tsx`
392+
393+
**Type definitions:**
394+
- `/apps/mesh/src/web/components/chat/types.ts` (UserAskToolPart)
395+
396+
<Callout type="info">
397+
Built-in tools are automatically registered with the Decopilot API and available in all chat sessions without additional configuration.
398+
</Callout>

0 commit comments

Comments
 (0)