diff --git a/app/lib/workflows/runAgentStep.ts b/app/lib/workflows/runAgentStep.ts index aebebfe46..33db67143 100644 --- a/app/lib/workflows/runAgentStep.ts +++ b/app/lib/workflows/runAgentStep.ts @@ -36,6 +36,12 @@ export type RunAgentStepInput = { * is added to `experimental_context` right before each model call. */ agentContext: DurableAgentContext; + /** + * Whether a user is present to answer `ask_user_question`. Interactive chat + * sets true (default); headless runs (`/api/chat/runs`, `customer-prompt-task`) + * set false so the tool is withheld — no one is there to reply. + */ + interactive?: boolean; /** * Stable id to assign to the assistant message produced by this * step. Generated once in `runAgentWorkflow` so: @@ -107,7 +113,7 @@ export async function runAgentStep(input: RunAgentStepInput): Promise { const tools = buildAgentTools() as Record; expect(tools.ask_user_question?.execute).toBeUndefined(); }); + it("omits `ask_user_question` in non-interactive (headless) mode — no user to answer", () => { + const tools = buildAgentTools({ interactive: false }); + expect(tools).not.toHaveProperty("ask_user_question"); + for (const name of ALWAYS_PRESENT.filter(n => n !== "ask_user_question")) { + expect(tools).toHaveProperty(name); + } + }); + + it("keeps `ask_user_question` when interactive (default and explicit true)", () => { + expect(buildAgentTools()).toHaveProperty("ask_user_question"); + expect(buildAgentTools({ interactive: true })).toHaveProperty("ask_user_question"); + }); }); diff --git a/lib/agent/buildAgentTools.ts b/lib/agent/buildAgentTools.ts index 728334b11..1ca91a2db 100644 --- a/lib/agent/buildAgentTools.ts +++ b/lib/agent/buildAgentTools.ts @@ -38,8 +38,13 @@ import type { SkillMetadata } from "@/lib/skills/skillTypes"; * @param options.skills - Discovered skill catalog. When empty / undefined, * `skill` is omitted from the tool record so the model doesn't see it. */ -export function buildAgentTools(options: { skills?: SkillMetadata[] } = {}) { +export function buildAgentTools(options: { skills?: SkillMetadata[]; interactive?: boolean } = {}) { const hasSkills = (options.skills?.length ?? 0) > 0; + // ask_user_question has no server execute — only a streaming chat UI can + // fulfill it. In headless/async runs (customer-prompt-task, /api/chat/runs) + // there is no user to answer, so it's a dead-end; omit it there and force the + // agent to act (send an honest result or stop) rather than hang on a question. + const interactive = options.interactive ?? true; return { bash: bashTool, read: readFileTool, @@ -50,7 +55,7 @@ export function buildAgentTools(options: { skills?: SkillMetadata[] } = {}) { todo_write: todoWriteTool, web_fetch: webFetchTool, task: taskTool, - ask_user_question: askUserQuestionTool, + ...(interactive ? { ask_user_question: askUserQuestionTool } : {}), ...(hasSkills ? { skill: skillTool } : {}), }; } diff --git a/lib/chat/buildRunAgentInput.ts b/lib/chat/buildRunAgentInput.ts index 0630bf562..cc487ddd9 100644 --- a/lib/chat/buildRunAgentInput.ts +++ b/lib/chat/buildRunAgentInput.ts @@ -23,6 +23,8 @@ export type BuildRunAgentInputParams = { * `/api/chat/runs`). Omitted when absent so the service key never leaks. */ recoupAccessToken?: string; + /** True for interactive chat (default), false for headless runs (withholds ask_user_question). */ + interactive?: boolean; /** * Row id of an ephemeral key minted for a headless run, so the workflow can * delete it on run end (recoupable/chat#1813). Interactive callers omit it. @@ -49,6 +51,7 @@ export function buildRunAgentInput({ skills, recoupAccessToken, ephemeralKeyId, + interactive, }: BuildRunAgentInputParams): RunAgentWorkflowInput { const repoIds = parseGitHubRepoIdentifiers(cloneUrl); const recoupOrgId = cloneUrl ? (extractOrgId(cloneUrl) ?? undefined) : undefined; @@ -60,6 +63,7 @@ export function buildRunAgentInput({ accountId, modelId, sessionTitle, + interactive, repoOwner: repoIds?.owner, repoName: repoIds?.repo, agentContext: { diff --git a/lib/chat/runs/handleStartChatRun.ts b/lib/chat/runs/handleStartChatRun.ts index 2db6a305a..b9d0125a2 100644 --- a/lib/chat/runs/handleStartChatRun.ts +++ b/lib/chat/runs/handleStartChatRun.ts @@ -64,6 +64,7 @@ export async function handleStartChatRun(request: NextRequest): Promise