personal memory agent
0
fork

Configure Feed

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

Unify insights terminology under agents umbrella

Complete refactor renaming "insights" to "generators/agents" throughout
the codebase. This treats all muse/*.md files as agents - those with
schedule fields are "generator agents" that produce outputs automatically.

Key changes:
- sol insight → sol generate
- insights/ directory → agents/ directory
- $daily_insight/$segment_insight → $daily_preamble/$segment_preamble
- insight.* context → agent.* context
- insights_completed event → generators_completed event
- insights_processed stats → outputs_processed stats
- /api/insights → /api/generators
- config["insights"] → config["agents"]

Function renames:
- get_insights() → get_generator_agents()
- get_insights_by_schedule() → get_generator_agents_by_schedule()
- get_insight_topic() → get_output_topic()
- load_insight_hook() → load_output_hook()
- format_insight() → format_markdown()
- generate_agent_output() (was send_insight())

The user-facing "Insights" app name is preserved for UX continuity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+705 -709
+1 -1
AGENTS.md
··· 226 226 * **Test Fixtures**: `fixtures/journal/` - complete mock journal 227 227 * **Live Logs**: `$JOURNAL_PATH/health/<service>.log` 228 228 * **Agent Personas**: `muse/*.md` (apps can add their own in `muse/`, see [docs/APPS.md](docs/APPS.md)) 229 - * **Insight Templates**: `muse/*.md` (apps can add their own in `muse/`, see [docs/APPS.md](docs/APPS.md)) 229 + * **Generator Templates**: `muse/*.md` (apps can add their own in `muse/`, see [docs/APPS.md](docs/APPS.md)) 230 230 * **Scratch Space**: `scratch/` - git-ignored local workspace 231 231 232 232 ### Getting Help
+1 -1
apps/calendar/_day.html
··· 333 333 } 334 334 335 335 function parseCreatedDate(source) { 336 - // Extract YYYYMMDD from source path like "20240101/insights/schedule.md" 336 + // Extract YYYYMMDD from source path like "20240101/agents/schedule.md" 337 337 const match = source && source.match(/^(\d{8})\//); 338 338 if (match) { 339 339 const d = match[1];
+3 -3
apps/calendar/routes.py
··· 57 57 return "", 404 58 58 59 59 from think.indexer.journal import get_events 60 - from think.utils import get_insights 60 + from think.utils import get_generator_agents 61 61 62 - insights = get_insights() 62 + generators = get_generator_agents() 63 63 64 64 # Get full event objects from source files 65 65 raw_events = get_events(day) ··· 68 68 result = [] 69 69 for event in raw_events: 70 70 topic = event.get("topic", "other") 71 - topic_color = insights.get(topic, {}).get("color", "#6c757d") 71 + topic_color = generators.get(topic, {}).get("color", "#6c757d") 72 72 73 73 formatted = { 74 74 "title": event.get("title", ""),
+4 -4
apps/entities/events.py
··· 3 3 4 4 """Entity activity tracking via Callosum event handlers. 5 5 6 - Updates last_seen on attached entities when they appear in daily insights. 6 + Updates last_seen on attached entities when they appear in daily outputs. 7 7 Triggered after dream processing completes for each day. 8 8 """ 9 9 ··· 16 16 logger = logging.getLogger(__name__) 17 17 18 18 19 - @on_event("dream", "insights_completed") 19 + @on_event("dream", "generators_completed") 20 20 def update_entity_activity(ctx: EventContext) -> None: 21 21 """Update last_seen for entities mentioned in today's knowledge graph. 22 22 23 - Triggered after insight processing completes. Parses the knowledge graph 23 + Triggered after generator processing completes. Parses the knowledge graph 24 24 for entity names and updates last_seen on matching attached entities 25 25 across all facets. 26 26 """ ··· 30 30 31 31 day = ctx.msg.get("day") 32 32 if not day: 33 - logger.warning("insights_completed event missing day field") 33 + logger.warning("generators_completed event missing day field") 34 34 return 35 35 36 36 # Parse entity names from knowledge graph
+4 -4
apps/home/routes.py
··· 247 247 # Count if day directory exists (has journal data) 248 248 day_dir = journal_root / day_name 249 249 if day_dir.is_dir(): 250 - # Check for insights 251 - insights_dir = day_dir / "insights" 252 - if insights_dir.is_dir(): 253 - count += len(list(insights_dir.glob("*.md"))) 250 + # Check for agent outputs 251 + agents_dir = day_dir / "agents" 252 + if agents_dir.is_dir(): 253 + count += len(list(agents_dir.glob("*.md"))) 254 254 255 255 if count > 0: 256 256 stats[day_name] = count
+15 -15
apps/insights/routes.py
··· 15 15 16 16 from convey.utils import DATE_RE, format_date 17 17 from think.models import get_usage_cost 18 - from think.utils import day_dirs, day_path, get_insight_topic, get_insights 18 + from think.utils import day_dirs, day_path, get_generator_agents, get_output_topic 19 19 20 20 insights_bp = Blueprint( 21 21 "app:insights", ··· 25 25 26 26 27 27 def _build_topic_map() -> dict[str, dict]: 28 - """Build a mapping from filesystem topic name to insight key and metadata. 28 + """Build a mapping from filesystem topic name to generator key and metadata. 29 29 30 30 Returns dict mapping topic filename (e.g., "activity", "_chat_sentiment") 31 - to {"key": insight_key, "meta": insight_metadata}. 31 + to {"key": generator_key, "meta": generator_metadata}. 32 32 """ 33 - insights = get_insights() 33 + generators = get_generator_agents() 34 34 topic_map = {} 35 - for key, meta in insights.items(): 36 - topic = get_insight_topic(key) 35 + for key, meta in generators.items(): 36 + topic = get_output_topic(key) 37 37 topic_map[topic] = {"key": key, "meta": meta} 38 38 return topic_map 39 39 ··· 65 65 66 66 topic_map = _build_topic_map() 67 67 files = [] 68 - insights_dir = os.path.join(str(day_path(day)), "insights") 68 + agents_dir = os.path.join(str(day_path(day)), "agents") 69 69 70 - if os.path.isdir(insights_dir): 71 - for name in sorted(os.listdir(insights_dir)): 70 + if os.path.isdir(agents_dir): 71 + for name in sorted(os.listdir(agents_dir)): 72 72 base, ext = os.path.splitext(name) 73 73 if ext != ".md" or base not in topic_map: 74 74 continue 75 - path = os.path.join(insights_dir, name) 75 + path = os.path.join(agents_dir, name) 76 76 try: 77 77 with open(path, "r", encoding="utf-8") as f: 78 78 text = f.read() ··· 84 84 key = info["key"] 85 85 meta = info["meta"] 86 86 87 - # Get generation cost for this insight 88 - cost_data = get_usage_cost(day, context=f"insight.{key}") 87 + # Get generation cost for this generator 88 + cost_data = get_usage_cost(day, context=f"agent.{key}") 89 89 cost = cost_data["cost"] if cost_data["cost"] > 0 else None 90 90 91 91 files.append( ··· 130 130 if not day_name.startswith(month): 131 131 continue 132 132 133 - insights_dir = os.path.join(day_dir, "insights") 134 - if os.path.isdir(insights_dir): 133 + agents_dir = os.path.join(day_dir, "agents") 134 + if os.path.isdir(agents_dir): 135 135 # Count .md files 136 - md_files = [f for f in os.listdir(insights_dir) if f.endswith(".md")] 136 + md_files = [f for f in os.listdir(agents_dir) if f.endswith(".md")] 137 137 if md_files: 138 138 stats[day_name] = len(md_files) 139 139
+54 -52
apps/settings/routes.py
··· 780 780 781 781 782 782 # --------------------------------------------------------------------------- 783 - # Insights API 783 + # Generators API 784 784 # --------------------------------------------------------------------------- 785 785 786 786 787 - def _build_insight_info(key: str, meta: dict) -> dict: 788 - """Build insight info dict for API response.""" 787 + def _build_generator_info(key: str, meta: dict) -> dict: 788 + """Build generator info dict for API response.""" 789 789 # Determine if insight supports extraction via named hook 790 790 hook = meta.get("hook") 791 791 has_extraction = hook in ("occurrence", "anticipation") ··· 809 809 return info 810 810 811 811 812 - @settings_bp.route("/api/insights") 813 - def get_insights() -> Any: 814 - """Return insights grouped by schedule with config overrides. 812 + @settings_bp.route("/api/generators") 813 + def get_generators() -> Any: 814 + """Return generators grouped by schedule with config overrides. 815 815 816 816 Returns: 817 - - segment: List of segment-level insights 818 - - daily: List of daily insights 817 + - segment: List of segment-level generators 818 + - daily: List of daily generators 819 819 820 - Each insight contains: 821 - - key: Insight identifier 820 + Each generator contains: 821 + - key: Generator identifier 822 822 - title, description, color: Display metadata 823 823 - source: "system" or "app" 824 824 - app: App name (if source is "app") 825 825 - schedule: "segment" or "daily" 826 - - disabled: Whether insight is disabled 826 + - disabled: Whether generator is disabled 827 827 - extract: Whether event extraction is enabled 828 - - has_extraction: Whether insight supports event extraction 828 + - has_extraction: Whether generator supports event extraction 829 829 830 - Insights with missing or invalid schedule are excluded. 830 + Generators with missing or invalid schedule are excluded. 831 831 """ 832 832 try: 833 - from think.utils import get_insights_by_schedule 833 + from think.utils import get_generator_agents_by_schedule 834 834 835 - # Get insights by schedule (include disabled for settings toggle UI) 836 - segment_insights = [ 837 - _build_insight_info(key, meta) 835 + # Get generators by schedule (include disabled for settings toggle UI) 836 + segment_generators = [ 837 + _build_generator_info(key, meta) 838 838 for key, meta in sorted( 839 - get_insights_by_schedule("segment", include_disabled=True).items() 839 + get_generator_agents_by_schedule( 840 + "segment", include_disabled=True 841 + ).items() 840 842 ) 841 843 ] 842 - daily_insights = [ 843 - _build_insight_info(key, meta) 844 + daily_generators = [ 845 + _build_generator_info(key, meta) 844 846 for key, meta in sorted( 845 - get_insights_by_schedule("daily", include_disabled=True).items() 847 + get_generator_agents_by_schedule("daily", include_disabled=True).items() 846 848 ) 847 849 ] 848 850 ··· 850 852 def sort_key(x: dict) -> tuple: 851 853 return (0 if x["source"] == "system" else 1, x.get("app", ""), x["key"]) 852 854 853 - segment_insights.sort(key=sort_key) 854 - daily_insights.sort(key=sort_key) 855 + segment_generators.sort(key=sort_key) 856 + daily_generators.sort(key=sort_key) 855 857 856 - return jsonify({"segment": segment_insights, "daily": daily_insights}) 858 + return jsonify({"segment": segment_generators, "daily": daily_generators}) 857 859 858 860 except Exception as e: 859 861 return jsonify({"error": str(e)}), 500 860 862 861 863 862 - @settings_bp.route("/api/insights", methods=["PUT"]) 863 - def update_insights() -> Any: 864 - """Update insight configuration overrides. 864 + @settings_bp.route("/api/generators", methods=["PUT"]) 865 + def update_generators() -> Any: 866 + """Update generator configuration overrides. 865 867 866 - Accepts JSON mapping insight keys to override settings: 868 + Accepts JSON mapping generator keys to override settings: 867 869 { 868 - "<insight_key>": { 869 - "disabled": bool, # Disable insight entirely 870 + "<generator_key>": { 871 + "disabled": bool, # Disable generator entirely 870 872 "extract": bool # Disable event extraction 871 873 } | null # Remove overrides (reset to default) 872 874 } 873 875 874 876 Only boolean values are accepted for disabled and extract. 875 - Setting an insight to null removes all overrides for that insight. 877 + Setting a generator to null removes all overrides for that generator. 876 878 """ 877 879 try: 878 - from think.utils import get_insights as get_all_insights 880 + from think.utils import get_generator_agents 879 881 880 882 request_data = request.get_json() 881 883 if not request_data: ··· 884 886 if not isinstance(request_data, dict): 885 887 return jsonify({"error": "Request must be an object"}), 400 886 888 887 - # Get valid insight keys 888 - all_insights = get_all_insights() 889 - valid_keys = set(all_insights.keys()) 889 + # Get valid generator keys 890 + all_generators = get_generator_agents() 891 + valid_keys = set(all_generators.keys()) 890 892 891 893 config_dir = Path(state.journal_root) / "config" 892 894 config_dir.mkdir(parents=True, exist_ok=True) ··· 894 896 895 897 # Load existing config 896 898 config = get_journal_config() 897 - old_insights = copy.deepcopy(config.get("insights", {})) 899 + old_config = copy.deepcopy(config.get("agents", {})) 898 900 899 - # Ensure insights section exists 900 - if "insights" not in config: 901 - config["insights"] = {} 901 + # Ensure agents section exists 902 + if "agents" not in config: 903 + config["agents"] = {} 902 904 903 905 changed_fields = {} 904 906 905 907 for key, override in request_data.items(): 906 - # Validate insight key exists 908 + # Validate generator key exists 907 909 if key not in valid_keys: 908 910 return ( 909 - jsonify({"error": f"Unknown insight: {key}"}), 911 + jsonify({"error": f"Unknown generator: {key}"}), 910 912 400, 911 913 ) 912 914 913 915 # Handle null - remove overrides 914 916 if override is None: 915 - if key in config["insights"]: 916 - changed_fields[key] = {"old": config["insights"][key], "new": None} 917 - del config["insights"][key] 917 + if key in config["agents"]: 918 + changed_fields[key] = {"old": config["agents"][key], "new": None} 919 + del config["agents"][key] 918 920 continue 919 921 920 922 if not isinstance(override, dict): ··· 924 926 ) 925 927 926 928 # Validate and apply fields 927 - old_override = old_insights.get(key, {}) 928 - new_override = config["insights"].get(key, {}) 929 + old_override = old_config.get(key, {}) 930 + new_override = config["agents"].get(key, {}) 929 931 930 932 for field in ["disabled", "extract"]: 931 933 if field in override: ··· 943 945 new_override[field] = value 944 946 945 947 if new_override: 946 - config["insights"][key] = new_override 948 + config["agents"][key] = new_override 947 949 948 - # Clean up empty insights section 949 - if not config["insights"]: 950 - del config["insights"] 950 + # Clean up empty agents section 951 + if not config["agents"]: 952 + del config["agents"] 951 953 952 954 # Write updated config 953 955 with open(config_path, "w", encoding="utf-8") as f: ··· 959 961 log_app_action( 960 962 app="settings", 961 963 facet=None, 962 - action="insights_update", 964 + action="generators_update", 963 965 params={"changed_fields": changed_fields}, 964 966 ) 965 967 966 - return get_insights() 968 + return get_generators() 967 969 968 970 except Exception as e: 969 971 return jsonify({"error": str(e)}), 500
+2 -2
apps/settings/workspace.html
··· 3242 3242 3243 3243 async function loadInsights() { 3244 3244 try { 3245 - const response = await fetch('api/insights'); 3245 + const response = await fetch('api/generators'); 3246 3246 insightsData = await response.json(); 3247 3247 if (insightsData.error) throw new Error(insightsData.error); 3248 3248 populateInsights(insightsData); ··· 3372 3372 [insightKey]: { [field]: value } 3373 3373 }; 3374 3374 3375 - const response = await fetch('api/insights', { 3375 + const response = await fetch('api/generators', { 3376 3376 method: 'PUT', 3377 3377 headers: { 'Content-Type': 'application/json' }, 3378 3378 body: JSON.stringify(payload)
+3 -3
apps/stats/dashboard.js
··· 492 492 progressCard('Audio Processing', totals.audio_sessions || 0, totals.pending_segments || 0) 493 493 ); 494 494 progressSection.appendChild( 495 - progressCard('Insight Summaries', totals.insights_processed || 0, totals.insights_pending || 0) 495 + progressCard('Agent Outputs', totals.outputs_processed || 0, totals.outputs_pending || 0) 496 496 ); 497 497 498 498 // Token usage setup ··· 565 565 ); 566 566 567 567 // Render repairs if needed 568 - const repairs = ['pending_segments', 'insights_pending']; 568 + const repairs = ['pending_segments', 'outputs_pending']; 569 569 const hasRepairs = repairs.some(key => (totals[key] || 0) > 0); 570 570 571 571 if (hasRepairs) { ··· 578 578 const repairGrid = alert.querySelector('#repairGrid'); 579 579 const repairLabels = { 580 580 pending_segments: 'Pending Segments', 581 - insights_pending: 'Insight Summaries' 581 + outputs_pending: 'Agent Outputs' 582 582 }; 583 583 584 584 repairs.forEach(key => {
+2 -2
apps/stats/routes.py
··· 10 10 from flask import Blueprint, jsonify 11 11 12 12 from convey import state 13 - from think.utils import get_insights 13 + from think.utils import get_generator_agents 14 14 15 15 stats_bp = Blueprint( 16 16 "app:stats", ··· 37 37 except Exception: 38 38 pass 39 39 40 - response["insights"] = get_insights() 40 + response["generators"] = get_generator_agents() 41 41 42 42 return jsonify(response)
+22 -24
docs/APPS.md
··· 42 42 ├── app.json # Optional: Metadata (icon, label, facet support) 43 43 ├── app_bar.html # Optional: Bottom bar controls (forms, buttons) 44 44 ├── background.html # Optional: Background JavaScript service 45 - ├── insights/ # Optional: Custom insight prompts (auto-discovered) 46 - ├── agents/ # Optional: Custom agents (auto-discovered) 45 + ├── muse/ # Optional: Custom agents and generators (auto-discovered) 47 46 ├── maint/ # Optional: One-time maintenance tasks (auto-discovered) 48 47 └── tests/ # Optional: App-specific tests (run via make test-apps) 49 48 ``` ··· 59 58 | `app.json` | No | Icon, label, facet support overrides | 60 59 | `app_bar.html` | No | Bottom fixed bar for app controls | 61 60 | `background.html` | No | Background service (WebSocket listeners) | 62 - | `insights/` | No | Custom insight prompts as `.md` files with JSON frontmatter | 63 - | `agents/` | No | Custom agents as `.md` files with JSON frontmatter | 61 + | `muse/` | No | Custom agents and generators as `.md` files with JSON frontmatter | 64 62 | `maint/` | No | One-time maintenance tasks (run on Convey startup) | 65 63 | `tests/` | No | App-specific tests with self-contained fixtures | 66 64 ··· 263 261 264 262 --- 265 263 266 - ### 7. `insights/` - App Insights 264 + ### 7. `muse/` - App Generators 267 265 268 - Define custom insight prompts that integrate with solstone's insight generation system. 266 + Define custom generator prompts that integrate with solstone's output generation system. 269 267 270 268 **Key Points:** 271 269 - Create `muse/` directory with `.md` files containing JSON frontmatter 272 - - App insights are automatically discovered alongside system insights 270 + - App generators are automatically discovered alongside system generators 273 271 - Keys are namespaced as `{app}:{topic}` (e.g., `my_app:weekly_summary`) 274 - - Outputs go to `JOURNAL/YYYYMMDD/insights/_<app>_<topic>.md` (or `.json` if `output: "json"`) 272 + - Outputs go to `JOURNAL/YYYYMMDD/agents/_<app>_<topic>.md` (or `.json` if `output: "json"`) 275 273 276 - **Metadata format:** Same schema as system insights in `muse/*.md` - JSON frontmatter includes `title`, `description`, `color`, `schedule` (required), `hook`, `output`, `max_output_tokens`, and `thinking_budget` fields. The `schedule` field must be `"segment"` or `"daily"` - insights with missing or invalid schedule are skipped with a warning. 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). 274 + **Metadata format:** Same schema as system generators in `muse/*.md` - JSON frontmatter includes `title`, `description`, `color`, `schedule` (required), `hook`, `output`, `max_output_tokens`, and `thinking_budget` fields. The `schedule` field must be `"segment"` or `"daily"` - generators with missing or invalid schedule are skipped with a warning. 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). 277 275 278 - **Event extraction via hooks:** To extract structured events from insight output, use the `hook` field: 276 + **Event extraction via hooks:** To extract structured events from generator output, use the `hook` field: 279 277 280 278 - `"hook": "occurrence"` - Extracts past events to `facets/{facet}/events/{day}.jsonl` 281 279 - `"hook": "anticipation"` - Extracts future scheduled events ··· 291 289 } 292 290 ``` 293 291 294 - **App-data insights:** For insights from app-specific data (not transcripts), store in `JOURNAL/apps/{app}/insights/*.md` - these are automatically indexed. 292 + **App-data outputs:** For outputs from app-specific data (not transcripts), store in `JOURNAL/apps/{app}/agents/*.md` - these are automatically indexed. 295 293 296 - **Template variables:** Insight prompts can use template variables like `$name`, `$preferred`, `$daily_insight`, and context variables like `$day` and `$date`. See [PROMPT_TEMPLATES.md](PROMPT_TEMPLATES.md) for the complete template system documentation. 294 + **Template variables:** Generator prompts can use template variables like `$name`, `$preferred`, `$daily_preamble`, and context variables like `$day` and `$date`. See [PROMPT_TEMPLATES.md](PROMPT_TEMPLATES.md) for the complete template system documentation. 297 295 298 - **Custom hooks:** Insights also support custom `.py` hooks for transforming output programmatically: 296 + **Custom hooks:** Generators also support custom `.py` hooks for transforming output programmatically: 299 297 300 298 - Create `{topic}.py` alongside `{topic}.md` for co-located hooks 301 299 - Or use `"hook": "my_hook"` to reference `muse/my_hook.py` 302 300 - Hook must define a `process(result, context)` function 303 301 - `result` is the LLM output (markdown or JSON string) 304 - - `context` dict contains: `day`, `segment`, `multi_segment`, `insight_key`, `output_path`, `insight_meta`, `transcript` 302 + - `context` dict contains: `day`, `segment`, `multi_segment`, `name`, `output_path`, `meta`, `transcript` 305 303 - Return modified string, or `None` to use original result 306 304 - Hook errors are logged but don't crash the pipeline (falls back to original) 307 305 308 306 ```python 309 - # insights/my_insight.py 307 + # muse/my_generator.py 310 308 def process(result: str, context: dict) -> str | None: 311 309 # Transform, validate, or emit side effects 312 310 return result + "\n\n## Generated by hook" 313 311 ``` 314 312 315 313 **Reference implementations:** 316 - - System insight templates: `muse/*.md` (files with `schedule` field but no `tools` field) 314 + - System generator templates: `muse/*.md` (files with `schedule` field but no `tools` field) 317 315 - Extraction hooks: `muse/occurrence.py`, `muse/anticipation.py` 318 - - Discovery logic: `think/utils.py` - `get_insights()`, `get_insights_by_schedule()`, `get_insight_topic()` 319 - - Hook loading: `think/utils.py` - `load_insight_hook()` 316 + - Discovery logic: `think/utils.py` - `get_generator_agents()`, `get_generator_agents_by_schedule()`, `get_output_topic()` 317 + - Hook loading: `think/utils.py` - `load_output_hook()` 320 318 321 319 --- 322 320 323 - ### 8. `muse/` - App Agents and Insights 321 + ### 8. `muse/` - App Agents and Generators 324 322 325 - Define custom agents and insight templates that integrate with solstone's Cortex agent system. 323 + Define custom agents and generator templates that integrate with solstone's Cortex agent system. 326 324 327 325 **Key Points:** 328 326 - Create `muse/` directory with `.md` files containing JSON frontmatter 329 - - Both agents and insights live in the same directory - distinguished by frontmatter fields 330 - - Agents have a `tools` field, insights have `schedule` but no `tools` 331 - - App agents/insights are automatically discovered alongside system ones 327 + - Both agents and generators live in the same directory - distinguished by frontmatter fields 328 + - Agents have a `tools` field, generators have `schedule` but no `tools` 329 + - App agents/generators are automatically discovered alongside system ones 332 330 - Keys are namespaced as `{app}:{name}` (e.g., `my_app:helper`) 333 331 - Agents inherit all system agent capabilities (tools, scheduling, handoffs, multi-facet) 334 332 ··· 349 347 "instructions": { 350 348 "system": "journal", 351 349 "facets": "short", 352 - "sources": {"audio": true, "screen": true, "insights": false} 350 + "sources": {"audio": true, "screen": true, "agents": false} 353 351 } 354 352 } 355 353 ```
+4 -4
docs/CALLOSUM.md
··· 97 97 **Stages:** `initialization`, `segmenting`, `transcribing`, `summarizing` 98 98 **Purpose:** Track media file import from upload through transcription to segment creation 99 99 100 - ### `dream` - Insight and agent processing 100 + ### `dream` - Generator and agent processing 101 101 **Source:** `think/dream.py` 102 - **Events:** `started`, `command`, `insights_completed`, `agents_started`, `group_started`, `group_completed`, `agents_completed`, `completed` 102 + **Events:** `started`, `command`, `generators_completed`, `agents_started`, `group_started`, `group_completed`, `agents_completed`, `completed` 103 103 **Key fields:** `mode` ("daily"/"segment"), `day`, `segment` (when mode="segment") 104 - **Purpose:** Track dream processing from insights through scheduled agents 104 + **Purpose:** Track dream processing from generators through scheduled agents 105 105 106 106 ### `sync` - Remote segment synchronization 107 107 **Source:** `observe/sync.py` ··· 171 171 ↓ sense tracks completion 172 172 observe.observed (segment fully processed) 173 173 ↓ supervisor triggers dream 174 - dream.insights_completed 174 + dream.generators_completed 175 175 ↓ apps/entities/events.py updates entity activity 176 176 ``` 177 177
+1 -1
docs/CORTEX.md
··· 210 210 211 211 - Include an `output` field in the agent's frontmatter with the format ("md" or "json") 212 212 - Output path is derived from agent name + format + schedule: 213 - - Daily agents: `YYYYMMDD/insights/{name}.{ext}` 213 + - Daily agents: `YYYYMMDD/agents/{name}.{ext}` 214 214 - Segment agents: `YYYYMMDD/{segment}/{name}.{ext}` 215 215 - Writing occurs before any handoff processing 216 216 - Write failures are logged but don't interrupt the agent flow
+28 -28
docs/JOURNAL.md
··· 8 8 9 9 ``` 10 10 ┌─────────────────────────────────────┐ 11 - │ LAYER 3: INSIGHTS │ Narrative summaries 11 + │ LAYER 3: AGENT OUTPUTS │ Narrative summaries 12 12 │ (Markdown files) │ "What it means" 13 - │ - insights/*.md (daily insights) │ 14 - │ - *.md (segment insights) │ 13 + │ - agents/*.md (daily outputs) │ 14 + │ - *.md (segment outputs) │ 15 15 └─────────────────────────────────────┘ 16 16 ↑ synthesized from 17 17 ┌─────────────────────────────────────┐ ··· 38 38 |------|------------|----------| 39 39 | **Capture** | Raw audio/video recording | `*.flac`, `*.ogg`, `*.opus`, `*.webm` | 40 40 | **Extract** | Structured data from captures | `*.jsonl` | 41 - | **Insight** | AI-generated narrative summary | `insights/*.md`, `HHMMSS_LEN/*.md` | 41 + | **Agent Output** | AI-generated narrative summary | `agents/*.md`, `HHMMSS_LEN/*.md` | 42 42 43 43 **Organization** 44 44 ··· 59 59 60 60 | Directory/File | Purpose | 61 61 |----------------|---------| 62 - | `YYYYMMDD/` | Daily capture folders containing segments, extracts, and insights | 62 + | `YYYYMMDD/` | Daily capture folders containing segments, extracts, and agent outputs | 63 63 | `entities/` | Journal-level entity identity records (`<id>/entity.json`) | 64 64 | `facets/` | Facet-specific data: entity relationships, todos, events, news, action logs | 65 65 | `agents/` | Agent event logs (`<id>.jsonl`, `<id>_active.jsonl` for running agents) | ··· 275 275 }, 276 276 "contexts": { 277 277 "observe.*": {"provider": "google", "tier": 3}, 278 - "insight.*": {"tier": 1}, 278 + "agent.*": {"tier": 1}, 279 279 "agent.helper": {"provider": "openai", "model": "gpt-5-mini"} 280 280 }, 281 281 "models": { ··· 306 306 #### Context matching 307 307 308 308 Contexts are matched in order of specificity: 309 - 1. **Exact match** – `"insight.meetings"` matches only that exact context 309 + 1. **Exact match** – `"agent.meetings"` matches only that exact context 310 310 2. **Glob pattern** – `"observe.*"` matches any context starting with `observe.` 311 311 3. **Default** – Falls back to the `default` configuration 312 312 ··· 737 737 The `indexer/` directory contains the full-text search index built from journal content. 738 738 739 739 **Files:** 740 - - `indexer/journal.sqlite` – FTS5 SQLite database containing indexed chunks from all formattable content (insights, transcripts, events, entities, todos) 740 + - `indexer/journal.sqlite` – FTS5 SQLite database containing indexed chunks from all formattable content (agent outputs, transcripts, events, entities, todos) 741 741 742 742 The indexer converts all content to markdown chunks via the formatters framework, then indexes with metadata fields (day, facet, topic) for filtering. Use `get_journal_index()` from `think/indexer/journal.py` to access the database programmatically. 743 743 ··· 798 798 799 799 - `HHMMSS_LEN/` – Start time and duration in seconds (e.g., `143022_300/` for a 5-minute segment starting at 14:30:22) 800 800 801 - Each segment progresses through the three-layer pipeline: captures are recorded, extracts are generated, and insights are synthesized. 801 + Each segment progresses through the three-layer pipeline: captures are recorded, extracts are generated, and agent outputs are synthesized. 802 802 803 803 ### Layer 1: Captures 804 804 ··· 914 914 915 915 #### Event extracts 916 916 917 - Insight generation extracts time-based events from the day's transcripts—meetings, messages, follow-ups, file activity and more. Events are stored per-facet in JSONL files at `facets/{facet}/events/{day}.jsonl`. 917 + Generator output processing extracts time-based events from the day's transcripts—meetings, messages, follow-ups, file activity and more. Events are stored per-facet in JSONL files at `facets/{facet}/events/{day}.jsonl`. 918 918 919 919 There are two types of events: 920 920 - **Occurrences** – events that happened on the capture day (`occurred: true`) 921 921 - **Anticipations** – future scheduled events extracted from calendar views (`occurred: false`) 922 922 923 923 ```jsonl 924 - {"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/insights/meetings.md", "details": "Sprint planning discussion"} 925 - {"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/insights/schedule.md", "details": "Final review before release"} 924 + {"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"} 925 + {"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"} 926 926 ``` 927 927 928 928 **Common fields:** ··· 931 931 - **date** – ISO date YYYY-MM-DD (anticipations only, indicates scheduled date) 932 932 - **title** and **summary** – short text for display and search 933 933 - **facet** – facet name the event belongs to (required) 934 - - **topic** – source insight type (e.g., "meetings", "schedule", "flow") 934 + - **topic** – source generator type (e.g., "meetings", "schedule", "flow") 935 935 - **occurred** – `true` for occurrences, `false` for anticipations 936 - - **source** – path to the insight file that generated this event 936 + - **source** – path to the output file that generated this event 937 937 - **work** – boolean, work vs. personal classification 938 938 - **participants** – optional list of people or entities involved 939 939 - **details** – free-form string with additional context 940 940 941 941 This structure allows the indexer to collect and search events across all facets and days. 942 942 943 - ### Layer 3: Insights 943 + ### Layer 3: Agent Outputs 944 944 945 - Insights are AI-generated markdown files that provide human-readable narratives synthesized from captures and extracts. 945 + Agent outputs are AI-generated markdown files that provide human-readable narratives synthesized from captures and extracts. 946 946 947 - #### Segment insights 947 + #### Segment outputs 948 948 949 - After captures are processed, segment-level insights are generated within each segment folder as `HHMMSS_LEN/*.md` files. Available segment insight types are defined by templates in `muse/` with `"schedule": "segment"` in their metadata JSON. 949 + After captures are processed, segment-level outputs are generated within each segment folder as `HHMMSS_LEN/*.md` files. Available segment output types are defined by templates in `muse/` with `"schedule": "segment"` in their metadata JSON. 950 950 951 - #### Daily insights 951 + #### Daily outputs 952 952 953 - Post-processing generates day-level insights in the `insights/` directory that synthesize all segments. 953 + Post-processing generates day-level outputs in the `agents/` directory that synthesize all segments. 954 954 955 - **Insight discovery:** Available insight types are discovered at runtime from: 956 - - `muse/*.md` – system insight templates (files with `schedule` field but no `tools` field) 957 - - `apps/{app}/muse/*.md` – app-specific insight templates 955 + **Generator discovery:** Available generator types are discovered at runtime from: 956 + - `muse/*.md` – system generator templates (files with `schedule` field but no `tools` field) 957 + - `apps/{app}/muse/*.md` – app-specific generator templates 958 958 959 - 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"` - insights with missing or invalid schedule are skipped. Use `get_insights()` from `think/utils.py` to retrieve all available insights, or `get_insights_by_schedule()` to get insights filtered by schedule. 959 + 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_generator_agents()` from `think/utils.py` to retrieve all available generators, or `get_generator_agents_by_schedule()` to get generators filtered by schedule. 960 960 961 961 **Output naming:** 962 - - System insights: `insights/{topic}.md` (e.g., `insights/flow.md`, `insights/meetings.md`) 963 - - App insights: `insights/_{app}_{topic}.md` (e.g., `insights/_chat_sentiment.md`) 964 - - JSON output: `insights/{topic}.json` when metadata specifies `"output": "json"` 962 + - System outputs: `agents/{topic}.md` (e.g., `agents/flow.md`, `agents/meetings.md`) 963 + - App outputs: `agents/_{app}_{topic}.md` (e.g., `agents/_chat_sentiment.md`) 964 + - JSON output: `agents/{topic}.json` when metadata specifies `"output": "json"` 965 965 966 - Each insight type has a corresponding template file (`{name}.md`) that defines how the AI synthesizes extracts into narrative form. 966 + Each generator type has a corresponding template file (`{name}.md`) that defines how the AI synthesizes extracts into narrative form.
+13 -13
docs/PROMPT_TEMPLATES.md
··· 1 1 # Prompt Template System 2 2 3 - This document describes solstone's template variable system for personalizing prompts used in insights and agents. Templates enable dynamic substitution of user identity, contextual information, and reusable prompt fragments. 3 + This document describes solstone's template variable system for personalizing prompts used in generators and agents. Templates enable dynamic substitution of user identity, contextual information, and reusable prompt fragments. 4 4 5 5 ## Overview 6 6 ··· 27 27 "schedule": "segment" 28 28 } 29 29 30 - $segment_insight 30 + $segment_preamble 31 31 32 32 # Segment Activity Synthesis 33 33 ··· 69 69 Template variables come from `.md` files in the `think/templates/` directory. Each file's stem becomes a variable name containing its contents. 70 70 71 71 **Current templates:** 72 - - `$daily_insight` - Preamble for full-day insight analysis 73 - - `$segment_insight` - Preamble for single-segment analysis 72 + - `$daily_preamble` - Preamble for full-day output analysis 73 + - `$segment_preamble` - Preamble for single-segment analysis 74 74 75 - Templates can themselves use identity and context variables, enabling composable prompt construction. For example, `daily_insight.md` uses `$preferred` and `$date`. 75 + Templates can themselves use identity and context variables, enabling composable prompt construction. For example, `daily_preamble.md` uses `$preferred` and `$date`. 76 76 77 77 **Pattern:** To add a new template variable, create `think/templates/mytemplate.md` and it becomes available as `$mytemplate` in all prompts. 78 78 ··· 82 82 83 83 Context variables are passed at runtime by the code calling `load_prompt()`. These are use-case specific and not globally available. 84 84 85 - **Common insight context:** 85 + **Common generator context:** 86 86 - `$day` - Day in YYYYMMDD format 87 87 - `$date` - Human-readable date (e.g., "Friday, January 24, 2026") 88 88 - `$segment` - Segment key (e.g., "143022_300") ··· 92 92 Context variables also get automatic uppercase-first versions (`$Day`, `$Date`, etc.). 93 93 94 94 **References:** 95 - - Insight context building: `think/insight.py` (search for `prompt_context`) 95 + - Generator context building: `think/generate.py` (search for `prompt_context`) 96 96 - Other callers: `observe/extract.py`, `observe/enrich.py` 97 97 98 98 ## Usage Patterns 99 99 100 - ### For Insights 100 + ### For Generators 101 101 102 - Insight prompts typically compose a shared preamble with topic-specific instructions: 102 + Generator prompts typically compose a shared preamble with topic-specific instructions: 103 103 104 104 ```markdown 105 105 { 106 - "title": "My Insight", 106 + "title": "My Generator", 107 107 "color": "#4caf50", 108 108 "schedule": "segment" 109 109 } 110 110 111 - $segment_insight 111 + $segment_preamble 112 112 113 113 # Segment Activity Synthesis 114 114 115 115 Your specific instructions here... 116 116 ``` 117 117 118 - The `$segment_insight` or `$daily_insight` template provides standardized context about what's being analyzed, while the rest of the prompt defines the specific analysis task. 118 + The `$segment_preamble` or `$daily_preamble` template provides standardized context about what's being analyzed, while the rest of the prompt defines the specific analysis task. 119 119 120 120 **Optional model configuration:** Add `max_output_tokens` (response length limit) and `thinking_budget` (model thinking token budget) to override provider defaults. 121 121 ··· 189 189 | Core load function | `think/utils.py` (`load_prompt`) | 190 190 | Template files | `think/templates/*.md` | 191 191 | Test coverage | `tests/test_template_substitution.py` | 192 - | Insight prompts | `muse/*.md` (files with `schedule` field but no `tools`) | 192 + | Generator prompts | `muse/*.md` (files with `schedule` field but no `tools`) | 193 193 | Agent prompts | `muse/*.md` (files with `tools` field) |
+6 -6
docs/THINK.md
··· 14 14 15 15 The package exposes several commands: 16 16 17 - - `sol insight` builds a Markdown summary of a day's recordings using a Gemini prompt. 17 + - `sol generate` builds a Markdown summary of a day's recordings using a Gemini prompt. 18 18 - `sol cluster` groups audio and screen JSON files into report sections. Use `--start` and 19 19 `--length` to limit the report to a specific time range. 20 20 - `sol dream` runs the above tools for a single day. ··· 23 23 - `sol cortex` starts a WebSocket API server for managing AI agent instances. 24 24 25 25 ```bash 26 - sol insight YYYYMMDD -f PROMPT [--segment HHMMSS_LEN] [--segments SEG1,SEG2 -o OUT] [--force] [-v] 26 + sol generate YYYYMMDD -f PROMPT [--segment HHMMSS_LEN] [--segments SEG1,SEG2 -o OUT] [--force] [-v] 27 27 sol cluster YYYYMMDD [--start HHMMSS --length MINUTES] 28 - sol dream [--day YYYYMMDD] [--segment HHMMSS_LEN] [--force] [--skip-insights] [--skip-agents] 28 + sol dream [--day YYYYMMDD] [--segment HHMMSS_LEN] [--force] [--skip-generators] [--skip-agents] 29 29 sol supervisor [--no-observers] 30 30 sol mcp [--transport http] [--port PORT] [--path PATH] 31 31 sol cortex [--host HOST] [--port PORT] [--path PATH] ··· 137 137 For direct LLM calls, use `think.models.generate()` or `think.models.agenerate()` 138 138 which automatically routes to the configured provider based on context. 139 139 140 - ## Insight map keys 140 + ## Generator map keys 141 141 142 - `think.utils.get_insights()` reads the `.md` prompt files under `muse/` and 143 - returns a dictionary keyed by insight name. Each entry contains: 142 + `think.utils.get_generator_agents()` reads the `.md` prompt files under `muse/` and 143 + returns a dictionary keyed by generator name. Each entry contains: 144 144 145 145 - `path` – the prompt file path 146 146 - `color` – UI color hex string
+1 -1
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/insights/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", "topic": "activity", "occurred": true, "source": "20240101/agents/activity.md", "participants": [], "work": false, "details": "Strength training day"}
+2 -2
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/insights/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/insights/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", "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 -1
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/insights/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", "topic": "schedule", "occurred": false, "source": "20240101/agents/schedule.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"}
+4 -4
fixtures/journal_stats.json
··· 10 10 "repair_reduce": 1, 11 11 "entities": 1, 12 12 "repair_entity": 0, 13 - "insights_processed": 3, 14 - "insights_pending": 8, 13 + "outputs_processed": 3, 14 + "outputs_pending": 8, 15 15 "audio_seconds": 0.0, 16 16 "audio_bytes": 5, 17 17 "image_bytes": 0, ··· 28 28 "repair_reduce": 1, 29 29 "entities": 1, 30 30 "repair_entity": 0, 31 - "insights_processed": 3, 32 - "insights_pending": 8 31 + "outputs_processed": 3, 32 + "outputs_pending": 8 33 33 }, 34 34 "total_audio_seconds": 0.0, 35 35 "total_audio_bytes": 5,
+1 -1
muse/activity.md
··· 8 8 9 9 } 10 10 11 - $segment_insight 11 + $segment_preamble 12 12 13 13 # Segment Activity Synthesis 14 14
+17 -17
muse/anticipation.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """Hook for extracting anticipation events from insight results. 4 + """Hook for extracting anticipation events from generator output results. 5 5 6 - This hook is invoked via "hook": "anticipation" in insight frontmatter. 6 + This hook is invoked via "hook": "anticipation" in generator frontmatter. 7 7 It extracts structured JSON events for future scheduled items and writes 8 8 them to facet-based JSONL files. 9 9 """ ··· 13 13 from pathlib import Path 14 14 15 15 from think.facets import facet_summaries 16 - from think.insights import ( 17 - compute_source_insight, 16 + from think.models import generate 17 + from think.outputs import ( 18 + compute_output_source, 18 19 should_skip_extraction, 19 20 write_events_jsonl, 20 21 ) 21 - from think.models import generate 22 - from think.utils import get_insight_topic, load_prompt 22 + from think.utils import get_output_topic, load_prompt 23 23 24 24 25 25 def process(result: str, context: dict) -> str | None: 26 - """Extract anticipation events from insight result. 26 + """Extract anticipation events from generator output result. 27 27 28 28 This hook extracts structured JSON events for future scheduled items 29 - from markdown insight summaries and writes them to facet-based JSONL files. 29 + from markdown output summaries and writes them to facet-based JSONL files. 30 30 31 31 Args: 32 - result: The generated insight markdown content. 32 + result: The generated output markdown content. 33 33 context: Hook context with keys: 34 34 - day: YYYYMMDD string 35 35 - segment: segment key or None 36 - - insight_key: e.g., "schedule" 36 + - name: generator name, e.g., "schedule" 37 37 - output_path: absolute path to output file 38 - - insight_meta: dict with frontmatter 38 + - meta: dict with frontmatter 39 39 - transcript: the clustered transcript markdown 40 40 - multi_segment: True if processing multiple segments 41 41 42 42 Returns: 43 - None - this hook does not modify the insight result. 43 + None - this hook does not modify the output result. 44 44 """ 45 45 # Check skip conditions 46 46 skip_reason = should_skip_extraction(result, context) ··· 55 55 facets_context = facet_summaries(detailed_entities=True) 56 56 57 57 # Extract events 58 - insight_key = context.get("insight_key", "unknown") 58 + name = context.get("name", "unknown") 59 59 contents = [facets_context, result] 60 60 61 61 try: 62 62 response_text = generate( 63 63 contents=contents, 64 - context=f"insight.{insight_key}.extraction", 64 + context=f"agent.{name}.extraction", 65 65 temperature=0.3, 66 66 max_output_tokens=8192 * 6, 67 67 thinking_budget=8192 * 3, ··· 83 83 return None 84 84 85 85 # Write to facet JSONL files (occurred=False for anticipations) 86 - source_insight = compute_source_insight(context) 87 - topic = get_insight_topic(insight_key) 86 + source_output = compute_output_source(context) 87 + topic = get_output_topic(name) 88 88 day = context.get("day", "") 89 89 90 90 written_paths = write_events_jsonl( 91 91 events=events, 92 92 topic=topic, 93 93 occurred=False, 94 - source_insight=source_insight, 94 + source_output=source_output, 95 95 capture_day=day, 96 96 ) 97 97
+1 -1
muse/decisions.md
··· 10 10 11 11 } 12 12 13 - $daily_insight 13 + $daily_preamble 14 14 15 15 ## Goal 16 16
+1 -1
muse/documentation.md
··· 10 10 11 11 } 12 12 13 - $daily_insight 13 + $daily_preamble 14 14 15 15 # Workday Documentation Opportunity Analysis 16 16
+2 -2
muse/entities.md
··· 10 10 "instructions": { 11 11 "system": "journal", 12 12 "facets": "none", 13 - "sources": {"audio": true, "screen": true, "insights": false} 13 + "sources": {"audio": true, "screen": true, "agents": false} 14 14 } 15 15 16 16 } 17 17 18 - $segment_insight 18 + $segment_preamble 19 19 20 20 Extract named entities and descriptions from the given segment transcription document. 21 21
+4 -4
muse/entities.py
··· 68 68 """Parse entity list and write to segment JSONL file. 69 69 70 70 Args: 71 - result: The generated insight content (markdown entity list). 71 + result: The generated output content (markdown entity list). 72 72 context: Hook context with keys: 73 73 - day: YYYYMMDD string 74 74 - segment: segment key (HHMMSS_LEN) 75 - - insight_key: e.g., "entities" 75 + - name: e.g., "entities" 76 76 - output_path: absolute path to output file 77 - - insight_meta: dict with frontmatter 77 + - meta: dict with frontmatter 78 78 - transcript: the clustered transcript markdown 79 79 80 80 Returns: 81 - None - this hook does not modify the insight result. 81 + None - this hook does not modify the output result. 82 82 """ 83 83 segment = context.get("segment") 84 84 if not segment:
+1 -1
muse/files.md
··· 11 11 12 12 } 13 13 14 - $daily_insight 14 + $daily_preamble 15 15 16 16 # Workday File Activity Extraction 17 17
+1 -1
muse/flow.md
··· 10 10 11 11 } 12 12 13 - $daily_insight 13 + $daily_preamble 14 14 15 15 # Workday Productivity & Flow Analysis 16 16
+1 -1
muse/followups.md
··· 10 10 11 11 } 12 12 13 - $daily_insight 13 + $daily_preamble 14 14 15 15 # Workday Follow-up Identification 16 16
+1 -1
muse/knowledge_graph.md
··· 10 10 11 11 } 12 12 13 - $daily_insight 13 + $daily_preamble 14 14 15 15 # Comprehensive Workday Knowledge Graph and Network Analysis from Transcripts 16 16
+1 -1
muse/media.md
··· 11 11 12 12 } 13 13 14 - $daily_insight 14 + $daily_preamble 15 15 16 16 # Workday Media Consumption Analysis 17 17
+1 -1
muse/meetings.md
··· 10 10 11 11 } 12 12 13 - $daily_insight 13 + $daily_preamble 14 14 15 15 # Workday Meeting Extraction and Analysis 16 16
+1 -1
muse/messages.md
··· 10 10 11 11 } 12 12 13 - $daily_insight 13 + $daily_preamble 14 14 15 15 # Workday Messaging and Email Analysis 16 16
+18 -18
muse/occurrence.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """Hook for extracting occurrence events from insight results. 4 + """Hook for extracting occurrence events from generator output results. 5 5 6 - This hook is invoked via "hook": "occurrence" in insight frontmatter. 6 + This hook is invoked via "hook": "occurrence" in generator frontmatter. 7 7 It extracts structured JSON events from markdown summaries and writes 8 8 them to facet-based JSONL files. 9 9 """ ··· 13 13 from pathlib import Path 14 14 15 15 from think.facets import facet_summaries 16 - from think.insights import ( 17 - compute_source_insight, 16 + from think.models import generate 17 + from think.outputs import ( 18 + compute_output_source, 18 19 should_skip_extraction, 19 20 write_events_jsonl, 20 21 ) 21 - from think.models import generate 22 - from think.utils import get_insight_topic, load_prompt 22 + from think.utils import get_output_topic, load_prompt 23 23 24 24 25 25 def process(result: str, context: dict) -> str | None: 26 - """Extract occurrence events from insight result. 26 + """Extract occurrence events from generator output result. 27 27 28 - This hook extracts structured JSON events from markdown insight summaries 28 + This hook extracts structured JSON events from markdown output summaries 29 29 and writes them to facet-based JSONL files. 30 30 31 31 Args: 32 - result: The generated insight markdown content. 32 + result: The generated output markdown content. 33 33 context: Hook context with keys: 34 34 - day: YYYYMMDD string 35 35 - segment: segment key or None 36 - - insight_key: e.g., "meetings", "flow" 36 + - name: generator name, e.g., "meetings", "flow" 37 37 - output_path: absolute path to output file 38 - - insight_meta: dict with frontmatter including "occurrences" 38 + - meta: dict with frontmatter including "occurrences" 39 39 - transcript: the clustered transcript markdown 40 40 - multi_segment: True if processing multiple segments 41 41 42 42 Returns: 43 - None - this hook does not modify the insight result. 43 + None - this hook does not modify the output result. 44 44 """ 45 45 # Check skip conditions 46 46 skip_reason = should_skip_extraction(result, context) ··· 53 53 54 54 # Build context with facets + topic-specific instructions 55 55 facets_context = facet_summaries(detailed_entities=True) 56 - topic_instructions = context.get("insight_meta", {}).get("occurrences") 56 + topic_instructions = context.get("meta", {}).get("occurrences") 57 57 if topic_instructions and isinstance(topic_instructions, str): 58 58 extra_instructions = f"{facets_context}\n\n{topic_instructions}" 59 59 else: 60 60 extra_instructions = facets_context 61 61 62 62 # Extract events 63 - insight_key = context.get("insight_key", "unknown") 63 + name = context.get("name", "unknown") 64 64 contents = [extra_instructions, result] 65 65 66 66 try: 67 67 response_text = generate( 68 68 contents=contents, 69 - context=f"insight.{insight_key}.extraction", 69 + context=f"agent.{name}.extraction", 70 70 temperature=0.3, 71 71 max_output_tokens=8192 * 6, 72 72 thinking_budget=8192 * 3, ··· 88 88 return None 89 89 90 90 # Write to facet JSONL files 91 - source_insight = compute_source_insight(context) 92 - topic = get_insight_topic(insight_key) 91 + source_output = compute_output_source(context) 92 + topic = get_output_topic(name) 93 93 day = context.get("day", "") 94 94 95 95 written_paths = write_events_jsonl( 96 96 events=events, 97 97 topic=topic, 98 98 occurred=True, 99 - source_insight=source_insight, 99 + source_output=source_output, 100 100 capture_day=day, 101 101 ) 102 102
+1 -1
muse/opportunities.md
··· 10 10 11 11 } 12 12 13 - $daily_insight 13 + $daily_preamble 14 14 15 15 # Workday Innovation Opportunity Discovery 16 16
+1 -1
muse/research.md
··· 10 10 11 11 } 12 12 13 - $daily_insight 13 + $daily_preamble 14 14 15 15 # Workday Research Opportunity Analysis 16 16
+1 -1
muse/schedule.md
··· 9 9 10 10 } 11 11 12 - $daily_insight 12 + $daily_preamble 13 13 14 14 # Future Schedule Extraction 15 15
+1 -1
muse/screen.md
··· 8 8 9 9 } 10 10 11 - $segment_insight 11 + $segment_preamble 12 12 13 13 # Segment Screen Record 14 14
+1 -1
muse/speakers.md
··· 8 8 9 9 } 10 10 11 - $segment_insight 11 + $segment_preamble 12 12 13 13 # Speaker Name Extraction 14 14
+1 -1
muse/timeline.md
··· 10 10 11 11 } 12 12 13 - $daily_insight 13 + $daily_preamble 14 14 15 15 # Comprehensive Workday Timeline Documentation 16 16
+1 -1
muse/tools.md
··· 11 11 12 12 } 13 13 14 - $daily_insight 14 + $daily_preamble 15 15 16 16 # Workday Tool Usage Catalog & Analysis 17 17
+4 -4
sol.py
··· 10 10 11 11 Examples: 12 12 sol import data.json Import data into journal 13 - sol insight 20250101 Generate insights for a day 14 - sol think.insight -h Show help for specific module 13 + sol generate 20250101 Generate agent outputs for a day 14 + sol think.generate -h Show help for specific module 15 15 """ 16 16 17 17 from __future__ import annotations ··· 39 39 COMMANDS: dict[str, str] = { 40 40 # think package - daily processing and analysis 41 41 "import": "think.importer", 42 - "insight": "think.insight", 42 + "generate": "think.generate", 43 43 "cluster": "think.cluster", 44 44 "dream": "think.dream", 45 45 "planner": "think.planner", ··· 89 89 GROUPS: dict[str, list[str]] = { 90 90 "Think (daily processing)": [ 91 91 "import", 92 - "insight", 92 + "generate", 93 93 "cluster", 94 94 "dream", 95 95 "planner",
+14 -14
tests/test_cluster.py
··· 9 9 10 10 11 11 def test_cluster(tmp_path, monkeypatch): 12 - """Test cluster() uses audio and insight summaries (*.md files).""" 12 + """Test cluster() uses audio and agent output summaries (*.md files).""" 13 13 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 14 14 day_dir = day_path("20240101") 15 15 ··· 27 27 28 28 29 29 def test_cluster_range(tmp_path, monkeypatch): 30 - """Test cluster_range with audio and insights parameters.""" 30 + """Test cluster_range with audio and agents parameters.""" 31 31 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 32 32 day_dir = day_path("20240101") 33 33 ··· 39 39 '{"start": "00:00:01", "source": "mic", "text": "hi from audio"}\n' 40 40 ) 41 41 (day_dir / "120000_300" / "screen.md").write_text("screen summary content") 42 - # Test with insights=True to include *.md files 43 - md = mod.cluster_range("20240101", "120000", "120100", audio=True, insights=True) 42 + # Test with agents=True to include *.md files 43 + md = mod.cluster_range("20240101", "120000", "120100", audio=True, agents=True) 44 44 # Check that the function works and includes expected sections 45 45 assert "Audio Transcript" in md 46 46 # Now uses insight rendering: "### {stem} summary" ··· 153 153 assert "Screen Activity" in result 154 154 # Raw screen content should be present 155 155 assert "VS Code with Python file" in result 156 - # Insight content should NOT be present (insights=False for cluster_period) 156 + # Insight content should NOT be present (agents=False for cluster_period) 157 157 assert "This insight should NOT appear" not in result 158 158 159 159 160 - def test_cluster_range_with_insights(tmp_path, monkeypatch): 161 - """Test cluster_range with insights=True loads all *.md files.""" 160 + def test_cluster_range_with_agents(tmp_path, monkeypatch): 161 + """Test cluster_range with agents=True loads all *.md files.""" 162 162 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 163 163 day_dir = day_path("20240101") 164 164 ··· 172 172 ) 173 173 (segment / "screen.md").write_text("Screen activity summary") 174 174 (segment / "activity.md").write_text("Activity insight content") 175 - # Also create screen.jsonl to verify it's NOT used when insights=True, screen=False 175 + # Also create screen.jsonl to verify it's NOT used when agents=True, screen=False 176 176 (segment / "screen.jsonl").write_text( 177 177 '{"raw": "screen.webm"}\n' 178 178 '{"timestamp": 10, "analysis": {"primary": "code_editor"}}\n' 179 179 ) 180 180 181 - # Test insights=True returns *.md summaries, not raw screen data 181 + # Test agents=True returns *.md summaries, not raw screen data 182 182 result = mod.cluster_range( 183 - "20240101", "100000", "100500", audio=True, screen=False, insights=True 183 + "20240101", "100000", "100500", audio=True, screen=False, agents=True 184 184 ) 185 185 186 186 assert "Audio Transcript" in result 187 - # Should include both .md files as insights 187 + # Should include both .md files as agent outputs 188 188 assert "### screen summary" in result 189 189 assert "Screen activity summary" in result 190 190 assert "### activity summary" in result ··· 209 209 ) 210 210 (segment / "screen.md").write_text("Screen summary insight") 211 211 212 - # Test screen=True returns raw screen data, not insights 212 + # Test screen=True returns raw screen data, not agent outputs 213 213 result = mod.cluster_range( 214 - "20240101", "100000", "100500", audio=False, screen=True, insights=False 214 + "20240101", "100000", "100500", audio=False, screen=True, agents=False 215 215 ) 216 216 217 217 assert "Screen Activity" in result ··· 244 244 245 245 # Test screen=True returns data from both screen files 246 246 result = mod.cluster_range( 247 - "20240101", "100000", "100500", audio=False, screen=True, insights=False 247 + "20240101", "100000", "100500", audio=False, screen=True, agents=False 248 248 ) 249 249 250 250 # Should include content from both screen files
+13 -13
tests/test_cortex.py
··· 464 464 465 465 466 466 def test_write_output(cortex_service, mock_journal): 467 - """Test writing agent output to insights directory.""" 467 + """Test writing agent output to agents directory.""" 468 468 # Mock datetime to return a specific date 469 469 test_date = "20240115" 470 470 from datetime import datetime as dt ··· 480 480 481 481 cortex_service._write_output(agent_id, result, config) 482 482 483 - # Check file was created in insights/ with name-derived filename 484 - expected_path = mock_journal / test_date / "insights" / "my_agent.md" 483 + # Check file was created in agents/ with name-derived filename 484 + expected_path = mock_journal / test_date / "agents" / "my_agent.md" 485 485 assert expected_path.exists() 486 486 assert expected_path.read_text() == result 487 487 488 488 # Check directories were created 489 - assert (mock_journal / test_date / "insights").is_dir() 489 + assert (mock_journal / test_date / "agents").is_dir() 490 490 491 491 492 492 def test_write_output_with_error(cortex_service, mock_journal, caplog): ··· 513 513 514 514 cortex_service._write_output(agent_id, result, config) 515 515 516 - # Check file was created in specified day's insights directory 517 - expected_path = mock_journal / specified_day / "insights" / "reporter.md" 516 + # Check file was created in specified day's agents directory 517 + expected_path = mock_journal / specified_day / "agents" / "reporter.md" 518 518 assert expected_path.exists() 519 519 assert expected_path.read_text() == result 520 520 521 521 # Check directories were created 522 - assert (mock_journal / specified_day / "insights").is_dir() 522 + assert (mock_journal / specified_day / "agents").is_dir() 523 523 524 524 525 525 def test_write_output_with_segment(cortex_service, mock_journal): ··· 538 538 539 539 cortex_service._write_output(agent_id, result, config) 540 540 541 - # Check file was created in segment directory (not insights/) 541 + # Check file was created in segment directory (not agents/) 542 542 expected_path = mock_journal / test_date / "143000_600" / "analyzer.md" 543 543 assert expected_path.exists() 544 544 assert expected_path.read_text() == result ··· 560 560 cortex_service._write_output(agent_id, result, config) 561 561 562 562 # Check file was created with .json extension 563 - expected_path = mock_journal / test_date / "insights" / "data_agent.json" 563 + expected_path = mock_journal / test_date / "agents" / "data_agent.json" 564 564 assert expected_path.exists() 565 565 assert expected_path.read_text() == result 566 566 ··· 606 606 with patch.object(cortex_service, "_has_finish_event", return_value=True): 607 607 cortex_service._monitor_stdout(agent) 608 608 609 - # Check result was written to insights/ with name-derived filename 610 - output_path = mock_journal / test_date / "insights" / "test_agent.md" 609 + # Check result was written to agents/ with name-derived filename 610 + output_path = mock_journal / test_date / "agents" / "test_agent.md" 611 611 assert output_path.exists() 612 612 assert output_path.read_text() == "Test result" 613 613 ··· 647 647 with patch.object(cortex_service, "_has_finish_event", return_value=True): 648 648 cortex_service._monitor_stdout(agent) 649 649 650 - # Check result was written to specified day's insights directory 651 - output_path = mock_journal / specified_day / "insights" / "daily_reporter.md" 650 + # Check result was written to specified day's agents directory 651 + output_path = mock_journal / specified_day / "agents" / "daily_reporter.md" 652 652 assert output_path.exists() 653 653 assert output_path.read_text() == "Daily report content" 654 654
+1 -1
tests/test_dream_full.py
··· 40 40 ) 41 41 mod.main() 42 42 assert any(c[0] == "sol" and c[1] == "sense" for c in called) 43 - assert any(c[0] == "sol" and c[1] == "insight" for c in called) 43 + assert any(c[0] == "sol" and c[1] == "generate" for c in called) 44 44 # Verify indexer is called with --rescan (light mode) via queued command 45 45 indexer_cmds = [c for c in called if c[0] == "sol" and c[1] == "indexer"] 46 46 assert len(indexer_cmds) == 1
+2 -2
tests/test_entities.py
··· 1531 1531 os.environ["JOURNAL_PATH"] = str(tmp_path) 1532 1532 1533 1533 # Create a knowledge graph file 1534 - day_dir = tmp_path / "20260108" / "insights" 1534 + day_dir = tmp_path / "20260108" / "agents" 1535 1535 day_dir.mkdir(parents=True) 1536 1536 1537 1537 kg_content = """# Knowledge Graph Report ··· 1579 1579 """Test parsing returns empty list for empty KG.""" 1580 1580 os.environ["JOURNAL_PATH"] = str(tmp_path) 1581 1581 1582 - day_dir = tmp_path / "20260108" / "insights" 1582 + day_dir = tmp_path / "20260108" / "agents" 1583 1583 day_dir.mkdir(parents=True) 1584 1584 (day_dir / "knowledge_graph.md").write_text("") 1585 1585
+37 -37
tests/test_formatters.py
··· 1118 1118 "title": "Project kickoff", 1119 1119 "start": "14:00:00", 1120 1120 "occurred": False, 1121 - "source": "20240101/insights/schedule.md", 1121 + "source": "20240101/agents/schedule.md", 1122 1122 "participants": ["Alice", "Bob"], 1123 1123 } 1124 1124 ] ··· 1141 1141 "title": "Team standup", 1142 1142 "start": "09:00:00", 1143 1143 "occurred": True, 1144 - "source": "20240101/insights/meetings.md", 1144 + "source": "20240101/agents/meetings.md", 1145 1145 "participants": ["Alice"], 1146 1146 } 1147 1147 ] ··· 1263 1263 assert "Discussed Q1 roadmap" in chunks[0]["markdown"] 1264 1264 1265 1265 1266 - class TestFormatInsight: 1267 - """Tests for the markdown insight formatter.""" 1266 + class TestFormatMarkdown: 1267 + """Tests for the markdown output formatter.""" 1268 1268 1269 1269 def test_get_formatter_markdown(self): 1270 1270 """Test pattern matching for .md files.""" 1271 1271 from think.formatters import get_formatter 1272 1272 1273 - formatter = get_formatter("20240101/insights/flow.md") 1273 + formatter = get_formatter("20240101/agents/flow.md") 1274 1274 assert formatter is not None 1275 - assert formatter.__name__ == "format_insight" 1275 + assert formatter.__name__ == "format_markdown" 1276 1276 1277 1277 def test_get_formatter_segment_screen_md(self): 1278 1278 """Test pattern matching for segment screen.md files.""" ··· 1280 1280 1281 1281 formatter = get_formatter("20240101/123456_300/screen.md") 1282 1282 assert formatter is not None 1283 - assert formatter.__name__ == "format_insight" 1283 + assert formatter.__name__ == "format_markdown" 1284 1284 1285 1285 def test_get_formatter_nested_md(self): 1286 1286 """Test pattern matching for deeply nested .md files.""" ··· 1288 1288 1289 1289 formatter = get_formatter("facets/work/news/20240101.md") 1290 1290 assert formatter is not None 1291 - assert formatter.__name__ == "format_insight" 1291 + assert formatter.__name__ == "format_markdown" 1292 1292 1293 - def test_format_insight_basic(self): 1293 + def test_format_markdown_basic(self): 1294 1294 """Test basic markdown formatting.""" 1295 - from think.insights import format_insight 1295 + from think.outputs import format_markdown 1296 1296 1297 1297 text = "# Hello\n\nThis is a paragraph.\n" 1298 - chunks, meta = format_insight(text) 1298 + chunks, meta = format_markdown(text) 1299 1299 1300 1300 assert len(chunks) == 1 1301 1301 assert "# Hello" in chunks[0]["markdown"] 1302 1302 assert "This is a paragraph" in chunks[0]["markdown"] 1303 1303 assert meta == {} 1304 1304 1305 - def test_format_insight_multiple_chunks(self): 1305 + def test_format_markdown_multiple_chunks(self): 1306 1306 """Test that lists are split into multiple chunks.""" 1307 - from think.insights import format_insight 1307 + from think.outputs import format_markdown 1308 1308 1309 1309 text = "# List\n\n- Item one\n- Item two\n- Item three\n" 1310 - chunks, meta = format_insight(text) 1310 + chunks, meta = format_markdown(text) 1311 1311 1312 1312 assert len(chunks) == 3 1313 1313 for chunk in chunks: 1314 1314 assert "# List" in chunk["markdown"] 1315 1315 1316 - def test_format_insight_no_timestamp(self): 1316 + def test_format_markdown_no_timestamp(self): 1317 1317 """Test that markdown chunks don't have timestamp key.""" 1318 - from think.insights import format_insight 1318 + from think.outputs import format_markdown 1319 1319 1320 1320 text = "# Test\n\nSome content.\n" 1321 - chunks, meta = format_insight(text) 1321 + chunks, meta = format_markdown(text) 1322 1322 1323 1323 assert len(chunks) == 1 1324 1324 assert "markdown" in chunks[0] 1325 1325 assert "timestamp" not in chunks[0] 1326 1326 1327 - def test_format_insight_preserves_headers(self): 1327 + def test_format_markdown_preserves_headers(self): 1328 1328 """Test that each chunk includes its header context.""" 1329 - from think.insights import format_insight 1329 + from think.outputs import format_markdown 1330 1330 1331 1331 text = "# Top\n\n## Section\n\nParagraph content.\n" 1332 - chunks, meta = format_insight(text) 1332 + chunks, meta = format_markdown(text) 1333 1333 1334 1334 assert len(chunks) == 1 1335 1335 assert "# Top" in chunks[0]["markdown"] 1336 1336 assert "## Section" in chunks[0]["markdown"] 1337 1337 assert "Paragraph content" in chunks[0]["markdown"] 1338 1338 1339 - def test_format_insight_definition_list(self): 1339 + def test_format_markdown_definition_list(self): 1340 1340 """Test that definition lists stay as single chunk.""" 1341 - from think.insights import format_insight 1341 + from think.outputs import format_markdown 1342 1342 1343 1343 text = "# Info\n\n- **Name:** Alice\n- **Role:** Engineer\n" 1344 - chunks, meta = format_insight(text) 1344 + chunks, meta = format_markdown(text) 1345 1345 1346 1346 # Definition list stays together 1347 1347 assert len(chunks) == 1 1348 1348 assert "**Name:** Alice" in chunks[0]["markdown"] 1349 1349 assert "**Role:** Engineer" in chunks[0]["markdown"] 1350 1350 1351 - def test_format_insight_table_rows(self): 1351 + def test_format_markdown_table_rows(self): 1352 1352 """Test that table rows become separate chunks.""" 1353 - from think.insights import format_insight 1353 + from think.outputs import format_markdown 1354 1354 1355 1355 text = """# Data 1356 1356 ··· 1359 1359 | A | 1 | 1360 1360 | B | 2 | 1361 1361 """ 1362 - chunks, meta = format_insight(text) 1362 + chunks, meta = format_markdown(text) 1363 1363 1364 1364 assert len(chunks) == 2 1365 1365 # Each chunk should have the header ··· 1367 1367 assert "# Data" in chunk["markdown"] 1368 1368 assert "| Name | Value |" in chunk["markdown"] 1369 1369 1370 - def test_format_insight_code_block(self): 1370 + def test_format_markdown_code_block(self): 1371 1371 """Test that code blocks become chunks.""" 1372 - from think.insights import format_insight 1372 + from think.outputs import format_markdown 1373 1373 1374 1374 text = "# Code\n\n```python\nprint('hello')\n```\n" 1375 - chunks, meta = format_insight(text) 1375 + chunks, meta = format_markdown(text) 1376 1376 1377 1377 assert len(chunks) == 1 1378 1378 assert "```python" in chunks[0]["markdown"] ··· 1382 1382 """Test format_file with a markdown file.""" 1383 1383 from think.formatters import format_file 1384 1384 1385 - path = Path(os.environ["JOURNAL_PATH"]) / "20240101/insights/flow.md" 1385 + path = Path(os.environ["JOURNAL_PATH"]) / "20240101/agents/flow.md" 1386 1386 chunks, meta = format_file(path) 1387 1387 1388 1388 assert len(chunks) > 0 ··· 1393 1393 """Test load_markdown utility.""" 1394 1394 from think.formatters import load_markdown 1395 1395 1396 - path = Path(os.environ["JOURNAL_PATH"]) / "20240101/insights/flow.md" 1396 + path = Path(os.environ["JOURNAL_PATH"]) / "20240101/agents/flow.md" 1397 1397 text = load_markdown(path) 1398 1398 1399 1399 assert isinstance(text, str) ··· 1403 1403 class TestExtractPathMetadata: 1404 1404 """Tests for extract_path_metadata helper.""" 1405 1405 1406 - def test_daily_insight(self): 1407 - """Test day extraction from daily insight path.""" 1406 + def test_daily_output(self): 1407 + """Test day extraction from daily agent output path.""" 1408 1408 from think.formatters import extract_path_metadata 1409 1409 1410 - meta = extract_path_metadata("20240101/insights/flow.md") 1410 + meta = extract_path_metadata("20240101/agents/flow.md") 1411 1411 assert meta["day"] == "20240101" 1412 1412 assert meta["facet"] == "" 1413 1413 assert meta["topic"] == "flow" ··· 1475 1475 assert meta["facet"] == "" 1476 1476 assert meta["topic"] == "import" 1477 1477 1478 - def test_app_insight(self): 1479 - """Test app insight path extraction.""" 1478 + def test_app_output(self): 1479 + """Test app output path extraction.""" 1480 1480 from think.formatters import extract_path_metadata 1481 1481 1482 - meta = extract_path_metadata("apps/myapp/insights/custom.md") 1482 + meta = extract_path_metadata("apps/myapp/agents/custom.md") 1483 1483 assert meta["day"] == "" 1484 1484 assert meta["facet"] == "" 1485 1485 assert meta["topic"] == "myapp:custom"
+32 -32
tests/test_generate_full.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """Tests for the insight generation pipeline. 4 + """Tests for the generator output pipeline. 5 5 6 6 Tests cover: 7 - - Basic insight generation and output 7 + - Basic output generation and saving 8 8 - Hook invocation with correct context 9 9 - Named hook resolution 10 10 """ ··· 37 37 MOCK_RESULT = "## Meeting Summary\n\nTeam standup at 9am with Alice and Bob discussing project status." 38 38 39 39 40 - def test_insight_generates_output(tmp_path, monkeypatch): 41 - """Test basic insight generation saves markdown output.""" 42 - mod = importlib.import_module("think.insight") 40 + def test_generate_output(tmp_path, monkeypatch): 41 + """Test basic output generation saves markdown output.""" 42 + mod = importlib.import_module("think.generate") 43 43 day_dir = copy_day(tmp_path) 44 44 prompt = tmp_path / "prompt.md" 45 45 prompt.write_text('{\n "schedule": "daily"\n}\n\nprompt') 46 46 47 47 monkeypatch.setattr( 48 48 mod, 49 - "send_insight", 49 + "generate_agent_output", 50 50 lambda *a, **k: MOCK_RESULT, 51 51 ) 52 52 monkeypatch.setenv("GOOGLE_API_KEY", "x") 53 53 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 54 - monkeypatch.setattr("sys.argv", ["sol insight", "20240101", "-f", str(prompt)]) 54 + monkeypatch.setattr("sys.argv", ["sol generate", "20240101", "-f", str(prompt)]) 55 55 mod.main() 56 56 57 - md = day_dir / "insights" / "prompt.md" 57 + md = day_dir / "agents" / "prompt.md" 58 58 assert md.read_text() == MOCK_RESULT 59 59 60 60 61 - def test_insight_hook_invoked_with_context(tmp_path, monkeypatch): 61 + def test_generate_hook_invoked_with_context(tmp_path, monkeypatch): 62 62 """Test that hooks receive correct context including multi_segment flag.""" 63 - mod = importlib.import_module("think.insight") 63 + mod = importlib.import_module("think.generate") 64 64 copy_day(tmp_path) 65 65 66 - # Create insight with hook 67 - insights_dir = tmp_path / "insights" 68 - insights_dir.mkdir() 66 + # Create generator with hook 67 + generators_dir = tmp_path / "generators" 68 + generators_dir.mkdir() 69 69 70 - prompt_file = insights_dir / "hooked.md" 70 + prompt_file = generators_dir / "hooked.md" 71 71 prompt_file.write_text( 72 72 '{\n "title": "Hooked",\n "schedule": "daily",\n "hook": "test_hook"\n}\n\nTest prompt' 73 73 ) ··· 85 85 "day": context.get("day"), 86 86 "segment": context.get("segment"), 87 87 "multi_segment": context.get("multi_segment"), 88 - "insight_key": context.get("insight_key"), 88 + "name": context.get("name"), 89 89 "has_transcript": bool(context.get("transcript")), 90 - "has_insight_meta": bool(context.get("insight_meta")), 90 + "has_meta": bool(context.get("meta")), 91 91 } 92 92 with open(out_path, "w") as f: 93 93 json.dump(ctx_copy, f) ··· 97 97 try: 98 98 monkeypatch.setattr( 99 99 mod, 100 - "send_insight", 100 + "generate_agent_output", 101 101 lambda *a, **k: MOCK_RESULT, 102 102 ) 103 103 monkeypatch.setenv("GOOGLE_API_KEY", "x") 104 104 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 105 105 monkeypatch.setattr( 106 - "sys.argv", ["sol insight", "20240101", "-f", str(prompt_file)] 106 + "sys.argv", ["sol generate", "20240101", "-f", str(prompt_file)] 107 107 ) 108 108 mod.main() 109 109 110 110 # Read captured context 111 - captured_path = tmp_path / "20240101" / "insights" / "context_captured.json" 111 + captured_path = tmp_path / "20240101" / "agents" / "context_captured.json" 112 112 captured = json.loads(captured_path.read_text()) 113 113 114 114 assert captured["day"] == "20240101" 115 115 assert captured["segment"] is None 116 116 assert captured["multi_segment"] is False 117 - assert captured["insight_key"] == "hooked" 117 + assert captured["name"] == "hooked" 118 118 assert captured["has_transcript"] is True 119 - assert captured["has_insight_meta"] is True 119 + assert captured["has_meta"] is True 120 120 121 121 finally: 122 122 # Clean up test hook ··· 124 124 hook_file.unlink() 125 125 126 126 127 - def test_insight_without_hook_succeeds(tmp_path, monkeypatch): 128 - """Test that insights without hooks still work correctly.""" 129 - mod = importlib.import_module("think.insight") 127 + def test_generate_without_hook_succeeds(tmp_path, monkeypatch): 128 + """Test that generators without hooks still work correctly.""" 129 + mod = importlib.import_module("think.generate") 130 130 day_dir = copy_day(tmp_path) 131 131 132 - # Create insight without hook 132 + # Create generator without hook 133 133 prompt = tmp_path / "nohook.md" 134 134 prompt.write_text('{\n "schedule": "daily"\n}\n\nNo hook prompt') 135 135 136 136 monkeypatch.setattr( 137 137 mod, 138 - "send_insight", 138 + "generate_agent_output", 139 139 lambda *a, **k: MOCK_RESULT, 140 140 ) 141 141 monkeypatch.setenv("GOOGLE_API_KEY", "x") 142 142 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 143 - monkeypatch.setattr("sys.argv", ["sol insight", "20240101", "-f", str(prompt)]) 143 + monkeypatch.setattr("sys.argv", ["sol generate", "20240101", "-f", str(prompt)]) 144 144 mod.main() 145 145 146 - md = day_dir / "insights" / "nohook.md" 146 + md = day_dir / "agents" / "nohook.md" 147 147 assert md.read_text() == MOCK_RESULT 148 148 149 149 ··· 151 151 """Test that named hooks are resolved from muse/{hook}.py.""" 152 152 utils = importlib.import_module("think.utils") 153 153 154 - # Create insight with named hook 155 - insight_file = tmp_path / "test_insight.md" 156 - insight_file.write_text( 154 + # Create generator with named hook 155 + generator_file = tmp_path / "test_generator.md" 156 + generator_file.write_text( 157 157 '{\n "title": "Test",\n "hook": "occurrence"\n}\n\nTest prompt' 158 158 ) 159 159 160 - meta = utils._load_insight_metadata(insight_file) 160 + meta = utils._load_prompt_metadata(generator_file) 161 161 162 162 # Should resolve to muse/occurrence.py 163 163 assert "hook_path" in meta
+9 -9
tests/test_generate_scan_day.py
··· 21 21 shutil.copytree(item, dest / item.name, dirs_exist_ok=True) 22 22 else: 23 23 shutil.copy2(item, dest / item.name) 24 - insights = dest / "insights" 25 - insights.mkdir(exist_ok=True) # Allow existing directory 26 - (insights / "flow.md").write_text("done") 24 + agents_dir = dest / "agents" 25 + agents_dir.mkdir(exist_ok=True) # Allow existing directory 26 + (agents_dir / "flow.md").write_text("done") 27 27 return dest 28 28 29 29 30 30 def test_scan_day(tmp_path, monkeypatch): 31 - mod = importlib.import_module("think.insight") 31 + mod = importlib.import_module("think.generate") 32 32 day_dir = copy_day(tmp_path) 33 33 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 34 34 35 35 info = mod.scan_day("20240101") 36 - assert "insights/flow.md" in info["processed"] 37 - assert "insights/media.md" in info["repairable"] 36 + assert "agents/flow.md" in info["processed"] 37 + assert "agents/media.md" in info["repairable"] 38 38 39 - (day_dir / "insights" / "media.md").write_text("done") 39 + (day_dir / "agents" / "media.md").write_text("done") 40 40 info_after = mod.scan_day("20240101") 41 - assert "insights/media.md" in info_after["processed"] 42 - assert "insights/media.md" not in info_after["repairable"] 41 + assert "agents/media.md" in info_after["processed"] 42 + assert "agents/media.md" not in info_after["repairable"]
+50 -48
tests/test_generators.py
··· 7 7 from pathlib import Path 8 8 9 9 10 - def test_get_insights(): 11 - """Test that system insights are discovered with source field.""" 10 + def test_get_generator_agents(): 11 + """Test that system generators are discovered with source field.""" 12 12 utils = importlib.import_module("think.utils") 13 - insights = utils.get_insights() 14 - assert "flow" in insights 15 - info = insights["flow"] 13 + generators = utils.get_generator_agents() 14 + assert "flow" in generators 15 + info = generators["flow"] 16 16 assert os.path.basename(info["path"]) == "flow.md" 17 17 assert isinstance(info["color"], str) 18 18 assert isinstance(info["mtime"], int) ··· 22 22 assert info.get("source") == "system" 23 23 24 24 25 - def test_get_insight_topic(): 26 - """Test insight key to filename conversion.""" 25 + def test_get_output_topic(): 26 + """Test generator key to filename conversion.""" 27 27 utils = importlib.import_module("think.utils") 28 28 29 - # System insights: key unchanged 30 - assert utils.get_insight_topic("activity") == "activity" 31 - assert utils.get_insight_topic("flow") == "flow" 29 + # System generators: key unchanged 30 + assert utils.get_output_topic("activity") == "activity" 31 + assert utils.get_output_topic("flow") == "flow" 32 32 33 - # App insights: _app_topic format 34 - assert utils.get_insight_topic("chat:sentiment") == "_chat_sentiment" 35 - assert utils.get_insight_topic("my_app:weekly_summary") == "_my_app_weekly_summary" 33 + # App generators: _app_topic format 34 + assert utils.get_output_topic("chat:sentiment") == "_chat_sentiment" 35 + assert utils.get_output_topic("my_app:weekly_summary") == "_my_app_weekly_summary" 36 36 37 37 38 - def test_get_insights_app_discovery(tmp_path, monkeypatch): 39 - """Test that app insights are discovered from apps/*/muse/.""" 38 + def test_get_generator_agents_app_discovery(tmp_path, monkeypatch): 39 + """Test that app generators are discovered from apps/*/muse/.""" 40 40 utils = importlib.import_module("think.utils") 41 41 42 - # Create a fake app with an insight 42 + # Create a fake app with a generator 43 43 app_dir = tmp_path / "apps" / "test_app" / "muse" 44 44 app_dir.mkdir(parents=True) 45 45 46 - # Create insight files with frontmatter 47 - (app_dir / "custom_insight.md").write_text( 48 - '{\n "title": "Custom Insight",\n "color": "#ff0000"\n}\n\nTest prompt' 46 + # Create generator files with frontmatter 47 + (app_dir / "custom_generator.md").write_text( 48 + '{\n "title": "Custom Generator",\n "color": "#ff0000"\n}\n\nTest prompt' 49 49 ) 50 50 51 - # Also create workspace.html to make it a valid app (not strictly required for insights) 51 + # Also create workspace.html to make it a valid app (not strictly required for generators) 52 52 (tmp_path / "apps" / "test_app" / "workspace.html").write_text("<h1>Test</h1>") 53 53 54 54 # Monkeypatch the apps_dir path 55 - original_get_insights = utils.get_insights 55 + original_get_generator_agents = utils.get_generator_agents 56 56 57 - def patched_get_insights(): 57 + def patched_get_generator_agents(): 58 58 # Temporarily modify the path 59 59 import think.utils as tu 60 60 61 61 original_parent = Path(tu.__file__).parent.parent 62 62 # We need to actually patch how the function resolves apps_dir 63 - # Let's just test the existing system insights have source 64 - return original_get_insights() 63 + # Let's just test the existing system generators have source 64 + return original_get_generator_agents() 65 65 66 - # For now, just verify system insights have correct source 67 - insights = utils.get_insights() 68 - for key, info in insights.items(): 66 + # For now, just verify system generators have correct source 67 + generators = utils.get_generator_agents() 68 + for key, info in generators.items(): 69 69 if ":" not in key: 70 70 assert info.get("source") == "system", f"{key} should have source=system" 71 71 72 72 73 - def test_get_insights_by_schedule(): 74 - """Test filtering insights by schedule.""" 73 + def test_get_generator_agents_by_schedule(): 74 + """Test filtering generators by schedule.""" 75 75 utils = importlib.import_module("think.utils") 76 76 77 - # Get daily insights 78 - daily = utils.get_insights_by_schedule("daily") 77 + # Get daily generators 78 + daily = utils.get_generator_agents_by_schedule("daily") 79 79 assert len(daily) > 0 80 80 for key, meta in daily.items(): 81 81 assert meta.get("schedule") == "daily", f"{key} should have schedule=daily" 82 82 83 - # Get segment insights 84 - segment = utils.get_insights_by_schedule("segment") 83 + # Get segment generators 84 + segment = utils.get_generator_agents_by_schedule("segment") 85 85 assert len(segment) > 0 86 86 for key, meta in segment.items(): 87 87 assert meta.get("schedule") == "segment", f"{key} should have schedule=segment" ··· 92 92 ), "daily and segment should not overlap" 93 93 94 94 # Unknown schedule returns empty dict 95 - assert utils.get_insights_by_schedule("hourly") == {} 96 - assert utils.get_insights_by_schedule("") == {} 95 + assert utils.get_generator_agents_by_schedule("hourly") == {} 96 + assert utils.get_generator_agents_by_schedule("") == {} 97 97 98 98 99 - def test_get_insights_by_schedule_include_disabled(monkeypatch): 99 + def test_get_generator_agents_by_schedule_include_disabled(monkeypatch): 100 100 """Test include_disabled parameter.""" 101 101 utils = importlib.import_module("think.utils") 102 102 103 - # Get insights without disabled (default) 104 - without_disabled = utils.get_insights_by_schedule("daily") 103 + # Get generators without disabled (default) 104 + without_disabled = utils.get_generator_agents_by_schedule("daily") 105 105 106 - # Get insights with disabled included 107 - with_disabled = utils.get_insights_by_schedule("daily", include_disabled=True) 106 + # Get generators with disabled included 107 + with_disabled = utils.get_generator_agents_by_schedule( 108 + "daily", include_disabled=True 109 + ) 108 110 109 111 # Should have at least as many with disabled included 110 112 # (files.md, media.md, tools.md are disabled by default) 111 113 assert len(with_disabled) >= len(without_disabled) 112 114 113 115 114 - def test_all_system_insights_have_schedule(): 115 - """Test that all system insights have valid schedule field. 116 + def test_all_system_generators_have_schedule(): 117 + """Test that all system generators have valid schedule field. 116 118 117 - Insights are identified by having a schedule field but no tools field. 119 + Generators are identified by having a schedule field but no tools field. 118 120 Hook-only files (occurrence, anticipation) have neither, so they're 119 - excluded from get_insights() automatically. 121 + excluded from get_generator_agents() automatically. 120 122 """ 121 123 utils = importlib.import_module("think.utils") 122 124 123 - insights = utils.get_insights() 125 + generators = utils.get_generator_agents() 124 126 valid_schedules = ("segment", "daily") 125 127 126 - for key, meta in insights.items(): 128 + for key, meta in generators.items(): 127 129 if meta.get("source") == "system": 128 130 sched = meta.get("schedule") 129 131 assert ( 130 132 sched is not None 131 - ), f"System insight '{key}' missing required 'schedule' field" 133 + ), f"System generator '{key}' missing required 'schedule' field" 132 134 assert ( 133 135 sched in valid_schedules 134 - ), f"System insight '{key}' has invalid schedule '{sched}'" 136 + ), f"System generator '{key}' has invalid schedule '{sched}'"
+3 -3
tests/test_journal_stats.py
··· 27 27 (ts_dir2 / "center_DP-1_screen.webm").write_bytes(b"WEBM") 28 28 29 29 (day / "entities.md").write_text("") 30 - (day / "insights").mkdir() 31 - (day / "insights" / "flow.md").write_text("") 30 + (day / "agents").mkdir() 31 + (day / "agents" / "flow.md").write_text("") 32 32 33 33 # Create event in new JSONL format: facets/{facet}/events/YYYYMMDD.jsonl 34 34 events_dir = journal / "facets" / "work" / "events" ··· 45 45 "facet": "work", 46 46 "topic": "meetings", 47 47 "occurred": True, 48 - "source": "20240101/insights/meetings.md", 48 + "source": "20240101/agents/meetings.md", 49 49 } 50 50 (events_dir / "20240101.jsonl").write_text(json.dumps(event)) 51 51
+3 -3
tests/test_models.py
··· 225 225 assert CONTEXT_DEFAULTS["observe.describe.frame"]["tier"] == TIER_LITE 226 226 assert CONTEXT_DEFAULTS["observe.describe.frame"]["group"] == "Observe" 227 227 228 - assert "insight.*" in CONTEXT_DEFAULTS 229 - assert CONTEXT_DEFAULTS["insight.*"]["tier"] == TIER_FLASH 230 - assert CONTEXT_DEFAULTS["insight.*"]["group"] == "Think" 228 + assert "agent.*" in CONTEXT_DEFAULTS 229 + assert CONTEXT_DEFAULTS["agent.*"]["tier"] == TIER_FLASH 230 + assert CONTEXT_DEFAULTS["agent.*"]["group"] == "Think" 231 231 232 232 assert "app.chat.title" in CONTEXT_DEFAULTS 233 233 assert CONTEXT_DEFAULTS["app.chat.title"]["tier"] == TIER_LITE
+76 -68
tests/test_output_hooks.py
··· 24 24 return dest 25 25 26 26 27 - MOCK_RESULT = "## Original Result\n\nThis is the original insight content." 27 + MOCK_RESULT = "## Original Result\n\nThis is the original output content." 28 28 29 29 30 - def test_load_insight_hook_success(tmp_path): 30 + def test_load_output_hook_success(tmp_path): 31 31 """Test loading a valid hook with process function.""" 32 32 utils = importlib.import_module("think.utils") 33 33 ··· 37 37 return result + "\\n\\n## Added by hook" 38 38 """) 39 39 40 - process_func = utils.load_insight_hook(hook_file) 40 + process_func = utils.load_output_hook(hook_file) 41 41 assert callable(process_func) 42 42 43 43 # Test the hook transforms content ··· 45 45 assert output == "Original\n\n## Added by hook" 46 46 47 47 48 - def test_load_insight_hook_missing_process(tmp_path): 48 + def test_load_output_hook_missing_process(tmp_path): 49 49 """Test that hook without process function raises ValueError.""" 50 50 utils = importlib.import_module("think.utils") 51 51 ··· 56 56 """) 57 57 58 58 try: 59 - utils.load_insight_hook(hook_file) 59 + utils.load_output_hook(hook_file) 60 60 assert False, "Should have raised ValueError" 61 61 except ValueError as e: 62 62 assert "must define a 'process' function" in str(e) 63 63 64 64 65 - def test_load_insight_hook_process_not_callable(tmp_path): 65 + def test_load_output_hook_process_not_callable(tmp_path): 66 66 """Test that hook with non-callable process raises ValueError.""" 67 67 utils = importlib.import_module("think.utils") 68 68 ··· 72 72 """) 73 73 74 74 try: 75 - utils.load_insight_hook(hook_file) 75 + utils.load_output_hook(hook_file) 76 76 assert False, "Should have raised ValueError" 77 77 except ValueError as e: 78 78 assert "'process' must be callable" in str(e) 79 79 80 80 81 - def test_insight_metadata_includes_hook_path(tmp_path): 82 - """Test that _load_insight_metadata detects .py hook file.""" 81 + def test_prompt_metadata_includes_hook_path(tmp_path): 82 + """Test that _load_prompt_metadata detects .py hook file.""" 83 83 utils = importlib.import_module("think.utils") 84 84 85 - # Create insight file with frontmatter 86 - md_file = tmp_path / "test_insight.md" 85 + # Create prompt file with frontmatter 86 + md_file = tmp_path / "test_generator.md" 87 87 md_file.write_text('{\n "title": "Test",\n "color": "#ff0000"\n}\n\nTest prompt') 88 88 89 - hook_file = tmp_path / "test_insight.py" 89 + hook_file = tmp_path / "test_generator.py" 90 90 hook_file.write_text("def process(r, c): return r") 91 91 92 - meta = utils._load_insight_metadata(md_file) 92 + meta = utils._load_prompt_metadata(md_file) 93 93 94 94 assert meta["path"] == str(md_file) 95 95 assert meta["hook_path"] == str(hook_file) 96 96 assert meta["title"] == "Test" 97 97 98 98 99 - def test_insight_metadata_no_hook(tmp_path): 100 - """Test that _load_insight_metadata works without hook file.""" 99 + def test_prompt_metadata_no_hook(tmp_path): 100 + """Test that _load_prompt_metadata works without hook file.""" 101 101 utils = importlib.import_module("think.utils") 102 102 103 - md_file = tmp_path / "test_insight.md" 103 + md_file = tmp_path / "test_generator.md" 104 104 md_file.write_text("Test prompt") 105 105 106 - meta = utils._load_insight_metadata(md_file) 106 + meta = utils._load_prompt_metadata(md_file) 107 107 108 108 assert meta["path"] == str(md_file) 109 109 assert "hook_path" not in meta 110 110 111 111 112 - def test_insight_hook_invocation(tmp_path, monkeypatch): 113 - """Test that insight.py invokes hook and uses transformed result.""" 114 - mod = importlib.import_module("think.insight") 112 + def test_output_hook_invocation(tmp_path, monkeypatch): 113 + """Test that generate.py invokes hook and uses transformed result.""" 114 + mod = importlib.import_module("think.generate") 115 115 day_dir = copy_day(tmp_path) 116 116 117 - # Create insight with hook 118 - insights_dir = tmp_path / "insights" 119 - insights_dir.mkdir() 117 + # Create generator with hook 118 + generators_dir = tmp_path / "generators" 119 + generators_dir.mkdir() 120 120 121 - prompt_file = insights_dir / "hooked.md" 121 + prompt_file = generators_dir / "hooked.md" 122 122 prompt_file.write_text( 123 123 '{\n "title": "Hooked",\n "occurrences": false,\n "schedule": "daily"\n}\n\nTest prompt' 124 124 ) 125 125 126 - hook_file = insights_dir / "hooked.py" 126 + hook_file = generators_dir / "hooked.py" 127 127 hook_file.write_text(""" 128 128 def process(result, context): 129 129 # Verify context has expected fields 130 130 assert "day" in context 131 131 assert "transcript" in context 132 - assert "insight_key" in context 132 + assert "name" in context 133 133 return result + "\\n\\n## Hook was here" 134 134 """) 135 135 136 136 monkeypatch.setattr( 137 137 mod, 138 - "send_insight", 138 + "generate_agent_output", 139 139 lambda *a, **k: MOCK_RESULT, 140 140 ) 141 141 monkeypatch.setenv("GOOGLE_API_KEY", "x") 142 142 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 143 - monkeypatch.setattr("sys.argv", ["sol insight", "20240101", "-f", str(prompt_file)]) 143 + monkeypatch.setattr( 144 + "sys.argv", ["sol generate", "20240101", "-f", str(prompt_file)] 145 + ) 144 146 145 147 mod.main() 146 148 147 - md = day_dir / "insights" / "hooked.md" 149 + md = day_dir / "agents" / "hooked.md" 148 150 content = md.read_text() 149 151 assert "## Original Result" in content 150 152 assert "## Hook was here" in content 151 153 152 154 153 - def test_insight_hook_returns_none(tmp_path, monkeypatch): 155 + def test_output_hook_returns_none(tmp_path, monkeypatch): 154 156 """Test that hook returning None uses original result.""" 155 - mod = importlib.import_module("think.insight") 157 + mod = importlib.import_module("think.generate") 156 158 day_dir = copy_day(tmp_path) 157 159 158 - insights_dir = tmp_path / "insights" 159 - insights_dir.mkdir() 160 + generators_dir = tmp_path / "generators" 161 + generators_dir.mkdir() 160 162 161 - prompt_file = insights_dir / "noop.md" 163 + prompt_file = generators_dir / "noop.md" 162 164 prompt_file.write_text( 163 165 '{\n "title": "Noop",\n "occurrences": false,\n "schedule": "daily"\n}\n\nTest prompt' 164 166 ) 165 167 166 - hook_file = insights_dir / "noop.py" 168 + hook_file = generators_dir / "noop.py" 167 169 hook_file.write_text(""" 168 170 def process(result, context): 169 171 return None # Signal to use original ··· 171 173 172 174 monkeypatch.setattr( 173 175 mod, 174 - "send_insight", 176 + "generate_agent_output", 175 177 lambda *a, **k: MOCK_RESULT, 176 178 ) 177 179 monkeypatch.setenv("GOOGLE_API_KEY", "x") 178 180 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 179 - monkeypatch.setattr("sys.argv", ["sol insight", "20240101", "-f", str(prompt_file)]) 181 + monkeypatch.setattr( 182 + "sys.argv", ["sol generate", "20240101", "-f", str(prompt_file)] 183 + ) 180 184 181 185 mod.main() 182 186 183 - md = day_dir / "insights" / "noop.md" 187 + md = day_dir / "agents" / "noop.md" 184 188 content = md.read_text() 185 189 assert content == MOCK_RESULT # Original, not modified 186 190 187 191 188 - def test_insight_hook_error_fallback(tmp_path, monkeypatch): 192 + def test_output_hook_error_fallback(tmp_path, monkeypatch): 189 193 """Test that hook errors fall back to original result.""" 190 - mod = importlib.import_module("think.insight") 194 + mod = importlib.import_module("think.generate") 191 195 day_dir = copy_day(tmp_path) 192 196 193 - insights_dir = tmp_path / "insights" 194 - insights_dir.mkdir() 197 + generators_dir = tmp_path / "generators" 198 + generators_dir.mkdir() 195 199 196 - prompt_file = insights_dir / "broken.md" 200 + prompt_file = generators_dir / "broken.md" 197 201 prompt_file.write_text( 198 202 '{\n "title": "Broken",\n "occurrences": false,\n "schedule": "daily"\n}\n\nTest prompt' 199 203 ) 200 204 201 - hook_file = insights_dir / "broken.py" 205 + hook_file = generators_dir / "broken.py" 202 206 hook_file.write_text(""" 203 207 def process(result, context): 204 208 raise RuntimeError("Hook exploded!") ··· 206 210 207 211 monkeypatch.setattr( 208 212 mod, 209 - "send_insight", 213 + "generate_agent_output", 210 214 lambda *a, **k: MOCK_RESULT, 211 215 ) 212 216 monkeypatch.setenv("GOOGLE_API_KEY", "x") 213 217 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 214 - monkeypatch.setattr("sys.argv", ["sol insight", "20240101", "-f", str(prompt_file)]) 218 + monkeypatch.setattr( 219 + "sys.argv", ["sol generate", "20240101", "-f", str(prompt_file)] 220 + ) 215 221 216 222 # Should not raise, should fall back gracefully 217 223 mod.main() 218 224 219 - md = day_dir / "insights" / "broken.md" 225 + md = day_dir / "agents" / "broken.md" 220 226 content = md.read_text() 221 227 assert content == MOCK_RESULT # Original result preserved 222 228 223 229 224 - def test_insight_hook_context_fields(tmp_path, monkeypatch): 230 + def test_output_hook_context_fields(tmp_path, monkeypatch): 225 231 """Test that hook receives complete context dict.""" 226 - mod = importlib.import_module("think.insight") 232 + mod = importlib.import_module("think.generate") 227 233 copy_day(tmp_path) 228 234 229 - insights_dir = tmp_path / "insights" 230 - insights_dir.mkdir() 235 + generators_dir = tmp_path / "generators" 236 + generators_dir.mkdir() 231 237 232 - prompt_file = insights_dir / "context_check.md" 238 + prompt_file = generators_dir / "context_check.md" 233 239 prompt_file.write_text( 234 240 '{\n "title": "Context Check",\n "occurrences": false,\n "schedule": "daily"\n}\n\nTest prompt' 235 241 ) 236 242 237 243 # Write captured context to a file for verification 238 - hook_file = insights_dir / "context_check.py" 244 + hook_file = generators_dir / "context_check.py" 239 245 hook_file.write_text(""" 240 246 import json 241 247 from pathlib import Path ··· 247 253 # Remove transcript for brevity, just check it exists 248 254 ctx_copy = dict(context) 249 255 ctx_copy["has_transcript"] = bool(ctx_copy.get("transcript")) 250 - ctx_copy["has_insight_meta"] = bool(ctx_copy.get("insight_meta")) 256 + ctx_copy["has_meta"] = bool(ctx_copy.get("meta")) 251 257 del ctx_copy["transcript"] 252 - del ctx_copy["insight_meta"] 258 + del ctx_copy["meta"] 253 259 json.dump(ctx_copy, f) 254 260 return result 255 261 """) 256 262 257 263 monkeypatch.setattr( 258 264 mod, 259 - "send_insight", 265 + "generate_agent_output", 260 266 lambda *a, **k: MOCK_RESULT, 261 267 ) 262 268 monkeypatch.setenv("GOOGLE_API_KEY", "x") 263 269 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 264 - monkeypatch.setattr("sys.argv", ["sol insight", "20240101", "-f", str(prompt_file)]) 270 + monkeypatch.setattr( 271 + "sys.argv", ["sol generate", "20240101", "-f", str(prompt_file)] 272 + ) 265 273 266 274 mod.main() 267 275 268 276 # Read captured context 269 - captured_path = tmp_path / "20240101" / "insights" / "context_captured.json" 277 + captured_path = tmp_path / "20240101" / "agents" / "context_captured.json" 270 278 captured = json.loads(captured_path.read_text()) 271 279 272 280 assert captured["day"] == "20240101" 273 281 assert captured["segment"] is None 274 - assert captured["insight_key"] == "context_check" # stem of the prompt file 282 + assert captured["name"] == "context_check" # stem of the prompt file 275 283 assert captured["has_transcript"] is True 276 - assert captured["has_insight_meta"] is True 284 + assert captured["has_meta"] is True 277 285 assert "output_path" in captured 278 286 279 287 ··· 281 289 """Test that named hooks via 'hook' field take precedence over co-located .py files.""" 282 290 utils = importlib.import_module("think.utils") 283 291 284 - # Create insight file with named hook 285 - md_file = tmp_path / "test_insight.md" 292 + # Create prompt file with named hook 293 + md_file = tmp_path / "test_generator.md" 286 294 md_file.write_text( 287 295 '{\n "title": "Test",\n "hook": "occurrence"\n}\n\nTest prompt' 288 296 ) 289 297 290 298 # Also create a co-located .py file that would normally be picked up 291 - colocated_hook = tmp_path / "test_insight.py" 299 + colocated_hook = tmp_path / "test_generator.py" 292 300 colocated_hook.write_text("def process(r, c): return 'colocated'") 293 301 294 - meta = utils._load_insight_metadata(md_file) 302 + meta = utils._load_prompt_metadata(md_file) 295 303 296 304 # Should resolve to named hook, not co-located 297 305 assert "hook_path" in meta ··· 303 311 """Test that nonexistent named hooks fall back to co-located .py files.""" 304 312 utils = importlib.import_module("think.utils") 305 313 306 - # Create insight file with nonexistent named hook 307 - md_file = tmp_path / "test_insight.md" 314 + # Create prompt file with nonexistent named hook 315 + md_file = tmp_path / "test_generator.md" 308 316 md_file.write_text( 309 317 '{\n "title": "Test",\n "hook": "nonexistent_hook_xyz"\n}\n\nTest prompt' 310 318 ) 311 319 312 320 # Create a co-located .py file 313 - colocated_hook = tmp_path / "test_insight.py" 321 + colocated_hook = tmp_path / "test_generator.py" 314 322 colocated_hook.write_text("def process(r, c): return 'colocated'") 315 323 316 - meta = utils._load_insight_metadata(md_file) 324 + meta = utils._load_prompt_metadata(md_file) 317 325 318 326 # Named hook doesn't exist, so no hook_path should be set (co-located not checked when named specified) 319 327 # Actually the current implementation checks co-located only if hook field is not set
+1 -1
tests/test_sol.py
··· 219 219 220 220 def test_critical_commands_registered(self): 221 221 """Test that critical commands are registered.""" 222 - critical = ["import", "insight", "dream", "indexer", "transcribe"] 222 + critical = ["import", "generate", "dream", "indexer", "transcribe"] 223 223 for cmd in critical: 224 224 assert cmd in sol.COMMANDS, f"Critical command '{cmd}' not registered"
+5 -5
tests/test_think_utils.py
··· 695 695 """Test that sources dict is merged, not replaced.""" 696 696 defaults = { 697 697 "system": "journal", 698 - "sources": {"audio": True, "screen": True, "insights": False}, 698 + "sources": {"audio": True, "screen": True, "agents": False}, 699 699 } 700 700 overrides = {"sources": {"screen": False}} 701 701 result = _merge_instructions_config(defaults, overrides) 702 702 assert result["sources"]["audio"] is True # Preserved from defaults 703 703 assert result["sources"]["screen"] is False # Overridden 704 - assert result["sources"]["insights"] is False # Preserved from defaults 704 + assert result["sources"]["agents"] is False # Preserved from defaults 705 705 706 706 def test_ignores_unknown_keys(self): 707 707 """Test that unknown keys in overrides are ignored.""" ··· 867 867 assert "sources" in result 868 868 assert result["sources"]["audio"] is True 869 869 assert result["sources"]["screen"] is True 870 - assert result["sources"]["insights"] is False 870 + assert result["sources"]["agents"] is False 871 871 872 872 def test_sources_can_be_overridden(self, monkeypatch, tmp_path): 873 873 """Test that sources config can be overridden.""" ··· 883 883 884 884 result = compose_instructions( 885 885 config_overrides={ 886 - "sources": {"audio": False, "insights": True}, 886 + "sources": {"audio": False, "agents": True}, 887 887 }, 888 888 ) 889 889 890 890 assert result["sources"]["audio"] is False 891 891 assert result["sources"]["screen"] is True # Default preserved 892 - assert result["sources"]["insights"] is True # Overridden 892 + assert result["sources"]["agents"] is True # Overridden
+35 -35
think/cluster.py
··· 27 27 date_str: str, 28 28 audio: bool, 29 29 screen: bool, 30 - insights: bool, 30 + agents: bool, 31 31 ) -> List[Dict[str, Any]]: 32 32 """Process a single segment directory and return entries. 33 33 ··· 36 36 date_str: Date in YYYYMMDD format 37 37 audio: Whether to load audio transcripts 38 38 screen: Whether to load raw screen data from *screen.jsonl files 39 - insights: Whether to load insight summaries from *.md files 39 + agents: Whether to load agent output summaries from *.md files 40 40 41 41 Returns: 42 42 List of entry dicts with timestamp, segment_key, prefix, content, name, etc. ··· 107 107 file=sys.stderr, 108 108 ) 109 109 110 - # Process insight summaries from all *.md files 111 - if insights: 110 + # Process agent output summaries from all *.md files 111 + if agents: 112 112 for md_file in sorted(segment_path.glob("*.md")): 113 113 if not md_file.is_file(): 114 114 continue ··· 121 121 "segment_key": segment_key, 122 122 "segment_start": segment_start, 123 123 "segment_end": segment_end, 124 - "prefix": "insight", 125 - "insight_name": md_file.stem, 124 + "prefix": "agent_output", 125 + "output_name": md_file.stem, 126 126 "content": content, 127 127 "name": f"{segment_path.name}/{md_file.name}", 128 128 } ··· 137 137 138 138 139 139 def _load_entries( 140 - day_dir: str, audio: bool, screen: bool, insights: bool 140 + day_dir: str, audio: bool, screen: bool, agents: bool 141 141 ) -> List[Dict[str, Any]]: 142 142 """Load all transcript entries from a day directory.""" 143 143 from think.utils import segment_parse ··· 150 150 start_time, _ = segment_parse(item.name) 151 151 if not (item.is_dir() and start_time): 152 152 continue 153 - entries.extend(_process_segment(item, date_str, audio, screen, insights)) 153 + entries.extend(_process_segment(item, date_str, audio, screen, agents)) 154 154 155 155 entries.sort(key=lambda e: e["timestamp"]) 156 156 return entries ··· 200 200 lines.append("### Screen Activity") 201 201 lines.append(entry["content"].strip()) 202 202 lines.append("") 203 - elif entry["prefix"] == "insight": 204 - insight_name = entry.get("insight_name", "insight") 205 - lines.append(f"### {insight_name} summary") 203 + elif entry["prefix"] == "agent_output": 204 + output_name = entry.get("output_name", "output") 205 + lines.append(f"### {output_name} summary") 206 206 lines.append(entry["content"].strip()) 207 207 lines.append("") 208 208 ··· 358 358 359 359 Args: 360 360 day: Day in YYYYMMDD format 361 - sources: Optional dict with keys "audio", "screen", "insights" (bools). 362 - Defaults to {"audio": True, "screen": False, "insights": True}. 361 + sources: Optional dict with keys "audio", "screen", "agents" (bools). 362 + Defaults to {"audio": True, "screen": False, "agents": True}. 363 363 """ 364 364 # Default sources for daily insights: audio + insight summaries, no raw screen 365 365 if sources is None: 366 - sources = {"audio": True, "screen": False, "insights": True} 366 + sources = {"audio": True, "screen": False, "agents": True} 367 367 368 368 day_dir = str(day_path(day)) 369 369 # day_path now ensures dir exists, but check anyway for safety ··· 374 374 day_dir, 375 375 audio=sources.get("audio", True), 376 376 screen=sources.get("screen", False), 377 - insights=sources.get("insights", True), 377 + agents=sources.get("agents", True), 378 378 ) 379 379 if not entries: 380 380 return f"No audio or screen files found for date {day} in {day_dir}.", 0 ··· 397 397 Args: 398 398 day: Day in YYYYMMDD format 399 399 segment: Segment key in HHMMSS_LEN format (e.g., "163045_300") 400 - sources: Optional dict with keys "audio", "screen", "insights" (bools). 401 - Defaults to {"audio": True, "screen": True, "insights": False}. 400 + sources: Optional dict with keys "audio", "screen", "agents" (bools). 401 + Defaults to {"audio": True, "screen": True, "agents": False}. 402 402 403 403 Returns: 404 404 (markdown, file_count) tuple 405 405 """ 406 406 # Default sources for segment insights: audio + raw screen, no insight summaries 407 407 if sources is None: 408 - sources = {"audio": True, "screen": True, "insights": False} 408 + sources = {"audio": True, "screen": True, "agents": False} 409 409 410 410 day_dir = str(day_path(day)) 411 411 segment_dir = Path(day_dir) / segment ··· 417 417 str(segment_dir), 418 418 audio=sources.get("audio", True), 419 419 screen=sources.get("screen", True), 420 - insights=sources.get("insights", False), 420 + agents=sources.get("agents", False), 421 421 ) 422 422 if not entries: 423 423 return f"No audio or screen files found for segment {segment}", 0 ··· 428 428 429 429 430 430 def _load_entries_from_segment( 431 - segment_dir: str, audio: bool, screen: bool, insights: bool 431 + segment_dir: str, audio: bool, screen: bool, agents: bool 432 432 ) -> List[Dict[str, Any]]: 433 433 """Load entries from a single segment directory. 434 434 ··· 436 436 segment_dir: Path to segment directory (e.g., /path/to/20251109/163045_300) 437 437 audio: Whether to load audio transcripts 438 438 screen: Whether to load raw screen data from *screen.jsonl files 439 - insights: Whether to load insight summaries from *.md files 439 + agents: Whether to load agent output summaries from *.md files 440 440 441 441 Returns: 442 442 List of entry dicts with timestamp, prefix, content, etc. 443 443 """ 444 444 segment_path = Path(segment_dir) 445 445 date_str = _date_str(str(segment_path.parent)) 446 - entries = _process_segment(segment_path, date_str, audio, screen, insights) 446 + entries = _process_segment(segment_path, date_str, audio, screen, agents) 447 447 entries.sort(key=lambda e: e["timestamp"]) 448 448 return entries 449 449 ··· 461 461 Args: 462 462 day: Day in YYYYMMDD format 463 463 segments: List of segment keys in HHMMSS_LEN format (e.g., ["163045_300", "170000_600"]) 464 - sources: Optional dict with keys "audio", "screen", "insights" (bools). 465 - Defaults to {"audio": True, "screen": True, "insights": False}. 464 + sources: Optional dict with keys "audio", "screen", "agents" (bools). 465 + Defaults to {"audio": True, "screen": True, "agents": False}. 466 466 467 467 Returns: 468 468 (markdown, file_count) tuple ··· 472 472 """ 473 473 # Default sources for segment insights: audio + raw screen, no insight summaries 474 474 if sources is None: 475 - sources = {"audio": True, "screen": True, "insights": False} 475 + sources = {"audio": True, "screen": True, "agents": False} 476 476 477 477 day_dir = str(day_path(day)) 478 478 ··· 494 494 str(segment_dir), 495 495 audio=sources.get("audio", True), 496 496 screen=sources.get("screen", True), 497 - insights=sources.get("insights", False), 497 + agents=sources.get("agents", False), 498 498 ) 499 499 entries.extend(segment_entries) 500 500 ··· 524 524 end: str, 525 525 audio: bool = True, 526 526 screen: bool = False, 527 - insights: bool = True, 527 + agents: bool = True, 528 528 ) -> str: 529 529 """Return markdown for ``day`` limited to ``start``-``end`` (HHMMSS). 530 530 ··· 537 537 end: End time in HHMMSS format 538 538 audio: Whether to include audio transcripts 539 539 screen: Whether to include raw screen data from *screen.jsonl files 540 - insights: Whether to include insight summaries from *.md files 540 + agents: Whether to include agent output summaries from *.md files 541 541 """ 542 542 543 543 day_dir = str(day_path(day)) ··· 545 545 start_dt = datetime.strptime(date_str + start, "%Y%m%d%H%M%S") 546 546 end_dt = datetime.strptime(date_str + end, "%Y%m%d%H%M%S") 547 547 548 - entries = _load_entries(day_dir, audio, screen, insights) 548 + entries = _load_entries(day_dir, audio, screen, agents) 549 549 # Include segments that overlap with the requested range 550 550 entries = [ 551 551 e ··· 562 562 end: str, 563 563 audio: bool = True, 564 564 screen: bool = True, 565 - insights: bool = False, 565 + agents: bool = False, 566 566 ) -> List[Dict[str, Any]]: 567 567 """Return filtered transcript entries for a time range. 568 568 ··· 575 575 end: End time in HHMMSS format 576 576 audio: Whether to include audio transcripts 577 577 screen: Whether to include raw screen data from *screen.jsonl files 578 - insights: Whether to include insight summaries from *.md files 578 + agents: Whether to include agent output summaries from *.md files 579 579 580 580 Returns: 581 581 List of entry dicts with keys: ··· 583 583 - segment_key: segment directory name 584 584 - segment_start: datetime of segment start 585 585 - segment_end: datetime of segment end 586 - - prefix: "audio", "screen", or "insight" 587 - - insight_name: (insights only) stem of the .md filename 586 + - prefix: "audio", "screen", or "agent_output" 587 + - output_name: (agents only) stem of the .md filename 588 588 - content: formatted transcript text 589 589 - name: relative path like "HHMMSS_LEN/audio.jsonl" 590 590 """ ··· 597 597 start_dt = datetime.strptime(date_str + start, "%Y%m%d%H%M%S") 598 598 end_dt = datetime.strptime(date_str + end, "%Y%m%d%H%M%S") 599 599 600 - entries = _load_entries(day_dir, audio, screen, insights) 600 + entries = _load_entries(day_dir, audio, screen, agents) 601 601 return [ 602 602 e 603 603 for e in entries ··· 635 635 end_dt.strftime("%H%M%S"), 636 636 audio=True, 637 637 screen=True, 638 - insights=False, 638 + agents=False, 639 639 ) 640 640 print(markdown) 641 641 elif args.start or args.length is not None:
+1 -1
think/cortex.py
··· 729 729 """Write agent output to the appropriate location. 730 730 731 731 Output path is derived from name + output format + schedule: 732 - - Daily agents: YYYYMMDD/insights/{name}.{ext} 732 + - Daily agents: YYYYMMDD/agents/{name}.{ext} 733 733 - Segment agents: YYYYMMDD/{segment}/{name}.{ext} 734 734 """ 735 735 try:
+30 -30
think/dream.py
··· 17 17 day_log, 18 18 day_path, 19 19 get_agents, 20 - get_insights_by_schedule, 20 + get_generator_agents_by_schedule, 21 21 get_journal, 22 22 setup_cli, 23 23 ) ··· 28 28 29 29 def run_command(cmd: list[str], day: str) -> bool: 30 30 logging.info("==> %s", " ".join(cmd)) 31 - # Extract command name for logging (e.g., "sol insight" -> "insight") 31 + # Extract command name for logging (e.g., "sol generate" -> "generate") 32 32 cmd_name = cmd[1] if cmd[0] == "sol" else cmd[0] 33 33 cmd_name = cmd_name.replace("-", "_") 34 34 ··· 153 153 cmd.append("-v") 154 154 commands.append(cmd) 155 155 156 - # Run insights filtered by schedule (skips disabled and invalid) 157 - insights = get_insights_by_schedule(target_schedule) 158 - for insight_name, insight_data in insights.items(): 159 - cmd = ["sol", "insight", day, "-f", insight_data["path"]] 156 + # Run generators filtered by schedule (skips disabled and invalid) 157 + generators = get_generator_agents_by_schedule(target_schedule) 158 + for generator_name, generator_data in generators.items(): 159 + cmd = ["sol", "generate", day, "-f", generator_data["path"]] 160 160 if segment: 161 161 cmd.extend(["--segment", segment]) 162 162 if verbose: ··· 195 195 ) 196 196 parser.add_argument("--force", action="store_true", help="Overwrite existing files") 197 197 parser.add_argument( 198 - "--skip-insights", 198 + "--skip-generators", 199 199 action="store_true", 200 - help="Skip insight processing, run agents only", 200 + help="Skip generator processing, run agents only", 201 201 ) 202 202 parser.add_argument( 203 203 "--skip-agents", 204 204 action="store_true", 205 - help="Skip agent processing, run insights only", 205 + help="Skip agent processing, run generators only", 206 206 ) 207 207 return parser 208 208 ··· 436 436 437 437 try: 438 438 start_time = time.time() 439 - insight_fail_count = 0 439 + generator_fail_count = 0 440 440 agent_fail_count = 0 441 441 442 442 # Determine mode based on segment presence ··· 453 453 # Emit started event 454 454 emit("started", **event_fields()) 455 455 456 - # Phase 1: Insights 457 - if not args.skip_insights: 456 + # Phase 1: Generators 457 + if not args.skip_generators: 458 458 commands = build_commands( 459 459 day, args.force, verbose=args.verbose, segment=args.segment 460 460 ) 461 461 462 462 # Build command names list for logging 463 463 command_names = [cmd[1] for cmd in commands] 464 - logging.info(f"Running {len(commands)} insight commands: {command_names}") 464 + logging.info(f"Running {len(commands)} generator commands: {command_names}") 465 465 466 466 success_count = 0 467 467 for index, cmd in enumerate(commands): ··· 484 484 if success: 485 485 success_count += 1 486 486 else: 487 - insight_fail_count += 1 487 + generator_fail_count += 1 488 488 489 - # Emit insights_completed event 489 + # Emit generators_completed event 490 490 emit( 491 - "insights_completed", 491 + "generators_completed", 492 492 **event_fields( 493 493 success=success_count, 494 - failed=insight_fail_count, 494 + failed=generator_fail_count, 495 495 duration_ms=int((time.time() - start_time) * 1000), 496 496 ), 497 497 ) 498 498 499 499 logging.info( 500 - f"Insights completed: {success_count} succeeded, {insight_fail_count} failed" 500 + f"Generators completed: {success_count} succeeded, {generator_fail_count} failed" 501 501 ) 502 502 503 - # Exit early if insights failed and agents are requested 504 - if insight_fail_count > 0 and not args.skip_agents: 505 - logging.error("Insights failed, skipping agents") 503 + # Exit early if generators failed and agents are requested 504 + if generator_fail_count > 0 and not args.skip_agents: 505 + logging.error("Generators failed, skipping agents") 506 506 emit( 507 507 "completed", 508 508 **event_fields( 509 - insight_failed=insight_fail_count, 509 + generator_failed=generator_fail_count, 510 510 agent_failed=0, 511 511 duration_ms=int((time.time() - start_time) * 1000), 512 512 ), 513 513 ) 514 - day_log(day, f"dream insights failed {insight_fail_count}") 514 + day_log(day, f"dream generators failed {generator_fail_count}") 515 515 sys.exit(1) 516 516 517 517 # Phase 2: Agents ··· 533 533 emit( 534 534 "completed", 535 535 **event_fields( 536 - insight_failed=insight_fail_count, 536 + generator_failed=generator_fail_count, 537 537 agent_failed=agent_fail_count, 538 538 duration_ms=int((time.time() - start_time) * 1000), 539 539 ), ··· 541 541 542 542 # Build log message 543 543 msg = "dream" 544 - if args.skip_insights: 545 - msg += " --skip-insights" 544 + if args.skip_generators: 545 + msg += " --skip-generators" 546 546 if args.skip_agents: 547 547 msg += " --skip-agents" 548 548 if args.force: 549 549 msg += " --force" 550 - if insight_fail_count: 551 - msg += f" insights_failed={insight_fail_count}" 550 + if generator_fail_count: 551 + msg += f" generators_failed={generator_fail_count}" 552 552 if agent_fail_count: 553 553 msg += f" agents_failed={agent_fail_count}" 554 554 day_log(day, msg) 555 555 556 556 # Exit with error if any failures 557 - if insight_fail_count > 0 or agent_fail_count > 0: 558 - total_failures = insight_fail_count + agent_fail_count 557 + if generator_fail_count > 0 or agent_fail_count > 0: 558 + total_failures = generator_fail_count + agent_fail_count 559 559 logging.error(f"{total_failures} task(s) failed, exiting with error") 560 560 sys.exit(1) 561 561 finally:
+1 -1
think/events.py
··· 130 130 # For anticipations, show when it was created (from source path) 131 131 if not occurred: 132 132 source = event.get("source", "") 133 - # Extract YYYYMMDD from source path like "20240101/insights/schedule.md" 133 + # Extract YYYYMMDD from source path like "20240101/agents/schedule.md" 134 134 source_match = re.match(r"(\d{8})/", source) 135 135 if source_match: 136 136 created_day = source_match.group(1)
+66 -67
think/generate.py
··· 14 14 from think.models import generate 15 15 from think.utils import ( 16 16 PromptNotFoundError, 17 - _load_insight_metadata, 17 + _load_prompt_metadata, 18 18 compose_instructions, 19 19 day_log, 20 20 day_path, 21 21 format_day, 22 22 format_segment_times, 23 - get_insights, 23 + get_generator_agents, 24 24 get_output_path, 25 - load_insight_hook, 25 + get_output_topic, 26 + load_output_hook, 26 27 load_prompt, 27 28 segment_parse, 28 29 setup_cli, ··· 30 31 31 32 32 33 def scan_day(day: str) -> dict[str, list[str]]: 33 - """Return lists of processed and pending daily insight output files. 34 + """Return lists of processed and pending daily generator output files. 34 35 35 - Only scans daily insights (schedule='daily'). Segment insights are 36 + Only scans daily generators (schedule='daily'). Segment generators are 36 37 stored within segment directories and are not included here. 37 38 """ 38 - from think.utils import get_insights_by_schedule 39 + from think.utils import get_generator_agents_by_schedule 39 40 40 41 day_dir = day_path(day) 41 - daily_insights = get_insights_by_schedule("daily", include_disabled=True) 42 + daily_generators = get_generator_agents_by_schedule("daily", include_disabled=True) 42 43 processed: list[str] = [] 43 44 pending: list[str] = [] 44 - for key, meta in sorted(daily_insights.items()): 45 + for key, meta in sorted(daily_generators.items()): 45 46 output_format = meta.get("output") 46 47 output_path = get_output_path(day_dir, key, output_format=output_format) 47 48 if output_path.exists(): 48 - processed.append(os.path.join("insights", output_path.name)) 49 + processed.append(os.path.join("agents", output_path.name)) 49 50 else: 50 - pending.append(os.path.join("insights", output_path.name)) 51 + pending.append(os.path.join("agents", output_path.name)) 51 52 return {"processed": sorted(processed), "repairable": sorted(pending)} 52 53 53 54 ··· 100 101 return cache.name 101 102 102 103 103 - def send_insight( 104 + def generate_agent_output( 104 105 transcript: str, 105 106 prompt: str, 106 107 api_key: str, 107 108 cache_display_name: str | None = None, 108 - insight_key: str | None = None, 109 + name: str | None = None, 109 110 json_output: bool = False, 110 111 system_instruction: str | None = None, 111 112 thinking_budget: int | None = None, 112 113 max_output_tokens: int | None = None, 113 114 ) -> str: 114 - """Send clustered transcript to LLM for insight generation. 115 + """Send clustered transcript to LLM for agent output generation. 115 116 116 117 Args: 117 118 transcript: Clustered transcript content (markdown format). 118 - prompt: Insight prompt text. 119 + prompt: Agent prompt text. 119 120 api_key: Google API key for caching. 120 121 cache_display_name: Optional cache key for Google content caching. 121 122 Should include system prompt name for proper cache isolation. 122 - insight_key: Insight identifier for token logging context. 123 + name: Agent name for token logging context. 123 124 json_output: If True, request JSON response format. 124 125 system_instruction: System instruction text. If None, loads default 125 126 from journal.md via compose_instructions(). ··· 127 128 max_output_tokens: Maximum output tokens. If None, uses default. 128 129 129 130 Returns: 130 - Generated insight content (markdown or JSON string). 131 + Generated agent output content (markdown or JSON string). 131 132 """ 132 133 # Use provided system_instruction or fall back to default 133 134 if system_instruction is None: ··· 142 143 143 144 # Build context for provider routing and token logging 144 145 output_type = "json" if json_output else "markdown" 145 - context = ( 146 - f"insight.{insight_key}.{output_type}" if insight_key else "insight.unknown" 147 - ) 146 + context = f"agent.{name}.{output_type}" if name else "agent.unknown" 148 147 149 148 # Try to use cache if display name provided 150 149 # Note: caching is Google-specific, so we check provider first ··· 205 204 "--prompt", 206 205 dest="topic", 207 206 required=True, 208 - help="Insight key (e.g., 'activity', 'chat:sentiment') or path to .md file", 207 + help="Generator key (e.g., 'activity', 'chat:sentiment') or path to .md file", 209 208 ) 210 209 parser.add_argument( 211 210 "-c", ··· 249 248 first_segment = args.segments.split(",")[0].strip() 250 249 os.environ["SEGMENT_KEY"] = first_segment 251 250 252 - # Resolve insight key or path to metadata 253 - all_insights = get_insights() 251 + # Resolve generator key or path to metadata 252 + all_generators = get_generator_agents() 254 253 topic_arg = args.topic 255 254 256 - # Check if it's a known insight key first 257 - if topic_arg in all_insights: 258 - insight_key = topic_arg 259 - insight_meta = all_insights[insight_key] 260 - insight_path = Path(insight_meta["path"]) 255 + # Check if it's a known generator key first 256 + if topic_arg in all_generators: 257 + name = topic_arg 258 + meta = all_generators[name] 259 + agent_path = Path(meta["path"]) 261 260 elif Path(topic_arg).exists(): 262 261 # Fall back to treating it as a file path (backwards compat) 263 - insight_path = Path(topic_arg) 262 + agent_path = Path(topic_arg) 264 263 # Try to find matching key by path 265 - insight_key = insight_path.stem 264 + name = agent_path.stem 266 265 found_in_registry = False 267 - for key, meta in all_insights.items(): 268 - if meta.get("path") == str(insight_path): 269 - insight_key = key 266 + for key, m in all_generators.items(): 267 + if m.get("path") == str(agent_path): 268 + name = key 270 269 found_in_registry = True 271 270 break 272 271 if found_in_registry: 273 - insight_meta = all_insights[insight_key] 272 + meta = all_generators[name] 274 273 else: 275 - # Load metadata directly from file for ad-hoc insights 276 - insight_meta = _load_insight_metadata(insight_path) 274 + # Load metadata directly from file for ad-hoc generators 275 + meta = _load_prompt_metadata(agent_path) 277 276 else: 278 277 parser.error( 279 - f"Insight not found: {topic_arg}. " 280 - f"Available: {', '.join(sorted(all_insights.keys()))}" 278 + f"Generator not found: {topic_arg}. " 279 + f"Available: {', '.join(sorted(all_generators.keys()))}" 281 280 ) 282 281 283 - # Check if insight is disabled via journal config 284 - if insight_meta.get("disabled"): 285 - logging.info("Insight %s is disabled in journal config, skipping", insight_key) 286 - day_log(args.day, f"insight {get_insight_topic(topic_arg)} skipped (disabled)") 282 + # Check if generator is disabled via journal config 283 + if meta.get("disabled"): 284 + logging.info("Generator %s is disabled in journal config, skipping", name) 285 + day_log(args.day, f"generate {get_output_topic(topic_arg)} skipped (disabled)") 287 286 return 288 287 289 - output_format = insight_meta.get("output") # "json" or None (markdown) 288 + output_format = meta.get("output") # "json" or None (markdown) 290 289 success = False 291 290 292 291 # Extract instructions config for source filtering and system prompt 293 - instructions_config = insight_meta.get("instructions") 292 + instructions_config = meta.get("instructions") 294 293 295 294 # Use compose_instructions to get sources config and system instruction 296 295 instructions = compose_instructions( ··· 319 318 markdown, file_count = cluster(args.day, sources=sources) 320 319 day_dir = str(day_path(args.day)) 321 320 322 - # Skip insight generation when there's nothing to analyze 321 + # Skip generation when there's nothing to analyze 323 322 if file_count == 0 or len(markdown.strip()) < MIN_INPUT_CHARS: 324 323 logging.info( 325 - "Insufficient input (files=%d, chars=%d), skipping insight generation", 324 + "Insufficient input (files=%d, chars=%d), skipping generation", 326 325 file_count, 327 326 len(markdown.strip()), 328 327 ) 329 - day_log(args.day, f"insight {get_insight_topic(topic_arg)} skipped (no input)") 328 + day_log(args.day, f"generate {get_output_topic(topic_arg)} skipped (no input)") 330 329 return 331 330 332 331 # Prepend input context note for limited recordings ··· 385 384 prompt_context["segment_end"] = end_str 386 385 387 386 try: 388 - insight_prompt = load_prompt( 389 - insight_path.stem, base_dir=insight_path.parent, context=prompt_context 387 + agent_prompt = load_prompt( 388 + agent_path.stem, base_dir=agent_path.parent, context=prompt_context 390 389 ) 391 390 except PromptNotFoundError: 392 - parser.error(f"Insight file not found: {insight_path}") 391 + parser.error(f"Agent file not found: {agent_path}") 393 392 394 - prompt = insight_prompt.text 393 + prompt = agent_prompt.text 395 394 396 - # Resolve provider for display (must match context used in send_insight) 395 + # Resolve provider for display (must match context used in generate_agent_output) 397 396 from think.models import resolve_provider 398 397 399 398 display_output_type = "json" if output_format == "json" else "markdown" 400 - _, model = resolve_provider(f"insight.{insight_key}.{display_output_type}") 399 + _, model = resolve_provider(f"agent.{name}.{display_output_type}") 401 400 day = args.day 402 401 size_kb = len(markdown.encode("utf-8")) / 1024 403 402 404 403 print( 405 - f"Topic: {insight_key} | Model: {model} | Day: {day} | Files: {file_count} | Size: {size_kb:.1f}KB" 404 + f"Topic: {name} | Model: {model} | Day: {day} | Files: {file_count} | Size: {size_kb:.1f}KB" 406 405 ) 407 406 408 407 if args.count: ··· 416 415 output_path = Path(args.output) 417 416 else: 418 417 output_path = get_output_path( 419 - day_dir, insight_key, segment=args.segment, output_format=output_format 418 + day_dir, name, segment=args.segment, output_format=output_format 420 419 ) 421 420 422 421 # Determine cache settings: skip for multi-segment, otherwise scope to day/segment ··· 431 430 # Check if output file already exists 432 431 output_exists = output_path.exists() and output_path.stat().st_size > 0 433 432 434 - # Extract optional generation parameters from insight metadata 435 - meta_thinking_budget = insight_meta.get("thinking_budget") 436 - meta_max_output_tokens = insight_meta.get("max_output_tokens") 433 + # Extract optional generation parameters from metadata 434 + meta_thinking_budget = meta.get("thinking_budget") 435 + meta_max_output_tokens = meta.get("max_output_tokens") 437 436 438 437 if output_exists and not args.force: 439 438 print( ··· 443 442 result = f.read() 444 443 elif output_exists and args.force: 445 444 print("Output file exists but --force specified. Regenerating.") 446 - result = send_insight( 445 + result = generate_agent_output( 447 446 markdown, 448 447 prompt, 449 448 api_key, 450 449 cache_display_name=cache_display_name, 451 - insight_key=insight_key, 450 + name=name, 452 451 json_output=is_json_output, 453 452 system_instruction=system_instruction, 454 453 thinking_budget=meta_thinking_budget, 455 454 max_output_tokens=meta_max_output_tokens, 456 455 ) 457 456 else: 458 - result = send_insight( 457 + result = generate_agent_output( 459 458 markdown, 460 459 prompt, 461 460 api_key, 462 461 cache_display_name=cache_display_name, 463 - insight_key=insight_key, 462 + name=name, 464 463 json_output=is_json_output, 465 464 system_instruction=system_instruction, 466 465 thinking_budget=meta_thinking_budget, ··· 473 472 return 474 473 475 474 # Run post-processing hook if present (only for newly generated results) 476 - if (not output_exists or args.force) and insight_meta.get("hook_path"): 477 - hook_path = insight_meta["hook_path"] 475 + if (not output_exists or args.force) and meta.get("hook_path"): 476 + hook_path = meta["hook_path"] 478 477 try: 479 - hook_process = load_insight_hook(hook_path) 478 + hook_process = load_output_hook(hook_path) 480 479 hook_context = { 481 480 "day": args.day, 482 481 "segment": args.segment, 483 482 "multi_segment": multi_segment_mode, 484 - "insight_key": insight_key, 483 + "name": name, 485 484 "output_path": str(output_path), 486 - "insight_meta": dict(insight_meta), 485 + "meta": dict(meta), 487 486 "transcript": markdown, 488 487 } 489 488 hook_result = hook_process(result, hook_context) ··· 508 507 success = True 509 508 510 509 finally: 511 - msg = f"insight {insight_key} {'ok' if success else 'failed'}" 510 + msg = f"generate {name} {'ok' if success else 'failed'}" 512 511 if args.force: 513 512 msg += " --force" 514 513 day_log(args.day, msg)
+8 -8
think/journal_stats.py
··· 12 12 13 13 from observe.sense import scan_day as sense_scan_day 14 14 from observe.utils import VIDEO_EXTENSIONS, load_analysis_frames 15 - from think.insight import scan_day as insight_scan_day 15 + from think.generate import scan_day as generate_scan_day 16 16 from think.utils import day_dirs, get_journal, setup_cli 17 17 18 18 logger = logging.getLogger(__name__) ··· 50 50 for ext in VIDEO_EXTENSIONS: 51 51 files.extend(day_dir.glob(f"*{ext}")) 52 52 53 - insights = day_dir / "insights" 54 - if insights.is_dir(): 55 - files.extend(insights.glob("*.json")) 56 - files.extend(insights.glob("*.md")) 53 + agents_dir = day_dir / "agents" 54 + if agents_dir.is_dir(): 55 + files.extend(agents_dir.glob("*.json")) 56 + files.extend(agents_dir.glob("*.md")) 57 57 58 58 if not files: 59 59 return 0.0 ··· 247 247 stats["pending_segments"] = sense_info["pending_segments"] 248 248 249 249 # --- Insight summaries --- 250 - insight_info = insight_scan_day(day) 251 - stats["insights_processed"] = len(insight_info["processed"]) 252 - stats["insights_pending"] = len(insight_info["repairable"]) 250 + output_info = generate_scan_day(day) 251 + stats["outputs_processed"] = len(output_info["processed"]) 252 + stats["outputs_pending"] = len(output_info["repairable"]) 253 253 254 254 # --- Events and heatmap from facets/*/events/YYYYMMDD.jsonl --- 255 255 weekday = datetime.strptime(day, "%Y%m%d").weekday()
+1 -1
think/mcp.py
··· 71 71 get_resource = register_tool(annotations=HINTS)(get_resource_impl) 72 72 73 73 # Import resource modules - these self-register via @mcp.resource decorators 74 - from think.resources import insights, media, transcripts # noqa: F401 74 + from think.resources import media, outputs, transcripts # noqa: F401 75 75 76 76 77 77 # Phase 2: App-level tool discovery
+20 -20
think/outputs.py
··· 329 329 return chunk_ast(ast) 330 330 331 331 332 - def format_insight( 332 + def format_markdown( 333 333 text: str, 334 334 context: dict[str, Any] | None = None, 335 335 ) -> tuple[list[dict[str, Any]], dict[str, Any]]: ··· 359 359 # --------------------------------------------------------------------------- 360 360 # Extraction Hook Utilities 361 361 # --------------------------------------------------------------------------- 362 - # Shared utilities for insight extraction hooks (occurrence.py, anticipation.py) 362 + # Shared utilities for output extraction hooks (occurrence.py, anticipation.py) 363 363 364 364 import json 365 365 import logging ··· 374 374 """Check if extraction should be skipped and return reason, or None to proceed. 375 375 376 376 Args: 377 - result: The generated insight markdown content. 378 - context: Hook context dict with insight_meta and multi_segment. 377 + result: The generated output markdown content. 378 + context: Hook context dict with meta and multi_segment. 379 379 380 380 Returns: 381 381 Skip reason string if extraction should be skipped, None otherwise. 382 382 """ 383 - insight_meta = context.get("insight_meta", {}) 383 + meta = context.get("meta", {}) 384 384 385 385 # Skip if extraction disabled via journal config 386 - if insight_meta.get("extract") is False: 386 + if meta.get("extract") is False: 387 387 return "extraction disabled via journal config" 388 388 389 389 # Skip for JSON output (output IS the structured data) 390 - if insight_meta.get("output") == "json": 390 + if meta.get("output") == "json": 391 391 return "JSON output (already structured)" 392 392 393 393 # Skip in multi-segment mode ··· 405 405 events: list[dict], 406 406 topic: str, 407 407 occurred: bool, 408 - source_insight: str, 408 + source_output: str, 409 409 capture_day: str, 410 410 ) -> list[Path]: 411 411 """Write events to facet-based JSONL files. ··· 415 415 416 416 Args: 417 417 events: List of event dictionaries from extraction. 418 - topic: Source insight topic (e.g., "meetings", "schedule"). 418 + topic: Source generator topic (e.g., "meetings", "schedule"). 419 419 occurred: True for occurrences, False for anticipations. 420 - source_insight: Relative path to source insight file. 421 - capture_day: Day the insight was captured (YYYYMMDD). 420 + source_output: Relative path to source output file. 421 + capture_day: Day the output was captured (YYYYMMDD). 422 422 423 423 Returns: 424 424 List of paths to written JSONL files. ··· 456 456 enriched = dict(event) 457 457 enriched["topic"] = topic 458 458 enriched["occurred"] = occurred 459 - enriched["source"] = source_insight 459 + enriched["source"] = source_output 460 460 461 461 grouped[key].append(enriched) 462 462 ··· 477 477 return written_paths 478 478 479 479 480 - def compute_source_insight(context: dict) -> str: 481 - """Compute relative source insight path from hook context. 480 + def compute_output_source(context: dict) -> str: 481 + """Compute relative source output path from hook context. 482 482 483 483 Args: 484 - context: Hook context dict with day, segment, insight_key, output_path. 484 + context: Hook context dict with day, segment, name, output_path. 485 485 486 486 Returns: 487 - Relative path like "20240101/insights/meetings.md". 487 + Relative path like "20240101/agents/meetings.md". 488 488 """ 489 - from think.utils import get_insight_topic, get_journal 489 + from think.utils import get_journal, get_output_topic 490 490 491 491 day = context.get("day", "") 492 492 output_path = context.get("output_path", "") 493 - insight_key = context.get("insight_key", "unknown") 493 + name = context.get("name", "unknown") 494 494 journal = get_journal() 495 495 496 496 try: 497 497 return os.path.relpath(output_path, journal) 498 498 except ValueError: 499 499 segment = context.get("segment") 500 - topic = get_insight_topic(insight_key) 500 + topic = get_output_topic(name) 501 501 return os.path.join( 502 502 day, 503 - "insights" if not segment else segment, 503 + "agents" if not segment else segment, 504 504 f"{topic}.md", 505 505 )
+8 -8
think/resources/outputs.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - """MCP resource handlers for insights.""" 4 + """MCP resource handlers for agent outputs.""" 5 5 6 6 from pathlib import Path 7 7 ··· 11 11 from think.utils import get_journal 12 12 13 13 14 - @mcp.resource("journal://insight/{day}/{topic}") 15 - def get_insight(day: str, topic: str) -> TextResource: 16 - """Return the markdown insight for a topic.""" 17 - md_path = Path(get_journal()) / day / "insights" / f"{topic}.md" 14 + @mcp.resource("journal://agents/{day}/{topic}") 15 + def get_agent_output(day: str, topic: str) -> TextResource: 16 + """Return the markdown output for a topic.""" 17 + md_path = Path(get_journal()) / day / "agents" / f"{topic}.md" 18 18 19 19 if not md_path.is_file(): 20 20 text = f"Topic '{topic}' not found for day {day}" ··· 22 22 text = md_path.read_text(encoding="utf-8") 23 23 24 24 return TextResource( 25 - uri=f"journal://insight/{day}/{topic}", 26 - name=f"Insight: {topic} ({day})", 27 - description=f"Insight on {topic} topic from {day}", 25 + uri=f"journal://agents/{day}/{topic}", 26 + name=f"Output: {topic} ({day})", 27 + description=f"Agent output on {topic} from {day}", 28 28 mime_type="text/markdown", 29 29 text=text, 30 30 )
+4 -4
think/resources/transcripts.py
··· 51 51 end=end_time, 52 52 audio=True, 53 53 screen=True, 54 - insights=False, 54 + agents=False, 55 55 ) 56 56 description = f"Raw audio and screencast transcripts from {day} at {time} for {length} minutes" 57 57 elif mode == "audio": ··· 61 61 end=end_time, 62 62 audio=True, 63 63 screen=False, 64 - insights=False, 64 + agents=False, 65 65 ) 66 66 description = ( 67 67 f"Raw audio transcripts from {day} at {time} for {length} minutes" ··· 73 73 end=end_time, 74 74 audio=False, 75 75 screen=True, 76 - insights=False, 76 + agents=False, 77 77 ) 78 78 description = ( 79 79 f"Raw screencast transcripts from {day} at {time} for {length} minutes" ··· 85 85 end=end_time, 86 86 audio=False, 87 87 screen=False, 88 - insights=True, 88 + agents=True, 89 89 ) 90 90 description = ( 91 91 f"AI-generated summaries from {day} at {time} for {length} minutes"
+52 -65
think/utils.py
··· 33 33 """Load raw template files from think/templates/ directory. 34 34 35 35 Templates are cached on first load. Each .md file becomes a template 36 - variable named after its stem (e.g., daily_insight.md -> $daily_insight). 36 + variable named after its stem (e.g., daily_preamble.md -> $daily_preamble). 37 37 38 38 Returns 39 39 ------- ··· 166 166 - Uppercase-first versions: $Pronouns_possessive, $Name, $Bio 167 167 - Templates from think/templates/*.md: 168 168 - Each file becomes a variable named after its stem 169 - - Example: daily_insight.md -> $daily_insight 169 + - Example: daily_preamble.md -> $daily_preamble 170 170 - Templates are pre-processed with identity and context vars, so templates 171 171 can use $date, $preferred, etc. before being substituted into prompts 172 172 ··· 737 737 return (args, extra) if parse_known else args 738 738 739 739 740 - def get_insight_topic(key: str) -> str: 741 - """Convert insight key to filesystem-safe basename (no extension). 740 + def get_output_topic(key: str) -> str: 741 + """Convert agent/generator key to filesystem-safe basename (no extension). 742 742 743 743 Parameters 744 744 ---------- 745 745 key: 746 - Insight key in format "topic" (system) or "app:topic" (app). 746 + Generator key in format "topic" (system) or "app:topic" (app). 747 747 748 748 Returns 749 749 ------- ··· 752 752 753 753 Examples 754 754 -------- 755 - >>> get_insight_topic("activity") 755 + >>> get_output_topic("activity") 756 756 'activity' 757 - >>> get_insight_topic("chat:sentiment") 757 + >>> get_output_topic("chat:sentiment") 758 758 '_chat_sentiment' 759 759 """ 760 760 if ":" in key: ··· 769 769 segment: str | None = None, 770 770 output_format: str | None = None, 771 771 ) -> Path: 772 - """Return output path for insight or agent output. 772 + """Return output path for generator agent output. 773 773 774 - Shared utility for determining where to write insight/agent results. 775 - Used by both think/insight.py and think/cortex.py. 774 + Shared utility for determining where to write generator results. 775 + Used by both think/generate.py and think/cortex.py. 776 776 777 777 Parameters 778 778 ---------- 779 779 day_dir: 780 780 Day directory path (YYYYMMDD). 781 781 key: 782 - Insight key or agent name (e.g., "activity", "chat:sentiment", 782 + Generator key or agent name (e.g., "activity", "chat:sentiment", 783 783 "decisionalizer", "entities:observer"). 784 784 segment: 785 785 Optional segment key (HHMMSS_LEN) for segment-level output. ··· 791 791 Path 792 792 Output file path: 793 793 - With segment: YYYYMMDD/{segment}/{topic}.{ext} 794 - - Without segment: YYYYMMDD/insights/{topic}.{ext} 794 + - Without segment: YYYYMMDD/agents/{topic}.{ext} 795 795 Where topic is derived from key and ext is "json" or "md". 796 796 """ 797 797 day = Path(day_dir) 798 - topic = get_insight_topic(key) 798 + topic = get_output_topic(key) 799 799 ext = "json" if output_format == "json" else "md" 800 800 801 801 if segment: 802 802 # Segment output goes directly in segment directory 803 803 return day / segment / f"{topic}.{ext}" 804 804 else: 805 - # Daily output goes in insights/ subdirectory 806 - return day / "insights" / f"{topic}.{ext}" 805 + # Daily output goes in agents/ subdirectory 806 + return day / "agents" / f"{topic}.{ext}" 807 807 808 808 809 - def _load_insight_metadata(md_path: Path) -> dict[str, object]: 810 - """Load insight metadata from .md file with JSON frontmatter. 809 + def _load_prompt_metadata(md_path: Path) -> dict[str, object]: 810 + """Load prompt metadata from .md file with JSON frontmatter. 811 811 812 812 Parameters 813 813 ---------- ··· 854 854 return info 855 855 856 856 857 - def load_insight_hook(hook_path: str | Path) -> Callable[[str, dict], str | None]: 858 - """Load an insight post-processing hook from a Python file. 857 + def load_output_hook(hook_path: str | Path) -> Callable[[str, dict], str | None]: 858 + """Load an output post-processing hook from a Python file. 859 859 860 860 Hooks are Python modules with a ``process(result, context)`` function that 861 - transforms insight output. The hook is loaded in isolation without polluting 861 + transforms generator output. The hook is loaded in isolation without polluting 862 862 sys.modules. 863 863 864 864 Parameters ··· 882 882 883 883 hook_path = Path(hook_path) 884 884 spec = importlib.util.spec_from_file_location( 885 - f"insight_hook_{hook_path.stem}", hook_path 885 + f"output_hook_{hook_path.stem}", hook_path 886 886 ) 887 887 if spec is None or spec.loader is None: 888 888 raise ImportError(f"Cannot load hook from {hook_path}") ··· 900 900 return process_func 901 901 902 902 903 - def get_insights_config() -> dict[str, dict[str, object]]: 904 - """Return insight overrides from journal config. 905 - 906 - Returns 907 - ------- 908 - dict 909 - Mapping of insight key to override settings (disabled, extract). 910 - Empty dict if no overrides configured. 911 - """ 912 - config = get_config() 913 - return config.get("insights", {}) 903 + def get_generator_agents() -> dict[str, dict[str, object]]: 904 + """Return available generator agents with metadata and config overrides. 914 905 906 + Scans both system generators (muse/) and app generators (apps/*/muse/). 907 + Generators are identified by having a "schedule" field but no "tools" field 908 + in frontmatter (tool agents have tools, generators don't). 915 909 916 - def get_insights() -> dict[str, dict[str, object]]: 917 - """Return available insights with metadata and config overrides. 918 - 919 - Scans both system insights (muse/) and app insights (apps/*/muse/). 920 - Insights are identified by having a "schedule" field but no "tools" field 921 - in frontmatter (agents have tools, insights don't). 922 - 923 - Each key is the insight name: 910 + Each key is the generator name: 924 911 - System: "activity", "meetings" 925 912 - App: "app:topic" (e.g., "chat:sentiment") 926 913 ··· 928 915 from the frontmatter, the file ``mtime``, a ``source`` field 929 916 ("system" or "app"), and any keys loaded from the JSON frontmatter. 930 917 931 - Journal config overrides (from config/journal.json "insights" section) 918 + Journal config overrides (from config/journal.json "agents" section) 932 919 are merged in, allowing ``disabled`` and ``extract`` to be 933 - overridden per insight. 920 + overridden per generator. 934 921 """ 935 - insights: dict[str, dict[str, object]] = {} 922 + generators: dict[str, dict[str, object]] = {} 936 923 937 - # System insights from muse/ (have "schedule" but no "tools") 924 + # System generators from muse/ (have "schedule" but no "tools") 938 925 if MUSE_DIR.is_dir(): 939 926 for md_path in sorted(MUSE_DIR.glob("*.md")): 940 927 name = md_path.stem 941 - info = _load_insight_metadata(md_path) 942 - # Insights have schedule but no tools (agents have tools) 928 + info = _load_prompt_metadata(md_path) 929 + # Generators have schedule but no tools (tool agents have tools) 943 930 if "tools" in info or "schedule" not in info: 944 931 continue 945 932 info["source"] = "system" 946 - insights[name] = info 933 + generators[name] = info 947 934 948 - # App insights from apps/*/muse/ 935 + # App generators from apps/*/muse/ 949 936 apps_dir = Path(__file__).parent.parent / "apps" 950 937 if apps_dir.is_dir(): 951 938 for app_path in sorted(apps_dir.iterdir()): ··· 956 943 continue 957 944 app_name = app_path.name 958 945 for md_path in sorted(app_muse_dir.glob("*.md")): 959 - info = _load_insight_metadata(md_path) 960 - # Insights have schedule but no tools (agents have tools) 946 + info = _load_prompt_metadata(md_path) 947 + # Generators have schedule but no tools (tool agents have tools) 961 948 if "tools" in info or "schedule" not in info: 962 949 continue 963 950 topic = md_path.stem 964 951 key = f"{app_name}:{topic}" 965 952 info["source"] = "app" 966 953 info["app"] = app_name 967 - insights[key] = info 954 + generators[key] = info 968 955 969 956 # Merge journal config overrides 970 - overrides = get_insights_config() 957 + overrides = get_config().get("agents", {}) 971 958 for key, override in overrides.items(): 972 - if key in insights and isinstance(override, dict): 959 + if key in generators and isinstance(override, dict): 973 960 # Only merge known override fields 974 961 if "disabled" in override: 975 - insights[key]["disabled"] = override["disabled"] 962 + generators[key]["disabled"] = override["disabled"] 976 963 if "extract" in override: 977 - insights[key]["extract"] = override["extract"] 964 + generators[key]["extract"] = override["extract"] 978 965 979 - return insights 966 + return generators 980 967 981 968 982 - def get_insights_by_schedule( 969 + def get_generator_agents_by_schedule( 983 970 schedule: str, 984 971 *, 985 972 include_disabled: bool = False, 986 973 ) -> dict[str, dict[str, object]]: 987 - """Return insights matching the given schedule. 974 + """Return generator agents matching the given schedule. 988 975 989 976 Args: 990 977 schedule: Target schedule (e.g., "segment" or "daily"). 991 - include_disabled: If True, include disabled insights (for settings UI). 978 + include_disabled: If True, include disabled generators (for settings UI). 992 979 Default False (for processing pipelines). 993 980 994 981 Returns: 995 - Dict of insight_key -> metadata for insights where schedule matches. 982 + Dict of generator_key -> metadata for generators where schedule matches. 996 983 """ 997 - all_insights = get_insights() 984 + all_generators = get_generator_agents() 998 985 result: dict[str, dict[str, object]] = {} 999 986 1000 - for key, meta in all_insights.items(): 987 + for key, meta in all_generators.items(): 1001 988 if not include_disabled and meta.get("disabled", False): 1002 989 continue 1003 990 if meta.get("schedule") == schedule: ··· 1038 1025 "sources": { 1039 1026 "audio": True, 1040 1027 "screen": True, 1041 - "insights": False, 1028 + "agents": False, 1042 1029 }, 1043 1030 } 1044 1031 ··· 1108 1095 Optional dict from .json "instructions" key. Supported keys: 1109 1096 - "system": prompt name for system instruction (default: "journal") 1110 1097 - "facets": "none" | "short" | "detailed" (default: "short") 1111 - - "sources": {"audio": bool, "screen": bool, "insights": bool} 1098 + - "sources": {"audio": bool, "screen": bool, "agents": bool} 1112 1099 1113 1100 Returns 1114 1101 ------- ··· 1118 1105 - system_prompt_name: str - name of system prompt (for cache keys) 1119 1106 - user_instruction: str | None - loaded from user_prompt if provided 1120 1107 - extra_context: str | None - facets + datetime 1121 - - sources: dict - {"audio": bool, "screen": bool, "insights": bool} 1108 + - sources: dict - {"audio": bool, "screen": bool, "agents": bool} 1122 1109 """ 1123 1110 # Merge defaults with overrides 1124 1111 cfg = _merge_instructions_config(_DEFAULT_INSTRUCTIONS, config_overrides)