Merge branch 'cw/radar' into cw-merge-1772825408137

This commit is contained in:
Lukas May
2026-03-06 20:30:08 +01:00
22 changed files with 3186 additions and 5 deletions

View File

@@ -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;
}),
};
}

View File

@@ -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,
}));
}),
};
}