mirror of
https://github.com/vee1e/workorder-desk.git
synced 2026-09-01 09:50:13 +00:00
feat(backend): return the created run id from manual triage
runTriage now resolves to { outcome, runId } so the admin API can hand the
run id back to the UI for deep-linking into the run detail.
This commit is contained in:
parent
691980735a
commit
1ca37a6504
4 changed files with 31 additions and 22 deletions
|
|
@ -12,6 +12,11 @@ import type { ProviderMessage, ProviderResult } from './provider.js';
|
|||
|
||||
export type TriageOutcome = 'done' | 'skipped' | 'failed' | 'retry';
|
||||
|
||||
export interface TriageResult {
|
||||
outcome: TriageOutcome;
|
||||
runId: string | null;
|
||||
}
|
||||
|
||||
type TriageProvider = typeof import('./provider.js');
|
||||
|
||||
const SYSTEM_PROMPT =
|
||||
|
|
@ -38,33 +43,35 @@ export async function ensureTriageConfig(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
export async function runTriage(payloadRef: string): Promise<TriageOutcome> {
|
||||
if (!env.AI_ENABLED) return 'skipped';
|
||||
export async function runTriage(payloadRef: string): Promise<TriageResult> {
|
||||
if (!env.AI_ENABLED) return { outcome: 'skipped', runId: null };
|
||||
|
||||
let config = await agentRepo.getAgentConfig('triage');
|
||||
if (!config) {
|
||||
await ensureTriageConfig();
|
||||
config = await agentRepo.getAgentConfig('triage');
|
||||
}
|
||||
if (!config || !config.enabled) return 'skipped';
|
||||
if (!isWorkingHours(config.workingHours, new Date())) return 'skipped';
|
||||
if (!config || !config.enabled) return { outcome: 'skipped', runId: null };
|
||||
if (!isWorkingHours(config.workingHours, new Date())) return { outcome: 'skipped', runId: null };
|
||||
|
||||
const wo = await workOrderRepo.findById(payloadRef);
|
||||
if (!wo) return 'skipped';
|
||||
if (!wo) return { outcome: 'skipped', runId: null };
|
||||
|
||||
const owner = wo.owner as unknown as { _id?: { toString(): string }; name?: string; email?: string } | null;
|
||||
const ownerId = owner?._id?.toString();
|
||||
if (ownerId) {
|
||||
const ownerUser = await userRepo.findById(ownerId);
|
||||
if (ownerUser && ownerUser.aiEnabled === false) return 'skipped';
|
||||
if (ownerUser && ownerUser.aiEnabled === false) return { outcome: 'skipped', runId: null };
|
||||
}
|
||||
|
||||
const suggestionsToday = await agentRepo.countSuggestionsToday();
|
||||
if (suggestionsToday >= config.dailyActionCap) return 'skipped';
|
||||
if (suggestionsToday >= config.dailyActionCap) return { outcome: 'skipped', runId: null };
|
||||
|
||||
const agentSpend = await agentRepo.getSpend('agent:triage');
|
||||
const globalSpend = await agentRepo.getSpend('global');
|
||||
if (agentSpend >= env.AGENT_DAILY_SPEND_USD || globalSpend >= env.AI_GLOBAL_DAILY_SPEND_USD) return 'skipped';
|
||||
if (agentSpend >= env.AGENT_DAILY_SPEND_USD || globalSpend >= env.AI_GLOBAL_DAILY_SPEND_USD) {
|
||||
return { outcome: 'skipped', runId: null };
|
||||
}
|
||||
|
||||
const provider = await import('./provider.js');
|
||||
const run = await agentRepo.createRun({
|
||||
|
|
@ -74,17 +81,19 @@ export async function runTriage(payloadRef: string): Promise<TriageOutcome> {
|
|||
agentName: 'triage',
|
||||
model: env.AI_MODEL,
|
||||
});
|
||||
const runId = run._id.toString();
|
||||
|
||||
try {
|
||||
return await runAttempts(config, run._id.toString(), payloadRef, wo, provider);
|
||||
const outcome = await runAttempts(config, runId, payloadRef, wo, provider);
|
||||
return { outcome, runId };
|
||||
} catch (err) {
|
||||
const transient = err instanceof provider.ProviderError;
|
||||
await agentRepo.finishRun(run._id.toString(), {
|
||||
await agentRepo.finishRun(runId, {
|
||||
status: 'error',
|
||||
finishedAt: new Date(),
|
||||
errorCode: transient ? 'AI_UNAVAILABLE' : 'INTERNAL',
|
||||
});
|
||||
return transient ? 'retry' : 'failed';
|
||||
return { outcome: transient ? 'retry' : 'failed', runId };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ export const agentAdminController = {
|
|||
} else {
|
||||
assertValidObjectId(workOrderId);
|
||||
}
|
||||
const outcome = await runTriage(workOrderId);
|
||||
res.status(200).json({ success: true, data: { outcome } });
|
||||
const { outcome, runId } = await runTriage(workOrderId);
|
||||
res.status(200).json({ success: true, data: { outcome, runId } });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ async function main(): Promise<void> {
|
|||
concurrency += 1;
|
||||
void (async () => {
|
||||
try {
|
||||
const outcome = await runTriage(event.payloadRef);
|
||||
const { outcome } = await runTriage(event.payloadRef);
|
||||
if (outcome === 'done' || outcome === 'skipped') {
|
||||
await agentRepo.completeOutbox(event._id.toString());
|
||||
} else if (event.attempts >= env.AGENT_MAX_ATTEMPTS) {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ describe('triage agent', () => {
|
|||
providerResult({ content: proposal(), inputTokens: 100, outputTokens: 20 }),
|
||||
);
|
||||
const outcome = await runTriage(wo.id);
|
||||
expect(outcome).toBe('done');
|
||||
expect(outcome.outcome).toBe('done');
|
||||
const suggestions = await TriageSuggestion.find({ workOrderId: wo.id });
|
||||
expect(suggestions).toHaveLength(1);
|
||||
expect(suggestions[0]).toMatchObject({ applied: false, suggestedPriority: 'high', flagForDispatcher: false });
|
||||
|
|
@ -90,7 +90,7 @@ describe('triage agent', () => {
|
|||
await agentRepo.updateAgentConfig('triage', { mode: 'auto-apply' }, 'admin');
|
||||
providerMock.chatComplete.mockResolvedValue(providerResult({ content: proposal({ suggestedPriority: 'high' }) }));
|
||||
const outcome = await runTriage(wo.id);
|
||||
expect(outcome).toBe('done');
|
||||
expect(outcome.outcome).toBe('done');
|
||||
const fresh = await WorkOrder.findById(wo.id);
|
||||
expect(fresh?.priority).toBe('high');
|
||||
const suggestions = await TriageSuggestion.find({ workOrderId: wo.id });
|
||||
|
|
@ -100,7 +100,7 @@ describe('triage agent', () => {
|
|||
it('skips triage when the owner has AI disabled', async () => {
|
||||
const { wo } = await makeWorkOrder({ aiEnabled: false });
|
||||
const outcome = await runTriage(wo.id);
|
||||
expect(outcome).toBe('skipped');
|
||||
expect(outcome.outcome).toBe('skipped');
|
||||
expect(providerMock.chatComplete).not.toHaveBeenCalled();
|
||||
expect(await TriageSuggestion.countDocuments()).toBe(0);
|
||||
});
|
||||
|
|
@ -109,7 +109,7 @@ describe('triage agent', () => {
|
|||
const { wo } = await makeWorkOrder();
|
||||
providerMock.chatComplete.mockResolvedValue(providerResult({ content: 'definitely not json' }));
|
||||
const outcome = await runTriage(wo.id);
|
||||
expect(outcome).toBe('failed');
|
||||
expect(outcome.outcome).toBe('failed');
|
||||
expect(providerMock.chatComplete).toHaveBeenCalledTimes(3);
|
||||
const runs = (await agentRepo.listAdminRuns(1, 10)).items.filter((run) => run.mode === 'autonomous');
|
||||
expect(runs[0]).toMatchObject({ status: 'error', errorCode: 'AI_UNAVAILABLE' });
|
||||
|
|
@ -122,7 +122,7 @@ describe('triage agent', () => {
|
|||
const excluding = (hour + 2) % 24;
|
||||
await agentRepo.updateAgentConfig('triage', { workingHours: `${excluding}-${excluding}` }, 'admin');
|
||||
const outcome = await runTriage(wo.id);
|
||||
expect(outcome).toBe('skipped');
|
||||
expect(outcome.outcome).toBe('skipped');
|
||||
expect(providerMock.chatComplete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -139,7 +139,7 @@ describe('triage agent', () => {
|
|||
applied: false,
|
||||
});
|
||||
const outcome = await runTriage(wo.id);
|
||||
expect(outcome).toBe('skipped');
|
||||
expect(outcome.outcome).toBe('skipped');
|
||||
expect(providerMock.chatComplete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ describe('triage agent', () => {
|
|||
const { wo } = await makeWorkOrder();
|
||||
await agentRepo.chargeSpend('agent:triage', 100);
|
||||
const outcome = await runTriage(wo.id);
|
||||
expect(outcome).toBe('skipped');
|
||||
expect(outcome.outcome).toBe('skipped');
|
||||
expect(providerMock.chatComplete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ describe('triage agent', () => {
|
|||
const { wo } = await makeWorkOrder();
|
||||
providerMock.chatComplete.mockRejectedValue(new providerMock.ProviderError('upstream down', 'network'));
|
||||
const outcome = await runTriage(wo.id);
|
||||
expect(outcome).toBe('retry');
|
||||
expect(outcome.outcome).toBe('retry');
|
||||
const runs = (await agentRepo.listAdminRuns(1, 10)).items.filter((run) => run.mode === 'autonomous');
|
||||
expect(runs[0]).toMatchObject({ status: 'error', errorCode: 'AI_UNAVAILABLE' });
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue