import { v } from 'convex/values'; import type { Doc } from './_generated/dataModel'; import { mutation, query } from './_generated/server'; import { getOwnedSpoon, getRequiredUserId, optionalText } from './model'; const reasoningEffort = v.union( v.literal('none'), v.literal('minimal'), v.literal('low'), v.literal('medium'), v.literal('high'), v.literal('xhigh'), ); const agentModel = v.union( v.literal('gpt-5.1-codex'), v.literal('gpt-5.5'), v.literal('gpt-5.5-pro'), v.literal('gpt-5.4'), v.literal('gpt-5.4-mini'), ); const defaults = { enabled: true, branchPrefix: 'spoon/agent', agentModel: 'gpt-5.1-codex', reasoningEffort: 'high' as const, maxJobDurationMs: 1_800_000, maxOutputBytes: 200_000, }; export const getForSpoon = query({ args: { spoonId: v.id('spoons') }, handler: async (ctx, { spoonId }) => { const ownerId = await getRequiredUserId(ctx); const spoon = await getOwnedSpoon(ctx, spoonId, ownerId); const settings = await ctx.db .query('spoonAgentSettings') .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId)) .first(); return ( settings ?? { spoonId, ownerId, ...defaults, defaultBaseBranch: spoon.forkDefaultBranch ?? spoon.upstreamDefaultBranch, } ); }, }); export const update = mutation({ args: { spoonId: v.id('spoons'), enabled: v.optional(v.boolean()), defaultBaseBranch: v.optional(v.string()), branchPrefix: v.optional(v.string()), installCommand: v.optional(v.string()), checkCommand: v.optional(v.string()), testCommand: v.optional(v.string()), agentModel: v.optional(agentModel), reasoningEffort: v.optional(reasoningEffort), maxJobDurationMs: v.optional(v.number()), maxOutputBytes: v.optional(v.number()), }, handler: async (ctx, args) => { const ownerId = await getRequiredUserId(ctx); const spoon = await getOwnedSpoon(ctx, args.spoonId, ownerId); const now = Date.now(); let settings = await ctx.db .query('spoonAgentSettings') .withIndex('by_spoon', (q) => q.eq('spoonId', args.spoonId)) .first(); if (!settings) { const id = await ctx.db.insert('spoonAgentSettings', { spoonId: args.spoonId, ownerId, ...defaults, defaultBaseBranch: spoon.forkDefaultBranch ?? spoon.upstreamDefaultBranch, createdAt: now, updatedAt: now, }); settings = await ctx.db.get(id); } if (!settings) throw new Error('Agent settings not found.'); const patch: Partial> = { updatedAt: now }; if (args.enabled !== undefined) patch.enabled = args.enabled; if (args.defaultBaseBranch !== undefined) { patch.defaultBaseBranch = optionalText(args.defaultBaseBranch); } if (args.branchPrefix !== undefined) { patch.branchPrefix = optionalText(args.branchPrefix) ?? defaults.branchPrefix; } if (args.installCommand !== undefined) { patch.installCommand = optionalText(args.installCommand); } if (args.checkCommand !== undefined) { patch.checkCommand = optionalText(args.checkCommand); } if (args.testCommand !== undefined) { patch.testCommand = optionalText(args.testCommand); } if (args.agentModel !== undefined) { patch.agentModel = optionalText(args.agentModel) ?? defaults.agentModel; } if (args.reasoningEffort !== undefined) { patch.reasoningEffort = args.reasoningEffort; } if (args.maxJobDurationMs !== undefined) { patch.maxJobDurationMs = Math.max(60_000, args.maxJobDurationMs); } if (args.maxOutputBytes !== undefined) { patch.maxOutputBytes = Math.max(10_000, args.maxOutputBytes); } await ctx.db.patch(settings._id, patch); return { success: true }; }, });