Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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 });
Expand Down
8 changes: 8 additions & 0 deletions workspaces/augment/plugins/augment-common/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export interface ChatAgent {
agentRole?: AgentRole;
avatarUrl?: string;
createdAt?: string;
createdBy?: string;
description?: string;
framework?: string;
id: string;
Expand All @@ -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;
Expand All @@ -281,6 +285,7 @@ export interface ChatAgentConfig {
agentId: string;
avatarUrl?: string;
conversationStarters?: string[];
createdBy?: string;
description?: string;
displayName?: string;
featured: boolean;
Expand All @@ -290,6 +295,9 @@ export interface ChatAgentConfig {
promotedAt?: string;
promotedBy?: string;
published: boolean;
rejectedAt?: string;
rejectedBy?: string;
rejectionReason?: string;
version?: number;
visible: boolean;
}
Expand Down
16 changes: 16 additions & 0 deletions workspaces/augment/plugins/augment-common/src/types/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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;
}

/**
Expand Down
1 change: 1 addition & 0 deletions workspaces/augment/plugins/augment/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export interface AugmentApi {
demoteAgent(
agentId: string,
targetStage?: AgentLifecycleStage,
reason?: string,
): Promise<{
lifecycleStage: string;
}>;
Expand Down
5 changes: 4 additions & 1 deletion workspaces/augment/plugins/augment/src/api/AugmentApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>;

/**
Expand Down Expand Up @@ -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 }),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export function AgentLifecycleDetail({
const [lifecycleStage, setLifecycleStage] = useState<string>('draft');
const [publishLoading, setPublishLoading] = useState(false);
const [publishToast, setPublishToast] = useState<string | null>(null);
const [rejectionReason, setRejectionReason] = useState<string | undefined>();
const [rejectedBy, setRejectedBy] = useState<string | undefined>();

useEffect(() => {
let cancelled = false;
Expand All @@ -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 () => {
Expand Down Expand Up @@ -390,6 +394,24 @@ export function AgentLifecycleDetail({
</Alert>
)}

{lifecycleStage === 'draft' && rejectionReason && (
<Alert severity="warning" sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Returned from review
</Typography>
<Typography variant="body2">{rejectionReason}</Typography>
{rejectedBy && (
<Typography
variant="caption"
color="text.secondary"
sx={{ mt: 0.5, display: 'block' }}
>
Rejected by {rejectedBy}
</Typography>
)}
</Alert>
)}

{/* Lifecycle Tabs */}
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import TextField from '@mui/material/TextField';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import { useTheme, alpha } from '@mui/material/styles';
import { useApi } from '@backstage/core-plugin-api';
Expand All @@ -45,6 +50,8 @@ export function ReviewQueue() {
const [loading, setLoading] = useState(true);
const [toast, setToast] = useState<string | null>(null);
const [acting, setActing] = useState<string | null>(null);
const [rejectTarget, setRejectTarget] = useState<string | null>(null);
const [rejectReason, setRejectReason] = useState('');

const loadAgents = useCallback(() => {
setLoading(true);
Expand Down Expand Up @@ -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 (
<Box
Expand Down Expand Up @@ -241,7 +252,7 @@ export function ReviewQueue() {
variant="outlined"
color="error"
disabled={acting === agent.id}
onClick={() => handleReject(agent.id)}
onClick={() => openRejectDialog(agent.id)}
sx={{
textTransform: 'none',
fontWeight: 600,
Expand All @@ -259,6 +270,40 @@ export function ReviewQueue() {
</Box>
)}

<Dialog
open={!!rejectTarget}
onClose={() => setRejectTarget(null)}
maxWidth="sm"
fullWidth
>
<DialogTitle>Reject Agent</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Provide a reason so the creator knows what to improve.
</Typography>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
label="Rejection reason (optional)"
value={rejectReason}
onChange={e => setRejectReason(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setRejectTarget(null)}>Cancel</Button>
<Button
variant="contained"
color="error"
onClick={handleReject}
disabled={acting === rejectTarget}
>
Reject
</Button>
</DialogActions>
</Dialog>

<Snackbar
open={!!toast}
autoHideDuration={4000}
Expand Down
Loading