/** * Feedback Interview Agent * * Conducts AI-powered interviews to help users articulate bug reports * and feedback through a conversational experience. * * Features: * - Context-aware questions based on app information * - Human-in-the-loop input collection * - Generates well-structured reports from conversations */ import { v } from "convex/values"; import { Agent, createTool, createThread } from "@convex-dev/agent"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { z } from "zod"; import { components, internal, api } from "../_generated/api"; import { action, query, mutation, internalMutation } from "../_generated/server"; import type { Id } from "../_generated/dataModel"; import type { BugSeverity, FeedbackType, FeedbackPriority } from "../schema"; // Helper to create OpenRouter provider with a specific API key // This is created dynamically because Convex components don't inherit parent env vars function createOpenRouterProvider(apiKey: string) { return createOpenAICompatible({ name: "openrouter", baseURL: "https://openrouter.ai/api/v1", headers: { Authorization: `Bearer ${apiKey}`, }, }); } // Agent name constant const AGENT_NAME = "Feedback Interview Agent"; // ============================================================================ // Tools // ============================================================================ /** * Tool to request user input during the interview */ const requestUserInput = createTool({ description: `Request input from the user during the interview. Use this to gather information about their bug report or feedback. Choose the appropriate input type: - "text": For open-ended questions - "choice": When user should pick from specific options - "form": When you need multiple pieces of information at once`, args: z.object({ inputType: z.enum(["text", "choice", "form"]).describe("Type of input"), prompt: z.string().describe("The question to ask"), placeholder: z.string().optional().describe("Placeholder for text input"), multiline: z.boolean().optional().describe("Allow multiline text"), options: z.array(z.object({ label: z.string(), value: z.string(), description: z.string().optional(), })).optional().describe("Options for choice input"), fields: z.array(z.object({ name: z.string(), label: z.string(), type: z.enum(["text", "number", "email", "textarea"]), required: z.boolean(), placeholder: z.string().optional(), })).optional().describe("Fields for form input"), }), handler: async (ctx, args): Promise => { const config: Record = {}; if (args.inputType === "text") { if (args.placeholder) config.placeholder = args.placeholder; if (args.multiline !== undefined) config.multiline = args.multiline; } else if (args.inputType === "choice") { if (!args.options || args.options.length < 2) { return "Error: Choice input requires at least 2 options"; } config.options = args.options; } else if (args.inputType === "form") { if (!args.fields || args.fields.length === 0) { return "Error: Form input requires at least 1 field"; } config.fields = args.fields; } const toolCallId = `feedback-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; if (!ctx.threadId) { return "Error: No active thread."; } const requestId = await ctx.runMutation(internal.inputRequests.createRequest, { threadId: ctx.threadId, toolCallId, inputType: args.inputType, prompt: args.prompt, config: Object.keys(config).length > 0 ? config as any : undefined, agentName: AGENT_NAME, userId: ctx.userId, }); return JSON.stringify({ status: "WAITING_FOR_USER_INPUT", requestId: requestId, message: `Waiting for user response: ${args.prompt}`, }); }, }); /** * Tool to generate the final bug report */ const generateBugReport = createTool({ description: "Generate the final bug report from the interview. Call this when you have gathered enough information about the bug.", args: z.object({ title: z.string().describe("A clear, concise title for the bug report"), description: z.string().describe("Detailed description of the bug"), severity: z.enum(["low", "medium", "high", "critical"]).describe("Bug severity"), reproductionSteps: z.array(z.string()).optional().describe("Steps to reproduce the bug"), featureArea: z.string().optional().describe("Which feature area is affected"), }), handler: async (ctx, args): Promise => { // Get the session by threadId (threadId is available in tool context) if (!ctx.threadId) { console.warn("generateBugReport: No threadId in context, session not updated"); return JSON.stringify({ status: "REPORT_GENERATED", report: { title: args.title, description: args.description, severity: args.severity, reproductionSteps: args.reproductionSteps, featureArea: args.featureArea, }, message: "Bug report has been generated. The interview is complete.", }); } // Query the session by threadId and update it const session = await ctx.runQuery(api.agents.feedbackInterviewAgent.getSessionByThread, { threadId: ctx.threadId, }); if (session) { await ctx.runMutation(internal.agents.feedbackInterviewAgent.updateSession, { sessionId: session._id, generatedTitle: args.title, generatedDescription: args.description, generatedSeverity: args.severity, generatedFeatureArea: args.featureArea, generatedReproSteps: args.reproductionSteps, isComplete: true, }); } else { console.warn("generateBugReport: No session found for threadId", ctx.threadId); } return JSON.stringify({ status: "REPORT_GENERATED", report: { title: args.title, description: args.description, severity: args.severity, reproductionSteps: args.reproductionSteps, featureArea: args.featureArea, }, message: "Bug report has been generated. The interview is complete.", }); }, }); /** * Tool to generate the final feedback */ const generateFeedback = createTool({ description: "Generate the final feedback from the interview. Call this when you have gathered enough information about the user's suggestion or request.", args: z.object({ title: z.string().describe("A clear, concise title for the feedback"), description: z.string().describe("Detailed description of the feedback"), type: z.enum(["feature_request", "change_request", "general"]).describe("Type of feedback"), priority: z.enum(["nice_to_have", "important", "critical"]).describe("Priority level"), featureArea: z.string().optional().describe("Which feature area this relates to"), }), handler: async (ctx, args): Promise => { // Get the session by threadId (threadId is available in tool context) if (!ctx.threadId) { console.warn("generateFeedback: No threadId in context, session not updated"); return JSON.stringify({ status: "FEEDBACK_GENERATED", report: { title: args.title, description: args.description, type: args.type, priority: args.priority, featureArea: args.featureArea, }, message: "Feedback has been generated. The interview is complete.", }); } // Query the session by threadId and update it const session = await ctx.runQuery(api.agents.feedbackInterviewAgent.getSessionByThread, { threadId: ctx.threadId, }); if (session) { await ctx.runMutation(internal.agents.feedbackInterviewAgent.updateSession, { sessionId: session._id, generatedTitle: args.title, generatedDescription: args.description, generatedFeedbackType: args.type, generatedPriority: args.priority, generatedFeatureArea: args.featureArea, isComplete: true, }); } else { console.warn("generateFeedback: No session found for threadId", ctx.threadId); } return JSON.stringify({ status: "FEEDBACK_GENERATED", report: { title: args.title, description: args.description, type: args.type, priority: args.priority, featureArea: args.featureArea, }, message: "Feedback has been generated. The interview is complete.", }); }, }); // ============================================================================ // Internal Mutations // ============================================================================ /** * Create an interview session */ export const createSession = internalMutation({ args: { threadId: v.string(), reportType: v.union(v.literal("bug"), v.literal("feedback")), reporterType: v.union(v.literal("staff"), v.literal("customer")), reporterId: v.string(), reporterEmail: v.string(), reporterName: v.string(), context: v.optional(v.string()), }, returns: v.id("interviewSessions"), handler: async (ctx, args) => { const now = Date.now(); return await ctx.db.insert("interviewSessions", { threadId: args.threadId, reportType: args.reportType, reporterType: args.reporterType, reporterId: args.reporterId, reporterEmail: args.reporterEmail, reporterName: args.reporterName, context: args.context, isComplete: false, createdAt: now, updatedAt: now, }); }, }); /** * Update an interview session with generated report */ export const updateSession = internalMutation({ args: { sessionId: v.id("interviewSessions"), generatedTitle: v.optional(v.string()), generatedDescription: v.optional(v.string()), generatedSeverity: v.optional(v.union( v.literal("low"), v.literal("medium"), v.literal("high"), v.literal("critical") )), generatedFeedbackType: v.optional(v.union( v.literal("feature_request"), v.literal("change_request"), v.literal("general") )), generatedPriority: v.optional(v.union( v.literal("nice_to_have"), v.literal("important"), v.literal("critical") )), generatedFeatureArea: v.optional(v.string()), generatedReproSteps: v.optional(v.array(v.string())), isComplete: v.optional(v.boolean()), }, returns: v.null(), handler: async (ctx, args) => { const { sessionId, ...updates } = args; await ctx.db.patch(sessionId, { ...updates, updatedAt: Date.now(), }); return null; }, }); // ============================================================================ // Public Queries // ============================================================================ /** * Get an interview session by thread ID */ export const getSessionByThread = query({ args: { threadId: v.string(), }, returns: v.union( v.object({ _id: v.id("interviewSessions"), _creationTime: v.number(), threadId: v.string(), reportType: v.union(v.literal("bug"), v.literal("feedback")), reporterType: v.union(v.literal("staff"), v.literal("customer")), reporterId: v.string(), reporterEmail: v.string(), reporterName: v.string(), context: v.optional(v.string()), generatedTitle: v.optional(v.string()), generatedDescription: v.optional(v.string()), generatedSeverity: v.optional(v.union( v.literal("low"), v.literal("medium"), v.literal("high"), v.literal("critical") )), generatedFeedbackType: v.optional(v.union( v.literal("feature_request"), v.literal("change_request"), v.literal("general") )), generatedPriority: v.optional(v.union( v.literal("nice_to_have"), v.literal("important"), v.literal("critical") )), generatedFeatureArea: v.optional(v.string()), generatedReproSteps: v.optional(v.array(v.string())), isComplete: v.boolean(), createdAt: v.number(), updatedAt: v.number(), }), v.null() ), handler: async (ctx, args) => { return await ctx.db .query("interviewSessions") .withIndex("by_thread", (q) => q.eq("threadId", args.threadId)) .first(); }, }); // ============================================================================ // Public Actions // ============================================================================ /** * Build dynamic instructions based on context */ function buildBugInstructions(context?: { appName?: string; appDescription?: string; featureAreas?: Array<{ name: string; description?: string }>; knownIssues?: Array<{ title: string; description?: string }>; customQuestions?: { bug?: string[] }; additionalInstructions?: string; }): string { let instructions = `You are a helpful assistant that interviews users to gather detailed information about bugs they've encountered. Your goal is to understand the bug thoroughly and generate a high-quality bug report.`; if (context?.appName) { instructions += `\n\n## Application Context\nYou are gathering bug reports for **${context.appName}**.`; if (context.appDescription) { instructions += ` ${context.appDescription}`; } } if (context?.featureAreas && context.featureAreas.length > 0) { instructions += `\n\n## Feature Areas\nThe application has these feature areas:\n`; context.featureAreas.forEach((area) => { instructions += `- **${area.name}**${area.description ? `: ${area.description}` : ""}\n`; }); instructions += `\nWhen appropriate, ask the user which feature area their bug relates to using a choice input.`; } if (context?.knownIssues && context.knownIssues.length > 0) { instructions += `\n\n## Known Issues\nThese are known issues. If the user's bug matches any, mention it:\n`; context.knownIssues.forEach((issue) => { instructions += `- **${issue.title}**${issue.description ? `: ${issue.description}` : ""}\n`; }); } instructions += `\n\n## Interview Flow 1. **Greet briefly** and ask what went wrong (the bug they encountered). 2. **Understand the bug**: - What happened? (the actual behavior) - What did they expect to happen? (expected behavior) - How often does this occur? (always, sometimes, once) 3. **Get reproduction info**: - What steps led to this bug? - Can they reliably reproduce it?`; if (context?.customQuestions?.bug && context.customQuestions.bug.length > 0) { instructions += `\n\n4. **Custom questions** (ask these if relevant):\n`; context.customQuestions.bug.forEach((q) => { instructions += ` - ${q}\n`; }); instructions += `\n5. **Assess impact**:`; } else { instructions += `\n\n4. **Assess impact**:`; } instructions += ` - How does this affect their work? - How urgent is this for them? 5. **Generate the report** using the generateBugReport tool when you have enough information.`; if (context?.additionalInstructions) { instructions += `\n\n## Additional Guidelines\n${context.additionalInstructions}`; } instructions += `\n\n## Guidelines - Be conversational but efficient - aim for 3-5 exchanges - Use choice inputs when there are clear options - Use text inputs for open-ended questions - Don't ask for technical details the user might not know - Focus on understanding the user experience - Generate the report as soon as you have enough info`; return instructions; } function buildFeedbackInstructions(context?: { appName?: string; appDescription?: string; featureAreas?: Array<{ name: string; description?: string }>; customQuestions?: { feedback?: string[] }; additionalInstructions?: string; }): string { let instructions = `You are a helpful assistant that interviews users to gather detailed feedback and suggestions. Your goal is to understand their idea thoroughly and generate well-structured feedback.`; if (context?.appName) { instructions += `\n\n## Application Context\nYou are gathering feedback for **${context.appName}**.`; if (context.appDescription) { instructions += ` ${context.appDescription}`; } } if (context?.featureAreas && context.featureAreas.length > 0) { instructions += `\n\n## Feature Areas\nThe application has these feature areas:\n`; context.featureAreas.forEach((area) => { instructions += `- **${area.name}**${area.description ? `: ${area.description}` : ""}\n`; }); instructions += `\nWhen appropriate, ask which feature area their feedback relates to using a choice input.`; } instructions += `\n\n## Interview Flow 1. **Greet briefly** and ask about their idea or suggestion. 2. **Understand the need**: - What problem does this solve? - What's their current workaround (if any)?`; if (context?.customQuestions?.feedback && context.customQuestions.feedback.length > 0) { instructions += `\n\n3. **Custom questions** (ask these if relevant):\n`; context.customQuestions.feedback.forEach((q) => { instructions += ` - ${q}\n`; }); instructions += `\n4. **Clarify the request**:`; } else { instructions += `\n\n3. **Clarify the request**:`; } instructions += ` - What would the ideal solution look like? - Is this a new feature, change to existing, or general feedback? 4. **Assess importance**: - How important is this to them? - Who else might benefit? 5. **Generate the feedback** using the generateFeedback tool when you have enough information.`; if (context?.additionalInstructions) { instructions += `\n\n## Additional Guidelines\n${context.additionalInstructions}`; } instructions += `\n\n## Guidelines - Be conversational but efficient - aim for 3-5 exchanges - Use choice inputs when there are clear options - Use text inputs for open-ended questions - Help users articulate their ideas clearly - Focus on understanding the value and impact - Generate the feedback as soon as you have enough info`; return instructions; } /** * Start a bug report interview */ export const startBugInterview = action({ args: { openRouterApiKey: v.string(), reporterType: v.union(v.literal("staff"), v.literal("customer")), reporterId: v.string(), reporterEmail: v.string(), reporterName: v.string(), context: v.optional(v.object({ appName: v.optional(v.string()), appDescription: v.optional(v.string()), featureAreas: v.optional(v.array(v.object({ name: v.string(), description: v.optional(v.string()), }))), knownIssues: v.optional(v.array(v.object({ title: v.string(), description: v.optional(v.string()), }))), customQuestions: v.optional(v.object({ bug: v.optional(v.array(v.string())), feedback: v.optional(v.array(v.string())), })), additionalInstructions: v.optional(v.string()), })), }, returns: v.object({ threadId: v.string(), sessionId: v.string(), response: v.string(), waitingForInput: v.boolean(), pendingRequest: v.optional(v.object({ requestId: v.string(), inputType: v.string(), prompt: v.string(), config: v.optional(v.any()), })), }), handler: async (ctx, args) => { // Create OpenRouter provider with the passed API key const openrouter = createOpenRouterProvider(args.openRouterApiKey); // Create a thread for the interview const threadId = await createThread(ctx, components.agent, { title: `Bug Report Interview: ${args.reporterName}`, }); // Create interview session const sessionId = await ctx.runMutation(internal.agents.feedbackInterviewAgent.createSession, { threadId, reportType: "bug", reporterType: args.reporterType, reporterId: args.reporterId, reporterEmail: args.reporterEmail, reporterName: args.reporterName, context: args.context ? JSON.stringify(args.context) : undefined, }); // Build dynamic instructions based on context const dynamicInstructions = buildBugInstructions(args.context); // Create agent with dynamic instructions const dynamicAgent = new Agent(components.agent, { name: "Bug Report Interview Agent", languageModel: openrouter.languageModel("anthropic/claude-sonnet-4"), instructions: dynamicInstructions, tools: { requestUserInput, generateBugReport, }, maxSteps: 30, }); // Start the interview // Note: Tools look up session by threadId, so we don't need to pass sessionId via customCtx let result; try { result = await dynamicAgent.generateText( ctx, { threadId }, { prompt: "Start the bug report interview. Greet the user briefly and ask what bug or issue they encountered." } ); } catch (error) { // Provide more descriptive error messages based on the error type const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) { throw new Error("AI service authentication failed. Please check your OPENROUTER_API_KEY."); } else if (errorMessage.includes("429") || errorMessage.includes("rate limit")) { throw new Error("AI service rate limited. Please try again in a few moments."); } else if (errorMessage.includes("503") || errorMessage.includes("unavailable")) { throw new Error("AI service temporarily unavailable. Please try again later."); } else if (errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT")) { throw new Error("AI service request timed out. Please try again."); } throw new Error(`Failed to start interview: ${errorMessage}`); } // Check for pending input request const pendingRequest = await ctx.runQuery( api.inputRequests.getPendingForThread, { threadId } ); if (pendingRequest) { return { threadId, sessionId, response: result.text, waitingForInput: true, pendingRequest: { requestId: pendingRequest._id, inputType: pendingRequest.inputType, prompt: pendingRequest.prompt, config: pendingRequest.config, }, }; } return { threadId, sessionId, response: result.text, waitingForInput: false, }; }, }); /** * Start a feedback interview */ export const startFeedbackInterview = action({ args: { openRouterApiKey: v.string(), reporterType: v.union(v.literal("staff"), v.literal("customer")), reporterId: v.string(), reporterEmail: v.string(), reporterName: v.string(), context: v.optional(v.object({ appName: v.optional(v.string()), appDescription: v.optional(v.string()), featureAreas: v.optional(v.array(v.object({ name: v.string(), description: v.optional(v.string()), }))), knownIssues: v.optional(v.array(v.object({ title: v.string(), description: v.optional(v.string()), }))), customQuestions: v.optional(v.object({ bug: v.optional(v.array(v.string())), feedback: v.optional(v.array(v.string())), })), additionalInstructions: v.optional(v.string()), })), }, returns: v.object({ threadId: v.string(), sessionId: v.string(), response: v.string(), waitingForInput: v.boolean(), pendingRequest: v.optional(v.object({ requestId: v.string(), inputType: v.string(), prompt: v.string(), config: v.optional(v.any()), })), }), handler: async (ctx, args) => { // Create OpenRouter provider with the passed API key const openrouter = createOpenRouterProvider(args.openRouterApiKey); // Create a thread for the interview const threadId = await createThread(ctx, components.agent, { title: `Feedback Interview: ${args.reporterName}`, }); // Create interview session const sessionId = await ctx.runMutation(internal.agents.feedbackInterviewAgent.createSession, { threadId, reportType: "feedback", reporterType: args.reporterType, reporterId: args.reporterId, reporterEmail: args.reporterEmail, reporterName: args.reporterName, context: args.context ? JSON.stringify(args.context) : undefined, }); // Build dynamic instructions based on context const dynamicInstructions = buildFeedbackInstructions(args.context); // Create agent with dynamic instructions const dynamicAgent = new Agent(components.agent, { name: "Feedback Interview Agent", languageModel: openrouter.languageModel("anthropic/claude-sonnet-4"), instructions: dynamicInstructions, tools: { requestUserInput, generateFeedback, }, maxSteps: 30, }); // Start the interview // Note: Tools look up session by threadId, so we don't need to pass sessionId via customCtx let result; try { result = await dynamicAgent.generateText( ctx, { threadId }, { prompt: "Start the feedback interview. Greet the user briefly and ask about their idea or suggestion." } ); } catch (error) { // Provide more descriptive error messages based on the error type const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) { throw new Error("AI service authentication failed. Please check your OPENROUTER_API_KEY."); } else if (errorMessage.includes("429") || errorMessage.includes("rate limit")) { throw new Error("AI service rate limited. Please try again in a few moments."); } else if (errorMessage.includes("503") || errorMessage.includes("unavailable")) { throw new Error("AI service temporarily unavailable. Please try again later."); } else if (errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT")) { throw new Error("AI service request timed out. Please try again."); } throw new Error(`Failed to start interview: ${errorMessage}`); } // Check for pending input request const pendingRequest = await ctx.runQuery( api.inputRequests.getPendingForThread, { threadId } ); if (pendingRequest) { return { threadId, sessionId, response: result.text, waitingForInput: true, pendingRequest: { requestId: pendingRequest._id, inputType: pendingRequest.inputType, prompt: pendingRequest.prompt, config: pendingRequest.config, }, }; } return { threadId, sessionId, response: result.text, waitingForInput: false, }; }, }); /** * Continue the interview after user provides input */ export const continueInterview = action({ args: { openRouterApiKey: v.string(), threadId: v.string(), sessionId: v.string(), requestId: v.id("feedbackInputRequests"), response: v.string(), context: v.optional(v.object({ appName: v.optional(v.string()), appDescription: v.optional(v.string()), featureAreas: v.optional(v.array(v.object({ name: v.string(), description: v.optional(v.string()), }))), knownIssues: v.optional(v.array(v.object({ title: v.string(), description: v.optional(v.string()), }))), customQuestions: v.optional(v.object({ bug: v.optional(v.array(v.string())), feedback: v.optional(v.array(v.string())), })), additionalInstructions: v.optional(v.string()), })), reportType: v.union(v.literal("bug"), v.literal("feedback")), }, returns: v.object({ response: v.string(), waitingForInput: v.boolean(), isComplete: v.boolean(), generatedReport: v.optional(v.object({ title: v.string(), description: v.string(), severity: v.optional(v.union( v.literal("low"), v.literal("medium"), v.literal("high"), v.literal("critical") )), type: v.optional(v.union( v.literal("feature_request"), v.literal("change_request"), v.literal("general") )), priority: v.optional(v.union( v.literal("nice_to_have"), v.literal("important"), v.literal("critical") )), featureArea: v.optional(v.string()), reproductionSteps: v.optional(v.array(v.string())), })), pendingRequest: v.optional(v.object({ requestId: v.string(), inputType: v.string(), prompt: v.string(), config: v.optional(v.any()), })), }), handler: async (ctx, args) => { // Create OpenRouter provider with the passed API key const openrouter = createOpenRouterProvider(args.openRouterApiKey); // Submit the response await ctx.runMutation( api.inputRequests.submitResponse, { requestId: args.requestId, response: args.response, } ); // Build dynamic instructions based on context and report type const dynamicInstructions = args.reportType === "bug" ? buildBugInstructions(args.context) : buildFeedbackInstructions(args.context); // Create agent with appropriate tools const tools = args.reportType === "bug" ? { requestUserInput, generateBugReport } : { requestUserInput, generateFeedback }; const dynamicAgent = new Agent(components.agent, { name: args.reportType === "bug" ? "Bug Report Interview Agent" : "Feedback Interview Agent", languageModel: openrouter.languageModel("anthropic/claude-sonnet-4"), instructions: dynamicInstructions, tools, maxSteps: 30, }); // Continue the agent // Note: Tools look up session by threadId, so we don't need to pass sessionId via customCtx let result; try { result = await dynamicAgent.generateText( ctx, { threadId: args.threadId }, { prompt: `The user responded: ${args.response} Please continue the interview based on their response.`, } ); } catch (error) { // Provide more descriptive error messages based on the error type const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) { throw new Error("AI service authentication failed. Please check your OPENROUTER_API_KEY."); } else if (errorMessage.includes("429") || errorMessage.includes("rate limit")) { throw new Error("AI service rate limited. Please try again in a few moments."); } else if (errorMessage.includes("503") || errorMessage.includes("unavailable")) { throw new Error("AI service temporarily unavailable. Please try again later."); } else if (errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT")) { throw new Error("AI service request timed out. Please try again."); } throw new Error(`Failed to continue interview: ${errorMessage}`); } // Check if interview is complete (session has generated report) const session = await ctx.runQuery(api.agents.feedbackInterviewAgent.getSessionByThread, { threadId: args.threadId, }); const isComplete = session?.isComplete ?? false; let generatedReport; if (isComplete && session) { generatedReport = { title: session.generatedTitle ?? "", description: session.generatedDescription ?? "", severity: session.generatedSeverity, type: session.generatedFeedbackType, priority: session.generatedPriority, featureArea: session.generatedFeatureArea, reproductionSteps: session.generatedReproSteps, }; } // Check for new pending request const pendingRequest = await ctx.runQuery( api.inputRequests.getPendingForThread, { threadId: args.threadId } ); if (pendingRequest) { return { response: result.text, waitingForInput: true, isComplete, generatedReport, pendingRequest: { requestId: pendingRequest._id, inputType: pendingRequest.inputType, prompt: pendingRequest.prompt, config: pendingRequest.config, }, }; } return { response: result.text, waitingForInput: false, isComplete, generatedReport, }; }, });