import { type PrismaClientOrTransaction, type RuntimeEnvironmentType, type TaskTriggerSource, } from "@trigger.dev/database"; import { type ClickHouse } from "@internal/clickhouse"; import { z } from "zod"; import { $replica } from "~/db.server"; import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; import { singleton } from "~/utils/singleton"; import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server"; export type AgentListItem = { slug: string; filePath: string; createdAt: Date; triggerSource: TaskTriggerSource; config: unknown; }; export type AgentActiveState = { running: number; suspended: number; }; export class AgentListPresenter { constructor(private readonly _replica: PrismaClientOrTransaction) {} public async call({ organizationId, projectId, environmentId, environmentType, currentWorker: preloadedCurrentWorker, }: { organizationId: string; projectId: string; environmentId: string; environmentType: RuntimeEnvironmentType; /** Optional: pass the pre-resolved current worker to skip the lookup. Used * by `UnifiedTaskListPresenter` to share one lookup across both presenters. */ currentWorker?: Awaited>; }) { const clickhouse = await clickhouseFactory.getClickhouseForOrganization( organizationId, "standard" ); const currentWorker = preloadedCurrentWorker !== undefined ? preloadedCurrentWorker : await findCurrentWorkerFromEnvironment( { id: environmentId, type: environmentType, }, this._replica ); if (!currentWorker) { return { agents: [], activeStates: Promise.resolve({} as Record), conversationSparklines: Promise.resolve({} as Record), costSparklines: Promise.resolve({} as Record), tokenSparklines: Promise.resolve({} as Record), }; } const agents = await this._replica.backgroundWorkerTask.findMany({ where: { workerId: currentWorker.id, triggerSource: "AGENT", }, select: { id: true, slug: true, filePath: true, triggerSource: true, config: true, createdAt: true, }, orderBy: { slug: "asc", }, }); const slugs = agents.map((a) => a.slug); if (slugs.length === 0) { return { agents, activeStates: Promise.resolve({} as Record), conversationSparklines: Promise.resolve({} as Record), costSparklines: Promise.resolve({} as Record), tokenSparklines: Promise.resolve({} as Record), }; } // All queries are deferred for streaming const activeStates = this.#getActiveStates(clickhouse, environmentId, slugs); const conversationSparklines = this.#getConversationSparklines( clickhouse, environmentId, slugs ); const costSparklines = this.#getCostSparklines(clickhouse, environmentId, slugs); const tokenSparklines = this.#getTokenSparklines(clickhouse, environmentId, slugs); return { agents, activeStates, conversationSparklines, costSparklines, tokenSparklines }; } /** Count runs currently executing vs suspended per agent */ async #getActiveStates( clickhouse: ClickHouse, environmentId: string, slugs: string[] ): Promise> { const queryFn = clickhouse.reader.query({ name: "agentActiveStates", query: `SELECT task_identifier, countIf(status = 'EXECUTING') AS running, countIf(status IN ('WAITING_TO_RESUME', 'QUEUED_EXECUTING')) AS suspended FROM trigger_dev.task_runs_v2 WHERE environment_id = {environmentId: String} AND task_identifier IN {slugs: Array(String)} AND task_kind = 'AGENT' AND status IN ('EXECUTING', 'WAITING_TO_RESUME', 'QUEUED_EXECUTING') GROUP BY task_identifier`, params: z.object({ environmentId: z.string(), slugs: z.array(z.string()), }), schema: z.object({ task_identifier: z.string(), running: z.coerce.number(), suspended: z.coerce.number(), }), }); const [error, rows] = await queryFn({ environmentId, slugs }); if (error) { console.error("Agent active states query failed:", error); return {}; } const result: Record = {}; for (const row of rows) { result[row.task_identifier] = { running: row.running, suspended: row.suspended }; } return result; } /** 24h hourly sparkline of conversation (run) count per agent */ async #getConversationSparklines( clickhouse: ClickHouse, environmentId: string, slugs: string[] ): Promise> { const queryFn = clickhouse.reader.query({ name: "agentConversationSparklines", query: `SELECT task_identifier, toStartOfHour(created_at) AS bucket, count() AS val FROM trigger_dev.task_runs_v2 WHERE environment_id = {environmentId: String} AND task_identifier IN {slugs: Array(String)} AND task_kind = 'AGENT' AND created_at >= now() - INTERVAL 24 HOUR GROUP BY task_identifier, bucket ORDER BY task_identifier, bucket`, params: z.object({ environmentId: z.string(), slugs: z.array(z.string()), }), schema: z.object({ task_identifier: z.string(), bucket: z.string(), val: z.coerce.number(), }), }); return this.#buildSparklineMap(await queryFn({ environmentId, slugs }), slugs); } /** 24h hourly sparkline of LLM cost per agent */ async #getCostSparklines( clickhouse: ClickHouse, environmentId: string, slugs: string[] ): Promise> { const queryFn = clickhouse.reader.query({ name: "agentCostSparklines", query: `SELECT task_identifier, toStartOfHour(start_time) AS bucket, sum(total_cost) AS val FROM trigger_dev.llm_metrics_v1 WHERE environment_id = {environmentId: String} AND task_identifier IN {slugs: Array(String)} AND start_time >= now() - INTERVAL 24 HOUR GROUP BY task_identifier, bucket ORDER BY task_identifier, bucket`, params: z.object({ environmentId: z.string(), slugs: z.array(z.string()), }), schema: z.object({ task_identifier: z.string(), bucket: z.string(), val: z.coerce.number(), }), }); return this.#buildSparklineMap(await queryFn({ environmentId, slugs }), slugs); } /** 24h hourly sparkline of total tokens per agent */ async #getTokenSparklines( clickhouse: ClickHouse, environmentId: string, slugs: string[] ): Promise> { const queryFn = clickhouse.reader.query({ name: "agentTokenSparklines", query: `SELECT task_identifier, toStartOfHour(start_time) AS bucket, sum(total_tokens) AS val FROM trigger_dev.llm_metrics_v1 WHERE environment_id = {environmentId: String} AND task_identifier IN {slugs: Array(String)} AND start_time >= now() - INTERVAL 24 HOUR GROUP BY task_identifier, bucket ORDER BY task_identifier, bucket`, params: z.object({ environmentId: z.string(), slugs: z.array(z.string()), }), schema: z.object({ task_identifier: z.string(), bucket: z.string(), val: z.coerce.number(), }), }); return this.#buildSparklineMap(await queryFn({ environmentId, slugs }), slugs); } /** Convert ClickHouse query result to sparkline map with zero-filled 24 hourly buckets */ #buildSparklineMap( queryResult: [Error, null] | [null, { task_identifier: string; bucket: string; val: number }[]], slugs: string[] ): Record { const [error, rows] = queryResult; if (error) { console.error("Agent sparkline query failed:", error); return {}; } return this.#buildSparklineFromRows(rows, slugs); } #buildSparklineFromRows( rows: { task_identifier: string; bucket: string; val: number }[], slugs: string[] ): Record { const now = new Date(); const startHour = new Date( Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours() - 23, 0, 0, 0 ) ); const bucketKeys: string[] = []; for (let i = 0; i < 24; i++) { const h = new Date(startHour.getTime() + i * 3600_000); bucketKeys.push(h.toISOString().slice(0, 13).replace("T", " ") + ":00:00"); } const rowMap = new Map(); for (const row of rows) { rowMap.set(`${row.task_identifier}|${row.bucket}`, row.val); } const result: Record = {}; for (const slug of slugs) { result[slug] = bucketKeys.map((key) => rowMap.get(`${slug}|${key}`) ?? 0); } return result; } } export const agentListPresenter = singleton("agentListPresenter", setupAgentListPresenter); function setupAgentListPresenter() { return new AgentListPresenter($replica); }