Make job evaluation and execution run in the background

Refactors `runEvalInBackground` and `runWorkInBackground` to execute AI tasks asynchronously. Updates `pollJob` in `ui.ts` to handle 'evaluating', 'executing', and 'failed' states, and corrects `data.status` to `data.state` and `data.rejectionReason` to `data.reason`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ecf857ee-fa4d-47db-b4c1-b374ffb3815d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
alexpaynex
2026-03-18 21:00:43 +00:00
parent 1b5c7045da
commit b02efc9057
2 changed files with 127 additions and 108 deletions

View File

@@ -19,9 +19,112 @@ async function getInvoiceById(id: string) {
return rows[0] ?? null;
}
/**
* Runs the AI eval in a background task (fire-and-forget) so HTTP polls
* return immediately with "evaluating" state instead of blocking 5-8 seconds.
*/
async function runEvalInBackground(jobId: string, request: string): Promise<void> {
try {
const evalResult = await agentService.evaluateRequest(request);
if (evalResult.accepted) {
const inputEst = pricingService.estimateInputTokens(request);
const outputEst = pricingService.estimateOutputTokens(request);
const breakdown = await pricingService.calculateWorkFeeSats(
inputEst,
outputEst,
agentService.workModel,
);
const workInvoiceData = await lnbitsService.createInvoice(
breakdown.amountSats,
`Work fee for job ${jobId}`,
);
const workInvoiceId = randomUUID();
await db.transaction(async (tx) => {
await tx.insert(invoices).values({
id: workInvoiceId,
jobId,
paymentHash: workInvoiceData.paymentHash,
paymentRequest: workInvoiceData.paymentRequest,
amountSats: breakdown.amountSats,
type: "work",
paid: false,
});
await tx
.update(jobs)
.set({
state: "awaiting_work_payment",
workInvoiceId,
workAmountSats: breakdown.amountSats,
estimatedCostUsd: breakdown.estimatedCostUsd,
marginPct: breakdown.marginPct,
btcPriceUsd: breakdown.btcPriceUsd,
updatedAt: new Date(),
})
.where(eq(jobs.id, jobId));
});
} else {
await db
.update(jobs)
.set({ state: "rejected", rejectionReason: evalResult.reason, updatedAt: new Date() })
.where(eq(jobs.id, jobId));
}
} catch (err) {
const message = err instanceof Error ? err.message : "Evaluation error";
await db
.update(jobs)
.set({ state: "failed", errorMessage: message, updatedAt: new Date() })
.where(eq(jobs.id, jobId));
}
}
/**
* Runs the AI work execution in a background task so HTTP polls return fast.
*/
async function runWorkInBackground(jobId: string, request: string, workAmountSats: number, btcPriceUsd: number | null): Promise<void> {
try {
const workResult = await agentService.executeWork(request);
const actualCostUsd = pricingService.calculateActualCostUsd(
workResult.inputTokens,
workResult.outputTokens,
agentService.workModel,
);
const lockedBtcPrice = btcPriceUsd ?? 100_000;
const actualAmountSats = pricingService.calculateActualChargeSats(actualCostUsd, lockedBtcPrice);
const refundAmountSats = pricingService.calculateRefundSats(workAmountSats, actualAmountSats);
const refundState = refundAmountSats > 0 ? "pending" : "not_applicable";
await db
.update(jobs)
.set({
state: "complete",
result: workResult.result,
actualInputTokens: workResult.inputTokens,
actualOutputTokens: workResult.outputTokens,
actualCostUsd,
actualAmountSats,
refundAmountSats,
refundState,
updatedAt: new Date(),
})
.where(eq(jobs.id, jobId));
} catch (err) {
const message = err instanceof Error ? err.message : "Execution error";
await db
.update(jobs)
.set({ state: "failed", errorMessage: message, updatedAt: new Date() })
.where(eq(jobs.id, jobId));
}
}
/**
* Checks whether the active invoice for a job has been paid and, if so,
* advances the state machine. Returns the refreshed job after any transitions.
* advances the state machine. AI work runs in the background so this
* returns quickly. Returns the refreshed job after any DB transitions.
*/
async function advanceJob(job: Job): Promise<Job | null> {
if (job.state === "awaiting_eval_payment" && job.evalInvoiceId) {
@@ -46,60 +149,8 @@ async function advanceJob(job: Job): Promise<Job | null> {
if (!advanced) return getJobById(job.id);
try {
const evalResult = await agentService.evaluateRequest(job.request);
if (evalResult.accepted) {
const inputEst = pricingService.estimateInputTokens(job.request);
const outputEst = pricingService.estimateOutputTokens(job.request);
const breakdown = await pricingService.calculateWorkFeeSats(
inputEst,
outputEst,
agentService.workModel,
);
const workInvoiceData = await lnbitsService.createInvoice(
breakdown.amountSats,
`Work fee for job ${job.id}`,
);
const workInvoiceId = randomUUID();
await db.transaction(async (tx) => {
await tx.insert(invoices).values({
id: workInvoiceId,
jobId: job.id,
paymentHash: workInvoiceData.paymentHash,
paymentRequest: workInvoiceData.paymentRequest,
amountSats: breakdown.amountSats,
type: "work",
paid: false,
});
await tx
.update(jobs)
.set({
state: "awaiting_work_payment",
workInvoiceId,
workAmountSats: breakdown.amountSats,
estimatedCostUsd: breakdown.estimatedCostUsd,
marginPct: breakdown.marginPct,
btcPriceUsd: breakdown.btcPriceUsd,
updatedAt: new Date(),
})
.where(eq(jobs.id, job.id));
});
} else {
await db
.update(jobs)
.set({ state: "rejected", rejectionReason: evalResult.reason, updatedAt: new Date() })
.where(eq(jobs.id, job.id));
}
} catch (err) {
const message = err instanceof Error ? err.message : "Evaluation error";
await db
.update(jobs)
.set({ state: "failed", errorMessage: message, updatedAt: new Date() })
.where(eq(jobs.id, job.id));
}
// Fire AI eval in background — poll returns immediately with "evaluating"
setImmediate(() => { void runEvalInBackground(job.id, job.request); });
return getJobById(job.id);
}
@@ -126,51 +177,8 @@ async function advanceJob(job: Job): Promise<Job | null> {
if (!advanced) return getJobById(job.id);
try {
const workResult = await agentService.executeWork(job.request);
// ── Honest post-work accounting ───────────────────────────────────────
const actualCostUsd = pricingService.calculateActualCostUsd(
workResult.inputTokens,
workResult.outputTokens,
agentService.workModel,
);
// Re-use the BTC price locked at invoice time so sats arithmetic is
// consistent. Fall back to the cached oracle price only if the column
// is somehow null (should not happen in normal flow).
const lockedBtcPrice = job.btcPriceUsd ?? 100_000;
const actualAmountSats = pricingService.calculateActualChargeSats(
actualCostUsd,
lockedBtcPrice,
);
const refundAmountSats = pricingService.calculateRefundSats(
job.workAmountSats ?? 0,
actualAmountSats,
);
const refundState = refundAmountSats > 0 ? "pending" : "not_applicable";
await db
.update(jobs)
.set({
state: "complete",
result: workResult.result,
actualInputTokens: workResult.inputTokens,
actualOutputTokens: workResult.outputTokens,
actualCostUsd,
actualAmountSats,
refundAmountSats,
refundState,
updatedAt: new Date(),
})
.where(eq(jobs.id, job.id));
} catch (err) {
const message = err instanceof Error ? err.message : "Execution error";
await db
.update(jobs)
.set({ state: "failed", errorMessage: message, updatedAt: new Date() })
.where(eq(jobs.id, job.id));
}
// Fire AI work in background — poll returns immediately with "executing"
setImmediate(() => { void runWorkInBackground(job.id, job.request, job.workAmountSats ?? 0, job.btcPriceUsd); });
return getJobById(job.id);
}