Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/agent/ask-and-steer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ While the agent responds, you can see the active request above the activity log

## Questions from the agent

You may see a question card when the agent needs a choice before continuing, such as which audience a section should address. A single choice submits immediately. A multi-question card shows how many answers remain. Unanswered questions expire after 15 minutes so the run can continue.
You may see a question card when the agent needs a choice before continuing, such as which audience a section should address. A single choice submits immediately. A multi-question card shows how many answers remain. Click the **X** to decline without answering — the agent continues with the message that you do not want to respond. Unanswered questions expire after 15 minutes so the run can continue.

Authentication failures show a sticky **Agent auth failed** toast with sign in guidance. Other run failures show **Agent run failed** and remain in the activity log.

Expand Down
23 changes: 23 additions & 0 deletions src/lib/components/AgentModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,21 @@
onAnswerQuestion(card.id, answers);
}

/** Decline-without-answering reply sent when the user Xs out of a
* question card. The agent is still paused on AskUserQuestion, so we
* must resolve the tool call — an empty answers map would leave the
* agent without a clear signal; this fixed string tells it to move on. */
const DECLINE_ANSWER = "None, I don't want to respond to the question";

function dismissQuestion(card: PendingUserQuestion) {
const answers: Record<string, string> = {};
for (const q of card.questions) {
answers[q.question] = DECLINE_ANSWER;
}
clearCardSelections(card);
onAnswerQuestion(card.id, answers);
}

/** Single-select click. A one-question card submits immediately (the
* common quick-clarification case keeps its one-click feel); a card
* with more questions records the choice radio-style and waits for
Expand Down Expand Up @@ -206,6 +221,14 @@
<div class="modal-header">
<HelpCircle size={14} />
<span>Question from agent</span>
<button
class="close-btn"
title="Dismiss"
aria-label="Dismiss question"
onclick={() => dismissQuestion(card)}
>
<X size={14} />
</button>
</div>
<div class="modal-body">
{#each card.questions as q, qIdx}
Expand Down
31 changes: 28 additions & 3 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -1992,22 +1992,47 @@
* server, which resolves the SDK's paused tool call and lets the
* agent continue with the answers in context. `answers` is keyed by
* question text (multi-select labels comma-joined) — the shape the
* SDK's AskUserQuestion input schema requires. */
* SDK's AskUserQuestion input schema requires. Dismiss (X) sends a
* fixed decline string for every question so the agent knows to move on. */
async function answerUserQuestion(id: string, answers: Record<string, string>) {
pendingUserQuestions.update((list) => list.filter((q) => q.id !== id));
const values = Object.values(answers);
const declined =
values.length > 0 &&
values.every((v) => v === "None, I don't want to respond to the question");
try {
await fetch('/api/ask-user-reply', {
const res = await fetch('/api/ask-user-reply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, answers })
});
if (!res.ok) {
const detail = await res.text().catch(() => res.statusText);
console.error('Answer failed:', res.status, detail);
pushHistory({
type: 'notification',
timestamp: Date.now(),
text: `Failed to send answer to agent (${res.status}). The question may have timed out.`,
priority: 'high'
});
return;
}
} catch (e) {
console.error('Answer failed:', e);
pushHistory({
type: 'notification',
timestamp: Date.now(),
text: 'Failed to send answer to agent (network error).',
priority: 'high'
});
return;
}
pushHistory({
type: 'user_action',
timestamp: Date.now(),
description: `Answered: ${Object.values(answers).join(' · ')}`
description: declined
? 'Declined question (no response)'
: `Answered: ${values.join(' · ')}`
});
}

Expand Down
22 changes: 22 additions & 0 deletions src/routes/api/ask-user-pending/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { json, error } from '@sveltejs/kit';
import { dev } from '$app/environment';
import type { RequestHandler } from './$types';
import { registerPendingAskUser } from '$lib/server/ask-user-state';

/**
* POST /api/ask-user-pending (dev only)
* body: { id?: string }
*
* Parks a pending AskUserQuestion resolver so `/api/ask-user-reply` can
* be exercised without a live agent run (e.g. dismiss-via-X demos).
* No-ops in production. */
export const POST: RequestHandler = async ({ request }) => {
if (!dev) throw error(404, 'Not found');
const body = await request.json().catch(() => ({}));
const id =
typeof body.id === 'string' && body.id
? body.id
: 'q_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
registerPendingAskUser(id, () => {}, 15 * 60_000);
return json({ id });
};
Loading