board_get_activity
Query the activity log to audit tasks, reconstruct sessions, or track agent actions. Filter by task ID, session ID, agent name, or action, with results capped at a customizable limit.
Instructions
Query the activity_log. Filter by task_id, session_id, agent_name, or action. Results are ordered newest-first and capped at limit (default 50, max 200). Useful for auditing what happened on a task, reconstructing a session, or following an agent's actions.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| task_id | No | Filter by related task ID | |
| session_id | No | Filter by related session ID | |
| agent_name | No | Filter by agent name | |
| action | No | Filter by action type | |
| limit | No | Max entries to return (default 50, max 200) |
Implementation Reference
- src/tools/activity.ts:97-196 (handler)The async handler function for 'board_get_activity' tool. It queries Firestore 'activity_log' collection with optional filters (task_id, session_id, agent_name, action), paginates with a hard cap, applies remaining filters in JS to avoid composite indexes, and returns results ordered newest-first.
async ({ task_id, session_id, agent_name, action, limit }) => { // Build query with single-field filter then order+limit. Firestore // requires a composite index for multi-field filter+order; to avoid // that, we pick the most selective filter as the query filter and // apply any remaining filters in JS. let query: FirebaseFirestore.Query = db.collection("activity_log"); const jsFilters: Array<[string, unknown]> = []; // Pick one field to push to Firestore (ordered by selectivity for our // use cases). Remaining filters become JS predicates. if (task_id !== undefined) { query = query.where("task_id", "==", task_id); if (session_id !== undefined) jsFilters.push(["session_id", session_id]); if (agent_name !== undefined) jsFilters.push(["agent_name", agent_name]); if (action !== undefined) jsFilters.push(["action", action]); } else if (session_id !== undefined) { query = query.where("session_id", "==", session_id); if (agent_name !== undefined) jsFilters.push(["agent_name", agent_name]); if (action !== undefined) jsFilters.push(["action", action]); } else if (agent_name !== undefined) { query = query.where("agent_name", "==", agent_name); if (action !== undefined) jsFilters.push(["action", action]); } else if (action !== undefined) { query = query.where("action", "==", action); } // Else: unfiltered scan (bounded by limit). const effectiveLimit = Math.min(limit ?? 50, 200); // Order by created_at DESC directly in Firestore. Equality-filter + // order on a different field doesn't require a composite index for // our single-equality-filter cases (common composite-index requirement // only kicks in with range filters or multi-field equality + order). query = query.orderBy("created_at", "desc"); // Cursor pagination. When JS filters apply, fetch pages until we fill // `effectiveLimit` or hit a hard scan cap (prevents runaway reads on // highly-selective filters against huge collections). const PAGE_SIZE = 200; const HARD_SCAN_CAP = 2000; const results: Array<Record<string, unknown>> = []; let scanned = 0; let lastDoc: FirebaseFirestore.QueryDocumentSnapshot | null = null; let hitCap = false; const toISO = (v: unknown) => v && typeof v === "object" && "toDate" in (v as object) ? (v as { toDate(): Date }).toDate().toISOString() : null; while (results.length < effectiveLimit && scanned < HARD_SCAN_CAP) { let pageQuery = query.limit( Math.min(PAGE_SIZE, HARD_SCAN_CAP - scanned) ); if (lastDoc) pageQuery = pageQuery.startAfter(lastDoc); const snap = await pageQuery.get(); if (snap.empty) break; scanned += snap.size; for (const d of snap.docs) { const data = d.data(); const passes = jsFilters.every( ([k, v]) => (data as Record<string, unknown>)[k] === v ); if (!passes) continue; results.push({ id: d.id, ...data, created_at: toISO(data.created_at), }); if (results.length >= effectiveLimit) break; } if (snap.size < PAGE_SIZE) break; // reached end of collection lastDoc = snap.docs[snap.docs.length - 1]; } if (scanned >= HARD_SCAN_CAP && results.length < effectiveLimit) { hitCap = true; } return { content: [ { type: "text" as const, text: JSON.stringify( { entries: results, scanned, truncated: hitCap, note: hitCap ? `Scan cap ${HARD_SCAN_CAP} reached before filling limit ${effectiveLimit}. Results may be incomplete. Tighten filters or raise cap.` : undefined, }, null, 2 ), }, ], }; } - src/tools/activity.ts:64-96 (schema)Zod schema definitions for 'board_get_activity' tool, defining optional input parameters: task_id, session_id, agent_name, action (enum), limit (1-200, default 50).
"Query the activity_log. Filter by task_id, session_id, agent_name, or action. Results are ordered newest-first and capped at `limit` (default 50, max 200). Useful for auditing what happened on a task, reconstructing a session, or following an agent's actions.", { task_id: z.string().optional().describe("Filter by related task ID"), session_id: z .string() .optional() .describe("Filter by related session ID"), agent_name: z .string() .optional() .describe("Filter by agent name"), action: z .enum([ "created", "updated", "claimed", "blocked", "completed", "commented", "mode_changed", "session_started", "session_ended", ]) .optional() .describe("Filter by action type"), limit: z .number() .int() .min(1) .max(200) .optional() .describe("Max entries to return (default 50, max 200)"), }, - src/tools/activity.ts:62-197 (registration)Registration of 'board_get_activity' via server.tool() inside registerActivityTools(). The tool is registered with name, description, schema, and handler.
server.tool( "board_get_activity", "Query the activity_log. Filter by task_id, session_id, agent_name, or action. Results are ordered newest-first and capped at `limit` (default 50, max 200). Useful for auditing what happened on a task, reconstructing a session, or following an agent's actions.", { task_id: z.string().optional().describe("Filter by related task ID"), session_id: z .string() .optional() .describe("Filter by related session ID"), agent_name: z .string() .optional() .describe("Filter by agent name"), action: z .enum([ "created", "updated", "claimed", "blocked", "completed", "commented", "mode_changed", "session_started", "session_ended", ]) .optional() .describe("Filter by action type"), limit: z .number() .int() .min(1) .max(200) .optional() .describe("Max entries to return (default 50, max 200)"), }, async ({ task_id, session_id, agent_name, action, limit }) => { // Build query with single-field filter then order+limit. Firestore // requires a composite index for multi-field filter+order; to avoid // that, we pick the most selective filter as the query filter and // apply any remaining filters in JS. let query: FirebaseFirestore.Query = db.collection("activity_log"); const jsFilters: Array<[string, unknown]> = []; // Pick one field to push to Firestore (ordered by selectivity for our // use cases). Remaining filters become JS predicates. if (task_id !== undefined) { query = query.where("task_id", "==", task_id); if (session_id !== undefined) jsFilters.push(["session_id", session_id]); if (agent_name !== undefined) jsFilters.push(["agent_name", agent_name]); if (action !== undefined) jsFilters.push(["action", action]); } else if (session_id !== undefined) { query = query.where("session_id", "==", session_id); if (agent_name !== undefined) jsFilters.push(["agent_name", agent_name]); if (action !== undefined) jsFilters.push(["action", action]); } else if (agent_name !== undefined) { query = query.where("agent_name", "==", agent_name); if (action !== undefined) jsFilters.push(["action", action]); } else if (action !== undefined) { query = query.where("action", "==", action); } // Else: unfiltered scan (bounded by limit). const effectiveLimit = Math.min(limit ?? 50, 200); // Order by created_at DESC directly in Firestore. Equality-filter + // order on a different field doesn't require a composite index for // our single-equality-filter cases (common composite-index requirement // only kicks in with range filters or multi-field equality + order). query = query.orderBy("created_at", "desc"); // Cursor pagination. When JS filters apply, fetch pages until we fill // `effectiveLimit` or hit a hard scan cap (prevents runaway reads on // highly-selective filters against huge collections). const PAGE_SIZE = 200; const HARD_SCAN_CAP = 2000; const results: Array<Record<string, unknown>> = []; let scanned = 0; let lastDoc: FirebaseFirestore.QueryDocumentSnapshot | null = null; let hitCap = false; const toISO = (v: unknown) => v && typeof v === "object" && "toDate" in (v as object) ? (v as { toDate(): Date }).toDate().toISOString() : null; while (results.length < effectiveLimit && scanned < HARD_SCAN_CAP) { let pageQuery = query.limit( Math.min(PAGE_SIZE, HARD_SCAN_CAP - scanned) ); if (lastDoc) pageQuery = pageQuery.startAfter(lastDoc); const snap = await pageQuery.get(); if (snap.empty) break; scanned += snap.size; for (const d of snap.docs) { const data = d.data(); const passes = jsFilters.every( ([k, v]) => (data as Record<string, unknown>)[k] === v ); if (!passes) continue; results.push({ id: d.id, ...data, created_at: toISO(data.created_at), }); if (results.length >= effectiveLimit) break; } if (snap.size < PAGE_SIZE) break; // reached end of collection lastDoc = snap.docs[snap.docs.length - 1]; } if (scanned >= HARD_SCAN_CAP && results.length < effectiveLimit) { hitCap = true; } return { content: [ { type: "text" as const, text: JSON.stringify( { entries: results, scanned, truncated: hitCap, note: hitCap ? `Scan cap ${HARD_SCAN_CAP} reached before filling limit ${effectiveLimit}. Results may be incomplete. Tighten filters or raise cap.` : undefined, }, null, 2 ), }, ], }; } ); - src/index.ts:31-31 (registration)The call site where registerActivityTools(server, db) is invoked, which registers both board_log_activity and board_get_activity.
registerActivityTools(server, db); - src/types.ts:57-74 (helper)ActivityLog interface type definition used by the activity tools, defining the shape of activity log entries (task_id, session_id, agent_name, action, details, metadata, created_at).
export interface ActivityLog { task_id: string | null; session_id: string | null; agent_name: string; action: | "created" | "updated" | "claimed" | "blocked" | "completed" | "commented" | "mode_changed" | "session_started" | "session_ended"; details: string | null; metadata: Record<string, unknown>; created_at: Timestamp; }