import { env } from "@code/env/convex"; import { ConvexError, v } from "convex/values"; import type { Id } from "./_generated/dataModel"; import { mutation, query } from "./_generated/server"; const requireAgent = (token: string): void => { if (token !== env.FLUE_DB_TOKEN) { throw new ConvexError("Invalid agent control token"); } }; export const listProjects = query({ args: { organizationId: v.id("organizations"), token: v.string() }, handler: async (ctx, args) => { requireAgent(args.token); const projects = await ctx.db .query("projects") .withIndex("by_organizationId_and_createdAt", (q) => q.eq("organizationId", args.organizationId) ) .order("desc") .take(50); return projects.map((project) => ({ _id: project._id, description: project.description ?? null, name: project.name, })); }, }); export const listEvidence = query({ args: { organizationId: v.id("organizations"), token: v.string() }, handler: async (ctx, args) => { requireAgent(args.token); const conversation = await ctx.db .query("conversations") .withIndex("by_organizationId", (q) => q.eq("organizationId", args.organizationId) ) .unique(); if (!conversation) { return []; } const messages = await ctx.db .query("conversationMessages") .withIndex("by_conversationId_and_ordinal", (q) => q.eq("conversationId", conversation._id) ) .order("desc") .take(100); const evidence: { messageId: string; rawText: string; createdAt: number; submissionId: string | null; }[] = []; for (const message of messages) { if (message.role !== "user") { continue; } const turn = await ctx.db.get(message.turnId); if (!turn || !["running", "completed"].includes(turn.status)) { continue; } const consumed = await ctx.db .query("signalSources") .withIndex("by_messageId", (q) => q.eq("messageId", message._id)) .first(); if (!consumed) { evidence.push({ createdAt: message.createdAt, messageId: String(message._id), rawText: message.content, submissionId: turn.submissionId ?? null, }); } } return evidence; }, }); export const createSignal = mutation({ args: { messageIds: v.array(v.string()), organizationId: v.id("organizations"), problemStatement: v.object({ constraints: v.array(v.string()), desiredOutcome: v.string(), summary: v.string(), title: v.string(), }), processedByAgentInstanceId: v.string(), projectId: v.id("projects"), token: v.string(), }, handler: async (ctx, args) => { requireAgent(args.token); const project = await ctx.db.get(args.projectId); if (!project || project.organizationId !== args.organizationId) { throw new ConvexError("Project not found"); } const conversation = await ctx.db .query("conversations") .withIndex("by_organizationId", (q) => q.eq("organizationId", args.organizationId) ) .unique(); if (!conversation || args.messageIds.length === 0) { throw new ConvexError("Signal evidence is required"); } const sourceKey = JSON.stringify(args.messageIds); const existing = await ctx.db .query("signals") .withIndex("by_organization_and_sourceKey", (q) => q.eq("organizationId", args.organizationId).eq("sourceKey", sourceKey) ) .unique(); if (existing) { return { signalId: existing._id }; } const messages = []; for (const messageId of args.messageIds) { const message = await ctx.db.get(messageId as Id<"conversationMessages">); if ( !message || message.conversationId !== conversation._id || message.role !== "user" ) { throw new ConvexError(`Source message not found: ${messageId}`); } const turn = await ctx.db.get(message.turnId); if (!turn || !["running", "completed"].includes(turn.status)) { throw new ConvexError(`Source message is not admitted: ${messageId}`); } messages.push(message); } const signalId = await ctx.db.insert("signals", { conversationId: conversation._id, createdAt: Date.now(), desiredOutcome: args.problemStatement.desiredOutcome, organizationId: args.organizationId, processedByAgentInstanceId: args.processedByAgentInstanceId, processedByAgentName: "zopu", projectId: args.projectId, sourceKey, summary: args.problemStatement.summary, title: args.problemStatement.title, }); for (const [ ordinal, constraint, ] of args.problemStatement.constraints.entries()) { await ctx.db.insert("signalConstraints", { ordinal, signalId, value: constraint, }); } for (const [ordinal, message] of messages.entries()) { await ctx.db.insert("signalSources", { messageId: message._id, ordinal, rawTextSnapshot: message.content, signalId, sourceCreatedAt: message.createdAt, }); } return { signalId }; }, });