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
8 changes: 7 additions & 1 deletion app/lib/workflows/runAgentStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -107,7 +113,7 @@ export async function runAgentStep(input: RunAgentStepInput): Promise<RunAgentSt
// without it, a hung tool keeps streamText awaiting forever on stop.
const tools = wrapToolsWithAbort(
addCacheControlToTools({
tools: buildAgentTools({ skills: input.agentContext.skills }),
tools: buildAgentTools({ skills: input.agentContext.skills, interactive: input.interactive }),
model: input.modelId,
}),
cancelController.signal,
Expand Down
2 changes: 2 additions & 0 deletions app/lib/workflows/runAgentWorkflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export type RunAgentWorkflowInput = {
*/
accountId: string;
modelId: string;
/** Whether an interactive user is present (chat UI) to answer ask_user_question. */
interactive?: boolean;
/**
* Optional chat title — used as context for the auto-commit
* message-generation LLM call.
Expand Down
12 changes: 12 additions & 0 deletions lib/agent/__tests__/buildAgentTools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,16 @@ describe("buildAgentTools", () => {
const tools = buildAgentTools() as Record<string, { execute?: unknown }>;
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");
});
});
9 changes: 7 additions & 2 deletions lib/agent/buildAgentTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 } : {}),
};
}
Expand Down
4 changes: 4 additions & 0 deletions lib/chat/buildRunAgentInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -49,6 +51,7 @@ export function buildRunAgentInput({
skills,
recoupAccessToken,
ephemeralKeyId,
interactive,
}: BuildRunAgentInputParams): RunAgentWorkflowInput {
const repoIds = parseGitHubRepoIdentifiers(cloneUrl);
const recoupOrgId = cloneUrl ? (extractOrgId(cloneUrl) ?? undefined) : undefined;
Expand All @@ -60,6 +63,7 @@ export function buildRunAgentInput({
accountId,
modelId,
sessionTitle,
interactive,
repoOwner: repoIds?.owner,
repoName: repoIds?.repo,
agentContext: {
Expand Down
1 change: 1 addition & 0 deletions lib/chat/runs/handleStartChatRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export async function handleStartChatRun(request: NextRequest): Promise<Response
skills: provisioned.skills,
recoupAccessToken: rawKey,
ephemeralKeyId: keyId,
interactive: false,
}),
]);

Expand Down
Loading