|
1 | 1 | import { Router } from 'express'; |
2 | 2 | import { scrollPoints, getCollectionInfo, computeEffectiveConfidence } from '../services/qdrant.js'; |
3 | | -// Briefing primarily uses Qdrant — structured store imports kept for potential future use |
4 | 3 |
|
5 | 4 | export const briefingRouter = Router(); |
6 | 5 |
|
| 6 | +const COMPACT_MAX_CONTENT = 200; |
| 7 | +const IMPORTANCE_RANK = { critical: 4, high: 3, medium: 2, low: 1 }; |
| 8 | + |
7 | 9 | // GET /briefing — Session briefing: what happened since timestamp |
| 10 | +// format=compact (default): truncated content, skip low-importance events — lean for agents |
| 11 | +// format=summary: counts + headlines only — minimal tokens |
| 12 | +// format=full: complete content — original behavior |
8 | 13 | briefingRouter.get('/', async (req, res) => { |
9 | 14 | try { |
10 | | - const { agent, since, include } = req.query; |
| 15 | + const { agent, since, include, limit: limitParam, format: formatParam } = req.query; |
| 16 | + const format = ['compact', 'summary', 'full'].includes(formatParam) ? formatParam : 'compact'; |
11 | 17 |
|
12 | 18 | if (!since) { |
13 | 19 | return res.status(400).json({ |
14 | 20 | error: 'Missing required parameter: since (ISO 8601 timestamp)', |
15 | | - example: '/briefing?since=2026-03-09T00:00:00Z&agent=claude-code', |
| 21 | + example: '/briefing?since=2026-03-09T00:00:00Z&agent=claude-code&format=compact', |
16 | 22 | }); |
17 | 23 | } |
18 | 24 |
|
19 | | - const briefing = { |
20 | | - since, |
21 | | - requesting_agent: agent || 'unknown', |
22 | | - generated_at: new Date().toISOString(), |
23 | | - events: [], |
24 | | - facts_updated: [], |
25 | | - status_changes: [], |
26 | | - decisions: [], |
27 | | - summary: {}, |
28 | | - }; |
29 | | - |
30 | | - // Get recent events from Qdrant (semantic store has everything) |
| 25 | + // Get recent events from Qdrant |
| 26 | + const scrollLimit = Math.min(Math.max(parseInt(limitParam) || 100, 1), 500); |
31 | 27 | const filter = { created_after: since }; |
32 | | - const recent = await scrollPoints(filter, 100); |
| 28 | + const recent = await scrollPoints(filter, scrollLimit); |
33 | 29 | const points = recent.points || []; |
34 | 30 |
|
35 | | - // Categorize results |
36 | | - for (const point of points) { |
| 31 | + // Filter points: exclude requesting agent's own entries (unless include=all) |
| 32 | + const filteredPoints = points.filter(point => { |
37 | 33 | const p = point.payload; |
38 | | - // Skip entries from the requesting agent (they already know what they did) |
39 | | - // Unless include=all is set |
40 | | - if (agent && p.source_agent === agent && include !== 'all') continue; |
41 | | - |
42 | | - const entry = { |
43 | | - id: point.id, |
44 | | - content: p.text, |
45 | | - source_agent: p.source_agent, |
46 | | - client_id: p.client_id, |
47 | | - category: p.category, |
48 | | - importance: p.importance, |
49 | | - confidence: computeEffectiveConfidence(p), |
50 | | - created_at: p.created_at, |
51 | | - }; |
| 34 | + if (agent && p.source_agent === agent && include !== 'all') return false; |
| 35 | + return true; |
| 36 | + }); |
52 | 37 |
|
53 | | - switch (p.type) { |
54 | | - case 'event': briefing.events.push(entry); break; |
55 | | - case 'fact': briefing.facts_updated.push(entry); break; |
56 | | - case 'status': briefing.status_changes.push(entry); break; |
57 | | - case 'decision': briefing.decisions.push(entry); break; |
58 | | - } |
59 | | - } |
60 | | - |
61 | | - // Sort each by created_at descending |
62 | | - const sortDesc = (a, b) => new Date(b.created_at) - new Date(a.created_at); |
63 | | - briefing.events.sort(sortDesc); |
64 | | - briefing.facts_updated.sort(sortDesc); |
65 | | - briefing.status_changes.sort(sortDesc); |
66 | | - briefing.decisions.sort(sortDesc); |
| 38 | + // In compact mode, skip low-importance events (keep facts/statuses/decisions regardless) |
| 39 | + const relevantPoints = format === 'compact' |
| 40 | + ? filteredPoints.filter(p => p.payload.type !== 'event' || p.payload.importance !== 'low') |
| 41 | + : filteredPoints; |
67 | 42 |
|
68 | | - // Collect entities mentioned across all briefing entries |
| 43 | + // Collect entity counts (before any truncation) |
69 | 44 | const entityCounts = {}; |
70 | | - for (const point of points) { |
71 | | - const entities = point.payload.entities || []; |
72 | | - for (const ent of entities) { |
73 | | - const key = ent.name; |
74 | | - if (!entityCounts[key]) entityCounts[key] = { name: ent.name, type: ent.type, count: 0 }; |
75 | | - entityCounts[key].count++; |
| 45 | + for (const point of relevantPoints) { |
| 46 | + for (const ent of (point.payload.entities || [])) { |
| 47 | + if (!entityCounts[ent.name]) entityCounts[ent.name] = { name: ent.name, type: ent.type, count: 0 }; |
| 48 | + entityCounts[ent.name].count++; |
76 | 49 | } |
77 | 50 | } |
78 | 51 | const entitiesMentioned = Object.values(entityCounts).sort((a, b) => b.count - a.count); |
79 | 52 |
|
80 | | - // Summary stats |
81 | | - briefing.summary = { |
82 | | - total_entries: points.length, |
83 | | - events: briefing.events.length, |
84 | | - facts_updated: briefing.facts_updated.length, |
85 | | - status_changes: briefing.status_changes.length, |
86 | | - decisions: briefing.decisions.length, |
| 53 | + // Build summary (always included) |
| 54 | + const summary = { |
| 55 | + total_entries: relevantPoints.length, |
| 56 | + total_in_period: points.length, |
| 57 | + events: 0, facts_updated: 0, status_changes: 0, decisions: 0, |
87 | 58 | agents_active: [...new Set(points.flatMap(p => p.payload.observed_by || [p.payload.source_agent]))], |
88 | 59 | clients_mentioned: [...new Set(points.map(p => p.payload.client_id).filter(c => c !== 'global'))], |
89 | | - entities_mentioned: entitiesMentioned.slice(0, 20), |
| 60 | + entities_mentioned: entitiesMentioned.slice(0, 15), |
| 61 | + }; |
| 62 | + |
| 63 | + // Categorize |
| 64 | + const buckets = { events: [], facts_updated: [], status_changes: [], decisions: [] }; |
| 65 | + for (const point of relevantPoints) { |
| 66 | + const p = point.payload; |
| 67 | + const confidence = computeEffectiveConfidence(p); |
| 68 | + |
| 69 | + // Build entry based on format |
| 70 | + let entry; |
| 71 | + if (format === 'summary') { |
| 72 | + // Minimal: first line of content only (headline) |
| 73 | + const firstLine = (p.text || '').split('\n')[0].slice(0, 120); |
| 74 | + entry = { |
| 75 | + id: point.id, |
| 76 | + headline: firstLine, |
| 77 | + source_agent: p.source_agent, |
| 78 | + client_id: p.client_id, |
| 79 | + importance: p.importance, |
| 80 | + created_at: p.created_at, |
| 81 | + }; |
| 82 | + } else if (format === 'compact') { |
| 83 | + // Truncated: first 200 chars + flag if truncated |
| 84 | + const text = p.text || ''; |
| 85 | + const truncated = text.length > COMPACT_MAX_CONTENT; |
| 86 | + entry = { |
| 87 | + id: point.id, |
| 88 | + content: truncated ? text.slice(0, COMPACT_MAX_CONTENT) + '...' : text, |
| 89 | + truncated, |
| 90 | + source_agent: p.source_agent, |
| 91 | + client_id: p.client_id, |
| 92 | + importance: p.importance, |
| 93 | + confidence: +confidence.toFixed(3), |
| 94 | + created_at: p.created_at, |
| 95 | + }; |
| 96 | + } else { |
| 97 | + // Full: original behavior |
| 98 | + entry = { |
| 99 | + id: point.id, |
| 100 | + content: p.text, |
| 101 | + source_agent: p.source_agent, |
| 102 | + client_id: p.client_id, |
| 103 | + category: p.category, |
| 104 | + importance: p.importance, |
| 105 | + confidence: +confidence.toFixed(4), |
| 106 | + created_at: p.created_at, |
| 107 | + }; |
| 108 | + } |
| 109 | + |
| 110 | + switch (p.type) { |
| 111 | + case 'event': buckets.events.push(entry); summary.events++; break; |
| 112 | + case 'fact': buckets.facts_updated.push(entry); summary.facts_updated++; break; |
| 113 | + case 'status': buckets.status_changes.push(entry); summary.status_changes++; break; |
| 114 | + case 'decision': buckets.decisions.push(entry); summary.decisions++; break; |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + // Sort by importance then recency (critical/high first, then by date) |
| 119 | + const sortByImportanceAndDate = (a, b) => { |
| 120 | + const impDiff = (IMPORTANCE_RANK[b.importance] || 0) - (IMPORTANCE_RANK[a.importance] || 0); |
| 121 | + if (impDiff !== 0) return impDiff; |
| 122 | + return new Date(b.created_at) - new Date(a.created_at); |
| 123 | + }; |
| 124 | + for (const arr of Object.values(buckets)) { |
| 125 | + arr.sort(sortByImportanceAndDate); |
| 126 | + } |
| 127 | + |
| 128 | + const briefing = { |
| 129 | + since, |
| 130 | + format, |
| 131 | + requesting_agent: agent || 'unknown', |
| 132 | + generated_at: new Date().toISOString(), |
| 133 | + summary, |
| 134 | + ...buckets, |
90 | 135 | }; |
91 | 136 |
|
92 | | - // Get collection stats |
93 | | - try { |
94 | | - const info = await getCollectionInfo(); |
95 | | - briefing.brain_stats = { |
96 | | - total_memories: info.points_count, |
97 | | - vectors_count: info.vectors_count, |
98 | | - }; |
99 | | - } catch (e) { |
100 | | - // Non-critical |
| 137 | + // Collection stats (compact/full only — skip for summary to save tokens) |
| 138 | + if (format !== 'summary') { |
| 139 | + try { |
| 140 | + const info = await getCollectionInfo(); |
| 141 | + briefing.brain_stats = { |
| 142 | + total_memories: info.points_count, |
| 143 | + vectors_count: info.vectors_count, |
| 144 | + }; |
| 145 | + } catch (e) { /* non-critical */ } |
101 | 146 | } |
102 | 147 |
|
103 | 148 | res.json(briefing); |
|
0 commit comments