personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rename "topic" to "agent" across the codebase

The "topic" field originally named the agent/generator that produced
output. Rename to "agent" consistently: Python identifiers, SQLite FTS5
schema column, CLI flags (--topic/-t → --agent/-a), CLI commands
(topics → agents), JSON event fields, stats cache keys, API response
keys, UI constants and templates, documentation, and muse prompts.

Key renames:
- get_output_topic() → get_output_name() (returns filename stem)
- meta["indexer"]["topic"] → meta["indexer"]["agent"]
- write_events_jsonl(topic=) → write_events_jsonl(agent=)
- TOPIC_ICONS/LABELS → AGENT_ICONS/LABELS
- topic_counts/minutes/counts_by_day → agent_counts/minutes/counts_by_day

Adds migration script (apps/search/maint/004_migrate_topic_to_agent.py)
for existing event JSONL and stats cache files. Index requires rebuild
via sol indexer --rebuild after migration.

Audio enrichment "topics" field (subject-matter keywords) is unchanged.
Natural-language uses of "topic" in muse prompts are preserved.

Also fixes a flaky test in test_cortex_client.py where callosum_server
fixture didn't match platform default path.

+625 -461
+27 -27
apps/calendar/_day.html
··· 500 500 const isPlanned = o.occurred === false; 501 501 const cardClass = isPlanned ? 'all-day-card planned' : 'all-day-card'; 502 502 const badge = isPlanned ? '<span class="card-badge">Planned</span>' : ''; 503 - const title = escapeHtml(o.title || o.summary || o.topic || 'Event'); 503 + const title = escapeHtml(o.title || o.summary || o.agent || 'Event'); 504 504 html += `<div class="${cardClass}" data-index="${idx}" style="border-color:${o.color || '#6c757d'}">`; 505 505 html += `<span class="card-title">${title}</span>${badge}</div>`; 506 506 }); ··· 556 556 557 557 // Process event blocks for column layout 558 558 const processedEvents = []; 559 - const topics = []; 559 + const agents = []; 560 560 561 561 timedEvents.forEach(o => { 562 562 const idx = list.indexOf(o); ··· 568 568 e = Math.min(e, endHour * 60); 569 569 if (e <= s) return; 570 570 571 - const topic = o.topic || 'other'; 572 - if (!topics.includes(topic)) topics.push(topic); 571 + const agent = o.agent || 'other'; 572 + if (!agents.includes(agent)) agents.push(agent); 573 573 574 574 processedEvents.push({ 575 - startMinutes: s, endMinutes: e, topic: topic, 576 - title: escapeHtml(o.title || o.summary || topic), 575 + startMinutes: s, endMinutes: e, agent: agent, 576 + title: escapeHtml(o.title || o.summary || agent), 577 577 color: o.color || '#6c757d', index: idx, 578 578 isPlanned: o.occurred === false 579 579 }); 580 580 }); 581 581 582 - const eventsByTopic = {}; 583 - topics.forEach(topic => { 584 - eventsByTopic[topic] = processedEvents.filter(e => e.topic === topic); 585 - eventsByTopic[topic].sort((a, b) => a.startMinutes - b.startMinutes); 582 + const eventsByAgent = {}; 583 + agents.forEach(agent => { 584 + eventsByAgent[agent] = processedEvents.filter(e => e.agent === agent); 585 + eventsByAgent[agent].sort((a, b) => a.startMinutes - b.startMinutes); 586 586 }); 587 587 588 - const topicColumnCounts = {}; 589 - topics.forEach(topic => { 590 - const topicEvents = eventsByTopic[topic]; 591 - const topicColumns = []; 592 - topicEvents.forEach(event => { 588 + const agentColumnCounts = {}; 589 + agents.forEach(agent => { 590 + const agentEvents = eventsByAgent[agent]; 591 + const agentColumns = []; 592 + agentEvents.forEach(event => { 593 593 let columnIndex = 0; 594 - while (columnIndex < topicColumns.length && 595 - topicColumns[columnIndex].some(existing => 594 + while (columnIndex < agentColumns.length && 595 + agentColumns[columnIndex].some(existing => 596 596 event.startMinutes < existing.endMinutes && 597 597 event.endMinutes > existing.startMinutes)) { 598 598 columnIndex++; 599 599 } 600 - if (columnIndex === topicColumns.length) topicColumns.push([]); 601 - topicColumns[columnIndex].push(event); 600 + if (columnIndex === agentColumns.length) agentColumns.push([]); 601 + agentColumns[columnIndex].push(event); 602 602 event.slugColumn = columnIndex; 603 603 }); 604 - topicColumnCounts[topic] = topicColumns.length; 604 + agentColumnCounts[agent] = agentColumns.length; 605 605 }); 606 606 607 607 let globalColumnOffset = 0; 608 - const topicColumnOffsets = {}; 609 - topics.forEach(topic => { 610 - topicColumnOffsets[topic] = globalColumnOffset; 611 - globalColumnOffset += topicColumnCounts[topic]; 608 + const agentColumnOffsets = {}; 609 + agents.forEach(agent => { 610 + agentColumnOffsets[agent] = globalColumnOffset; 611 + globalColumnOffset += agentColumnCounts[agent]; 612 612 }); 613 613 614 614 const totalColumns = globalColumnOffset; 615 615 const columnWidth = totalColumns > 0 ? 100 / totalColumns : 100; 616 616 617 617 processedEvents.forEach(event => { 618 - event.column = topicColumnOffsets[event.topic] + event.slugColumn; 618 + event.column = agentColumnOffsets[event.agent] + event.slugColumn; 619 619 }); 620 620 621 621 processedEvents.forEach(event => { ··· 671 671 if (!occ) return; 672 672 const body = document.getElementById('modalBody'); 673 673 const isPlanned = occ.occurred === false; 674 - const topic = occ.topic ? occ.topic.charAt(0).toUpperCase() + occ.topic.slice(1) : ''; 675 - const titleText = `${topic ? topic + ': ' : ''}${occ.title || ''}`.trim(); 674 + const agent = occ.agent ? occ.agent.charAt(0).toUpperCase() + occ.agent.slice(1) : ''; 675 + const titleText = `${agent ? agent + ': ' : ''}${occ.title || ''}`.trim(); 676 676 const subtitle = occ.subject || occ.summary || ''; 677 677 678 678 let html = '';
+4 -4
apps/calendar/routes.py
··· 66 66 # Transform events into timeline format 67 67 result = [] 68 68 for event in raw_events: 69 - topic = event.get("topic", "other") 70 - topic_color = generators.get(topic, {}).get("color", "#6c757d") 69 + agent = event.get("agent", "other") 70 + agent_color = generators.get(agent, {}).get("color", "#6c757d") 71 71 72 72 formatted = { 73 73 "title": event.get("title", ""), ··· 75 75 "subject": event.get("subject", ""), 76 76 "details": event.get("details", event.get("description", "")), 77 77 "participants": event.get("participants", []), 78 - "topic": topic, 79 - "color": topic_color, 78 + "agent": agent, 79 + "color": agent_color, 80 80 "facet": event.get("facet", ""), 81 81 "occurred": event.get("occurred", True), 82 82 "source": event.get("source", ""),
+3 -3
apps/chat/workspace.html
··· 1525 1525 } 1526 1526 } 1527 1527 1528 - // Format tool args into display-friendly params (query, facet, day, topic, etc.) 1528 + // Format tool args into display-friendly params (query, facet, day, agent, etc.) 1529 1529 function formatToolParams(args) { 1530 1530 if (!args) return []; 1531 1531 const params = []; ··· 1545 1545 if (args.day_to) { 1546 1546 params.push({label: 'to', value: formatDay(args.day_to)}); 1547 1547 } 1548 - if (args.topic) { 1549 - params.push({label: 'topic', value: args.topic}); 1548 + if (args.agent) { 1549 + params.push({label: 'agent', value: args.agent}); 1550 1550 } 1551 1551 1552 1552 return params;
+3 -3
apps/entities/muse/entities.md
··· 54 54 - The `entity` parameter can be entity_id, full name, or alias - if it matches an attached entity, uses that entity's canonical name 55 55 56 56 Discovery tools (note facet scoping): 57 - - `sol call journal read TOPIC` - read full agent output (e.g., knowledge_graph, followups) - GLOBAL 58 - - `sol call journal search QUERY -d DAY -t TOPIC -f FACET -n LIMIT` - unified search across all journal content - facet-scopable 57 + - `sol call journal read AGENT` - read full agent output (e.g., knowledge_graph, followups) - GLOBAL 58 + - `sol call journal search QUERY -d DAY -a AGENT -f FACET -n LIMIT` - unified search across all journal content - facet-scopable 59 59 - `sol call journal events [-f FACET]` - get structured events - **FACET-SCOPED when facet parameter provided** 60 60 61 61 **IMPORTANT**: When using GLOBAL search tools, you must actively filter results to find ONLY entities that participated in THIS facet's activities. Seeing an entity in a global search result does NOT automatically mean it belongs to this facet. ··· 91 91 92 92 **Priority 3: Insights and Transcripts** (use sparingly with extreme filtering) 93 93 - `sol call journal search "people OR companies OR organizations OR projects OR entities" -d $day_YYYYMMDD -n 10` - GLOBAL, may include other facets 94 - - `sol call journal search "[entity names]" -d $day_YYYYMMDD -t audio` - GLOBAL, must validate facet relevance 94 + - `sol call journal search "[entity names]" -d $day_YYYYMMDD -a audio` - GLOBAL, must validate facet relevance 95 95 - For each result: verify the entity was actively involved in THIS facet's context, not just mentioned 96 96 97 97 **Red flag check**: If you're finding many entities but facet events were empty, you're likely detecting entities from other facets. Stop and reassess.
+5 -5
apps/entities/muse/entity_assist.md
··· 39 39 40 40 Research tools (use sparingly, be quick): 41 41 - `sol call journal search QUERY -n 3` - find entity mentions in all journal content 42 - - `sol call journal search QUERY -t audio -n 3` - find entity in transcripts 43 - - `sol call journal search QUERY -t news -n 3` - find entity in facet news 42 + - `sol call journal search QUERY -a audio -n 3` - find entity in transcripts 43 + - `sol call journal search QUERY -a news -n 3` - find entity in facet news 44 44 - `sol call journal events -d DAY` - find entity in events for a specific day 45 - - `sol call journal read TOPIC` - read full agent output when snippet search is insufficient 45 + - `sol call journal read AGENT` - read full agent output when snippet search is insufficient 46 46 47 47 ## Quick Addition Process 48 48 ··· 72 72 ### Step 3: Quick Research 73 73 74 74 Execute a few targeted searches based on type: 75 - - **Person**: `sol call journal search "{name}" -n 3` or `sol call journal search "{name}" -t event -n 3` 76 - - **Company**: `sol call journal search "{name}" -t news -n 3` or `sol call journal search "{name}" -n 3` 75 + - **Person**: `sol call journal search "{name}" -n 3` or `sol call journal search "{name}" -a event -n 3` 76 + - **Company**: `sol call journal search "{name}" -a news -n 3` or `sol call journal search "{name}" -n 3` 77 77 - **Project**: `sol call journal search "{name}" -n 3` 78 78 - **Tool**: `sol call journal search "{name}" -n 3` 79 79
+1 -1
apps/entities/muse/entity_describe.md
··· 25 25 26 26 Use these `sol call` commands for quick research (be efficient, 2-3 calls max): 27 27 - `sol call journal search QUERY -f FACET -n LIMIT` - find mentions in journal content, scoped to facet 28 - - `sol call journal search QUERY -t audio -n LIMIT` - find mentions in transcripts 28 + - `sol call journal search QUERY -a audio -n LIMIT` - find mentions in transcripts 29 29 30 30 ## Process 31 31
+3 -3
apps/entities/muse/entity_observer.md
··· 34 34 - Use entity_id from `sol call entities observations` response for consistency 35 35 36 36 Discovery tools: 37 - - `sol call journal read TOPIC` - read full agent output (e.g., knowledge_graph, followups) 38 - - `sol call journal search QUERY -d DAY -t TOPIC -f FACET -n LIMIT` - unified search across journal content 37 + - `sol call journal read AGENT` - read full agent output (e.g., knowledge_graph, followups) 38 + - `sol call journal search QUERY -d DAY -a AGENT -f FACET -n LIMIT` - unified search across journal content 39 39 - `sol call journal events [-f FACET]` - get structured events 40 40 41 41 ## What Makes a Good Observation ··· 75 75 The response includes the resolved entity with its `id` field. 76 76 77 77 2. **Mine recent content** for factoids about this entity: 78 - - Search transcripts: `sol call journal search "{name}" -t audio -n 5` 78 + - Search transcripts: `sol call journal search "{name}" -a audio -n 5` 79 79 - Check knowledge graph: `sol call journal read knowledge_graph` 80 80 - Search insights: `sol call journal search "{name}" -n 5` 81 81
+10 -10
apps/home/routes.py
··· 95 95 }, 96 96 } 97 97 98 - # Events by topic (aggregated across facets for totals) 99 - events_by_topic: dict[str, int] = defaultdict(int) 98 + # Events by agent (aggregated across facets for totals) 99 + events_by_agent: dict[str, int] = defaultdict(int) 100 100 101 101 for facet_name in facet_names: 102 102 facet_config = facet_map.get(facet_name, {}) 103 103 facet_data: dict[str, Any] = { 104 104 "todos": [], 105 - "events_by_topic": {}, 105 + "events_by_agent": {}, 106 106 "entities": [], 107 107 "news_content": None, 108 108 } ··· 127 127 events = get_events(day, facet=facet_name) 128 128 facet_events: dict[str, int] = defaultdict(int) 129 129 for event in events: 130 - topic = event.get("topic", "other") 131 - facet_events[topic] += 1 132 - events_by_topic[topic] += 1 130 + agent = event.get("agent", "other") 131 + facet_events[agent] += 1 132 + events_by_agent[agent] += 1 133 133 result["totals"]["events"] += 1 134 - facet_data["events_by_topic"] = dict(facet_events) 134 + facet_data["events_by_agent"] = dict(facet_events) 135 135 except Exception: 136 136 pass 137 137 ··· 160 160 # Determine if facet has any activity for this day 161 161 has_activity = bool( 162 162 facet_data["todos"] 163 - or facet_data["events_by_topic"] 163 + or facet_data["events_by_agent"] 164 164 or facet_data["entities"] 165 165 ) 166 166 ··· 172 172 "has_activity": has_activity, 173 173 } 174 174 175 - # Add aggregated events by topic to totals 176 - result["totals"]["events_by_topic"] = dict(events_by_topic) 175 + # Add aggregated events by agent to totals 176 + result["totals"]["events_by_agent"] = dict(events_by_agent) 177 177 178 178 # Load recent entities (attached entities with last_seen in lookback window) 179 179 recent_entities = _get_recent_entities(day, facet_names)
+11 -11
apps/home/workspace.html
··· 399 399 400 400 function renderEvents(data, facet) { 401 401 const container = document.getElementById('events-content'); 402 - let eventsByTopic; 402 + let eventsByAgent; 403 403 404 404 if (facet) { 405 405 const facetData = data.facets[facet]; 406 - eventsByTopic = facetData ? facetData.events_by_topic : {}; 406 + eventsByAgent = facetData ? facetData.events_by_agent : {}; 407 407 } else { 408 - eventsByTopic = data.totals.events_by_topic || {}; 408 + eventsByAgent = data.totals.events_by_agent || {}; 409 409 } 410 410 411 - const topics = Object.keys(eventsByTopic); 412 - if (topics.length === 0) { 411 + const agents = Object.keys(eventsByAgent); 412 + if (agents.length === 0) { 413 413 container.innerHTML = '<div class="home-empty">No events</div>'; 414 414 return; 415 415 } 416 416 417 417 // Sort by count descending 418 - topics.sort((a, b) => eventsByTopic[b] - eventsByTopic[a]); 418 + agents.sort((a, b) => eventsByAgent[b] - eventsByAgent[a]); 419 419 420 420 // Find max count for scaling 421 - const maxCount = Math.max(...topics.map(t => eventsByTopic[t])); 421 + const maxCount = Math.max(...agents.map(t => eventsByAgent[t])); 422 422 423 423 container.innerHTML = ` 424 424 <div class="home-event-list"> 425 - ${topics.map(topic => { 426 - const count = eventsByTopic[topic]; 425 + ${agents.map(agent => { 426 + const count = eventsByAgent[agent]; 427 427 const percent = maxCount > 0 ? (count / maxCount) * 100 : 0; 428 428 return ` 429 429 <div class="home-event-row"> 430 430 <div class="home-event-bar" style="width: ${percent}%"></div> 431 - <span class="home-event-label">${topic}</span> 431 + <span class="home-event-label">${agent}</span> 432 432 </div> 433 433 `; 434 434 }).join('')} ··· 543 543 const facetData = data.facets[name] || {}; 544 544 const todos = facetData.todos || []; 545 545 const pendingTodos = todos.filter(t => !t.completed).length; 546 - const eventCount = Object.values(facetData.events_by_topic || {}).reduce((a, b) => a + b, 0); 546 + const eventCount = Object.values(facetData.events_by_agent || {}).reduce((a, b) => a + b, 0); 547 547 // For recent days, use recent entities count if no detected entities 548 548 const detectedCount = (facetData.entities || []).length; 549 549 const recentCount = recentDay ? (data.recent_entities?.[name] || []).length : 0;
+1 -1
apps/search/maint/003_migrate_index_stream.py
··· 20 20 21 21 logger = logging.getLogger(__name__) 22 22 23 - EXPECTED_COLUMNS = {"content", "path", "day", "facet", "topic", "stream", "idx"} 23 + EXPECTED_COLUMNS = {"content", "path", "day", "facet", "agent", "stream", "idx"} 24 24 25 25 26 26 def _get_db_path(journal: str) -> str:
+163
apps/search/maint/004_migrate_topic_to_agent.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Migrate journal event/stats keys from topic naming to agent naming.""" 5 + 6 + from __future__ import annotations 7 + 8 + import argparse 9 + import json 10 + from dataclasses import dataclass 11 + from pathlib import Path 12 + 13 + from think.utils import get_journal, setup_cli 14 + 15 + STATS_KEY_RENAMES = { 16 + "topic_data": "agent_data", 17 + "topic_counts": "agent_counts", 18 + "topic_minutes": "agent_minutes", 19 + "topic_counts_by_day": "agent_counts_by_day", 20 + } 21 + 22 + 23 + @dataclass 24 + class MigrationCounters: 25 + """Mutable counters for migration operations.""" 26 + 27 + events_modified: int = 0 28 + events_skipped: int = 0 29 + events_errors: int = 0 30 + stats_modified: int = 0 31 + stats_skipped: int = 0 32 + stats_errors: int = 0 33 + event_records_renamed: int = 0 34 + stats_keys_renamed: int = 0 35 + 36 + 37 + def _migrate_event_file( 38 + file_path: Path, *, dry_run: bool, counters: MigrationCounters 39 + ) -> None: 40 + """Migrate one events JSONL file from topic key to agent key.""" 41 + try: 42 + lines = file_path.read_text(encoding="utf-8").splitlines() 43 + out_lines: list[str] = [] 44 + modified = False 45 + 46 + for line in lines: 47 + stripped = line.strip() 48 + if not stripped: 49 + continue 50 + 51 + row = json.loads(stripped) 52 + if isinstance(row, dict) and "topic" in row: 53 + if "agent" not in row: 54 + row["agent"] = row["topic"] 55 + del row["topic"] 56 + modified = True 57 + counters.event_records_renamed += 1 58 + 59 + out_lines.append(json.dumps(row, ensure_ascii=False)) 60 + 61 + if not modified: 62 + counters.events_skipped += 1 63 + return 64 + 65 + counters.events_modified += 1 66 + if dry_run: 67 + print(f"[DRY-RUN] update {file_path}") 68 + return 69 + 70 + payload = "\n".join(out_lines) 71 + if payload: 72 + payload += "\n" 73 + file_path.write_text(payload, encoding="utf-8") 74 + except Exception as exc: 75 + counters.events_errors += 1 76 + print(f"[ERROR] events migration failed for {file_path}: {exc}") 77 + 78 + 79 + def _migrate_stats_file( 80 + file_path: Path, *, dry_run: bool, counters: MigrationCounters 81 + ) -> None: 82 + """Migrate one stats.json file from topic keys to agent keys.""" 83 + try: 84 + data = json.loads(file_path.read_text(encoding="utf-8")) 85 + if not isinstance(data, dict): 86 + counters.stats_skipped += 1 87 + return 88 + 89 + modified = False 90 + for old_key, new_key in STATS_KEY_RENAMES.items(): 91 + if old_key not in data: 92 + continue 93 + 94 + if new_key not in data: 95 + data[new_key] = data[old_key] 96 + del data[old_key] 97 + modified = True 98 + counters.stats_keys_renamed += 1 99 + 100 + if not modified: 101 + counters.stats_skipped += 1 102 + return 103 + 104 + counters.stats_modified += 1 105 + if dry_run: 106 + print(f"[DRY-RUN] update {file_path}") 107 + return 108 + 109 + file_path.write_text( 110 + json.dumps(data, indent=2, ensure_ascii=False) + "\n", 111 + encoding="utf-8", 112 + ) 113 + except Exception as exc: 114 + counters.stats_errors += 1 115 + print(f"[ERROR] stats migration failed for {file_path}: {exc}") 116 + 117 + 118 + def migrate_topic_to_agent(*, journal: str, dry_run: bool) -> MigrationCounters: 119 + """Run topic->agent key migration for events JSONL and stats JSON files.""" 120 + counters = MigrationCounters() 121 + journal_path = Path(journal) 122 + 123 + event_files = sorted(journal_path.glob("facets/*/events/*.jsonl")) 124 + stats_files = sorted(journal_path.rglob("stats.json")) 125 + 126 + for file_path in event_files: 127 + _migrate_event_file(file_path, dry_run=dry_run, counters=counters) 128 + 129 + for file_path in stats_files: 130 + _migrate_stats_file(file_path, dry_run=dry_run, counters=counters) 131 + 132 + print("Migration complete") 133 + print(f" events modified: {counters.events_modified}") 134 + print(f" events skipped: {counters.events_skipped}") 135 + print(f" events errors: {counters.events_errors}") 136 + print(f" stats modified: {counters.stats_modified}") 137 + print(f" stats skipped: {counters.stats_skipped}") 138 + print(f" stats errors: {counters.stats_errors}") 139 + print(f" event rows renamed: {counters.event_records_renamed}") 140 + print(f" stats keys renamed: {counters.stats_keys_renamed}") 141 + print("After migration, run: sol indexer --rebuild") 142 + 143 + return counters 144 + 145 + 146 + def main() -> None: 147 + """CLI entry point for topic->agent migration.""" 148 + parser = argparse.ArgumentParser(description=__doc__) 149 + parser.add_argument( 150 + "--dry-run", 151 + action="store_true", 152 + help="Preview file changes without writing them.", 153 + ) 154 + args = setup_cli(parser) 155 + 156 + if args.dry_run: 157 + print("[DRY-RUN] No files will be modified.") 158 + 159 + migrate_topic_to_agent(journal=get_journal(), dry_run=args.dry_run) 160 + 161 + 162 + if __name__ == "__main__": 163 + main()
+29 -29
apps/search/routes.py
··· 18 18 url_prefix="/app/search", 19 19 ) 20 20 21 - # Topic icons for display 22 - TOPIC_ICONS = { 21 + # Agent icons for display 22 + AGENT_ICONS = { 23 23 "flow": "📝", 24 24 "knowledge_graph": "🗺️", 25 25 "meetings": "📅", ··· 34 34 "import": "📥", 35 35 } 36 36 37 - # Topic display names 38 - TOPIC_LABELS = { 37 + # Agent display names 38 + AGENT_LABELS = { 39 39 "flow": "Flow", 40 40 "knowledge_graph": "Knowledge Graph", 41 41 "meetings": "Meetings", ··· 56 56 return request.args.get("facet", "").strip() or None 57 57 58 58 59 - def _parse_topic_filter() -> str | None: 60 - """Parse single topic filter from request args.""" 61 - return request.args.get("topic", "").strip() or None 59 + def _parse_agent_filter() -> str | None: 60 + """Parse single agent filter from request args.""" 61 + return request.args.get("agent", "").strip() or None 62 62 63 63 64 64 def _parse_stream_filter() -> str | None: ··· 85 85 def _format_result(result: dict, query: str, facets_map: dict) -> dict: 86 86 """Format a search result for API response.""" 87 87 meta = result.get("metadata", {}) 88 - topic = meta.get("topic", "") 88 + agent = meta.get("agent", "") 89 89 text = result.get("text", "") 90 90 facet_name = meta.get("facet", "") 91 91 ··· 105 105 return { 106 106 "id": result.get("id", ""), 107 107 "day": meta.get("day", ""), 108 - "topic": topic, 109 - "topic_icon": TOPIC_ICONS.get(topic, "📄"), 110 - "topic_label": TOPIC_LABELS.get(topic, topic.title()), 108 + "agent": agent, 109 + "agent_icon": AGENT_ICONS.get(agent, "📄"), 110 + "agent_label": AGENT_LABELS.get(agent, agent.title()), 111 111 "facet": facet_name, 112 112 "facet_title": facet_info.get("title", facet_name), 113 113 "facet_color": facet_info.get("color", ""), ··· 129 129 limit: Max results per day (default 5) 130 130 offset: Day offset for pagination (default 0) 131 131 facet: Filter by facet name (optional, empty string for no-facet items) 132 - topic: Filter by single topic (optional) 132 + agent: Filter by single agent (optional) 133 133 134 134 Returns: 135 135 JSON with: 136 136 - total: Total match count 137 137 - days: List of day groups, each with date info and results 138 138 - facets: List of facets with counts for filter sidebar 139 - - topics: List of topics with counts for filter sidebar 139 + - agents: List of agents with counts for filter sidebar 140 140 """ 141 141 query = request.args.get("q", "").strip() 142 142 ··· 144 144 results_per_day = int(request.args.get("limit", 5)) 145 145 day_offset = int(request.args.get("offset", 0)) 146 146 facet_filter = _parse_facet_filter() 147 - topic_filter = _parse_topic_filter() 147 + agent_filter = _parse_agent_filter() 148 148 stream_filter = _parse_stream_filter() 149 149 150 150 # Load facet metadata for enriching results ··· 154 154 # First get unfiltered counts for sidebar display 155 155 base_counts = search_counts(query, stream=stream_filter) 156 156 facet_counts = dict(base_counts["facets"]) 157 - topic_counts = dict(base_counts["topics"]) 157 + agent_counts = dict(base_counts["agents"]) 158 158 159 159 # Get filtered counts for results 160 160 filtered_counts = search_counts( 161 - query, facet=facet_filter, topic=topic_filter, stream=stream_filter 161 + query, facet=facet_filter, agent=agent_filter, stream=stream_filter 162 162 ) 163 163 day_counts = dict(filtered_counts["days"]) 164 164 ··· 177 177 offset=0, 178 178 day=day, 179 179 facet=facet_filter, 180 - topic=topic_filter, 180 + agent=agent_filter, 181 181 stream=stream_filter, 182 182 ) 183 183 total_in_day = day_counts.get(day, 0) ··· 212 212 # Sort by count descending 213 213 facets_list.sort(key=lambda x: x["count"], reverse=True) 214 214 215 - # Build topic list for sidebar (unfiltered counts for discovery) 216 - topics_list = [] 217 - for topic, count in sorted(topic_counts.items(), key=lambda x: -x[1]): 218 - topics_list.append( 215 + # Build agent list for sidebar (unfiltered counts for discovery) 216 + agents_list = [] 217 + for agent, count in sorted(agent_counts.items(), key=lambda x: -x[1]): 218 + agents_list.append( 219 219 { 220 - "name": topic, 221 - "label": TOPIC_LABELS.get(topic, topic.title()), 222 - "icon": TOPIC_ICONS.get(topic, "📄"), 220 + "name": agent, 221 + "label": AGENT_LABELS.get(agent, agent.title()), 222 + "icon": AGENT_ICONS.get(agent, "📄"), 223 223 "count": count, 224 224 } 225 225 ) ··· 231 231 "showing_days": len(days_response), 232 232 "days": days_response, 233 233 "facets": facets_list, 234 - "topics": topics_list, 234 + "agents": agents_list, 235 235 } 236 236 ) 237 237 ··· 246 246 offset: Result offset within the day (default 0) 247 247 limit: Max results (default 20) 248 248 facet: Facet filter (optional) 249 - topic: Single topic filter (optional) 249 + agent: Single agent filter (optional) 250 250 """ 251 251 query = request.args.get("q", "").strip() 252 252 day = request.args.get("day", "").strip() ··· 256 256 offset = int(request.args.get("offset", 0)) 257 257 limit = int(request.args.get("limit", 20)) 258 258 facet_filter = _parse_facet_filter() 259 - topic_filter = _parse_topic_filter() 259 + agent_filter = _parse_agent_filter() 260 260 stream_filter = _parse_stream_filter() 261 261 262 262 facets_map = get_facets() 263 263 264 264 # Get total count for this day with filters 265 265 counts = search_counts( 266 - query, day=day, facet=facet_filter, topic=topic_filter, stream=stream_filter 266 + query, day=day, facet=facet_filter, agent=agent_filter, stream=stream_filter 267 267 ) 268 268 total_in_day = counts["total"] 269 269 ··· 274 274 offset=offset, 275 275 day=day, 276 276 facet=facet_filter, 277 - topic=topic_filter, 277 + agent=agent_filter, 278 278 stream=stream_filter, 279 279 ) 280 280
+4 -4
apps/search/tests/test_maint_migrate_index.py
··· 30 30 path UNINDEXED, 31 31 day UNINDEXED, 32 32 facet UNINDEXED, 33 - topic UNINDEXED, 33 + agent UNINDEXED, 34 34 idx UNINDEXED 35 35 ) 36 36 """) 37 37 conn.execute( 38 - "INSERT INTO chunks(content, path, day, facet, topic, idx) " 38 + "INSERT INTO chunks(content, path, day, facet, agent, idx) " 39 39 "VALUES ('test', 'test.md', '20240101', 'work', 'flow', 0)" 40 40 ) 41 41 conn.commit() ··· 56 56 path UNINDEXED, 57 57 day UNINDEXED, 58 58 facet UNINDEXED, 59 - topic UNINDEXED, 59 + agent UNINDEXED, 60 60 stream UNINDEXED, 61 61 idx UNINDEXED 62 62 ) ··· 80 80 # Verify new schema accepts stream column 81 81 conn = sqlite3.connect(db_path) 82 82 conn.execute( 83 - "INSERT INTO chunks(content, path, day, facet, topic, stream, idx) " 83 + "INSERT INTO chunks(content, path, day, facet, agent, stream, idx) " 84 84 "VALUES ('test', 'test.md', '20240101', 'work', 'flow', 'archon', 0)" 85 85 ) 86 86 row = conn.execute("SELECT stream FROM chunks").fetchone()
+32 -32
apps/search/workspace.html
··· 142 142 flex-wrap: wrap; 143 143 } 144 144 145 - .result-topic { 145 + .result-agent { 146 146 display: flex; 147 147 align-items: center; 148 148 gap: 0.25rem; ··· 151 151 color: #374151; 152 152 } 153 153 154 - .result-topic-icon { 154 + .result-agent-icon { 155 155 font-size: 0.9rem; 156 156 } 157 157 ··· 289 289 </div> 290 290 </div> 291 291 292 - <div class="filter-section" id="topic-filters"> 293 - <h3>Topics</h3> 294 - <div class="filter-list" id="topic-list"> 292 + <div class="filter-section" id="agent-filters"> 293 + <h3>Agents</h3> 294 + <div class="filter-list" id="agent-list"> 295 295 <!-- Populated dynamically --> 296 296 </div> 297 297 </div> ··· 324 324 // State 325 325 let currentQuery = ''; 326 326 let currentFacet = ''; 327 - let currentTopic = ''; 327 + let currentAgent = ''; 328 328 let dayOffset = 0; 329 329 330 330 // DOM elements ··· 333 333 const searchEmpty = document.getElementById('search-empty'); 334 334 const loadMoreDaysBtn = document.getElementById('load-more-days'); 335 335 const facetList = document.getElementById('facet-list'); 336 - const topicList = document.getElementById('topic-list'); 336 + const agentList = document.getElementById('agent-list'); 337 337 338 338 // Initialize 339 339 loadFromHash(); ··· 353 353 const hashParams = new URLSearchParams(window.location.hash.slice(1)); 354 354 const q = hashParams.get('q') || ''; 355 355 const f = hashParams.get('facet') || ''; 356 - const t = hashParams.get('topic') || ''; 356 + const t = hashParams.get('agent') || ''; 357 357 358 - if (q !== currentQuery || f !== currentFacet || t !== currentTopic) { 358 + if (q !== currentQuery || f !== currentFacet || t !== currentAgent) { 359 359 currentQuery = q; 360 360 currentFacet = f; 361 - currentTopic = t; 361 + currentAgent = t; 362 362 363 363 // Notify app bar of query change 364 364 window.dispatchEvent(new CustomEvent('search.queryUpdate', { ··· 377 377 const params = new URLSearchParams(); 378 378 if (currentQuery) params.set('q', currentQuery); 379 379 if (currentFacet) params.set('facet', currentFacet); 380 - if (currentTopic) params.set('topic', currentTopic); 380 + if (currentAgent) params.set('agent', currentAgent); 381 381 window.location.hash = params.toString(); 382 382 } 383 383 ··· 403 403 url.searchParams.set('q', currentQuery); 404 404 url.searchParams.set('offset', dayOffset); 405 405 if (currentFacet) url.searchParams.set('facet', currentFacet); 406 - if (currentTopic) url.searchParams.set('topic', currentTopic); 406 + if (currentAgent) url.searchParams.set('agent', currentAgent); 407 407 408 408 fetch(url) 409 409 .then(r => r.json()) 410 410 .then(data => { 411 411 if (reset) { 412 412 resultsContainer.innerHTML = ''; 413 - renderFilters(data.facets, data.topics); 413 + renderFilters(data.facets, data.agents); 414 414 } 415 415 416 416 renderResults(data, reset); ··· 430 430 loadMoreDaysBtn.style.display = 'none'; 431 431 } 432 432 433 - function renderFilters(facets, topics) { 433 + function renderFilters(facets, agents) { 434 434 // Render facet filters 435 435 let facetHtml = ` 436 436 <label class="filter-item ${!currentFacet ? 'active' : ''}"> ··· 465 465 }; 466 466 }); 467 467 468 - // Render topic filters (radio buttons for single-select) 469 - let topicHtml = ` 470 - <label class="filter-item ${!currentTopic ? 'active' : ''}"> 471 - <input type="radio" name="topic" value="" ${!currentTopic ? 'checked' : ''}> 468 + // Render agent filters (radio buttons for single-select) 469 + let agentHtml = ` 470 + <label class="filter-item ${!currentAgent ? 'active' : ''}"> 471 + <input type="radio" name="agent" value="" ${!currentAgent ? 'checked' : ''}> 472 472 <span class="filter-label">All</span> 473 473 </label> 474 474 `; 475 - for (const t of topics) { 476 - const isActive = currentTopic === t.name; 477 - topicHtml += ` 475 + for (const t of agents) { 476 + const isActive = currentAgent === t.name; 477 + agentHtml += ` 478 478 <label class="filter-item ${isActive ? 'active' : ''}"> 479 - <input type="radio" name="topic" value="${escapeHtml(t.name)}" ${isActive ? 'checked' : ''}> 479 + <input type="radio" name="agent" value="${escapeHtml(t.name)}" ${isActive ? 'checked' : ''}> 480 480 <span class="filter-icon">${t.icon}</span> 481 481 <span class="filter-label">${escapeHtml(t.label)}</span> 482 482 <span class="filter-count">${t.count}</span> 483 483 </label> 484 484 `; 485 485 } 486 - topicList.innerHTML = topicHtml; 486 + agentList.innerHTML = agentHtml; 487 487 488 - // Add topic change handlers 489 - topicList.querySelectorAll('input[type="radio"]').forEach(input => { 488 + // Add agent change handlers 489 + agentList.querySelectorAll('input[type="radio"]').forEach(input => { 490 490 input.onchange = function() { 491 - currentTopic = this.value; 492 - topicList.querySelectorAll('.filter-item').forEach(item => { 493 - item.classList.toggle('active', item.querySelector('input').value === currentTopic); 491 + currentAgent = this.value; 492 + agentList.querySelectorAll('.filter-item').forEach(item => { 493 + item.classList.toggle('active', item.querySelector('input').value === currentAgent); 494 494 }); 495 495 updateHash(); 496 496 doSearch(true); ··· 572 572 573 573 item.innerHTML = ` 574 574 <div class="result-meta"> 575 - <span class="result-topic"> 576 - <span class="result-topic-icon">${result.topic_icon}</span> 577 - ${escapeHtml(result.topic_label)} 575 + <span class="result-agent"> 576 + <span class="result-agent-icon">${result.agent_icon}</span> 577 + ${escapeHtml(result.agent_label)} 578 578 </span> 579 579 ${facetBadge} 580 580 </div> ··· 594 594 url.searchParams.set('offset', currentOffset); 595 595 url.searchParams.set('limit', 20); 596 596 if (currentFacet) url.searchParams.set('facet', currentFacet); 597 - if (currentTopic) url.searchParams.set('topic', currentTopic); 597 + if (currentAgent) url.searchParams.set('agent', currentAgent); 598 598 599 599 fetch(url) 600 600 .then(r => r.json())
+1 -1
apps/stats/dashboard.js
··· 574 574 // Render Events stacked bar chart 575 575 buildStackedCategoryChart( 576 576 document.getElementById('eventsChart'), 577 - stats.topic_counts_by_day || {}, 577 + stats.agent_counts_by_day || {}, 578 578 data.generators || {} // Use generator metadata for titles/colors 579 579 ); 580 580
+3 -3
apps/todos/muse/daily.md
··· 33 33 - `sol call todos done LINE_NUMBER` – mark an entry complete 34 34 - `sol call todos upcoming -l LIMIT` – view upcoming todos 35 35 36 - You may combine these with discovery calls (`sol call journal search`, `sol call journal events`, `sol call journal read TOPIC`) to gather supporting evidence. Line numbers are stable identifiers—todos are never deleted, only cancelled. 36 + You may combine these with discovery calls (`sol call journal search`, `sol call journal events`, `sol call journal read AGENT`) to gather supporting evidence. Line numbers are stable identifiers—todos are never deleted, only cancelled. 37 37 38 38 ## Process 39 39 ··· 51 51 52 52 1. Call `sol call todos list` to see what activity agents already added 53 53 2. Call `sol call todos upcoming -l 50` to check for items already scheduled on future days 54 - 3. Search for per-activity follow-ups: `sol call journal search "followup" -d $day_YYYYMMDD -t followups` 55 - 4. Check facet news for announced commitments: `sol call journal search "" -t news -d $day_YYYYMMDD -f FACET -n 5` 54 + 3. Search for per-activity follow-ups: `sol call journal search "followup" -d $day_YYYYMMDD -a followups` 55 + 4. Check facet news for announced commitments: `sol call journal search "" -a news -d $day_YYYYMMDD -f FACET -n 5` 56 56 5. Cancel duplicates or stale items via `sol call todos cancel` 57 57 6. Add any high-value items missed by activity detection (e.g., cross-activity themes, carried commitments from follow-ups) 58 58
+5 -5
apps/todos/muse/weekly.md
··· 19 19 20 20 You have access to: 21 21 1. **Checklist history** – `sol call todos list -d DAY` for today and each of the prior six days 22 - 2. **Follow-up insights** – `sol call journal search "followup" -d {date} -t followups` for each day in scope (follow-ups are produced per-activity, so results may span multiple activities) 23 - 3. **Journal search** – `sol call journal search QUERY -d DAY -t TOPIC -f FACET -n LIMIT` and `sol call journal events -d DAY -f FACET` for discovery scoped to the date range 24 - 4. **Facet news** – `sol call journal search "[keywords]" -t news` or `sol call journal news -d DAY` for announced commitments 22 + 2. **Follow-up insights** – `sol call journal search "followup" -d {date} -a followups` for each day in scope (follow-ups are produced per-activity, so results may span multiple activities) 23 + 3. **Journal search** – `sol call journal search QUERY -d DAY -a AGENT -f FACET -n LIMIT` and `sol call journal events -d DAY -f FACET` for discovery scoped to the date range 24 + 4. **Facet news** – `sol call journal search "[keywords]" -a news` or `sol call journal news -d DAY` for announced commitments 25 25 5. **Current date and facet context** – for ordering, scheduling, and due-date decisions 26 26 27 27 ## Tooling ··· 47 47 - Remember you're working within a single facet scope (SOL_FACET handles this) 48 48 49 49 ### 2. Sweep Follow-up Insights 50 - - For each date in `date_range`, run `sol call journal search "followup" -d {date} -t followups` to gather per-activity follow-up outputs 50 + - For each date in `date_range`, run `sol call journal search "followup" -d {date} -a followups` to gather per-activity follow-up outputs 51 51 - Extract explicit commitments, implied obligations, and unresolved questions 52 - - Search for public commitments in facet newsletters via `sol call journal search "[keywords]" -t news` or `sol call journal news -d DAY` 52 + - Search for public commitments in facet newsletters via `sol call journal search "[keywords]" -a news` or `sol call journal news -d DAY` 53 53 - Run targeted `sol call journal search` queries when a follow-up reference needs deeper validation or completion evidence 54 54 55 55 ### 3. Validate Potential Work
+2 -2
apps/todos/todo.py
··· 680 680 meta["error"] = error_msg 681 681 logging.info(error_msg) 682 682 683 - # Indexer metadata - topic is always "todo" for todo items 684 - meta["indexer"] = {"topic": "todo"} 683 + # Indexer metadata - agent is always "todo" for todo items 684 + meta["indexer"] = {"agent": "todo"} 685 685 686 686 return chunks, meta
+5 -5
docs/APPS.md
··· 293 293 **Key Points:** 294 294 - Create `muse/` directory with `.md` files containing JSON frontmatter 295 295 - App generators are automatically discovered alongside system generators 296 - - Keys are namespaced as `{app}:{topic}` (e.g., `my_app:weekly_summary`) 297 - - Outputs go to `JOURNAL/YYYYMMDD/agents/_<app>_<topic>.md` (or `.json` if `output: "json"`) 296 + - Keys are namespaced as `{app}:{agent}` (e.g., `my_app:weekly_summary`) 297 + - Outputs go to `JOURNAL/YYYYMMDD/agents/_<app>_<agent>.md` (or `.json` if `output: "json"`) 298 298 299 299 **Metadata format:** Same schema as system generators in `muse/*.md` - JSON frontmatter includes `title`, `description`, `color`, `schedule` (required), `priority` (required for scheduled prompts), `hook`, `output`, `max_output_tokens`, and `thinking_budget` fields. The `schedule` field must be `"segment"` or `"daily"`. The `priority` field is required for all scheduled prompts - prompts without explicit priority will fail validation. Set `output: "json"` for structured JSON output instead of markdown. Optional `max_output_tokens` sets the maximum response length; `thinking_budget` sets the model's thinking token budget (provider-specific defaults apply if omitted). 300 300 ··· 309 309 - `"hook": {"post": "occurrence"}` - Extracts past events to `facets/{facet}/events/{day}.jsonl` 310 310 - `"hook": {"post": "anticipation"}` - Extracts future scheduled events 311 311 312 - The `occurrences` field (optional string) provides topic-specific extraction guidance when using the occurrence hook. Example: 312 + The `occurrences` field (optional string) provides agent-specific extraction guidance when using the occurrence hook. Example: 313 313 314 314 ```json 315 315 { ··· 361 361 **Reference implementations:** 362 362 - System generator templates: `muse/*.md` (files with `schedule` field but no `tools` field) 363 363 - Extraction hooks: `muse/occurrence.py`, `muse/anticipation.py` 364 - - Discovery logic: `think/muse.py` - `get_muse_configs(has_tools=False)`, `get_output_topic()` 364 + - Discovery logic: `think/muse.py` - `get_muse_configs(has_tools=False)`, `get_output_name()` 365 365 - Hook loading: `think/muse.py` - `load_pre_hook()`, `load_post_hook()` 366 366 367 367 --- ··· 406 406 - `false` - don't load this source type 407 407 - `true` - load if available 408 408 - `"required"` - load, and skip generation if no content found (useful for generators that only make sense with specific input types, e.g., `"audio": "required"` for speaker detection) 409 - - For `agents` only: a dict for selective filtering, e.g., `{"entities": true, "meetings": "required", "flow": false}`. Keys are agent names (system) or `"app:topic"` (app-namespaced). An empty dict `{}` means no agents. 409 + - For `agents` only: a dict for selective filtering, e.g., `{"entities": true, "meetings": "required", "flow": false}`. Keys are agent names (system) or `"app:agent"` (app-namespaced). An empty dict `{}` means no agents. 410 410 - `activity` - Activity-scheduled agents only: controls activity context in `extra_context`. Can be: 411 411 - `false` - no activity context (default) 412 412 - `true` - enable all activity context (shorthand for `{"context": true, "state": true, "focus": true}`)
+8 -8
docs/JOURNAL.md
··· 583 583 Activity-scheduled agents (`schedule: "activity"`) produce output that is stored alongside the activity records, organized by day and record ID: 584 584 585 585 ``` 586 - facets/{facet}/activities/{day}/{activity_id}/{topic}.{ext} 586 + facets/{facet}/activities/{day}/{activity_id}/{agent}.{ext} 587 587 ``` 588 588 589 589 For example, a `session_review` agent processing a coding activity would write to: ··· 813 813 **Files:** 814 814 - `indexer/journal.sqlite` – FTS5 SQLite database containing indexed chunks from agent outputs, events, entities, todos, and action logs 815 815 816 - The indexer converts content to markdown chunks via the formatters framework, then indexes with metadata fields (day, facet, topic) for filtering. Raw audio/screen transcripts are formattable but not indexed — agent outputs provide more useful search results. Use `get_journal_index()` from `think/indexer/journal.py` to access the database programmatically. 816 + The indexer converts content to markdown chunks via the formatters framework, then indexes with metadata fields (day, facet, agent) for filtering. Raw audio/screen transcripts are formattable but not indexed — agent outputs provide more useful search results. Use `get_journal_index()` from `think/indexer/journal.py` to access the database programmatically. 817 817 818 818 Which content gets indexed is controlled by the `FORMATTERS` registry in `think/formatters.py`. Each entry maps a glob pattern to a formatter function and an `indexed` flag. The registry patterns must be specific enough to use as `Path.glob()` arguments from the journal root — adding a new content location requires a new entry. 819 819 ··· 1011 1011 - **Anticipations** – future scheduled events extracted from calendar views (`occurred: false`) 1012 1012 1013 1013 ```jsonl 1014 - {"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "topic": "meetings", "occurred": true, "source": "20250101/agents/meetings.md", "details": "Sprint planning discussion"} 1015 - {"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "topic": "schedule", "occurred": false, "source": "20250101/agents/schedule.md", "details": "Final review before release"} 1014 + {"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "agent": "meetings", "occurred": true, "source": "20250101/agents/meetings.md", "details": "Sprint planning discussion"} 1015 + {"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "agent": "schedule", "occurred": false, "source": "20250101/agents/schedule.md", "details": "Final review before release"} 1016 1016 ``` 1017 1017 1018 1018 **Common fields:** ··· 1021 1021 - **date** – ISO date YYYY-MM-DD (anticipations only, indicates scheduled date) 1022 1022 - **title** and **summary** – short text for display and search 1023 1023 - **facet** – facet name the event belongs to (required) 1024 - - **topic** – source generator type (e.g., "meetings", "schedule", "flow") 1024 + - **agent** – source generator type (e.g., "meetings", "schedule", "flow") 1025 1025 - **occurred** – `true` for occurrences, `false` for anticipations 1026 1026 - **source** – path to the output file that generated this event 1027 1027 - **work** – boolean, work vs. personal classification ··· 1049 1049 Each template is a `.md` file with JSON frontmatter containing metadata (title, description, schedule, output format). The `schedule` field is required and must be `"segment"` or `"daily"` - generators with missing or invalid schedule are skipped. Use `get_muse_configs(has_tools=False)` from `think/muse.py` to retrieve all available generators, or `get_muse_configs(has_tools=False, schedule="daily")` to get generators filtered by schedule. 1050 1050 1051 1051 **Output naming:** 1052 - - System outputs: `agents/{topic}.md` (e.g., `agents/flow.md`, `agents/meetings.md`) 1053 - - App outputs: `agents/_{app}_{topic}.md` (e.g., `agents/_chat_sentiment.md`) 1054 - - JSON output: `agents/{topic}.json` when metadata specifies `"output": "json"` 1052 + - System outputs: `agents/{agent}.md` (e.g., `agents/flow.md`, `agents/meetings.md`) 1053 + - App outputs: `agents/_{app}_{agent}.md` (e.g., `agents/_chat_sentiment.md`) 1054 + - JSON output: `agents/{agent}.json` when metadata specifies `"output": "json"` 1055 1055 1056 1056 Each generator type has a corresponding template file (`{name}.md`) that defines how the AI synthesizes extracts into narrative form.
+1 -1
docs/PROMPT_TEMPLATES.md
··· 110 110 111 111 ### For Generators 112 112 113 - Generator prompts typically compose a shared preamble with topic-specific instructions: 113 + Generator prompts typically compose a shared preamble with agent-specific instructions: 114 114 115 115 ```markdown 116 116 {
+4 -4
muse/anticipation.py
··· 20 20 write_events_jsonl, 21 21 ) 22 22 from think.models import generate 23 - from think.muse import get_output_topic 23 + from think.muse import get_output_name 24 24 from think.prompts import load_prompt 25 25 26 26 ··· 47 47 # Load extraction prompt 48 48 prompt_content = load_prompt("anticipation", base_dir=Path(__file__).parent) 49 49 50 - # Build context with facets (anticipations don't have topic-specific instructions) 50 + # Build context with facets (anticipations don't have agent-specific instructions) 51 51 facets_context = facet_summaries(detailed=True) 52 52 53 53 # Extract events ··· 80 80 81 81 # Write to facet JSONL files (occurred=False for anticipations) 82 82 source_output = compute_output_source(context) 83 - topic = get_output_topic(name) 83 + output_name = get_output_name(name) 84 84 day = context.get("day", "") 85 85 86 86 written_paths = write_events_jsonl( 87 87 events=events, 88 - topic=topic, 88 + agent=output_name, 89 89 occurred=False, 90 90 source_output=source_output, 91 91 capture_day=day,
+9 -9
muse/decisionalizer.md
··· 38 38 **CRITICAL FIRST STEP**: Before any analysis, gather all decision outputs from the day's activities: 39 39 40 40 ``` 41 - sol call journal search "decision" -d $day_YYYYMMDD -t decisions 41 + sol call journal search "decision" -d $day_YYYYMMDD -a decisions 42 42 ``` 43 43 44 44 Decision-actions are now produced per-activity, so this search may return multiple result sets (one per activity that had decisions). Collect ALL decision-actions across all activities into a single working list. If no decision outputs exist for this day, stop and report this clearly. ··· 58 58 Use these tools in sequence: 59 59 60 60 1. **Find the decision moment:** 61 - - `sol call journal search "decision keywords here" -d $day_YYYYMMDD -t audio -n 10` 61 + - `sol call journal search "decision keywords here" -d $day_YYYYMMDD -a audio -n 10` 62 62 - Goal: Pinpoint exact time of decision (HH:MM:SS) 63 63 64 64 2. **Get full context:** ··· 72 72 - Goal: Find all people, teams, projects mentioned 73 73 74 74 2. **Map meeting participants:** 75 - - `sol call journal events` or `sol call journal search "[keywords]" -d $day_YYYYMMDD -t event` 76 - - `sol call journal search "[keywords]" -t news -f work -d $day_YYYYMMDD` for public announcements 75 + - `sol call journal events` or `sol call journal search "[keywords]" -d $day_YYYYMMDD -a event` 76 + - `sol call journal search "[keywords]" -a news -f work -d $day_YYYYMMDD` for public announcements 77 77 - Goal: Identify who needs to know about this decision 78 78 79 79 ### Step 3: Historical Precedent Mining (30-day lookback) ··· 83 83 - Goal: Discover patterns in how similar decisions were handled 84 84 85 85 2. **Check commitment history:** 86 - - `sol call journal search "entity decision approve cancel" -t audio -n 15` 86 + - `sol call journal search "entity decision approve cancel" -a audio -n 15` 87 87 - Goal: Identify typical follow-up patterns 88 88 89 89 ### Step 4: Forward Impact Assessment (2-6 hours post-decision) 90 90 91 91 1. **Check for communications:** 92 - - `sol call journal search "[keywords]" -d $day_YYYYMMDD -t audio -n 10` 93 - - `sol call journal search "[keywords]" -t news -d $day_YYYYMMDD` for decision announcements 92 + - `sol call journal search "[keywords]" -d $day_YYYYMMDD -a audio -n 10` 93 + - `sol call journal search "[keywords]" -a news -d $day_YYYYMMDD` for decision announcements 94 94 - Goal: Find follow-up notifications or discussions 95 95 96 96 2. **Review meetings:** 97 - - `sol call journal search "[keywords]" -d $day_YYYYMMDD -t event` 97 + - `sol call journal search "[keywords]" -d $day_YYYYMMDD -a event` 98 98 - Goal: See if decision was discussed 99 99 100 100 3. **Check messaging:** ··· 108 108 - Goal: Identify emerging issues 109 109 110 110 2. **Verify updates:** 111 - - `sol call journal search "document update change commit" -d $day_YYYYMMDD -t audio` 111 + - `sol call journal search "document update change commit" -d $day_YYYYMMDD -a audio` 112 112 - Goal: Confirm tracking artifacts were updated 113 113 114 114 OBLIGATION CATEGORIES (derive expectations from decision type + precedents; adapt to what the data shows)
+15 -15
muse/default.md
··· 10 10 11 11 } 12 12 13 - You are solstone, an advanced journal assistant specializing in helping $name explore, search, and understand personal journal entries. The journal contains daily transcripts from audio recordings and screenshot diffs that capture digital life, as well as pre-processed daily insights organized by topic and events extracted. 13 + You are solstone, an advanced journal assistant specializing in helping $name explore, search, and understand personal journal entries. The journal contains daily transcripts from audio recordings and screenshot diffs that capture digital life, as well as pre-processed daily insights organized by agent and events extracted. 14 14 15 15 ## Available Commands 16 16 17 17 Use `sol call` commands for journal exploration (see skills for full usage): 18 18 19 - - **Journal**: `sol call journal search`, `sol call journal events`, `sol call journal facet`, `sol call journal facets`, `sol call journal news`, `sol call journal topics`, `sol call journal read` 19 + - **Journal**: `sol call journal search`, `sol call journal events`, `sol call journal facet`, `sol call journal facets`, `sol call journal news`, `sol call journal agents`, `sol call journal read` 20 20 - **Transcripts**: `sol call transcripts read` (with `--full`, `--audio`, or `--screen`) 21 21 - **Todos**: `sol call todos list`, `sol call todos add`, `sol call todos done`, `sol call todos cancel`, `sol call todos upcoming` 22 22 - **Entities**: `sol call entities list`, `sol call entities detect`, `sol call entities attach` 23 23 24 24 ### Command Usage Strategy 25 25 26 - 1. **Discovery First**: Use `sol call journal search` to identify relevant topics, days, and time segments 26 + 1. **Discovery First**: Use `sol call journal search` to identify relevant agents, days, and time segments 27 27 2. **Deep Dive**: Use targeted searches and transcript reads for identified items 28 28 3. **Comprehensive Analysis**: Combine multiple calls to build complete pictures 29 29 30 30 Example workflow: 31 31 ```bash 32 - 1. sol call journal search "debugging session" # returns counts across facets, topics, and days 33 - 2. Review counts.top_days to identify most active days, counts.topics to see content types 34 - 3. sol call journal search "debugging" -d 20240115 -t tools # topic-specific search for that day 35 - 4. sol call journal search "error" -d 20240115 -t audio # find specific transcript windows 32 + 1. sol call journal search "debugging session" # returns counts across facets, agents, and days 33 + 2. Review counts.top_days to identify most active days, counts.agents to see content types 34 + 3. sol call journal search "debugging" -d 20240115 -a tools # agent-specific search for that day 35 + 4. sol call journal search "error" -d 20240115 -a audio # find specific transcript windows 36 36 5. sol call transcripts read 20240115 --start 143000 --length 60 --full # full hour context 37 - 6. sol call journal read 20240115 flow # read full agent output for a topic 37 + 6. sol call journal read 20240115 flow # read full agent output for an agent 38 38 ``` 39 39 40 40 ## Decision Framework ··· 54 54 - Looking for patterns, themes, or specific phrases across time 55 55 - Starting a multi-step search to identify relevant days before deep diving 56 56 57 - **Use topic filter ("flow", "event", "news", "entity:detected", etc.) when:** 57 + **Use agent filter ("flow", "event", "news", "entity:detected", etc.) when:** 58 58 - Looking for a specific type of content 59 59 - Narrowing search to agent outputs, events, or entities specifically 60 60 ··· 63 63 - Building a schedule or timeline of activities 64 64 - Query requests structured information about meetings or events 65 65 66 - **Use `sol call journal read TOPIC` when:** 66 + **Use `sol call journal read AGENT` when:** 67 67 - You need the full content of a specific agent output (e.g., flow, meetings, knowledge_graph) 68 68 - Search returned relevant snippets and you need the complete document 69 69 - Exploring per-segment outputs with `--segment HHMMSS_LEN` 70 70 71 - **Use `sol call journal topics` when:** 71 + **Use `sol call journal agents` when:** 72 72 - You need to discover what agent outputs exist for a specific day 73 - - Browsing available content before reading specific topics 73 + - Browsing available content before reading specific agents 74 74 - Use `--segment HHMMSS_LEN` to list per-segment outputs 75 75 76 76 **Use `sol call journal facets` when:** ··· 82 82 Start broad and narrow down using the counts metadata: 83 83 ```bash 84 84 Step 1: sol call journal search "project planning" # get overview with counts 85 - Step 2: Check counts.facets and counts.topics to understand the shape of results 85 + Step 2: Check counts.facets and counts.agents to understand the shape of results 86 86 Step 3: Check counts.top_days or counts.recent_days to identify when activity occurred 87 - Step 4: sol call journal search "sprint planning" -d 20240115 -t audio # narrow to specific day/type 87 + Step 4: sol call journal search "sprint planning" -d 20240115 -a audio # narrow to specific day/type 88 88 Step 5: sol call journal read 20240115 meeting_notes # full context if needed 89 89 ``` 90 90 ··· 99 99 - **Query syntax**: Searches match ALL words by default; use `OR` between words to match ANY (e.g., `apple OR orange`), quote phrases for exact matches (e.g., `"project meeting"`), and append `*` for prefix matching (e.g., `debug*`). 100 100 - Keep initial queries concise (2-5 words) 101 101 - If few results, broaden query by removing specific terms or using `OR` 102 - - If too many results, add distinguishing context or use topic filter 102 + - If too many results, add distinguishing context or use agent filter 103 103 104 104 ### 4. Pagination Awareness 105 105 - Start with default limits (10 results)
+1 -1
muse/facet_newsletter.md
··· 38 38 - Day insights (flow, opportunities, followups) 39 39 - Events and meetings 40 40 - Topic insights 41 - - Full insight markdown when needed via `sol call journal search QUERY -t TOPIC` 41 + - Full insight markdown when needed via `sol call journal search QUERY -a AGENT` 42 42 - Facet-specific transcripts and mentions 43 43 - Todo items with facet tags 44 44 - Filter through all the data to focus only on things that are clearly related to this specific facet, ignoring other facets (they have their own newsletter). Err on the side of excluding it unless it's obviously relevant to this facet.
+4 -4
muse/help.md
··· 71 71 ## `sol call` Command Reference 72 72 73 73 ### Journal 74 - - `sol call journal search [query] [-n limit] [--offset N] [-d YYYYMMDD] [--day-from YYYYMMDD] [--day-to YYYYMMDD] [-f facet] [-t topic]` - Search journal entries. 74 + - `sol call journal search [query] [-n limit] [--offset N] [-d YYYYMMDD] [--day-from YYYYMMDD] [--day-to YYYYMMDD] [-f facet] [-a agent]` - Search journal entries. 75 75 - `sol call journal events [day] [-f facet]` - List events for a day (day defaults to SOL_DAY). 76 76 - `sol call journal facet [name]` - Show facet details (name defaults to SOL_FACET). 77 77 - `sol call journal facets` - List all facets. 78 78 - `sol call journal news [name] [-d YYYYMMDD] [-n limit] [--cursor CURSOR] [-w]` - Get news feed for a facet (name defaults to SOL_FACET, -d defaults to SOL_DAY). 79 - - `sol call journal topics [day] [-s HHMMSS_LEN]` - List topics for a day (day defaults to SOL_DAY). 80 - - `sol call journal read <topic> [-d YYYYMMDD] [-s HHMMSS_LEN] [--max N]` - Read agent output for a topic (day defaults to SOL_DAY). 79 + - `sol call journal agents [day] [-s HHMMSS_LEN]` - List agents for a day (day defaults to SOL_DAY). 80 + - `sol call journal read <agent> [-d YYYYMMDD] [-s HHMMSS_LEN] [--max N]` - Read agent output for an agent (day defaults to SOL_DAY). 81 81 82 82 ### Entities 83 83 - `sol call entities list [facet] [-d day]` - List entities (facet defaults to SOL_FACET; without -d: attached, with -d: detected). ··· 105 105 - If asked "How do I search journal entries?": 106 106 - Use `sol call journal search "query"` for broad search. 107 107 - Add `-d YYYYMMDD` to focus one day. 108 - - Add `-t audio` or `-t flow` to narrow by topic. 108 + - Add `-a audio` or `-a flow` to narrow by agent. 109 109 - If asked "How do I inspect an agent run?": 110 110 - Use `sol muse logs` to find run IDs. 111 111 - Use `sol muse log <id>` for event details.
+10 -10
muse/joke_bot.md
··· 22 22 ### Research Strategy 23 23 The strategy is to conduct a three-phase data sweep of the analysis day's journal entries. We will start broad to understand the day's main themes and then narrow our focus to find specific, quote-worthy, or event-specific details that have comedic potential. 24 24 25 - 1. **Broad Overview**: Use `sol call journal search "" -d $day_YYYYMMDD` to get a complete list of all topics and structured activities from the analysis day. This creates a high-level map of the day. 26 - 2. **Detailed Search**: Use `sol call journal search ... -d $day_YYYYMMDD -t audio` with keywords related to emotion, humor, and conflict (e.g., "frustrating", "ridiculous", "error", "lol") to pinpoint specific moments of interest. 25 + 1. **Broad Overview**: Use `sol call journal search "" -d $day_YYYYMMDD` to get a complete list of all agents and structured activities from the analysis day. This creates a high-level map of the day. 26 + 2. **Detailed Search**: Use `sol call journal search ... -d $day_YYYYMMDD -a audio` with keywords related to emotion, humor, and conflict (e.g., "frustrating", "ridiculous", "error", "lol") to pinpoint specific moments of interest. 27 27 3. **Contextual Analysis**: Use transcript/insight retrieval to pull full context for the most promising findings from the previous phases. This raw material will be analyzed for comedic elements like irony, juxtaposition, or absurdity. 28 28 4. **Creative Synthesis & Delivery**: The final phase involves brainstorming joke concepts from the analyzed material, selecting the best one, and delivering it as the final response. 29 29 ··· 35 35 36 36 **Query syntax**: Searches match ALL words by default; use `OR` between words to match ANY (e.g., `apple OR orange`), quote phrases for exact matches (e.g., `"project meeting"`), and append `*` for prefix matching (e.g., `debug*`). 37 37 38 - 1. **Identify All Daily Topics**: 38 + 1. **Identify All Daily Agents**: 39 39 - **Command**: `sol call journal search "" -d $day_YYYYMMDD` 40 40 - **Purpose**: To get a complete list of all themes and activities discussed or worked on during the analysis day. This provides the main "characters" and "settings" for potential jokes. 41 - - **Expected Outcomes**: A list of all topic insights from the day, which will help identify recurring themes or unusual combinations of activities. 41 + - **Expected Outcomes**: A list of all agent insights from the day, which will help identify recurring themes or unusual combinations of activities. 42 42 43 43 2. **List All Structured Events**: 44 44 - **Command**: `sol call journal events` ··· 46 46 - **Expected Outcomes**: A timeline of the day's key events, such as "Project Phoenix Sync," "API Debugging Session," or "Team Standup." 47 47 48 48 3. **Find Emotionally Charged Moments**: 49 - - **Command**: `sol call journal search "\"this is ridiculous\" OR \"I'm so confused\" OR \"why is this not working\" OR \"error\" OR \"hilarious\" OR \"lol\"" -d $day_YYYYMMDD -t audio` 49 + - **Command**: `sol call journal search "\"this is ridiculous\" OR \"I'm so confused\" OR \"why is this not working\" OR \"error\" OR \"hilarious\" OR \"lol\"" -d $day_YYYYMMDD -a audio` 50 50 - **Purpose**: To find specific quotes or screen interactions that indicate frustration, confusion, or amusement. These raw emotional moments are often the most poignant and funny. 51 51 - **Expected Outcomes**: A list of specific timestamps and text snippets that can be investigated further for their comedic context. 52 52 ··· 54 54 55 55 1. **Retrieve Full Context for Key Findings**: 56 56 - **Retrieval**: 57 - - `sol call journal read {topic}` for the 2-3 most prominent or ironically named topics discovered in Phase 1. 57 + - `sol call journal read {agent}` for the 2-3 most prominent or ironically named agents discovered in Phase 1. 58 58 - `sol call transcripts read --start {time} --length {length} --full` for the most promising snippets found in the transcript search. Retrieve a 5-10 minute window around the snippet to understand the full conversation or activity. 59 59 - **Priority Order**: Prioritize transcript snippets first, as they contain direct quotes. Then, review insights for high-level irony. 60 60 - **Analysis Focus**: Read through the retrieved content, looking for: ··· 74 74 - Return the final joke as the agent's response. It will be saved as the agent output. 75 75 76 76 ### Query Optimization Strategy 77 - - **Primary Queries**: Broad, day-filtered searches to capture all topics and events from the analysis day. 78 - - **Alternative Queries**: For `sol call journal search ... -t audio`, if the initial emotional keywords yield no results, try searching for project codenames, specific colleagues' names, or technical terms that appeared frequently in the day's insights to find relevant conversations. 79 - - **Refinement Approach**: This is a single-day analysis, so the strategy is to gather more data rather than refine. If initial searches are sparse, the fallback is to read through all topic insights from the day to find something poignant, even if not overtly "funny." 77 + - **Primary Queries**: Broad, day-filtered searches to capture all agents and events from the analysis day. 78 + - **Alternative Queries**: For `sol call journal search ... -a audio`, if the initial emotional keywords yield no results, try searching for project codenames, specific colleagues' names, or technical terms that appeared frequently in the day's insights to find relevant conversations. 79 + - **Refinement Approach**: This is a single-day analysis, so the strategy is to gather more data rather than refine. If initial searches are sparse, the fallback is to read through all agent insights from the day to find something poignant, even if not overtly "funny." 80 80 81 81 ### Potential Research Challenges 82 82 - **A "Boring" Day**: If the analysis day's activities were routine, the plan is to focus on the "poignant" aspect. A joke can be crafted from the mundane nature of the day itself. ··· 84 84 - **Subjectivity of Humor**: The final joke may not land perfectly. The goal of this plan is to provide the best possible source material to maximize the chance of success. 85 85 86 86 ### Success Criteria 87 - - **Completeness Indicators**: A full list of topics, events, and a set of interesting transcript snippets from the analysis day have been collected and analyzed. 87 + - **Completeness Indicators**: A full list of agents, events, and a set of interesting transcript snippets from the analysis day have been collected and analyzed. 88 88 - **Quality Checkpoints**: The analysis has identified at least one moment of irony, absurdity, or relatable human struggle. 89 89 - **Coverage Verification**: The final response contains a joke that is clearly derived from the events of the analysis day.
+9 -9
muse/journal/SKILL.md
··· 20 20 ## search 21 21 22 22 ```bash 23 - sol call journal search [QUERY] [-n LIMIT] [--offset N] [-d DAY] [--day-from DAY] [--day-to DAY] [-f FACET] [-t TOPIC] 23 + sol call journal search [QUERY] [-n LIMIT] [--offset N] [-d DAY] [--day-from DAY] [--day-to DAY] [-f FACET] [-a AGENT] 24 24 ``` 25 25 26 26 Search the journal index across insights, transcripts, events, entities, and todos. ··· 31 31 - `-d, --day`: exact day filter (`YYYYMMDD`). 32 32 - `--day-from`, `--day-to`: inclusive date-range filters (`YYYYMMDD`). 33 33 - `-f, --facet`: facet filter (for example `work`, `personal`). 34 - - `-t, --topic`: topic/content filter (for example `flow`, `event`, `news`, `entity:detected`). 34 + - `-a, --agent`: agent/content filter (for example `flow`, `event`, `news`, `entity:detected`). 35 35 36 36 Behavior notes: 37 37 ··· 47 47 ```bash 48 48 sol call journal search "incident review" -n 20 -f work 49 49 sol call journal search "standup OR sync" --day-from 20260101 --day-to 20260107 50 - sol call journal search "" -d 20260115 -t audio 50 + sol call journal search "" -d 20260115 -a audio 51 51 ``` 52 52 53 53 ## events ··· 89 89 sol call journal facet # uses SOL_FACET 90 90 ``` 91 91 92 - ## topics 92 + ## agents 93 93 94 94 ```bash 95 - sol call journal topics [DAY] [-s SEGMENT] 95 + sol call journal agents [DAY] [-s SEGMENT] 96 96 ``` 97 97 98 98 List available agent outputs for a day. ··· 105 105 Example: 106 106 107 107 ```bash 108 - sol call journal topics 20260115 109 - sol call journal topics -s 091500_300 108 + sol call journal agents 20260115 109 + sol call journal agents -s 091500_300 110 110 ``` 111 111 112 112 ## read 113 113 114 114 ```bash 115 - sol call journal read TOPIC [-d DAY] [-s SEGMENT] [--max BYTES] 115 + sol call journal read AGENT [-d DAY] [-s SEGMENT] [--max BYTES] 116 116 ``` 117 117 118 118 Read full content of an agent output. 119 119 120 - - `TOPIC`: topic name, e.g. `flow`, `meetings`, `activity` (positional argument). 120 + - `AGENT`: agent name, e.g. `flow`, `meetings`, `activity` (positional argument). 121 121 - `-d, --day`: day in `YYYYMMDD` (default: `SOL_DAY` env). 122 122 - `-s, --segment`: optional segment key (default: `SOL_SEGMENT` env). 123 123 - `--max`: max output bytes (default `16384`, `0` for unlimited).
+7 -7
muse/occurrence.py
··· 20 20 write_events_jsonl, 21 21 ) 22 22 from think.models import generate 23 - from think.muse import get_output_topic 23 + from think.muse import get_output_name 24 24 from think.prompts import load_prompt 25 25 26 26 ··· 47 47 # Load extraction prompt 48 48 prompt_content = load_prompt("occurrence", base_dir=Path(__file__).parent) 49 49 50 - # Build context with facets + topic-specific instructions 50 + # Build context with facets + agent-specific instructions 51 51 facets_context = facet_summaries(detailed=True) 52 - topic_instructions = context.get("meta", {}).get("occurrences") 53 - if topic_instructions and isinstance(topic_instructions, str): 54 - extra_instructions = f"{facets_context}\n\n{topic_instructions}" 52 + agent_instructions = context.get("meta", {}).get("occurrences") 53 + if agent_instructions and isinstance(agent_instructions, str): 54 + extra_instructions = f"{facets_context}\n\n{agent_instructions}" 55 55 else: 56 56 extra_instructions = facets_context 57 57 ··· 85 85 86 86 # Write to facet JSONL files 87 87 source_output = compute_output_source(context) 88 - topic = get_output_topic(name) 88 + output_name = get_output_name(name) 89 89 day = context.get("day", "") 90 90 91 91 written_paths = write_events_jsonl( 92 92 events=events, 93 - topic=topic, 93 + agent=output_name, 94 94 occurred=True, 95 95 source_output=source_output, 96 96 capture_day=day,
+2 -2
observe/hear.py
··· 479 479 } 480 480 ) 481 481 482 - # Indexer metadata - topic is always "audio" for audio transcripts 483 - meta["indexer"] = {"topic": "audio"} 482 + # Indexer metadata - agent is always "audio" for audio transcripts 483 + meta["indexer"] = {"agent": "audio"} 484 484 485 485 return chunks, meta 486 486
+2 -2
observe/screen.py
··· 243 243 } 244 244 ) 245 245 246 - # Indexer metadata - topic is always "screen" for screen analysis 247 - meta["indexer"] = {"topic": "screen"} 246 + # Indexer metadata - agent is always "screen" for screen analysis 247 + meta["indexer"] = {"agent": "screen"} 248 248 249 249 return chunks, meta 250 250
+1 -1
tests/fixtures/journal/facets/personal/events/20240101.jsonl
··· 1 - {"type": "appointment", "start": "18:00:00", "end": "19:00:00", "title": "Gym session", "summary": "Evening workout", "facet": "personal", "topic": "activity", "occurred": true, "source": "20240101/agents/activity.md", "participants": [], "work": false, "details": "Strength training day"} 1 + {"type": "appointment", "start": "18:00:00", "end": "19:00:00", "title": "Gym session", "summary": "Evening workout", "facet": "personal", "agent": "activity", "occurred": true, "source": "20240101/agents/activity.md", "participants": [], "work": false, "details": "Strength training day"}
+2 -2
tests/fixtures/journal/facets/work/events/20240101.jsonl
··· 1 - {"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team standup", "summary": "Daily sync meeting", "facet": "work", "topic": "meetings", "occurred": true, "source": "20240101/agents/meetings.md", "participants": ["Alice", "Bob"], "work": true, "details": "Discussed sprint progress"} 2 - {"type": "task", "start": "10:00:00", "end": "12:00:00", "title": "Code review", "summary": "Review PR #123", "facet": "work", "topic": "activity", "occurred": true, "source": "20240101/agents/activity.md", "participants": [], "work": true, "details": "Reviewed authentication changes"} 1 + {"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team standup", "summary": "Daily sync meeting", "facet": "work", "agent": "meetings", "occurred": true, "source": "20240101/agents/meetings.md", "participants": ["Alice", "Bob"], "work": true, "details": "Discussed sprint progress"} 2 + {"type": "task", "start": "10:00:00", "end": "12:00:00", "title": "Code review", "summary": "Review PR #123", "facet": "work", "agent": "activity", "occurred": true, "source": "20240101/agents/activity.md", "participants": [], "work": true, "details": "Reviewed authentication changes"}
+1 -1
tests/fixtures/journal/facets/work/events/20240105.jsonl
··· 1 - {"type": "meeting", "date": "2024-01-05", "start": "14:00:00", "end": "15:00:00", "title": "Project kickoff", "summary": "Initial project planning", "facet": "work", "topic": "schedule", "occurred": false, "source": "20240101/agents/schedule.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"} 1 + {"type": "meeting", "date": "2024-01-05", "start": "14:00:00", "end": "15:00:00", "title": "Project kickoff", "summary": "Initial project planning", "facet": "work", "agent": "schedule", "occurred": false, "source": "20240101/agents/schedule.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"}
+13 -13
tests/test_call.py
··· 39 39 """Journal sub-app is registered and shows help.""" 40 40 result = runner.invoke(call_app, ["journal", "--help"]) 41 41 assert result.exit_code == 0 42 - for cmd in ("search", "events", "facet", "facets", "news", "topics", "read"): 42 + for cmd in ("search", "events", "facet", "facets", "news", "agents", "read"): 43 43 assert cmd in result.output 44 44 45 45 def test_journal_search(self): ··· 78 78 assert "Authentication" in result.output 79 79 80 80 def test_journal_search_shows_counts(self): 81 - """Search output includes facet/topic/day counts.""" 81 + """Search output includes facet/agent/day counts.""" 82 82 result = runner.invoke(call_app, ["journal", "search", ""]) 83 83 assert result.exit_code == 0 84 84 assert "results" in result.output 85 85 # Counts lines should appear when there are results 86 86 output = result.output 87 87 if "0 results" not in output: 88 - assert "Facets:" in output or "Topics:" in output 88 + assert "Facets:" in output or "Agents:" in output 89 89 90 90 def test_journal_events_shows_details(self): 91 91 """Events output includes participants and details.""" ··· 101 101 assert result.exit_code == 0 102 102 assert "test-facet" in result.output 103 103 104 - def test_journal_topics(self): 105 - """Topics command lists agent outputs for a day.""" 106 - result = runner.invoke(call_app, ["journal", "topics", "20240101"]) 104 + def test_journal_agents(self): 105 + """Agents command lists agent outputs for a day.""" 106 + result = runner.invoke(call_app, ["journal", "agents", "20240101"]) 107 107 assert result.exit_code == 0 108 108 assert "flow.md" in result.output 109 109 110 - def test_journal_topics_no_data(self): 111 - """Topics command reports no data for missing day.""" 112 - result = runner.invoke(call_app, ["journal", "topics", "19990101"]) 110 + def test_journal_agents_no_data(self): 111 + """Agents command reports no data for missing day.""" 112 + result = runner.invoke(call_app, ["journal", "agents", "19990101"]) 113 113 assert result.exit_code == 0 114 114 assert "No data" in result.output 115 115 ··· 140 140 assert len(result.output.strip()) > 100 141 141 142 142 def test_journal_read_not_found(self): 143 - """Read command reports missing topic.""" 143 + """Read command reports missing agent output.""" 144 144 result = runner.invoke( 145 145 call_app, ["journal", "read", "nonexistent", "--day", "20240101"] 146 146 ) ··· 270 270 result = runner.invoke(call_app, ["journal", "events"]) 271 271 assert result.exit_code != 0 272 272 273 - def test_topics_from_sol_day(self, monkeypatch): 274 - """topics with SOL_DAY env and no arg works.""" 273 + def test_agents_from_sol_day(self, monkeypatch): 274 + """agents with SOL_DAY env and no arg works.""" 275 275 monkeypatch.setenv("SOL_DAY", "20240101") 276 - result = runner.invoke(call_app, ["journal", "topics"]) 276 + result = runner.invoke(call_app, ["journal", "agents"]) 277 277 assert result.exit_code == 0 278 278 assert "flow.md" in result.output 279 279
+1 -1
tests/test_cluster.py
··· 475 475 mod = importlib.import_module("think.cluster") 476 476 477 477 # Create segment with app-namespaced agent output files 478 - # App agent output naming: "app:topic" -> "_app_topic.md" 478 + # App agent output naming: "app:agent" -> "_app_agent.md" 479 479 segment = day_dir / "default" / "120000_300" 480 480 segment.mkdir(parents=True) 481 481 (segment / "agents").mkdir()
+4 -4
tests/test_cortex_client.py
··· 160 160 assert agent_id is None 161 161 162 162 163 - def test_cortex_request_uses_default_path_when_journal_path_unset(callosum_server): 163 + def test_cortex_request_uses_default_path_when_journal_path_unset(monkeypatch): 164 164 """Test cortex_request uses platform default when JOURNAL_PATH unset.""" 165 - _ = callosum_server # Needed for side effects only 165 + monkeypatch.setattr("think.cortex_client.callosum_send", lambda *a, **kw: True) 166 166 old_path = os.environ.pop("JOURNAL_PATH", None) 167 167 try: 168 - # Should work (uses platform default) but no listener will respond 168 + # Should work with JOURNAL_PATH unset because send is mocked successful. 169 169 agent_id = cortex_request("test", "default", "openai") 170 - # Returns an agent_id since the request is queued 170 + # Returns an agent_id when request send succeeds. 171 171 assert agent_id is not None 172 172 assert len(agent_id) > 0 173 173 finally:
+31 -31
tests/test_formatters.py
··· 1238 1238 meta = extract_path_metadata("20240101/agents/flow.md") 1239 1239 assert meta["day"] == "20240101" 1240 1240 assert meta["facet"] == "" 1241 - assert meta["topic"] == "flow" 1241 + assert meta["agent"] == "flow" 1242 1242 1243 1243 def test_segment_markdown(self): 1244 - """Test day and topic extraction from segment markdown.""" 1244 + """Test day and agent extraction from segment markdown.""" 1245 1245 from think.formatters import extract_path_metadata 1246 1246 1247 1247 meta = extract_path_metadata("20240101/100000/agents/screen.md") 1248 1248 assert meta["day"] == "20240101" 1249 1249 assert meta["facet"] == "" 1250 - assert meta["topic"] == "screen" 1250 + assert meta["agent"] == "screen" 1251 1251 1252 - def test_segment_jsonl_no_topic(self): 1253 - """Test that JSONL files get empty topic (formatter provides it).""" 1252 + def test_segment_jsonl_no_agent(self): 1253 + """Test that JSONL files get empty agent (formatter provides it).""" 1254 1254 from think.formatters import extract_path_metadata 1255 1255 1256 1256 meta = extract_path_metadata("20240101/100000/audio.jsonl") 1257 1257 assert meta["day"] == "20240101" 1258 1258 assert meta["facet"] == "" 1259 - assert meta["topic"] == "" # Formatter provides topic for JSONL 1259 + assert meta["agent"] == "" # Formatter provides agent for JSONL 1260 1260 1261 1261 def test_facet_event(self): 1262 1262 """Test facet and day extraction from event path.""" ··· 1265 1265 meta = extract_path_metadata("facets/work/events/20240101.jsonl") 1266 1266 assert meta["day"] == "20240101" 1267 1267 assert meta["facet"] == "work" 1268 - assert meta["topic"] == "" # Formatter provides topic 1268 + assert meta["agent"] == "" # Formatter provides agent 1269 1269 1270 1270 def test_facet_entities_detected_personal(self): 1271 1271 """Test facet and day extraction from detected entities path.""" ··· 1274 1274 meta = extract_path_metadata("facets/personal/entities/20260115.jsonl") 1275 1275 assert meta["day"] == "20260115" 1276 1276 assert meta["facet"] == "personal" 1277 - assert meta["topic"] == "" # Formatter provides topic 1277 + assert meta["agent"] == "" # Formatter provides agent 1278 1278 1279 1279 def test_facet_entities_detected(self): 1280 1280 """Test facet and day extraction from detected entities path.""" ··· 1283 1283 meta = extract_path_metadata("facets/work/entities/20250101.jsonl") 1284 1284 assert meta["day"] == "20250101" 1285 1285 assert meta["facet"] == "work" 1286 - assert meta["topic"] == "" # Formatter provides topic 1286 + assert meta["agent"] == "" # Formatter provides agent 1287 1287 1288 1288 def test_facet_news(self): 1289 - """Test facet news markdown gets topic from path.""" 1289 + """Test facet news markdown gets agent from path.""" 1290 1290 from think.formatters import extract_path_metadata 1291 1291 1292 1292 meta = extract_path_metadata("facets/work/news/20240101.md") 1293 1293 assert meta["day"] == "20240101" 1294 1294 assert meta["facet"] == "work" 1295 - assert meta["topic"] == "news" 1295 + assert meta["agent"] == "news" 1296 1296 1297 1297 def test_import_summary(self): 1298 1298 """Test import summary path extraction.""" ··· 1301 1301 meta = extract_path_metadata("imports/20240101_093000/summary.md") 1302 1302 assert meta["day"] == "20240101" 1303 1303 assert meta["facet"] == "" 1304 - assert meta["topic"] == "import" 1304 + assert meta["agent"] == "import" 1305 1305 1306 1306 def test_app_output(self): 1307 1307 """Test app output path extraction.""" ··· 1310 1310 meta = extract_path_metadata("apps/myapp/agents/custom.md") 1311 1311 assert meta["day"] == "" 1312 1312 assert meta["facet"] == "" 1313 - assert meta["topic"] == "myapp:custom" 1313 + assert meta["agent"] == "myapp:custom" 1314 1314 1315 1315 def test_config_actions(self): 1316 1316 """Test journal-level action log path extraction.""" ··· 1319 1319 meta = extract_path_metadata("config/actions/20240101.jsonl") 1320 1320 assert meta["day"] == "20240101" 1321 1321 assert meta["facet"] == "" 1322 - assert meta["topic"] == "" 1322 + assert meta["agent"] == "" 1323 1323 1324 1324 def test_activity_output_path(self): 1325 1325 from think.formatters import extract_path_metadata ··· 1329 1329 ) 1330 1330 assert meta["day"] == "20260209" 1331 1331 assert meta["facet"] == "work" 1332 - assert meta["topic"] == "session_review" 1332 + assert meta["agent"] == "session_review" 1333 1333 1334 1334 def test_activity_output_path_json(self): 1335 1335 from think.formatters import extract_path_metadata ··· 1339 1339 ) 1340 1340 assert meta["day"] == "20260210" 1341 1341 assert meta["facet"] == "personal" 1342 - assert meta["topic"] == "" # JSONL/JSON topic set by formatter, not path 1342 + assert meta["agent"] == "" # JSONL/JSON agent set by formatter, not path 1343 1343 1344 1344 def test_activity_output_app_key(self): 1345 1345 from think.formatters import extract_path_metadata ··· 1349 1349 ) 1350 1350 assert meta["day"] == "20260209" 1351 1351 assert meta["facet"] == "work" 1352 - assert meta["topic"] == "_chat_review" 1352 + assert meta["agent"] == "_chat_review" 1353 1353 1354 1354 1355 1355 class TestFormatterIndexerMetadata: 1356 1356 """Tests verifying formatters return indexer metadata.""" 1357 1357 1358 1358 def test_format_audio_returns_indexer(self): 1359 - """Test format_audio returns indexer with topic.""" 1359 + """Test format_audio returns indexer with agent.""" 1360 1360 from observe.hear import format_audio 1361 1361 1362 1362 entries = [{"start": "00:00:01", "text": "Hello"}] 1363 1363 chunks, meta = format_audio(entries) 1364 1364 1365 1365 assert "indexer" in meta 1366 - assert meta["indexer"]["topic"] == "audio" 1366 + assert meta["indexer"]["agent"] == "audio" 1367 1367 1368 1368 def test_format_screen_returns_indexer(self): 1369 - """Test format_screen returns indexer with topic.""" 1369 + """Test format_screen returns indexer with agent.""" 1370 1370 from observe.screen import format_screen 1371 1371 1372 1372 entries = [{"timestamp": 0, "analysis": {"primary": "code"}}] 1373 1373 chunks, meta = format_screen(entries) 1374 1374 1375 1375 assert "indexer" in meta 1376 - assert meta["indexer"]["topic"] == "screen" 1376 + assert meta["indexer"]["agent"] == "screen" 1377 1377 1378 1378 def test_format_events_returns_indexer(self): 1379 - """Test format_events returns indexer with topic.""" 1379 + """Test format_events returns indexer with agent.""" 1380 1380 from think.events import format_events 1381 1381 1382 1382 entries = [{"type": "meeting", "title": "Test", "occurred": True}] 1383 1383 chunks, meta = format_events(entries) 1384 1384 1385 1385 assert "indexer" in meta 1386 - assert meta["indexer"]["topic"] == "event" 1386 + assert meta["indexer"]["agent"] == "event" 1387 1387 1388 1388 def test_format_entities_attached_returns_indexer(self): 1389 - """Test format_entities returns indexer with attached topic.""" 1389 + """Test format_entities returns indexer with attached agent.""" 1390 1390 from think.entities import format_entities 1391 1391 1392 1392 entries = [{"type": "Person", "name": "Alice", "description": "Test"}] ··· 1394 1394 chunks, meta = format_entities(entries) 1395 1395 1396 1396 assert "indexer" in meta 1397 - assert meta["indexer"]["topic"] == "entity:attached" 1397 + assert meta["indexer"]["agent"] == "entity:attached" 1398 1398 1399 1399 def test_format_entities_detected_returns_indexer(self): 1400 - """Test format_entities returns indexer with detected topic.""" 1400 + """Test format_entities returns indexer with detected agent.""" 1401 1401 from think.entities import format_entities 1402 1402 1403 1403 entries = [{"type": "Person", "name": "Alice", "description": "Test"}] ··· 1405 1405 chunks, meta = format_entities(entries, context) 1406 1406 1407 1407 assert "indexer" in meta 1408 - assert meta["indexer"]["topic"] == "entity:detected" 1408 + assert meta["indexer"]["agent"] == "entity:detected" 1409 1409 1410 1410 def test_format_todos_returns_indexer(self): 1411 - """Test format_todos returns indexer with topic.""" 1411 + """Test format_todos returns indexer with agent.""" 1412 1412 from apps.todos.todo import format_todos 1413 1413 1414 1414 entries = [{"text": "Test task", "completed": False}] 1415 1415 chunks, meta = format_todos(entries) 1416 1416 1417 1417 assert "indexer" in meta 1418 - assert meta["indexer"]["topic"] == "todo" 1418 + assert meta["indexer"]["agent"] == "todo" 1419 1419 1420 1420 1421 1421 class TestFormatterSourceKey: ··· 1641 1641 assert "action" in meta["error"] 1642 1642 1643 1643 def test_format_logs_returns_indexer(self): 1644 - """Test format_logs returns indexer with topic 'action'.""" 1644 + """Test format_logs returns indexer with agent 'action'.""" 1645 1645 from think.facets import format_logs 1646 1646 1647 1647 entries = [ ··· 1657 1657 chunks, meta = format_logs(entries) 1658 1658 1659 1659 assert "indexer" in meta 1660 - assert meta["indexer"]["topic"] == "action" 1660 + assert meta["indexer"]["agent"] == "action" 1661 1661 1662 1662 def test_format_logs_header_with_path(self): 1663 1663 """Test that header includes facet name and day from path."""
+6 -6
tests/test_generators.py
··· 24 24 assert info.get("source") == "system" 25 25 26 26 27 - def test_get_output_topic(): 27 + def test_get_output_name(): 28 28 """Test generator key to filename conversion.""" 29 29 muse = importlib.import_module("think.muse") 30 30 31 31 # System generators: key unchanged 32 - assert muse.get_output_topic("activity") == "activity" 33 - assert muse.get_output_topic("flow") == "flow" 32 + assert muse.get_output_name("activity") == "activity" 33 + assert muse.get_output_name("flow") == "flow" 34 34 35 - # App generators: _app_topic format 36 - assert muse.get_output_topic("chat:sentiment") == "_chat_sentiment" 37 - assert muse.get_output_topic("my_app:weekly_summary") == "_my_app_weekly_summary" 35 + # App generators: _app_name format 36 + assert muse.get_output_name("chat:sentiment") == "_chat_sentiment" 37 + assert muse.get_output_name("my_app:weekly_summary") == "_my_app_weekly_summary" 38 38 39 39 40 40 def test_get_muse_configs_app_discovery(tmp_path, monkeypatch):
+22 -22
tests/test_journal_index.py
··· 105 105 "title": "Standup", 106 106 "summary": "Daily sync meeting", 107 107 "facet": "work", 108 - "topic": "meetings", 108 + "agent": "meetings", 109 109 "occurred": True, 110 110 } 111 111 (events_dir / "20240101.jsonl").write_text(json.dumps(event)) ··· 161 161 162 162 scan_journal(str(journal_fixture)) 163 163 164 - total, results = search_journal("Standup", topic="event") 164 + total, results = search_journal("Standup", agent="event") 165 165 assert total >= 1 166 166 assert any("Standup" in r["text"] for r in results) 167 167 ··· 192 192 assert r["metadata"]["facet"] == "work" 193 193 194 194 195 - def test_search_journal_filter_by_topic(journal_fixture): 196 - """Test filtering search by topic.""" 195 + def test_search_journal_filter_by_agent(journal_fixture): 196 + """Test filtering search by agent.""" 197 197 from think.indexer.journal import scan_journal, search_journal 198 198 199 199 scan_journal(str(journal_fixture)) 200 200 201 - # Search events by topic 202 - total, results = search_journal("", topic="event") 201 + # Search events by agent 202 + total, results = search_journal("", agent="event") 203 203 assert total >= 1 204 204 for r in results: 205 - assert r["metadata"]["topic"] == "event" 205 + assert r["metadata"]["agent"] == "event" 206 206 207 207 208 208 def test_search_journal_facet_case_insensitive(journal_fixture): ··· 223 223 assert r["metadata"]["facet"] == "work" 224 224 225 225 226 - def test_search_journal_topic_case_insensitive(journal_fixture): 227 - """Test topic filtering is case-insensitive.""" 226 + def test_search_journal_agent_case_insensitive(journal_fixture): 227 + """Test agent filtering is case-insensitive.""" 228 228 from think.indexer.journal import scan_journal, search_journal 229 229 230 230 scan_journal(str(journal_fixture)) 231 231 232 - # Search with uppercase topic filter should find lowercase-indexed data 233 - total_upper, results_upper = search_journal("", topic="EVENT") 234 - total_lower, _ = search_journal("", topic="event") 235 - total_mixed, _ = search_journal("", topic="Event") 232 + # Search with uppercase agent filter should find lowercase-indexed data 233 + total_upper, results_upper = search_journal("", agent="EVENT") 234 + total_lower, _ = search_journal("", agent="event") 235 + total_mixed, _ = search_journal("", agent="Event") 236 236 237 237 assert total_upper == total_lower == total_mixed 238 238 assert total_upper >= 1 239 - # All results should have lowercase topic in metadata 239 + # All results should have lowercase agent in metadata 240 240 for r in results_upper: 241 - assert r["metadata"]["topic"] == "event" 241 + assert r["metadata"]["agent"] == "event" 242 242 243 243 244 244 def test_get_events(journal_fixture): ··· 420 420 assert "counts" in result 421 421 counts = result["counts"] 422 422 assert "facets" in counts 423 - assert "topics" in counts 423 + assert "agents" in counts 424 424 assert "recent_days" in counts 425 425 assert "top_days" in counts 426 426 assert "bucketed_days" in counts ··· 435 435 436 436 os.environ["JOURNAL_PATH"] = "tests/fixtures/journal" 437 437 438 - result = search_journal("test query", facet="work", topic="flow") 438 + result = search_journal("test query", facet="work", agent="flow") 439 439 440 440 assert "query" in result 441 441 assert result["query"]["text"] == "test query" 442 442 assert result["query"]["filters"]["facet"] == "work" 443 - assert result["query"]["filters"]["topic"] == "flow" 443 + assert result["query"]["filters"]["agent"] == "flow" 444 444 445 445 446 446 def test_search_journal_results_include_path(): ··· 472 472 "metadata": { 473 473 "day": "20240101", 474 474 "facet": "", 475 - "topic": "test", 475 + "agent": "test", 476 476 "path": "a.md", 477 477 "idx": 0, 478 478 }, 479 479 "score": 1.0, 480 480 } 481 481 ] 482 - fake_counts = {"facets": [], "topics": [], "days": []} 482 + fake_counts = {"facets": [], "agents": [], "days": []} 483 483 484 484 with ( 485 485 patch("think.tools.search.search_journal_impl", return_value=(1, fake_results)), ··· 541 541 scan_journal(str(journal_fixture), full=True) 542 542 543 543 # Verify event is indexed 544 - total, _ = search_journal("Standup", topic="event") 544 + total, _ = search_journal("Standup", agent="event") 545 545 assert total >= 1 546 546 547 547 # Delete the facet event file ··· 553 553 assert changed is True 554 554 555 555 # Event should no longer be searchable 556 - total, _ = search_journal("Standup", topic="event") 556 + total, _ = search_journal("Standup", agent="event") 557 557 assert total == 0 558 558 559 559
+2 -2
tests/test_journal_stats.py
··· 43 43 "participants": [], 44 44 "details": "", 45 45 "facet": "work", 46 - "topic": "meetings", 46 + "agent": "meetings", 47 47 "occurred": True, 48 48 "source": "20240101/agents/meetings.md", 49 49 } ··· 58 58 assert ( 59 59 js.days["20240101"]["pending_segments"] == 1 60 60 ) # Both files belong to same segment 61 - assert js.topic_counts["meetings"] == 1 61 + assert js.agent_counts["meetings"] == 1 62 62 assert js.facet_counts["work"] == 1 63 63 assert js.facet_minutes["work"] == 5.0 64 64 assert js.heatmap[0][0] == 5
+6 -6
tests/test_output_path.py
··· 6 6 import os 7 7 from pathlib import Path 8 8 9 - from think.muse import get_output_path, get_output_topic 9 + from think.muse import get_output_name, get_output_path 10 10 11 11 os.environ.setdefault("JOURNAL_PATH", "tests/fixtures/journal") 12 12 13 13 14 - class TestGetOutputTopic: 15 - """Tests for get_output_topic.""" 14 + class TestGetOutputName: 15 + """Tests for get_output_name.""" 16 16 17 17 def test_simple_key(self): 18 - assert get_output_topic("activity") == "activity" 18 + assert get_output_name("activity") == "activity" 19 19 20 20 def test_app_key(self): 21 - assert get_output_topic("chat:sentiment") == "_chat_sentiment" 21 + assert get_output_name("chat:sentiment") == "_chat_sentiment" 22 22 23 23 def test_entities_app_key(self): 24 - assert get_output_topic("entities:observer") == "_entities_observer" 24 + assert get_output_name("entities:observer") == "_entities_observer" 25 25 26 26 27 27 class TestGetOutputPath:
+2 -2
tests/test_screen_formatter.py
··· 255 255 256 256 257 257 def test_format_screen_returns_indexer_metadata(): 258 - """Test that format_screen returns indexer metadata with topic.""" 258 + """Test that format_screen returns indexer metadata with agent.""" 259 259 frames = [ 260 260 { 261 261 "timestamp": 0, ··· 266 266 chunks, meta = format_screen(frames) 267 267 268 268 assert "indexer" in meta 269 - assert meta["indexer"]["topic"] == "screen" 269 + assert meta["indexer"]["agent"] == "screen" 270 270 271 271 272 272 def test_load_category_formatter_finds_meeting():
+3 -3
tests/test_template_substitution.py
··· 196 196 mock_journal_with_config, mock_prompt_dir 197 197 ): 198 198 """Test that uppercase-first versions are created for context variables.""" 199 - context_prompt = """lowercase: $topic 200 - Uppercase: $Topic""" 199 + context_prompt = """lowercase: $agent 200 + Uppercase: $Agent""" 201 201 (mock_prompt_dir / "uppercase_test.md").write_text(context_prompt) 202 202 203 203 result = load_prompt( 204 204 "uppercase_test", 205 205 base_dir=mock_prompt_dir, 206 - context={"topic": "meetings"}, 206 + context={"agent": "meetings"}, 207 207 ) 208 208 209 209 assert "lowercase: meetings" in result.text
+4 -4
think/activities.py
··· 636 636 Output lives under the facet's activities directory, grouped by day 637 637 and activity record ID: 638 638 639 - facets/{facet}/activities/{day}/{activity_id}/{topic}.{ext} 639 + facets/{facet}/activities/{day}/{activity_id}/{agent}.{ext} 640 640 641 641 Args: 642 642 facet: Facet name ··· 648 648 Returns: 649 649 Absolute path for the output file 650 650 """ 651 - from think.muse import get_output_topic 651 + from think.muse import get_output_name 652 652 653 - topic = get_output_topic(key) 653 + output_name = get_output_name(key) 654 654 ext = "json" if output_format == "json" else "md" 655 655 return ( 656 656 Path(get_journal()) ··· 659 659 / "activities" 660 660 / day 661 661 / activity_id 662 - / f"{topic}.{ext}" 662 + / f"{output_name}.{ext}" 663 663 ) 664 664 665 665
+2 -2
think/cluster.py
··· 25 25 def _filename_to_agent_key(filename: str) -> str: 26 26 """Convert output filename stem to agent key. 27 27 28 - Reverse of get_output_topic(): converts filesystem names back to agent keys. 28 + Reverse of get_output_name(): converts filesystem names back to agent keys. 29 29 30 30 Args: 31 31 filename: Filename stem (e.g., "entities" or "_todos_review") ··· 34 34 Agent key (e.g., "entities" or "todos:review") 35 35 """ 36 36 if filename.startswith("_"): 37 - # App agent: "_app_topic" -> "app:topic" 37 + # App agent: "_app_name" -> "app:name" 38 38 parts = filename[1:].split("_", 1) 39 39 if len(parts) == 2: 40 40 return f"{parts[0]}:{parts[1]}"
+1 -1
think/dream.py
··· 1376 1376 ) 1377 1377 parser.add_argument( 1378 1378 "--segment", 1379 - help="Segment key in HHMMSS_LEN format (processes segment topics only)", 1379 + help="Segment key in HHMMSS_LEN format (processes segment agents only)", 1380 1380 ) 1381 1381 parser.add_argument( 1382 1382 "--refresh", action="store_true", help="Refresh existing outputs"
+3 -3
think/entities/formatting.py
··· 154 154 } 155 155 ) 156 156 157 - # Indexer metadata - topic depends on attached vs detected 158 - topic = "entity:detected" if is_detected else "entity:attached" 159 - meta["indexer"] = {"topic": topic} 157 + # Indexer metadata - agent depends on attached vs detected 158 + agent = "entity:detected" if is_detected else "entity:attached" 159 + meta["indexer"] = {"agent": agent} 160 160 161 161 return chunks, meta
+2 -2
think/events.py
··· 168 168 meta["error"] = error_msg 169 169 logging.info(error_msg) 170 170 171 - # Indexer metadata - topic is always "event" for events 172 - meta["indexer"] = {"topic": "event"} 171 + # Indexer metadata - agent is always "event" for events 172 + meta["indexer"] = {"agent": "event"} 173 173 174 174 return chunks, meta 175 175
+2 -2
think/facets.py
··· 842 842 meta["error"] = error_msg 843 843 logging.info(error_msg) 844 844 845 - # Indexer metadata - topic is "action" for action logs 846 - meta["indexer"] = {"topic": "action"} 845 + # Indexer metadata - agent is "action" for action logs 846 + meta["indexer"] = {"agent": "action"} 847 847 848 848 return chunks, meta 849 849
+14 -14
think/formatters.py
··· 20 20 - header: str - Optional header markdown (metadata summary, context, etc.) 21 21 - error: str - Optional error/warning message (e.g., skipped entries) 22 22 - indexer: dict - Indexing metadata with keys: 23 - - topic: str - Content type (e.g., "event", "audio", "screen") 24 - JSONL formatters must provide topic. Markdown topic is path-derived. 23 + - agent: str - Content type (e.g., "event", "audio", "screen") 24 + JSONL formatters must provide agent. Markdown agent is path-derived. 25 25 Day and facet are extracted from path by extract_path_metadata(). 26 26 27 27 JSONL formatters receive list[dict] entries and are responsible for: 28 28 - Extracting metadata from entries (typically first line) 29 29 - Building header from metadata if applicable 30 30 - Formatting content entries into chunks 31 - - Providing indexer.topic in the meta dict 31 + - Providing indexer.agent in the meta dict 32 32 33 33 Markdown formatters receive str text and perform semantic chunking. 34 34 """ ··· 49 49 """Extract indexing metadata from a journal-relative path. 50 50 51 51 Extracts day and facet from path structure. For markdown files, also 52 - derives topic from path. For JSONL files, topic should be provided 53 - by the formatter via meta["indexer"]["topic"]. 52 + derives agent from path. For JSONL files, agent should be provided 53 + by the formatter via meta["indexer"]["agent"]. 54 54 55 55 Args: 56 56 rel_path: Journal-relative path (e.g., "20240101/agents/flow.md") 57 57 58 58 Returns: 59 - Dict with keys: day, facet, topic 59 + Dict with keys: day, facet, agent 60 60 - day: YYYYMMDD string or empty 61 61 - facet: Facet name or empty 62 - - topic: Derived topic for .md files, empty for .jsonl 62 + - agent: Derived agent for .md files, empty for .jsonl 63 63 """ 64 64 parts = rel_path.replace("\\", "/").split("/") 65 65 filename = parts[-1] ··· 68 68 69 69 day = "" 70 70 facet = "" 71 - topic = "" 71 + agent = "" 72 72 73 73 # Extract day from YYYYMMDD directory prefix 74 74 if parts[0] and DATE_RE.fullmatch(parts[0]): ··· 104 104 if DATE_RE.fullmatch(basename): 105 105 day = basename 106 106 107 - # Derive topic for markdown files only 107 + # Derive agent for markdown files only 108 108 if is_markdown: 109 109 if parts[0] == "facets" and len(parts) >= 4 and parts[2] == "news": 110 - topic = "news" 110 + agent = "news" 111 111 elif parts[0] == "imports": 112 - topic = "import" 112 + agent = "import" 113 113 elif parts[0] == "apps" and len(parts) >= 4: 114 - topic = f"{parts[1]}:{basename}" 114 + agent = f"{parts[1]}:{basename}" 115 115 else: 116 116 # Daily agent outputs, segment markdown: use basename 117 - topic = basename 117 + agent = basename 118 118 119 - return {"day": day, "facet": facet, "topic": topic} 119 + return {"day": day, "facet": facet, "agent": agent} 120 120 121 121 122 122 # Registry mapping glob patterns to (module_path, function_name, indexed).
+6 -6
think/hooks.py
··· 93 93 94 94 def write_events_jsonl( 95 95 events: list[dict], 96 - topic: str, 96 + agent: str, 97 97 occurred: bool, 98 98 source_output: str, 99 99 capture_day: str, ··· 105 105 106 106 Args: 107 107 events: List of event dictionaries from extraction. 108 - topic: Source generator topic (e.g., "meetings", "schedule"). 108 + agent: Source generator agent (e.g., "meetings", "schedule"). 109 109 occurred: True for occurrences, False for anticipations. 110 110 source_output: Relative path to source output file. 111 111 capture_day: Day the output was captured (YYYYMMDD). ··· 144 144 145 145 # Enrich event with metadata 146 146 enriched = dict(event) 147 - enriched["topic"] = topic 147 + enriched["agent"] = agent 148 148 enriched["occurred"] = occurred 149 149 enriched["source"] = source_output 150 150 ··· 176 176 Returns: 177 177 Relative path like "20240101/agents/meetings.md". 178 178 """ 179 - from think.muse import get_output_topic 179 + from think.muse import get_output_name 180 180 from think.utils import get_journal 181 181 182 182 day = context.get("day", "") ··· 188 188 return os.path.relpath(output_path, journal) 189 189 except ValueError: 190 190 segment = context.get("segment") 191 - topic = get_output_topic(name) 191 + output_name = get_output_name(name) 192 192 # Check for facet in meta (for multi-facet agents) 193 193 meta = context.get("meta", {}) 194 194 facet = meta.get("facet") if meta else None 195 - filename = f"{topic}.md" 195 + filename = f"{output_name}.md" 196 196 if segment and facet: 197 197 return os.path.join(day, segment, "agents", facet, filename) 198 198 if segment:
+11 -10
think/indexer/cli.py
··· 31 31 """Display aggregated counts in a compact table format.""" 32 32 total = counts["total"] 33 33 facets = counts["facets"] # Counter 34 - topics = counts["topics"] # Counter 34 + agents = counts["agents"] # Counter 35 35 days = counts["days"] # Counter 36 36 37 37 print(f"Total: {total:,} chunks\n") 38 38 39 39 # Build columns 40 40 facet_col = _format_count_column(facets.most_common(top_n), len(facets), top_n) 41 - topic_col = _format_count_column(topics.most_common(top_n), len(topics), top_n) 41 + agent_col = _format_count_column(agents.most_common(top_n), len(agents), top_n) 42 42 day_col = _format_count_column( 43 43 sorted(days.items(), reverse=True)[:top_n], len(days), top_n 44 44 ) 45 45 46 46 # Header and rows 47 - print(f"{'Facet':<20} {'Topic':<20} {'Day':<20}") 47 + print(f"{'Facet':<20} {'Agent':<20} {'Day':<20}") 48 48 print("-" * 60) 49 49 50 50 from itertools import zip_longest 51 51 52 - for f, t, d in zip_longest(facet_col, topic_col, day_col, fillvalue=""): 53 - print(f"{f:<20} {t:<20} {d:<20}") 52 + for f, a, d in zip_longest(facet_col, agent_col, day_col, fillvalue=""): 53 + print(f"{f:<20} {a:<20} {d:<20}") 54 54 55 55 print() 56 56 ··· 72 72 meta = r.get("metadata", {}) 73 73 text = r.get("text", "").replace("\n", " ") 74 74 snippet = text[:100] + "..." if len(text) > 100 else text 75 - label = meta.get("topic") or meta.get("time") or "" 75 + label = meta.get("agent") or meta.get("time") or "" 76 76 facet = meta.get("facet") 77 77 facet_str = f" ({facet})" if facet else "" 78 78 print(f"{idx}. {meta.get('day')} {label}{facet_str}: {snippet}") ··· 120 120 help="Filter search results by facet name", 121 121 ) 122 122 parser.add_argument( 123 - "--topic", 124 - help="Filter search results by topic (e.g., 'flow', 'event', 'news')", 123 + "--agent", 124 + "-a", 125 + help="Filter search results by agent (e.g., 'flow', 'event', 'news')", 125 126 ) 126 127 parser.add_argument( 127 128 "--stream", ··· 193 194 query_kwargs["day_to"] = args.day_to 194 195 if args.facet: 195 196 query_kwargs["facet"] = args.facet 196 - if args.topic: 197 - query_kwargs["topic"] = args.topic 197 + if args.agent: 198 + query_kwargs["agent"] = args.agent 198 199 if args.stream: 199 200 query_kwargs["stream"] = args.stream 200 201
+29 -29
think/indexer/journal.py
··· 11 11 - Action logs (facet/journal-level JSONL) 12 12 13 13 All content is converted to markdown chunks via the formatters framework, 14 - then indexed with metadata fields for filtering (day, facet, topic). 14 + then indexed with metadata fields for filtering (day, facet, agent). 15 15 Raw audio/screen transcripts are formattable but not indexed by default. 16 16 """ 17 17 ··· 47 47 path UNINDEXED, 48 48 day UNINDEXED, 49 49 facet UNINDEXED, 50 - topic UNINDEXED, 50 + agent UNINDEXED, 51 51 stream UNINDEXED, 52 52 idx UNINDEXED 53 53 ) ··· 192 192 193 193 Metadata is sourced from two places: 194 194 - Path-derived: day and facet from extract_path_metadata() 195 - - Formatter-provided: topic from meta["indexer"]["topic"] 196 - For markdown files, topic is also path-derived. 195 + - Formatter-provided: agent from meta["indexer"]["agent"] 196 + For markdown files, agent is also path-derived. 197 197 """ 198 198 try: 199 199 chunks, meta = format_file(path) ··· 201 201 logger.warning("Skipping %s: %s", rel, e) 202 202 return 203 203 204 - # Get path-derived metadata (day, facet, topic for .md files) 204 + # Get path-derived metadata (day, facet, agent for .md files) 205 205 path_meta = extract_path_metadata(rel) 206 206 207 - # Get formatter-provided metadata (topic for JSONL files) 207 + # Get formatter-provided metadata (agent for JSONL files) 208 208 formatter_indexer = meta.get("indexer", {}) 209 209 210 210 # Merge: formatter values override path values, normalize to lowercase 211 211 day = formatter_indexer.get("day") or path_meta["day"] 212 212 facet = (formatter_indexer.get("facet") or path_meta["facet"]).lower() 213 - topic = (formatter_indexer.get("topic") or path_meta["topic"]).lower() 213 + agent = (formatter_indexer.get("agent") or path_meta["agent"]).lower() 214 214 215 215 if verbose: 216 216 logger.info( 217 - " %s chunks, day=%s, facet=%s, topic=%s, stream=%s", 217 + " %s chunks, day=%s, facet=%s, agent=%s, stream=%s", 218 218 len(chunks), 219 219 day, 220 220 facet, 221 - topic, 221 + agent, 222 222 stream, 223 223 ) 224 224 ··· 228 228 continue 229 229 230 230 conn.execute( 231 - "INSERT INTO chunks(content, path, day, facet, topic, stream, idx) VALUES (?, ?, ?, ?, ?, ?, ?)", 232 - (content, rel, day, facet, topic, stream, idx), 231 + "INSERT INTO chunks(content, path, day, facet, agent, stream, idx) VALUES (?, ?, ?, ?, ?, ?, ?)", 232 + (content, rel, day, facet, agent, stream, idx), 233 233 ) 234 234 235 235 ··· 356 356 day_from: str | None = None, 357 357 day_to: str | None = None, 358 358 facet: str | None = None, 359 - topic: str | None = None, 359 + agent: str | None = None, 360 360 stream: str | None = None, 361 361 ) -> tuple[str, list[Any]]: 362 362 """Build WHERE clause and params for FTS5 search. ··· 367 367 day_from: Filter by date range start (YYYYMMDD, inclusive) 368 368 day_to: Filter by date range end (YYYYMMDD, inclusive) 369 369 facet: Filter by facet name 370 - topic: Filter by topic 370 + agent: Filter by agent 371 371 stream: Filter by stream name 372 372 373 373 Returns: ··· 394 394 if facet: 395 395 where_clause += " AND facet=?" 396 396 params.append(facet.lower()) 397 - if topic: 398 - where_clause += " AND topic=?" 399 - params.append(topic.lower()) 397 + if agent: 398 + where_clause += " AND agent=?" 399 + params.append(agent.lower()) 400 400 if stream: 401 401 where_clause += " AND stream=?" 402 402 params.append(stream) ··· 413 413 day_from: str | None = None, 414 414 day_to: str | None = None, 415 415 facet: str | None = None, 416 - topic: str | None = None, 416 + agent: str | None = None, 417 417 stream: str | None = None, 418 418 ) -> tuple[int, list[dict[str, Any]]]: 419 419 """Search the journal index. ··· 427 427 day_from: Filter by date range start (YYYYMMDD, inclusive) 428 428 day_to: Filter by date range end (YYYYMMDD, inclusive) 429 429 facet: Filter by facet name 430 - topic: Filter by topic (e.g., "flow", "event", "news") 430 + agent: Filter by agent (e.g., "flow", "event", "news") 431 431 stream: Filter by stream name 432 432 433 433 Returns: 434 434 Tuple of (total_count, results) where each result has: 435 435 - id: "{path}:{idx}" 436 436 - text: The matched markdown chunk 437 - - metadata: {day, facet, topic, stream, path, idx} 437 + - metadata: {day, facet, agent, stream, path, idx} 438 438 - score: BM25 relevance score 439 439 """ 440 440 conn, _ = get_journal_index() 441 441 where_clause, params = _build_where_clause( 442 - query, day, day_from, day_to, facet, topic, stream 442 + query, day, day_from, day_to, facet, agent, stream 443 443 ) 444 444 445 445 # Get total count ··· 450 450 # Get results 451 451 cursor = conn.execute( 452 452 f""" 453 - SELECT content, path, day, facet, topic, stream, idx, bm25(chunks) as rank 453 + SELECT content, path, day, facet, agent, stream, idx, bm25(chunks) as rank 454 454 FROM chunks WHERE {where_clause} 455 455 ORDER BY rank LIMIT ? OFFSET ? 456 456 """, ··· 463 463 path, 464 464 day_val, 465 465 facet_val, 466 - topic_val, 466 + agent_val, 467 467 stream_val, 468 468 idx, 469 469 rank, ··· 475 475 "metadata": { 476 476 "day": day_val, 477 477 "facet": facet_val, 478 - "topic": topic_val, 478 + "agent": agent_val, 479 479 "stream": stream_val, 480 480 "path": path, 481 481 "idx": idx, ··· 495 495 day_from: str | None = None, 496 496 day_to: str | None = None, 497 497 facet: str | None = None, 498 - topic: str | None = None, 498 + agent: str | None = None, 499 499 stream: str | None = None, 500 500 ) -> dict[str, Any]: 501 501 """Get aggregated counts for a search query. ··· 508 508 day_from: Filter by date range start (YYYYMMDD, inclusive) 509 509 day_to: Filter by date range end (YYYYMMDD, inclusive) 510 510 facet: Filter by facet name 511 - topic: Filter by topic 511 + agent: Filter by agent 512 512 stream: Filter by stream name 513 513 514 514 Returns: 515 515 Dict with: 516 516 - total: Total matching chunks 517 517 - facets: Counter of facet_name -> count 518 - - topics: Counter of topic_name -> count 518 + - agents: Counter of agent_name -> count 519 519 - days: Counter of day -> count 520 520 - streams: Counter of stream_name -> count 521 521 """ ··· 523 523 524 524 conn, _ = get_journal_index() 525 525 where_clause, params = _build_where_clause( 526 - query, day, day_from, day_to, facet, topic, stream 526 + query, day, day_from, day_to, facet, agent, stream 527 527 ) 528 528 529 529 rows = conn.execute( 530 - f"SELECT facet, topic, day, stream FROM chunks WHERE {where_clause}", params 530 + f"SELECT facet, agent, day, stream FROM chunks WHERE {where_clause}", params 531 531 ).fetchall() 532 532 533 533 conn.close() ··· 535 535 return { 536 536 "total": len(rows), 537 537 "facets": Counter(r[0] for r in rows if r[0]), 538 - "topics": Counter(r[1] for r in rows if r[1]), 538 + "agents": Counter(r[1] for r in rows if r[1]), 539 539 "days": Counter(r[2] for r in rows if r[2]), 540 540 "streams": Counter(r[3] for r in rows if r[3]), 541 541 }
+24 -24
think/journal_stats.py
··· 24 24 self.totals: Counter[str] = Counter() 25 25 self.total_audio_duration = 0.0 26 26 self.total_screen_duration = 0.0 27 - self.topic_counts: Counter[str] = Counter() 28 - self.topic_minutes: Counter[str] = Counter() 27 + self.agent_counts: Counter[str] = Counter() 28 + self.agent_minutes: Counter[str] = Counter() 29 29 self.facet_counts: Counter[str] = Counter() 30 30 self.facet_minutes: Counter[str] = Counter() 31 31 self.heatmap: list[list[float]] = [[0.0 for _ in range(24)] for _ in range(7)] ··· 33 33 self.token_usage: Dict[str, Dict[str, Dict[str, int]]] = {} 34 34 # Total token usage by model: {model: {token_type: count}} 35 35 self.token_totals: Dict[str, Dict[str, int]] = {} 36 - # Per-day topic counts: {day: {topic: count}} 37 - self.topic_counts_by_day: Dict[str, Dict[str, int]] = {} 36 + # Per-day agent counts: {day: {agent: count}} 37 + self.agent_counts_by_day: Dict[str, Dict[str, int]] = {} 38 38 # Per-day facet counts: {day: {facet: count}} 39 39 self.facet_counts_by_day: Dict[str, Dict[str, int]] = {} 40 40 ··· 120 120 """Apply cached day stats to instance state.""" 121 121 # Extract components from cache 122 122 stats = cached_data.get("stats", {}) 123 - topic_data = cached_data.get("topic_data", {}) 123 + agent_data = cached_data.get("agent_data", {}) 124 124 heatmap_data = cached_data.get("heatmap_data", {}) 125 125 126 126 # Apply day stats ··· 138 138 self.total_audio_duration += stats.get("audio_duration", 0.0) 139 139 self.total_screen_duration += stats.get("screen_duration", 0.0) 140 140 141 - # Apply topic data 142 - day_topic_counts: Dict[str, int] = {} 143 - for topic, data in topic_data.items(): 141 + # Apply agent data 142 + day_agent_counts: Dict[str, int] = {} 143 + for agent, data in agent_data.items(): 144 144 count = data.get("count", 0) 145 - self.topic_counts[topic] += count 146 - self.topic_minutes[topic] += data.get("minutes", 0.0) 145 + self.agent_counts[agent] += count 146 + self.agent_minutes[agent] += data.get("minutes", 0.0) 147 147 if count > 0: 148 - day_topic_counts[topic] = count 149 - if day_topic_counts: 150 - self.topic_counts_by_day[day] = day_topic_counts 148 + day_agent_counts[agent] = count 149 + if day_agent_counts: 150 + self.agent_counts_by_day[day] = day_agent_counts 151 151 152 152 # Apply facet data 153 153 facet_data = cached_data.get("facet_data", {}) ··· 176 176 screen_duration = 0.0 177 177 day_dir = Path(path) 178 178 179 - # Track topic data for cache 180 - topic_data = {} 179 + # Track agent data for cache 180 + agent_data = {} 181 181 facet_data = {} 182 182 heatmap_hours = {} 183 183 ··· 278 278 except json.JSONDecodeError: 279 279 continue 280 280 281 - topic = event.get("topic", "unknown") 282 - if topic not in topic_data: 283 - topic_data[topic] = {"count": 0, "minutes": 0.0} 284 - topic_data[topic]["count"] += 1 281 + agent = event.get("agent", "unknown") 282 + if agent not in agent_data: 283 + agent_data[agent] = {"count": 0, "minutes": 0.0} 284 + agent_data[agent]["count"] += 1 285 285 286 286 start = event.get("start") 287 287 end = event.get("end") ··· 294 294 start_sec = sh * 3600 + sm * 60 + ss 295 295 end_sec = eh * 3600 + em * 60 + es 296 296 duration = max(0, end_sec - start_sec) 297 - topic_data[topic]["minutes"] += duration / 60 297 + agent_data[agent]["minutes"] += duration / 60 298 298 299 299 # Track facet stats 300 300 facet = event.get("facet", facet_name) ··· 329 329 330 330 return { 331 331 "stats": dict(stats), 332 - "topic_data": topic_data, 332 + "agent_data": agent_data, 333 333 "facet_data": facet_data, 334 334 "heatmap_data": {"weekday": weekday, "hours": heatmap_hours}, 335 335 } ··· 468 468 "totals": dict(self.totals), 469 469 "total_audio_duration": self.total_audio_duration, 470 470 "total_screen_duration": self.total_screen_duration, 471 - "topic_counts": dict(self.topic_counts), 472 - "topic_minutes": {k: round(v, 2) for k, v in self.topic_minutes.items()}, 473 - "topic_counts_by_day": self.topic_counts_by_day, 471 + "agent_counts": dict(self.agent_counts), 472 + "agent_minutes": {k: round(v, 2) for k, v in self.agent_minutes.items()}, 473 + "agent_counts_by_day": self.agent_counts_by_day, 474 474 "facet_counts": dict(self.facet_counts), 475 475 "facet_minutes": {k: round(v, 2) for k, v in self.facet_minutes.items()}, 476 476 "facet_counts_by_day": self.facet_counts_by_day,
+2 -2
think/markdown.py
··· 379 379 its full context (headers, intro paragraphs) rendered back to markdown. 380 380 381 381 Note: Unlike JSONL formatters, this does not return indexer metadata. 382 - Topic for markdown files is derived from path by extract_path_metadata(). 382 + Agent for markdown files is derived from path by extract_path_metadata(). 383 383 384 384 Args: 385 385 text: Markdown text to chunk ··· 389 389 Tuple of (chunks, meta) where: 390 390 - chunks: List of {"markdown": str} dicts (timestamp omitted) 391 391 - meta: Empty dict (no header or indexer - context is in each chunk, 392 - topic is path-derived) 392 + agent is path-derived) 393 393 """ 394 394 text = sanitize_markdown(text) 395 395 raw_chunks = chunk_markdown(text)
+15 -15
think/muse.py
··· 67 67 return f"muse.system.{key}" 68 68 69 69 70 - def get_output_topic(key: str) -> str: 71 - """Convert agent/generator key to filesystem-safe basename (no extension). 70 + def get_output_name(key: str) -> str: 71 + """Convert agent/generator key to filesystem-safe filename stem. 72 72 73 73 Parameters 74 74 ---------- 75 75 key: 76 - Generator key in format "topic" (system) or "app:topic" (app). 76 + Generator key in format "name" (system) or "app:name" (app). 77 77 78 78 Returns 79 79 ------- 80 80 str 81 - Filesystem-safe name: "topic" or "_app_topic". 81 + Filesystem-safe stem: "name" or "_app_name". 82 82 83 83 Examples 84 84 -------- 85 - >>> get_output_topic("activity") 85 + >>> get_output_name("activity") 86 86 'activity' 87 - >>> get_output_topic("chat:sentiment") 87 + >>> get_output_name("chat:sentiment") 88 88 '_chat_sentiment' 89 89 """ 90 90 if ":" in key: 91 - app, topic = key.split(":", 1) 92 - return f"_{app}_{topic}" 91 + app, name = key.split(":", 1) 92 + return f"_{app}_{name}" 93 93 return key 94 94 95 95 ··· 128 128 ------- 129 129 Path 130 130 Output file path: 131 - - Segment + no facet: YYYYMMDD/{stream}/{segment}/agents/{topic}.{ext} 132 - - Segment + facet: YYYYMMDD/{stream}/{segment}/agents/{facet}/{topic}.{ext} 133 - - Daily + no facet: YYYYMMDD/agents/{topic}.{ext} 134 - - Daily + facet: YYYYMMDD/agents/{facet}/{topic}.{ext} 135 - Where topic is derived from key and ext is "json" or "md". 131 + - Segment + no facet: YYYYMMDD/{stream}/{segment}/agents/{name}.{ext} 132 + - Segment + facet: YYYYMMDD/{stream}/{segment}/agents/{facet}/{name}.{ext} 133 + - Daily + no facet: YYYYMMDD/agents/{name}.{ext} 134 + - Daily + facet: YYYYMMDD/agents/{facet}/{name}.{ext} 135 + Where name is derived from key and ext is "json" or "md". 136 136 """ 137 137 day = Path(day_dir) 138 - topic = get_output_topic(key) 138 + name = get_output_name(key) 139 139 ext = "json" if output_format == "json" else "md" 140 - filename = f"{topic}.{ext}" 140 + filename = f"{name}.{ext}" 141 141 142 142 if segment: 143 143 if stream:
+8 -8
think/planner.md
··· 15 15 You have knowledge of these tools for planning purposes: 16 16 17 17 ### Search Tools 18 - - **search_journal**: Unified full-text search across all journal content (agent outputs, events, entities, todos). Supports filtering by `day`, `facet`, and `topic` (e.g., "event", "flow", "news"). Best for discovering themes, concepts, patterns, and specific content across the journal. Note: raw audio/screen transcripts are not indexed — use `sol call transcripts read` for transcript content. 18 + - **search_journal**: Unified full-text search across all journal content (agent outputs, events, entities, todos). Supports filtering by `day`, `facet`, and `agent` (e.g., "event", "flow", "news"). Best for discovering themes, concepts, patterns, and specific content across the journal. Note: raw audio/screen transcripts are not indexed — use `sol call transcripts read` for transcript content. 19 19 - **get_events**: Retrieves structured events for a specific day from facet event logs. Returns events with timestamps, titles, and descriptions. Best for finding scheduled activities, meetings, or notable occurrences on particular days. 20 20 21 21 ### Content Access 22 - - **sol call journal read DAY TOPIC**: Read full agent output markdown for a specific day and topic (e.g., `sol call journal read 20240115 flow`) 22 + - **sol call journal read DAY AGENT**: Read full agent output markdown for a specific day and agent (e.g., `sol call journal read 20240115 flow`) 23 23 - Use `--segment HHMMSS_LEN` for per-segment outputs (e.g., `sol call journal read 20240115 activity --segment 093000_300`) 24 - - **sol call journal topics DAY**: List all available agent outputs for a day 24 + - **sol call journal agents DAY**: List all available agent outputs for a day 25 25 - Use `--segment HHMMSS_LEN` to list outputs for a specific segment 26 26 - **sol call transcripts read DAY**: Read transcript content 27 27 - `--start HHMMSS --length MINUTES` for time ranges ··· 41 41 42 42 **Discovery Phase** (Use search tools to identify relevant content): 43 43 - Start broad with `search_journal` to identify relevant topics and time segments 44 - - Use `search_journal` with `topic="event"` to find structured activities related to the request 44 + - Use `search_journal` with `agent="event"` to find structured activities related to the request 45 45 - Use `sol call transcripts read` for raw transcript content when exact details are needed 46 46 - Use `get_events(day)` when you need all events for a specific day 47 47 48 48 **Deep Analysis Phase** (Use resources for complete information): 49 - - Access full agent outputs via `sol call journal read {day} {topic}` for identified topics 49 + - Access full agent outputs via `sol call journal read {day} {agent}` for identified agents 50 50 - Retrieve raw transcripts via `sol call transcripts read {day} --start {time} --length {length} --full` for detailed reconstruction 51 51 52 52 **Synthesis Phase** (Plan how to organize and present findings): ··· 74 74 1. **Initial Broad Search**: 75 75 - Tool: `search_journal` 76 76 - Query: [specific search terms] 77 - - Filters: [day, facet, topic as needed] 77 + - Filters: [day, facet, agent as needed] 78 78 - Purpose: [why this search first] 79 79 - Expected outcomes: [what information this should reveal] 80 80 81 81 2. **Targeted Searches**: 82 - - Tool: `search_journal` with topic filter or `get_events` 82 + - Tool: `search_journal` with agent filter or `get_events` 83 83 - Parameters: [specific filters or days] 84 84 - Purpose: [what specific information to find] 85 85 ··· 127 127 ## Special Considerations 128 128 129 129 - **Personal Sensitivity**: Plan with awareness that journal content may be personal or sensitive 130 - - **Temporal Context**: Consider how topics may have evolved over time in planning searches 130 + - **Temporal Context**: Consider how content may have evolved over time in planning searches 131 131 - **Resource Optimization**: Plan to use full resources (summaries/transcripts) judiciously to avoid information overload 132 132 - **Pattern Recognition**: Plan to identify themes and patterns that might not be explicitly requested but add value 133 133
+13 -13
think/tools/call.py
··· 44 44 None, "--day-to", help="Date range end YYYYMMDD." 45 45 ), 46 46 facet: str | None = typer.Option(None, "--facet", "-f", help="Filter by facet."), 47 - topic: str | None = typer.Option(None, "--topic", "-t", help="Filter by topic."), 47 + agent: str | None = typer.Option(None, "--agent", "-a", help="Filter by agent."), 48 48 ) -> None: 49 49 """Search the journal index.""" 50 50 kwargs = {} ··· 56 56 kwargs["day_to"] = day_to 57 57 if facet is not None: 58 58 kwargs["facet"] = facet 59 - if topic is not None: 60 - kwargs["topic"] = topic 59 + if agent is not None: 60 + kwargs["agent"] = agent 61 61 62 62 total, results = search_journal_impl(query, limit, offset, **kwargs) 63 63 ··· 70 70 parts = [f"{f}:{c}" for f, c in facet_counts.most_common(10)] 71 71 typer.echo(f"Facets: {', '.join(parts)}") 72 72 73 - topic_counts = counts.get("topics", {}) 74 - if topic_counts: 75 - parts = [f"{t}:{c}" for t, c in topic_counts.most_common(10)] 76 - typer.echo(f"Topics: {', '.join(parts)}") 73 + agent_counts = counts.get("agents", {}) 74 + if agent_counts: 75 + parts = [f"{a}:{c}" for a, c in agent_counts.most_common(10)] 76 + typer.echo(f"Agents: {', '.join(parts)}") 77 77 78 78 day_counts = counts.get("days", {}) 79 79 if day_counts: ··· 84 84 # Results 85 85 for r in results: 86 86 meta = r["metadata"] 87 - typer.echo(f"\n--- {meta['day']} | {meta['facet']} | {meta['topic']} ---") 87 + typer.echo(f"\n--- {meta['day']} | {meta['facet']} | {meta['agent']} ---") 88 88 typer.echo(r["text"].strip()) 89 89 90 90 ··· 200 200 201 201 202 202 @app.command() 203 - def topics( 203 + def agents( 204 204 day: str | None = typer.Argument( 205 205 default=None, help="Day YYYYMMDD (default: SOL_DAY env)." 206 206 ), ··· 279 279 280 280 @app.command() 281 281 def read( 282 - topic: str = typer.Argument(help="Topic name (e.g., flow, meetings, activity)."), 282 + agent: str = typer.Argument(help="Agent name (e.g., flow, meetings, activity)."), 283 283 day: str | None = typer.Option( 284 284 None, "--day", "-d", help="Day YYYYMMDD (default: SOL_DAY env)." 285 285 ), ··· 315 315 316 316 # Try common extensions 317 317 for ext in (".md", ".json", ".jsonl"): 318 - candidate = base_dir / f"{topic}{ext}" 318 + candidate = base_dir / f"{agent}{ext}" 319 319 if candidate.is_file(): 320 320 truncated_echo(candidate.read_text(encoding="utf-8"), max_bytes) 321 321 return ··· 324 324 available = _get_output_names(base_dir) 325 325 if available: 326 326 typer.echo( 327 - f"Topic '{topic}' not found. Available: {', '.join(available)}", err=True 327 + f"Agent '{agent}' not found. Available: {', '.join(available)}", err=True 328 328 ) 329 329 else: 330 - typer.echo(f"Topic '{topic}' not found and no outputs exist.", err=True) 330 + typer.echo(f"Agent '{agent}' not found and no outputs exist.", err=True) 331 331 raise typer.Exit(1)
+11 -11
think/tools/search.py
··· 83 83 day_from: str | None = None, 84 84 day_to: str | None = None, 85 85 facet: str | None = None, 86 - topic: str | None = None, 86 + agent: str | None = None, 87 87 stream: str | None = None, 88 88 ) -> dict[str, Any]: 89 89 """Search across all journal content using semantic full-text search. ··· 102 102 day_from: Filter by date range start (``YYYYMMDD``, inclusive) 103 103 day_to: Filter by date range end (``YYYYMMDD``, inclusive) 104 104 facet: Filter by facet name (e.g., "work", "personal") 105 - topic: Filter by topic (e.g., "flow", "event", "entity:detected", "news") 105 + agent: Filter by agent (e.g., "flow", "event", "entity:detected", "news") 106 106 stream: Filter by stream name (e.g., "archon", "import.apple") 107 107 108 108 Returns: ··· 111 111 - limit: Current limit value 112 112 - offset: Current offset value 113 113 - query: Echo of query text and applied filters 114 - - counts: Aggregation metadata with facets, topics, and bucketed days 115 - - results: List of matches with day, facet, topic, stream, text, path, and idx 114 + - counts: Aggregation metadata with facets, agents, and bucketed days 115 + - results: List of matches with day, facet, agent, stream, text, path, and idx 116 116 117 117 Examples: 118 118 - search_journal("machine learning") 119 119 - search_journal("meeting notes", day="20240101") 120 120 - search_journal("project planning", facet="work") 121 - - search_journal("standup", topic="event") 121 + - search_journal("standup", agent="event") 122 122 - search_journal("weekly sync", day_from="20241201", day_to="20241207") 123 - - search_journal(topic="flow", day="20240101") # Browse all flow for a day 123 + - search_journal(agent="flow", day="20240101") # Browse all flow for a day 124 124 - search_journal("meeting", stream="archon") # Filter by stream 125 125 """ 126 126 try: ··· 138 138 if facet is not None: 139 139 kwargs["facet"] = facet 140 140 filters["facet"] = facet 141 - if topic is not None: 142 - kwargs["topic"] = topic 143 - filters["topic"] = topic 141 + if agent is not None: 142 + kwargs["agent"] = agent 143 + filters["agent"] = agent 144 144 if stream is not None: 145 145 kwargs["stream"] = stream 146 146 filters["stream"] = stream ··· 163 163 item = { 164 164 "day": meta.get("day", ""), 165 165 "facet": meta.get("facet", ""), 166 - "topic": meta.get("topic", ""), 166 + "agent": meta.get("agent", ""), 167 167 "text": text, 168 168 "path": meta.get("path", ""), 169 169 "idx": meta.get("idx", 0), ··· 176 176 day_buckets = _bucket_day_counts(dict(counts_data["days"])) 177 177 counts = { 178 178 "facets": dict(counts_data["facets"]), 179 - "topics": dict(counts_data["topics"]), 179 + "agents": dict(counts_data["agents"]), 180 180 **day_buckets, 181 181 } 182 182