Merge branch 'cw/radar' into cw-merge-1772825408137
This commit is contained in:
@@ -11,7 +11,23 @@ import type { ProcedureBuilder } from '../trpc.js';
|
||||
import type { TRPCContext } from '../context.js';
|
||||
import type { AgentInfo, AgentResult, PendingQuestions } from '../../agent/types.js';
|
||||
import type { AgentOutputEvent } from '../../events/types.js';
|
||||
import { requireAgentManager, requireLogChunkRepository, requireTaskRepository, requireInitiativeRepository } from './_helpers.js';
|
||||
import { requireAgentManager, requireLogChunkRepository, requireTaskRepository, requireInitiativeRepository, requireConversationRepository } from './_helpers.js';
|
||||
|
||||
export type AgentRadarRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
mode: string;
|
||||
status: string;
|
||||
initiativeId: string | null;
|
||||
initiativeName: string | null;
|
||||
taskId: string | null;
|
||||
taskName: string | null;
|
||||
createdAt: string;
|
||||
questionsCount: number;
|
||||
messagesCount: number;
|
||||
subagentsCount: number;
|
||||
compactionsCount: number;
|
||||
};
|
||||
|
||||
export const spawnAgentInputSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
@@ -410,5 +426,177 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) {
|
||||
|
||||
return { content: truncateIfNeeded(raw) };
|
||||
}),
|
||||
|
||||
listForRadar: publicProcedure
|
||||
.input(z.object({
|
||||
timeRange: z.enum(['1h', '6h', '24h', '7d', 'all']).default('24h'),
|
||||
status: z.enum(['running', 'completed', 'crashed']).optional(),
|
||||
initiativeId: z.string().optional(),
|
||||
mode: z.enum(['execute', 'discuss', 'plan', 'detail', 'refine', 'chat', 'errand']).optional(),
|
||||
}))
|
||||
.query(async ({ ctx, input }): Promise<AgentRadarRow[]> => {
|
||||
const agentManager = requireAgentManager(ctx);
|
||||
const allAgents = await agentManager.list();
|
||||
|
||||
// Compute cutoff
|
||||
const cutoffMap: Record<string, number> = {
|
||||
'1h': Date.now() - 3_600_000,
|
||||
'6h': Date.now() - 21_600_000,
|
||||
'24h': Date.now() - 86_400_000,
|
||||
'7d': Date.now() - 604_800_000,
|
||||
};
|
||||
const cutoff = input.timeRange !== 'all' ? cutoffMap[input.timeRange] : null;
|
||||
|
||||
// Filter agents
|
||||
let filteredAgents = allAgents;
|
||||
if (cutoff !== null) {
|
||||
filteredAgents = filteredAgents.filter(a => a.createdAt.getTime() >= cutoff!);
|
||||
}
|
||||
if (input.status !== undefined) {
|
||||
const dbStatus = input.status === 'completed' ? 'stopped' : input.status;
|
||||
filteredAgents = filteredAgents.filter(a => a.status === dbStatus);
|
||||
}
|
||||
if (input.initiativeId !== undefined) {
|
||||
filteredAgents = filteredAgents.filter(a => a.initiativeId === input.initiativeId);
|
||||
}
|
||||
if (input.mode !== undefined) {
|
||||
filteredAgents = filteredAgents.filter(a => a.mode === input.mode);
|
||||
}
|
||||
|
||||
const matchingIds = filteredAgents.map(a => a.id);
|
||||
|
||||
// Batch fetch in parallel
|
||||
const logChunkRepo = requireLogChunkRepository(ctx);
|
||||
const conversationRepo = requireConversationRepository(ctx);
|
||||
const initiativeRepo = requireInitiativeRepository(ctx);
|
||||
const taskRepo = requireTaskRepository(ctx);
|
||||
|
||||
// Collect unique taskIds and initiativeIds for batch lookup
|
||||
const uniqueTaskIds = [...new Set(filteredAgents.map(a => a.taskId).filter(Boolean) as string[])];
|
||||
const uniqueInitiativeIds = [...new Set(filteredAgents.map(a => a.initiativeId).filter(Boolean) as string[])];
|
||||
|
||||
const [chunks, messageCounts, taskResults, initiativeResults] = await Promise.all([
|
||||
logChunkRepo.findByAgentIds(matchingIds),
|
||||
conversationRepo.countByFromAgentIds(matchingIds),
|
||||
Promise.all(uniqueTaskIds.map(id => taskRepo.findById(id))),
|
||||
Promise.all(uniqueInitiativeIds.map(id => initiativeRepo.findById(id))),
|
||||
]);
|
||||
|
||||
// Build lookup maps
|
||||
const taskMap = new Map(taskResults.filter(Boolean).map(t => [t!.id, t!.name]));
|
||||
const initiativeMap = new Map(initiativeResults.filter(Boolean).map(i => [i!.id, i!.name]));
|
||||
const messagesMap = new Map(messageCounts.map(m => [m.agentId, m.count]));
|
||||
|
||||
// Group chunks by agentId
|
||||
const chunksByAgent = new Map<string, typeof chunks>();
|
||||
for (const chunk of chunks) {
|
||||
const existing = chunksByAgent.get(chunk.agentId);
|
||||
if (existing) {
|
||||
existing.push(chunk);
|
||||
} else {
|
||||
chunksByAgent.set(chunk.agentId, [chunk]);
|
||||
}
|
||||
}
|
||||
|
||||
// Build result rows
|
||||
return filteredAgents.map(agent => {
|
||||
const agentChunks = chunksByAgent.get(agent.id) ?? [];
|
||||
let questionsCount = 0;
|
||||
let subagentsCount = 0;
|
||||
let compactionsCount = 0;
|
||||
|
||||
for (const chunk of agentChunks) {
|
||||
try {
|
||||
const parsed = JSON.parse(chunk.content);
|
||||
if (parsed.type === 'tool_use' && parsed.name === 'AskUserQuestion') {
|
||||
questionsCount += parsed.input?.questions?.length ?? 0;
|
||||
} else if (parsed.type === 'tool_use' && parsed.name === 'Agent') {
|
||||
subagentsCount++;
|
||||
} else if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.source === 'compact') {
|
||||
compactionsCount++;
|
||||
}
|
||||
} catch { /* skip malformed */ }
|
||||
}
|
||||
|
||||
return {
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
mode: agent.mode,
|
||||
status: agent.status,
|
||||
initiativeId: agent.initiativeId,
|
||||
initiativeName: agent.initiativeId ? (initiativeMap.get(agent.initiativeId) ?? null) : null,
|
||||
taskId: agent.taskId,
|
||||
taskName: agent.taskId ? (taskMap.get(agent.taskId) ?? null) : null,
|
||||
createdAt: agent.createdAt.toISOString(),
|
||||
questionsCount,
|
||||
messagesCount: messagesMap.get(agent.id) ?? 0,
|
||||
subagentsCount,
|
||||
compactionsCount,
|
||||
};
|
||||
});
|
||||
}),
|
||||
|
||||
getCompactionEvents: publicProcedure
|
||||
.input(z.object({ agentId: z.string().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const logChunkRepo = requireLogChunkRepository(ctx);
|
||||
const chunks = await logChunkRepo.findByAgentId(input.agentId);
|
||||
const results: { timestamp: string; sessionNumber: number }[] = [];
|
||||
for (const chunk of chunks) {
|
||||
try {
|
||||
const parsed = JSON.parse(chunk.content);
|
||||
if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.source === 'compact') {
|
||||
results.push({ timestamp: chunk.createdAt.toISOString(), sessionNumber: chunk.sessionNumber });
|
||||
}
|
||||
} catch { /* skip malformed */ }
|
||||
if (results.length >= 200) break;
|
||||
}
|
||||
return results;
|
||||
}),
|
||||
|
||||
getSubagentSpawns: publicProcedure
|
||||
.input(z.object({ agentId: z.string().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const logChunkRepo = requireLogChunkRepository(ctx);
|
||||
const chunks = await logChunkRepo.findByAgentId(input.agentId);
|
||||
const results: { timestamp: string; description: string; promptPreview: string; fullPrompt: string }[] = [];
|
||||
for (const chunk of chunks) {
|
||||
try {
|
||||
const parsed = JSON.parse(chunk.content);
|
||||
if (parsed.type === 'tool_use' && parsed.name === 'Agent') {
|
||||
const fullPrompt: string = parsed.input?.prompt ?? '';
|
||||
const description: string = parsed.input?.description ?? '';
|
||||
results.push({
|
||||
timestamp: chunk.createdAt.toISOString(),
|
||||
description,
|
||||
promptPreview: fullPrompt.slice(0, 200),
|
||||
fullPrompt,
|
||||
});
|
||||
}
|
||||
} catch { /* skip malformed */ }
|
||||
if (results.length >= 200) break;
|
||||
}
|
||||
return results;
|
||||
}),
|
||||
|
||||
getQuestionsAsked: publicProcedure
|
||||
.input(z.object({ agentId: z.string().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const logChunkRepo = requireLogChunkRepository(ctx);
|
||||
const chunks = await logChunkRepo.findByAgentId(input.agentId);
|
||||
type QuestionItem = { question: string; header: string; options: { label: string; description: string }[] };
|
||||
const results: { timestamp: string; questions: QuestionItem[] }[] = [];
|
||||
for (const chunk of chunks) {
|
||||
try {
|
||||
const parsed = JSON.parse(chunk.content);
|
||||
if (parsed.type === 'tool_use' && parsed.name === 'AskUserQuestion') {
|
||||
const questions: QuestionItem[] = parsed.input?.questions ?? [];
|
||||
results.push({ timestamp: chunk.createdAt.toISOString(), questions });
|
||||
}
|
||||
} catch { /* skip malformed */ }
|
||||
if (results.length >= 200) break;
|
||||
}
|
||||
return results;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -302,5 +302,31 @@ export function conversationProcedures(publicProcedure: ProcedureBuilder) {
|
||||
cleanup();
|
||||
}
|
||||
}),
|
||||
|
||||
getByFromAgent: publicProcedure
|
||||
.input(z.object({ agentId: z.string().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const repo = requireConversationRepository(ctx);
|
||||
const agentManager = requireAgentManager(ctx);
|
||||
|
||||
const convs = await repo.findByFromAgentId(input.agentId);
|
||||
|
||||
// Build toAgent name map without N+1
|
||||
const toAgentIds = [...new Set(convs.map(c => c.toAgentId))];
|
||||
const allAgents = toAgentIds.length > 0 ? await agentManager.list() : [];
|
||||
const agentNameMap = new Map(allAgents.map(a => [a.id, a.name]));
|
||||
|
||||
return convs.map(c => ({
|
||||
id: c.id,
|
||||
timestamp: c.createdAt.toISOString(),
|
||||
toAgentName: agentNameMap.get(c.toAgentId) ?? c.toAgentId,
|
||||
toAgentId: c.toAgentId,
|
||||
question: c.question,
|
||||
answer: c.answer ?? null,
|
||||
status: c.status as 'pending' | 'answered',
|
||||
taskId: c.taskId ?? null,
|
||||
phaseId: c.phaseId ?? null,
|
||||
}));
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user