From 53c1c1f602f76350ecca540fd0ab47745d543623 Mon Sep 17 00:00:00 2001 From: Raghuram Banda Date: Thu, 21 May 2026 11:57:23 -0400 Subject: [PATCH 1/2] feat: add rejection reason fields to ChatAgentConfig and demote route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an admin rejects an agent (demotes review→draft), they can now provide a reason. The backend stores rejectionReason, rejectedBy, and rejectedAt on the ChatAgentConfig. These fields are cleared when the agent is re-submitted for review. Changes: - Add rejectionReason, rejectedBy, rejectedAt to ChatAgentConfig and ChatAgent interfaces - Add createdBy field to both interfaces (prerequisite for downstream PRs) - Update PUT /agents/:id/demote to accept optional 'reason' in body - Surface rejection fields through overlayConfig in unified agent list - Update demoteAgent API method to accept optional reason parameter - Update report.api.md files for both augment and augment-common Part of Epic #3208 --- .../augment-backend/src/routes/agentRoutes.ts | 27 +++++++++++++++++-- .../plugins/augment-common/report.api.md | 8 ++++++ .../augment-common/src/types/shared.ts | 16 +++++++++++ .../augment/plugins/augment/report.api.md | 1 + .../plugins/augment/src/api/AugmentApi.ts | 5 +++- 5 files changed, 54 insertions(+), 3 deletions(-) diff --git a/workspaces/augment/plugins/augment-backend/src/routes/agentRoutes.ts b/workspaces/augment/plugins/augment-backend/src/routes/agentRoutes.ts index 9a831d9037..65aab3ca78 100644 --- a/workspaces/augment/plugins/augment-backend/src/routes/agentRoutes.ts +++ b/workspaces/augment/plugins/augment-backend/src/routes/agentRoutes.ts @@ -177,6 +177,10 @@ export function registerAgentRoutes( version: cfg?.version ?? 0, promotedAt: cfg?.promotedAt, promotedBy: cfg?.promotedBy, + createdBy: cfg?.createdBy, + rejectionReason: cfg?.rejectionReason, + rejectedBy: cfg?.rejectedBy, + rejectedAt: cfg?.rejectedAt, }; } @@ -311,6 +315,11 @@ export function registerAgentRoutes( existing.version = (existing.version ?? 0) + 1; existing.promotedAt = now; existing.promotedBy = userRef; + if (isSubmitForReview) { + existing.rejectionReason = undefined; + existing.rejectedBy = undefined; + existing.rejectedAt = undefined; + } } else { configs.push({ agentId, @@ -363,7 +372,10 @@ export function registerAgentRoutes( 'Failed to demote agent', async (req, res) => { const agentId = decodeURIComponent(req.params.agentId); - const { targetStage } = req.body as { targetStage?: string }; + const { targetStage, reason } = req.body as { + targetStage?: string; + reason?: string; + }; const resolved = targetStage ? normalizeLifecycleStage(targetStage) : undefined; @@ -393,6 +405,7 @@ export function registerAgentRoutes( const now = new Date().toISOString(); const isProd = isProductionStage(nextStage); + const isRejection = currentStage === 'review' && nextStage === 'draft'; if (existing) { existing.lifecycleStage = nextStage; @@ -403,6 +416,11 @@ export function registerAgentRoutes( } existing.promotedAt = now; existing.promotedBy = userRef; + if (isRejection) { + existing.rejectionReason = reason || undefined; + existing.rejectedBy = userRef; + existing.rejectedAt = now; + } } else { configs.push({ agentId, @@ -422,7 +440,12 @@ export function registerAgentRoutes( target: agentId, outcome: 'success', sourceIp: AuditLogger.extractIp(req), - meta: { from: currentStage, to: nextStage, direction: 'demote' }, + meta: { + from: currentStage, + to: nextStage, + direction: 'demote', + ...(isRejection && reason ? { rejectionReason: reason } : {}), + }, }); logger.info(`Agent "${agentId}" demoted to ${nextStage} by ${userRef}`); res.json({ success: true, agentId, lifecycleStage: nextStage }); diff --git a/workspaces/augment/plugins/augment-common/report.api.md b/workspaces/augment/plugins/augment-common/report.api.md index 217aed3085..e2aa65cf20 100644 --- a/workspaces/augment/plugins/augment-common/report.api.md +++ b/workspaces/augment/plugins/augment-common/report.api.md @@ -257,6 +257,7 @@ export interface ChatAgent { agentRole?: AgentRole; avatarUrl?: string; createdAt?: string; + createdBy?: string; description?: string; framework?: string; id: string; @@ -269,6 +270,9 @@ export interface ChatAgent { protocols?: string[]; providerType: string; published?: boolean; + rejectedAt?: string; + rejectedBy?: string; + rejectionReason?: string; source?: string; starters?: string[]; status: string; @@ -281,6 +285,7 @@ export interface ChatAgentConfig { agentId: string; avatarUrl?: string; conversationStarters?: string[]; + createdBy?: string; description?: string; displayName?: string; featured: boolean; @@ -290,6 +295,9 @@ export interface ChatAgentConfig { promotedAt?: string; promotedBy?: string; published: boolean; + rejectedAt?: string; + rejectedBy?: string; + rejectionReason?: string; version?: number; visible: boolean; } diff --git a/workspaces/augment/plugins/augment-common/src/types/shared.ts b/workspaces/augment/plugins/augment-common/src/types/shared.ts index 44d83f6334..c93759b893 100644 --- a/workspaces/augment/plugins/augment-common/src/types/shared.ts +++ b/workspaces/augment/plugins/augment-common/src/types/shared.ts @@ -404,6 +404,14 @@ export interface ChatAgentConfig { greeting?: string; /** Suggested prompts shown on the agent card and below the input */ conversationStarters?: string[]; + /** User ref of who originally created this agent config entry */ + createdBy?: string; + /** Reason provided by admin when rejecting (demoting review→draft) */ + rejectionReason?: string; + /** User ref of who rejected this agent */ + rejectedBy?: string; + /** ISO timestamp of when the rejection occurred */ + rejectedAt?: string; } /** @@ -614,6 +622,14 @@ export interface ChatAgent { promotedBy?: string; /** Role of this agent in the orchestration topology */ agentRole?: AgentRole; + /** User ref of who originally created this agent */ + createdBy?: string; + /** Reason provided by admin when rejecting (demoting review→draft) */ + rejectionReason?: string; + /** User ref of who rejected this agent */ + rejectedBy?: string; + /** ISO timestamp of when the rejection occurred */ + rejectedAt?: string; } /** diff --git a/workspaces/augment/plugins/augment/report.api.md b/workspaces/augment/plugins/augment/report.api.md index d342dcd378..442ff0f275 100644 --- a/workspaces/augment/plugins/augment/report.api.md +++ b/workspaces/augment/plugins/augment/report.api.md @@ -181,6 +181,7 @@ export interface AugmentApi { demoteAgent( agentId: string, targetStage?: AgentLifecycleStage, + reason?: string, ): Promise<{ lifecycleStage: string; }>; diff --git a/workspaces/augment/plugins/augment/src/api/AugmentApi.ts b/workspaces/augment/plugins/augment/src/api/AugmentApi.ts index e79f9af0c2..2583cd8844 100644 --- a/workspaces/augment/plugins/augment/src/api/AugmentApi.ts +++ b/workspaces/augment/plugins/augment/src/api/AugmentApi.ts @@ -93,10 +93,12 @@ export interface AugmentApi { /** * Demote an agent to a previous lifecycle stage (deployed → registered → draft). + * @param reason - optional rejection reason (used when demoting review→draft) */ demoteAgent( agentId: string, targetStage?: import('@red-hat-developer-hub/backstage-plugin-augment-common').AgentLifecycleStage, + reason?: string, ): Promise<{ lifecycleStage: string }>; /** @@ -775,11 +777,12 @@ export class AugmentApiClient implements AugmentApi { async demoteAgent( agentId: string, targetStage?: import('@red-hat-developer-hub/backstage-plugin-augment-common').AgentLifecycleStage, + reason?: string, ): Promise<{ lifecycleStage: string }> { return this.fetchJson(`/agents/${encodeURIComponent(agentId)}/demote`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ targetStage }), + body: JSON.stringify({ targetStage, reason }), }); } From 3e8b592cf37f095d14310e822c1e5d505cfcac69 Mon Sep 17 00:00:00 2001 From: Raghuram Banda Date: Thu, 21 May 2026 11:59:27 -0400 Subject: [PATCH 2/2] feat: show rejection reason to creators and add reason dialog for admins - AgentLifecycleDetail now shows a warning banner when a draft agent has a rejectionReason, displaying the reason and who rejected it - ReviewQueue reject button now opens a dialog where admins can enter an optional rejection reason before confirming the rejection - The reason is passed to demoteAgent which stores it on the config Part of Epic #3208. Requires PR E (rejection fields). --- .../KagentiPanels/AgentLifecycleDetail.tsx | 22 ++++++ .../components/CommandCenter/ReviewQueue.tsx | 77 +++++++++++++++---- 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/workspaces/augment/plugins/augment/src/components/AdminPanels/KagentiPanels/AgentLifecycleDetail.tsx b/workspaces/augment/plugins/augment/src/components/AdminPanels/KagentiPanels/AgentLifecycleDetail.tsx index 7f41dcd2f0..97373c75b5 100644 --- a/workspaces/augment/plugins/augment/src/components/AdminPanels/KagentiPanels/AgentLifecycleDetail.tsx +++ b/workspaces/augment/plugins/augment/src/components/AdminPanels/KagentiPanels/AgentLifecycleDetail.tsx @@ -109,6 +109,8 @@ export function AgentLifecycleDetail({ const [lifecycleStage, setLifecycleStage] = useState('draft'); const [publishLoading, setPublishLoading] = useState(false); const [publishToast, setPublishToast] = useState(null); + const [rejectionReason, setRejectionReason] = useState(); + const [rejectedBy, setRejectedBy] = useState(); useEffect(() => { let cancelled = false; @@ -118,6 +120,8 @@ export function AgentLifecycleDetail({ if (cancelled) return; const match = agents.find(a => a.id === agentId); setLifecycleStage(normalizeLifecycleStage(match?.lifecycleStage)); + setRejectionReason(match?.rejectionReason); + setRejectedBy(match?.rejectedBy); }) .catch(() => setLifecycleStage('draft')); return () => { @@ -390,6 +394,24 @@ export function AgentLifecycleDetail({ )} + {lifecycleStage === 'draft' && rejectionReason && ( + + + Returned from review + + {rejectionReason} + {rejectedBy && ( + + Rejected by {rejectedBy} + + )} + + )} + {/* Lifecycle Tabs */} (null); const [acting, setActing] = useState(null); + const [rejectTarget, setRejectTarget] = useState(null); + const [rejectReason, setRejectReason] = useState(''); const loadAgents = useCallback(() => { setLoading(true); @@ -94,21 +101,25 @@ export function ReviewQueue() { [api, loadAgents], ); - const handleReject = useCallback( - async (agentId: string) => { - setActing(agentId); - try { - await api.demoteAgent(agentId, 'draft'); - setToast(`Rejected: ${agentId} returned to draft`); - loadAgents(); - } catch (err) { - setToast(`Failed: ${err instanceof Error ? err.message : 'Unknown'}`); - } finally { - setActing(null); - } - }, - [api, loadAgents], - ); + const openRejectDialog = useCallback((agentId: string) => { + setRejectTarget(agentId); + setRejectReason(''); + }, []); + + const handleReject = useCallback(async () => { + if (!rejectTarget) return; + setActing(rejectTarget); + try { + await api.demoteAgent(rejectTarget, 'draft', rejectReason || undefined); + setToast(`Rejected: ${rejectTarget} returned to draft`); + setRejectTarget(null); + loadAgents(); + } catch (err) { + setToast(`Failed: ${err instanceof Error ? err.message : 'Unknown'}`); + } finally { + setActing(null); + } + }, [api, loadAgents, rejectTarget, rejectReason]); return ( handleReject(agent.id)} + onClick={() => openRejectDialog(agent.id)} sx={{ textTransform: 'none', fontWeight: 600, @@ -259,6 +270,40 @@ export function ReviewQueue() { )} + setRejectTarget(null)} + maxWidth="sm" + fullWidth + > + Reject Agent + + + Provide a reason so the creator knows what to improve. + + setRejectReason(e.target.value)} + /> + + + + + + +