Files
spoon/packages/backend/convex/agentJobsNode.ts
T
2026-06-22 10:37:26 -04:00

112 lines
3.3 KiB
TypeScript

'use node';
import { ConvexError, v } from 'convex/values';
import type { Doc } from './_generated/dataModel';
import { internal } from './_generated/api';
import { action } from './_generated/server';
import { decryptSecret } from './secretCrypto';
type ClaimedJob = {
job: Doc<'agentJobs'>;
spoon: Doc<'spoons'> | null;
aiSettings: Doc<'userAiSettings'> | null;
aiProviderProfile: Doc<'aiProviderProfiles'> | null;
agentSettings: Doc<'spoonAgentSettings'> | null;
secrets: Doc<'spoonSecrets'>[];
};
type WorkerClaim = {
job: Doc<'agentJobs'>;
spoon: Doc<'spoons'>;
openai: {
apiKey?: string;
model: string;
reasoningEffort: Doc<'agentJobs'>['reasoningEffort'];
};
aiProviderProfile?: {
id: string;
name: string;
provider: Doc<'aiProviderProfiles'>['provider'];
authType: Doc<'aiProviderProfiles'>['authType'];
secret?: string;
baseUrl?: string;
model: string;
reasoningEffort: Doc<'aiProviderProfiles'>['reasoningEffort'];
};
agentSettings: Doc<'spoonAgentSettings'> | null;
github: {
installationId?: string;
};
secrets: { name: string; value: string }[];
};
const requireWorkerToken = (workerToken: string) => {
const expected = process.env.SPOON_WORKER_TOKEN?.trim();
if (!expected) throw new ConvexError('SPOON_WORKER_TOKEN is not configured.');
if (workerToken !== expected) throw new ConvexError('Invalid worker token.');
};
export const claimNextForWorker = action({
args: {
workerId: v.string(),
workerToken: v.string(),
},
handler: async (ctx, args): Promise<WorkerClaim | null> => {
requireWorkerToken(args.workerToken);
const claimed = (await ctx.runMutation(
internal.agentJobs.claimNextInternal,
{
workerId: args.workerId,
},
)) as ClaimedJob | null;
if (!claimed) return null;
if (!claimed.spoon) {
throw new ConvexError('Claimed job points at a missing Spoon.');
}
if (!claimed.aiProviderProfile) {
throw new ConvexError(
'AI is not configured for this user. Add an AI provider in settings.',
);
}
if (
claimed.aiProviderProfile.authType !== 'none' &&
!claimed.aiProviderProfile.encryptedSecret
) {
throw new ConvexError('Selected AI provider is missing credentials.');
}
const profile = claimed.aiProviderProfile;
return {
job: claimed.job,
spoon: claimed.spoon,
openai: {
model: profile.defaultModel,
reasoningEffort: profile.reasoningEffort,
},
aiProviderProfile: {
id: profile._id,
name: profile.name,
provider: profile.provider,
authType: profile.authType,
secret: profile.encryptedSecret
? decryptSecret(profile.encryptedSecret)
: undefined,
baseUrl: profile.baseUrl,
model: profile.defaultModel,
reasoningEffort: profile.reasoningEffort,
},
agentSettings: claimed.agentSettings,
github: {
installationId:
claimed.job.githubInstallationId ??
claimed.spoon.githubInstallationId ??
process.env.GITHUB_APP_INSTALLATION_ID,
},
secrets: claimed.secrets.map((secret: Doc<'spoonSecrets'>) => ({
name: secret.name,
value: decryptSecret(secret.encryptedValue),
})),
};
},
});