feat(operations): מסך "סוכנים פעילים" + ניהול ריצות (live-runs/log/cancel) (G12/X15, #119)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
פאנל ב-/operations שמראה אילו סוכני Paperclip עובדים כעת (רצים+בתור), הפלט החי
שלהם, ושליטה מבוקרת: עצירת ריצה, איפוס session. סוגר את הנקודה-העיוורת שבה drain
מונע-סוכן (למשל ריקון תור הלכות ע"י ה-CEO heartbeat) עוקף את בקרת /operations
שמכירה רק שירותי pm2, והפלט הגולמי נגיש רק ב-Paperclip UI.
מקור-נתונים: Paperclip heartbeat-runs API (אומת חי):
GET /api/companies/{cid}/live-runs — רצים+בתור (agentName/status/issue/outputSilence)
GET /api/heartbeat-runs/{id}/log — NDJSON של פלט הסוכן
GET /api/heartbeat-runs/{id}/events — timeline
POST /api/heartbeat-runs/{id}/cancel — עצירה מבוקרת (לא kill — מכבד watchdog+checkpoint)
POST /api/agents/{id}/runtime-state/reset-session
ארכיטקטורה (G12/INV-PORT1): כל המגע החדש עם Paperclip דרך השער בלבד —
web/paperclip_client.py (shell) → re-export ב-web/agent_platform_port.py →
web/app.py צורך מהשער. leak_guard.py עובר (seam שלם). אסור kill ישיר על
process_pid (עוקף את השער).
Backend:
- paperclip_client: list_live_runs / get_run_log / get_run_events / cancel_run / reset_agent_session
- agent_platform_port: re-export pc_list_live_runs / pc_get_run_log / pc_get_run_events / pc_cancel_run / pc_reset_agent_session
- app.py: GET /api/operations/agents (אגרגציה CMP+CMPA, עמיד לכשל-חברה),
GET .../runs/{id}/log, GET .../runs/{id}/events, POST .../runs/{id}/cancel,
POST .../agents/{id}/reset-session
Frontend: פאנל "סוכנים פעילים" ב-/operations (polling 4s) + dialog ללוג חי
(פרסור NDJSON→טקסט קריא) + כפתורי עצור/אפס. הוספת hooks ל-operations.ts.
בטיחות: cancel על דריינר הלכות בטוח — חילוץ checkpointed per-chunk + resumable
+ self-heal לשורות processing.
Invariants: מקיים G12/INV-PORT1 (שער-הפלטפורמה). נוגע X6 (UI↔API).
api:types יורץ אחרי deploy (openapi.json חי).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -103,3 +103,88 @@ export function useDrainToggle() {
|
||||
onError: (e) => toast.error(`העדכון נכשל: ${String(e)}`),
|
||||
});
|
||||
}
|
||||
|
||||
// ── Live agents — which agent is working now + its output + controls ───────
|
||||
|
||||
export type AgentRun = {
|
||||
run_id: string;
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
company_id: string;
|
||||
company_label: string;
|
||||
status: string; // running | queued | ...
|
||||
invocation_source: string;
|
||||
trigger_detail: string;
|
||||
issue_id: string | null;
|
||||
adapter_type: string;
|
||||
started_at: string | null;
|
||||
created_at: string | null;
|
||||
last_output_at: string | null;
|
||||
continuation_attempt: number;
|
||||
silence_level: string; // "" | ok | suspicion | critical
|
||||
silence_age_ms: number;
|
||||
};
|
||||
|
||||
export type AgentRunsResponse = {
|
||||
runs: AgentRun[];
|
||||
running: number;
|
||||
queued: number;
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
export type RunLog = {
|
||||
runId: string;
|
||||
store: string;
|
||||
logRef: string;
|
||||
content: string; // NDJSON stream the adapter captured
|
||||
};
|
||||
|
||||
/** Queued + running heartbeat runs across all companies. */
|
||||
export function useAgentRuns() {
|
||||
return useQuery({
|
||||
queryKey: ["operations", "agents"],
|
||||
queryFn: ({ signal }) =>
|
||||
apiRequest<AgentRunsResponse>("/api/operations/agents", { signal }),
|
||||
refetchInterval: 4000, // live view of who's working now
|
||||
staleTime: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Full output log of one run — fetched on demand (drawer open). */
|
||||
export function useRunLog(runId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ["operations", "agents", "log", runId],
|
||||
queryFn: ({ signal }) =>
|
||||
apiRequest<RunLog>(`/api/operations/agents/runs/${runId}/log`, { signal }),
|
||||
enabled: !!runId,
|
||||
refetchInterval: runId ? 4000 : false, // live tail while open
|
||||
});
|
||||
}
|
||||
|
||||
/** Gracefully cancel a queued/running run (not a raw kill). */
|
||||
export function useCancelRun() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (runId: string) =>
|
||||
apiRequest(`/api/operations/agents/runs/${runId}/cancel`, { method: "POST" }),
|
||||
onSuccess: () => {
|
||||
toast.success("בקשת עצירה נשלחה");
|
||||
qc.invalidateQueries({ queryKey: ["operations", "agents"] });
|
||||
},
|
||||
onError: (e) => toast.error(`העצירה נכשלה: ${String(e)}`),
|
||||
});
|
||||
}
|
||||
|
||||
/** Reset a wedged agent session so its next wakeup starts clean. */
|
||||
export function useResetAgentSession() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (agentId: string) =>
|
||||
apiRequest(`/api/operations/agents/${agentId}/reset-session`, { method: "POST" }),
|
||||
onSuccess: () => {
|
||||
toast.success("ה-session אופס");
|
||||
qc.invalidateQueries({ queryKey: ["operations", "agents"] });
|
||||
},
|
||||
onError: (e) => toast.error(`האיפוס נכשל: ${String(e)}`),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user