diff --git a/apps/activities/call.py b/apps/activities/call.py index e01a02916..f245b3160 100644 --- a/apps/activities/call.py +++ b/apps/activities/call.py @@ -131,6 +131,7 @@ def _list_records_for_days( *, activity: str | None, entity: str | None, + source: str | None, include_hidden: bool, ) -> list[dict[str, Any]]: matches: list[dict[str, Any]] = [] @@ -142,6 +143,8 @@ def _list_records_for_days( ): if activity and record.get("activity") != activity: continue + if source and record.get("source") != source: + continue if entity_query: active_entities = record.get("active_entities", []) if not any( @@ -200,6 +203,11 @@ def list_records( "--entity", help="Filter by active entity.", ), + source: str | None = typer.Option( + None, + "--source", + help="Filter by record source: anticipated, user, or cogitate.", + ), include_all: bool = typer.Option( False, "--all", @@ -221,12 +229,20 @@ def list_records( else: resolved_days = [resolve_sol_day(None)] + if source and source not in {"anticipated", "cogitate", "user"}: + typer.echo( + "Error: --source must be 'anticipated', 'cogitate', or 'user'.", + err=True, + ) + raise typer.Exit(1) + facets = _resolve_list_facets(facet) records = _list_records_for_days( facets, resolved_days, activity=activity, entity=entity, + source=source, include_hidden=include_all, ) diff --git a/apps/activities/tests/test_call.py b/apps/activities/tests/test_call.py index 6ef5066a2..3fe49f0c7 100644 --- a/apps/activities/tests/test_call.py +++ b/apps/activities/tests/test_call.py @@ -109,6 +109,59 @@ def test_list_filters_by_entity(activities_env): assert [item["id"] for item in payload] == ["coding_090000_300"] +def test_list_filters_by_source(activities_env): + activities_env( + [ + { + "id": "anticipated_call_103000_0421", + "activity": "call", + "title": "Mari intro", + "description": "Planned", + "target_date": "2026-04-21", + "source": "anticipated", + "created_at": 1, + }, + { + "id": "coding_090000_300", + "activity": "coding", + "title": "Focused coding", + "description": "User created", + "source": "user", + "created_at": 2, + }, + { + "id": "meeting_100000_300", + "activity": "meeting", + "title": "Synthesized meeting", + "description": "Cogitate created", + "source": "cogitate", + "created_at": 3, + }, + ] + ) + + result = runner.invoke( + call_app, + ["activities", "list", "--facet", "work", "--source", "anticipated", "--json"], + ) + + assert result.exit_code == 0 + payload = json.loads(result.output) + assert [item["id"] for item in payload] == ["anticipated_call_103000_0421"] + + +def test_list_rejects_unknown_source(activities_env): + activities_env([]) + + result = runner.invoke( + call_app, + ["activities", "list", "--facet", "work", "--source", "calendar"], + ) + + assert result.exit_code == 1 + assert "--source must be 'anticipated', 'cogitate', or 'user'" in result.output + + def test_get_returns_hidden_record_with_json_output(activities_env): activities_env( [ diff --git a/apps/home/routes.py b/apps/home/routes.py index 47ad03c23..7e4ab7649 100644 --- a/apps/home/routes.py +++ b/apps/home/routes.py @@ -303,6 +303,7 @@ def _collect_todos(today: str) -> list[dict[str, Any]]: def _collect_events(today: str) -> list[dict[str, Any]]: """Collect calendar events across all facets.""" + from think.activities import load_activity_records from think.indexer.journal import get_events try: @@ -312,6 +313,30 @@ def _collect_events(today: str) -> list[dict[str, Any]]: event["start"] = "" if event.get("end") is None: event["end"] = "" + + for facet_name in get_facets(): + for record in load_activity_records(facet_name, today): + if record.get("source") != "anticipated": + continue + + participants = [] + for entry in record.get("participation", []): + if not isinstance(entry, dict) or entry.get("role") != "attendee": + continue + name = str(entry.get("name") or "").strip() + if name: + participants.append(name) + + events.append( + { + "title": record.get("title", ""), + "start": record.get("start") or "", + "end": record.get("end") or "", + "facet": facet_name, + "occurred": False, + "participants": participants, + } + ) return events except Exception: logger.warning("home: failed to collect events", exc_info=True) @@ -335,6 +360,8 @@ def _collect_activities(today: str) -> list[dict[str, Any]]: for facet_name in facets: records = load_activity_records(facet_name, today) for record in records: + if record.get("source") == "anticipated": + continue created_at = record.get("created_at", 0) if created_at < cutoff_ts: continue diff --git a/apps/settings/routes.py b/apps/settings/routes.py index 104b520aa..1ce7da6a9 100644 --- a/apps/settings/routes.py +++ b/apps/settings/routes.py @@ -432,12 +432,11 @@ def get_providers() -> Any: if "schedule" in info: context_defaults[context_key]["schedule"] = info["schedule"] context_defaults[context_key]["disabled"] = info.get("disabled", False) - # Include extract for generators with occurrence/anticipation hooks + # Include extract for generators with occurrence hooks hook = info.get("hook") has_extraction = ( - isinstance(hook, dict) - and hook.get("post") in ("occurrence", "anticipation") - ) or hook in ("occurrence", "anticipation") + isinstance(hook, dict) and hook.get("post") in ("occurrence",) + ) or hook in ("occurrence",) if has_extraction: context_defaults[context_key]["extract"] = info.get("extract", True) @@ -929,11 +928,11 @@ def _build_generator_info(key: str, info: dict) -> dict: Transforms talent config metadata into the format expected by the Settings UI Insights section. """ - # Determine if extraction is supported (occurrence/anticipation hooks) + # Determine if extraction is supported (occurrence hooks) hook = info.get("hook") has_extraction = ( - isinstance(hook, dict) and hook.get("post") in ("occurrence", "anticipation") - ) or hook in ("occurrence", "anticipation") + isinstance(hook, dict) and hook.get("post") in ("occurrence",) + ) or hook in ("occurrence",) return { "key": key, diff --git a/docs/APPS.md b/docs/APPS.md index a98f7e590..02c127183 100644 --- a/docs/APPS.md +++ b/docs/APPS.md @@ -294,7 +294,7 @@ Define custom generator prompts that integrate with solstone's output generation **Event extraction via hooks:** To extract structured events from generator output, use the `hook` field: - `"hook": {"post": "occurrence"}` - Extracts past events to `facets/{facet}/events/{day}.jsonl` -- `"hook": {"post": "anticipation"}` - Extracts future scheduled events +- `"hook": {"post": "schedule"}` - Writes future scheduled items as anticipated activity records The `occurrences` field (optional string) provides agent-specific extraction guidance when using the occurrence hook. Example: @@ -356,7 +356,7 @@ See `docs/coding-standards.md` L8/L9 for the broader principles. **Reference implementations:** - System generator templates: `talent/*.md` (files with `schedule` field but no `tools` field) -- Extraction hooks: `talent/occurrence.py`, `talent/anticipation.py` +- Event/schedule hooks: `talent/occurrence.py`, `talent/schedule.py` - Discovery logic: `think/talent.py` - `get_talent_configs(has_tools=False)`, `get_output_name()` - Hook loading: `think/talent.py` - `load_pre_hook()`, `load_post_hook()` diff --git a/docs/CORTEX.md b/docs/CORTEX.md index 6e9313c96..cda2d2be8 100644 --- a/docs/CORTEX.md +++ b/docs/CORTEX.md @@ -336,7 +336,7 @@ When an agent has `"multi_facet": true`: #### Daily Multi-Facet Agents -**Active Facet Detection**: By default, daily multi-facet agents only run for facets that had activity the previous day. Activity is determined by the presence of occurrence events (not anticipations) in `facets/{facet}/events/{day}.jsonl`. This prevents unnecessary agent runs for inactive facets. +**Active Facet Detection**: By default, daily multi-facet agents only run for facets that had activity the previous day. Activity is determined by the presence of occurrence events in `facets/{facet}/events/{day}.jsonl`. This prevents unnecessary agent runs for inactive facets. To force an agent to run for all facets regardless of activity, set `"always": true`: diff --git a/docs/THINK.md b/docs/THINK.md index 9d67a587f..36d77c1b0 100644 --- a/docs/THINK.md +++ b/docs/THINK.md @@ -183,7 +183,7 @@ returns a dictionary keyed by generator name. Each entry contains: - `mtime` – modification time of the `.md` file - Additional keys from JSON frontmatter such as `title`, `description`, `hook`, or `load` -The `hook` field enables event extraction by invoking named hooks like `"occurrence"` or `"anticipation"`. +The `hook` field enables output processing by invoking named hooks like `"occurrence"` or `"schedule"`. The `load` key controls transcript/percept/agent source filtering for generators. See [APPS.md](APPS.md#prompt-context-configuration) for the full schema. diff --git a/routines/templates/decision-review.md b/routines/templates/decision-review.md index 8b3c4a7ad..ff5a0fb3c 100644 --- a/routines/templates/decision-review.md +++ b/routines/templates/decision-review.md @@ -15,7 +15,7 @@ This is not a summary — it's a mirror. The goal is to help the owner see their 1. Use `sol call journal search "" -a decisions --day-from START --day-to END -n 20` for the past 30 days of decision agent output. 2. Use `sol call journal search "" -a pulse --day-from START --day-to END -n 15` for narrative context around major decisions. 3. Use `sol call entities intelligence PERSON` for people involved in the most consequential decisions. -4. Use `sol call calendar list YYYYMMDD` for days with major decisions to see what else was happening. +4. Use `sol call activities list --source anticipated --day YYYYMMDD` for days with major decisions to see what else was happening. 5. Use `sol call identity partner` for the owner's known decision style. ## Synthesize diff --git a/routines/templates/energy-audit.md b/routines/templates/energy-audit.md index 80800e315..1c7986760 100644 --- a/routines/templates/energy-audit.md +++ b/routines/templates/energy-audit.md @@ -10,7 +10,7 @@ You are preparing a weekly energy audit — a reflection on where the owner's ti ## Gather -1. Use `sol call calendar list YYYYMMDD` for each of the past 7 days to map meeting load. +1. Use `sol call activities list --source anticipated --day YYYYMMDD` for each of the past 7 days to map scheduled load. 2. Use `sol call journal search "" --day-from START --day-to END -n 30` to survey activity patterns. 3. Use `sol call todos list` to compare intended work against actual activity. 4. Use `sol call identity pulse` for the current state narrative. diff --git a/routines/templates/meeting-prep.md b/routines/templates/meeting-prep.md index 306442b3f..b6c8d33f9 100644 --- a/routines/templates/meeting-prep.md +++ b/routines/templates/meeting-prep.md @@ -13,7 +13,7 @@ The routine prompt already includes an `Upcoming Event` section with the title, ## Gather 1. Read the upcoming event details in the prompt carefully. -2. If you need broader context, call `sol call calendar list $day_YYYYMMDD` to see the surrounding schedule. +2. If you need broader context, call `sol call activities list --source anticipated --day $day_YYYYMMDD` to see the surrounding schedule. 3. For each listed participant, call `sol call entities intelligence PERSON --brief`. 4. Use `sol call journal search QUERY -n 10` to look for recent mentions of the meeting topic, project, or participants. 5. If a configured facet seems especially relevant, use `sol call journal news FACET --day $day_YYYYMMDD`. diff --git a/routines/templates/monthly-patterns.md b/routines/templates/monthly-patterns.md index 467047808..820e4bd54 100644 --- a/routines/templates/monthly-patterns.md +++ b/routines/templates/monthly-patterns.md @@ -16,7 +16,7 @@ Work at the month scale: look for durable changes in attention, habits, projects 2. Use `sol call journal search "" --day-from START --day-to END -n 40` to survey the month across the configured facets. 3. Use `sol call journal news FACET --day YYYYMMDD` for representative weekly or recent snapshots when they help summarize a facet. 4. Use `sol call entities intelligence PERSON` for people who appear central to the month. -5. Use `sol call calendar list YYYYMMDD` on representative days if calendar load seems important. +5. Use `sol call activities list --source anticipated --day YYYYMMDD` on representative days if scheduled load seems important. 6. Use `sol call identity pulse` to compare month-long patterns against the current state narrative. ## Synthesize diff --git a/routines/templates/morning-briefing.md b/routines/templates/morning-briefing.md index 031274733..9191add60 100644 --- a/routines/templates/morning-briefing.md +++ b/routines/templates/morning-briefing.md @@ -13,7 +13,7 @@ This is not a conversation. Gather the information, synthesize it, and write a c ## Gather 1. Call `sol call journal facets` to see the active facets if you need broader context. -2. Call `sol call calendar list $day_YYYYMMDD` to review today's events and participants. +2. Call `sol call activities list --source anticipated --day $day_YYYYMMDD` to review today's scheduled items and participants. 3. Call `sol call todos list` to see pending action items across facets. 4. Call `sol call identity pulse` to capture current narrative, priorities, and needs-you items. 5. Call `sol call journal search "" -a followups -n 10` to find recent follow-up items. diff --git a/routines/templates/weekly-review.md b/routines/templates/weekly-review.md index e91a0042d..779f91447 100644 --- a/routines/templates/weekly-review.md +++ b/routines/templates/weekly-review.md @@ -15,7 +15,7 @@ Gather evidence from the journal first, then synthesize a reflective but actiona 1. Use `sol call journal facets` to identify the facets in scope. 2. Use `sol call journal search "" --day-from $day_minus_7_YYYYMMDD --day-to $day_YYYYMMDD -n 25` to find notable entries and themes. 3. Use `sol call todos list` to review outstanding work and infer what likely got completed or deferred. -4. Use `sol call calendar list YYYYMMDD` across the last 7 days to understand meeting load and major time commitments. +4. Use `sol call activities list --source anticipated --day YYYYMMDD` across the last 7 days to understand scheduled load and major time commitments. 5. Use `sol call identity pulse` for the current state narrative. 6. Use `sol call journal news FACET --day YYYYMMDD` for any facet that needs a richer summary. diff --git a/skills/solstone/SKILL.md b/skills/solstone/SKILL.md index 818eaec44..496e1958d 100644 --- a/skills/solstone/SKILL.md +++ b/skills/solstone/SKILL.md @@ -69,7 +69,7 @@ sol call journal events sol call todos upcoming # Calendar events for today -sol call calendar list +sol call activities list --source anticipated # Latest facet news sol call journal news "" @@ -144,7 +144,7 @@ For richer answers, combine multiple commands: ```bash sol call journal events sol call todos upcoming -sol call calendar list +sol call activities list --source anticipated sol call entities strength --since $(date +%Y%m%d) ``` diff --git a/talent/anticipation.md b/talent/anticipation.md deleted file mode 100644 index f218b0079..000000000 --- a/talent/anticipation.md +++ /dev/null @@ -1,97 +0,0 @@ -{ - - "title": "Anticipation Extraction", - "description": "Extracts structured anticipation events (future scheduled items) from insight summaries.", - "color": "#4527a0" - -} - -# Anticipation JSON Conversion - -## Objective - -Extract future scheduled events from a Markdown summary and convert them into structured JSON anticipations. These are events that have not yet occurred but are planned or scheduled for future dates. - -## Instructions -1. **Extract every distinct future event** mentioned in the summary - meetings, deadlines, appointments, etc. -2. **Be comprehensive** - capture calendar events, scheduled meetings, deadlines, personal appointments, recurring events, travel, etc. -3. **Preserve date and timing information** - always extract the scheduled date, and time if known -4. **Handle uncertainty gracefully** - use `null` for start/end times when not specified -5. **Assign facets** - for every anticipation, choose the best matching facet from the Available Facets context. This field is required. -6. **Return only valid JSON** - no commentary, explanations, or wrapper objects -7. **Handle empty sources** - if the source indicates no future events, return an empty array: `[]` - -## Anticipation Fields -- **type** – the kind of event such as `meeting`, `deadline`, `appointment`, `event`, `travel`, `reminder`, etc. -- **date** – ISO date (YYYY-MM-DD) when the event is scheduled to occur. Required. -- **start** and **end** – HH:MM:SS timestamps for the event time, or `null` if time is unknown/TBD -- **title** – short descriptive title for display -- **summary** – concise one-sentence description of what is planned -- **work** – boolean classification: `true` for work-related, `false` for personal -- **participants** – optional list of people or entities involved (empty array if none) -- **facet** – required facet identifier; use the facet name/ID from Available Facets context -- **details** – free-form string capturing location, agenda, preparation notes, or other context - -## Handling Time Uncertainty -- If exact time is known (e.g., "1:00 PM"): use `"start": "13:00:00"` -- If time is vague (e.g., "Morning", "Afternoon"): use `null` and mention in details -- If time is TBD or not specified: use `null` -- If it's an all-day event: use `null` for both start and end - -## Handling Recurring Events -For recurring events (e.g., "every Wednesday"), extract the next upcoming instance with its specific date. Mention the recurrence pattern in details. - -## Output Format -Return a JSON array of anticipations only. Each anticipation must include all required fields. - -## Example -[ - { - "type": "meeting", - "date": "2025-11-05", - "start": "13:00:00", - "end": "14:30:00", - "title": "Meeting with Center for the Blind", - "summary": "Introductory meeting to discuss potential collaboration on AI-powered tools.", - "work": true, - "participants": ["Jack Anderson", "Center for the Blind Representatives"], - "facet": "blind_center", - "details": "Virtual meeting. Topics include grant writing and building an AI brain from their documents." - }, - { - "type": "deadline", - "date": "2025-11-10", - "start": null, - "end": null, - "title": "Airport System Deployment Target", - "summary": "Target date for deploying the cleaned-up UI and new restart scripts.", - "work": true, - "participants": ["Mitch Baumgartner"], - "facet": "aviation_networks", - "details": "Software deployment. No specific time set." - }, - { - "type": "event", - "date": "2025-10-27", - "start": null, - "end": null, - "title": "FMDS Partner Workshop", - "summary": "Multi-day in-person workshop with partners running through October 30.", - "work": true, - "participants": ["L3Harris", "GDIT", "Frequentis", "uAvionix"], - "facet": "fmds", - "details": "In-person, security clearance required. Day 1: Win strategy and demo dry run. Day 2: Solution definition. Day 3: Prototype planning. Runs Oct 27-30." - }, - { - "type": "appointment", - "date": "2025-11-15", - "start": null, - "end": null, - "title": "Gymnastics Meet", - "summary": "First gymnastics meet of the season.", - "work": false, - "participants": ["Blade"], - "facet": "family", - "details": "Location: Marshaltown Community Center." - } -] diff --git a/talent/anticipation.py b/talent/anticipation.py deleted file mode 100644 index af34df457..000000000 --- a/talent/anticipation.py +++ /dev/null @@ -1,101 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Hook for extracting anticipation events from generator output results. - -This hook is invoked via "hook": {"post": "anticipation"} in generator frontmatter. -It extracts structured JSON events for future scheduled items and writes -them to facet-based JSONL files. -""" - -import json -import logging -from pathlib import Path - -from think.facets import facet_summaries -from think.hooks import ( - compute_output_source, - log_extraction_failure, - should_skip_extraction, - write_events_jsonl, -) -from think.models import generate -from think.prompts import load_prompt -from think.talent import get_output_name - - -def post_process(result: str, context: dict) -> str | None: - """Extract anticipation events from generator output result. - - This hook extracts structured JSON events for future scheduled items - from markdown output summaries and writes them to facet-based JSONL files. - - Args: - result: The generated output markdown content. - context: Config dict with keys including day, segment, name, - output_path, meta, transcript, span, span_mode. - - Returns: - None - this hook does not modify the output result. - """ - # Check skip conditions - skip_reason = should_skip_extraction(result, context) - if skip_reason: - logging.info("Skipping anticipation extraction: %s", skip_reason) - return None - - # Load extraction prompt - prompt_content = load_prompt("anticipation", base_dir=Path(__file__).parent) - - # Build context with facets (anticipations don't have agent-specific instructions) - facets_context = facet_summaries(detailed=True) - - # Extract events - name = context.get("name", "unknown") - contents = [facets_context, result] - - try: - response_text = generate( - contents=contents, - context=f"talent.system.{name}", - temperature=0.3, - max_output_tokens=24576, - thinking_budget=0, - system_instruction=prompt_content.text, - json_output=True, - ) - except Exception as e: - log_extraction_failure(e, name) - return None - - try: - events = json.loads(response_text) - except json.JSONDecodeError as e: - logging.error("Invalid JSON from anticipation extraction: %s", e) - return None - - if not isinstance(events, list): - logging.error("Extraction did not return array") - return None - - # Write to facet JSONL files (occurred=False for anticipations) - source_output = compute_output_source(context) - output_name = get_output_name(name) - day = context.get("day", "") - - written_paths = write_events_jsonl( - events=events, - agent=output_name, - occurred=False, - source_output=source_output, - capture_day=day, - ) - - if written_paths: - print(f"Events written to {len(written_paths)} JSONL file(s):") - for p in written_paths: - print(f" {p}") - else: - print("No events with valid facets to write") - - return None # Don't modify insight result diff --git a/talent/awareness_tender.md b/talent/awareness_tender.md index bd57add33..8f2d8564e 100644 --- a/talent/awareness_tender.md +++ b/talent/awareness_tender.md @@ -21,7 +21,7 @@ Read current state using these tools: 1. `sol call awareness status` — processing, import, and journal state 2. `sol call identity self` — identity summary (skim for key changes) -3. `sol call calendar list` — today's events +3. `sol call activities list --source anticipated` — today's scheduled activity records 4. `sol call routines list` — active routines and recent outputs 5. `sol call entities search --since --limit 5` — recent entity activity ## Write awareness.md diff --git a/talent/journal/references/captures.md b/talent/journal/references/captures.md index a6df1f0ee..424c8df84 100644 --- a/talent/journal/references/captures.md +++ b/talent/journal/references/captures.md @@ -229,25 +229,20 @@ The vision analysis uses multi-stage conditional processing: ### Event extracts -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`. - -There are two types of events: -- **Occurrences** – events that happened on the capture day (`occurred: true`) -- **Anticipations** – future scheduled events extracted from calendar views (`occurred: false`) +Generator output processing extracts time-based events from the day's transcripts—meetings, messages, follow-ups, file activity and more. Occurrence events are stored per-facet in JSONL files at `facets/{facet}/events/{day}.jsonl`. Future scheduled items from the schedule talent are stored as anticipated activity records under `facets/{facet}/activities/{target_day}.jsonl` with `source: "anticipated"`. ```jsonl {"type": "meeting", "start": "09:00:00", "end": "09:30:00", "title": "Team stand-up", "summary": "Status update with the engineering team", "work": true, "participants": ["Jeremie Miller", "Alice", "Bob"], "facet": "work", "agent": "meetings", "occurred": true, "source": "20250101/talents/meetings.md", "details": "Sprint planning discussion"} -{"type": "deadline", "date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "summary": "Q1 deliverable due", "work": true, "participants": [], "facet": "work", "agent": "schedule", "occurred": false, "source": "20250101/talents/schedule.md", "details": "Final review before release"} +{"id": "anticipated_deadline_000000_0115", "activity": "deadline", "target_date": "2025-01-15", "start": null, "end": null, "title": "Project milestone", "description": "Q1 deliverable due.", "details": "Final review before release", "facet": "work", "source": "anticipated", "active_entities": [], "participation": [], "participation_confidence": 0.5, "cancelled": false} ``` **Common fields:** - **type** – event kind: `meeting`, `message`, `file`, `followup`, `documentation`, `research`, `media`, `deadline`, `appointment`, etc. -- **start** and **end** – HH:MM:SS timestamps (or `null` for anticipations without specific times) -- **date** – ISO date YYYY-MM-DD (anticipations only, indicates scheduled date) +- **start** and **end** – HH:MM:SS timestamps (or `null` when a time is not known) - **title** and **summary** – short text for display and search - **facet** – facet name the event belongs to (required) -- **agent** – source generator type (e.g., "meetings", "schedule", "flow") -- **occurred** – `true` for occurrences, `false` for anticipations +- **agent** – source generator type for occurrence events (e.g., "meetings", "flow") +- **occurred** – `true` for occurrence event rows in `facets/*/events/*.jsonl` - **source** – path to the output file that generated this event - **work** – boolean, work vs. personal classification - **participants** – optional list of people or entities involved diff --git a/talent/journal/references/config.md b/talent/journal/references/config.md index 4d65450c5..3761bf1d9 100644 --- a/talent/journal/references/config.md +++ b/talent/journal/references/config.md @@ -298,7 +298,7 @@ Other contexts follow the pattern `{module}.{feature}[.{operation}]`: - `tier` (integer) – Tier number (optional). - `model` (string) – Explicit model name (optional, overrides tier). - `disabled` (boolean) – Disable this talent config (optional, talent contexts only). -- `extract` (boolean) – Enable/disable event extraction for generators with occurrence/anticipation hooks (optional). +- `extract` (boolean) – Enable/disable event extraction for generators with occurrence hooks (optional). **models** – Per-provider tier overrides. Maps provider name to tier-model mappings: ```json diff --git a/talent/morning_briefing.md b/talent/morning_briefing.md index c15280f65..94227b8b7 100644 --- a/talent/morning_briefing.md +++ b/talent/morning_briefing.md @@ -21,14 +21,14 @@ Call all sources upfront. Some may return empty — that's expected, especially 1. `sol call journal facets` — list active facets 2. For each facet: `sol call journal news FACET --day $day_YYYYMMDD` — facet newsletter -3. `sol call calendar list $day_YYYYMMDD` — today's events with participants +3. `sol call activities list --source anticipated --day $day_YYYYMMDD` — today's scheduled items with participants 4. `sol call todos list` — pending action items across all facets 5. `sol call identity pulse` — current pulse narrative and needs-you items 6. `sol call identity partner` — owner behavioral profile (informs tone and emphasis) 7. `sol call journal search "" -d $day_YYYYMMDD -a followups -n 10` — follow-up items from today -8. `sol call journal search "" --day-from $day_YYYYMMDD -a anticipation -n 5` — forward-looking anticipations +8. `sol call activities list --source anticipated --from $day_YYYYMMDD --to <+7>` — forward-looking scheduled items 9. `sol call journal search "" -d $day_YYYYMMDD -a decisions -n 10` — yesterday's consequential decisions -10. For each of the next 7 days after today: `sol call calendar list YYYYMMDD` — upcoming events for forward look +10. For each of the next 7 days after today: `sol call activities list --source anticipated --day YYYYMMDD` — upcoming scheduled items for forward look For each person appearing in today's calendar events, also run: 11. `sol call entities intelligence PERSON --brief` — relationship context, recent interactions, observations (brief mode: last 20 signals + top 20 network, ~95% smaller payload) @@ -97,9 +97,9 @@ Grade highlights by evidence strength. **High** (corroborated by multiple source Attribute commitments and follow-ups to the originating segment: `(committed [date](sol://...))`, `(flagged [date](sol://...))`. For relationship items: `(last interaction [date])`. For inferred items: `(inferred from [source](sol://...))`. Grade action items by evidence strength. **High** (explicit commitment with date, or overdue todo): state assertively — "Follow up on Series A term sheet — committed March 20, now overdue." **Medium** (flagged by followups agent with moderate confidence, or clear single-source item): present with attribution — "Review CI pipeline logs (flagged yesterday)." **Low** (inferred obligation from ambiguous mention, or low-confidence followup): hedge — "Possible commitment to send deck to investors" or "May need to follow up on the API discussion." When upstream followup output includes a `Confidence:` score, use it: 0.85+ high, 0.50–0.84 medium, below 0.50 low. Never hedge explicit commitments with clear dates; never present inferred obligations as definite action items. -**Forward Look** — What's coming. Draw from anticipation agent output and upcoming calendar events (next 7 days). Note preparation needed for upcoming meetings or deadlines. -Attribute anticipation items: `(from [anticipation](sol://...))`. Data source: anticipation search result `id` path. -Grade forward items by evidence strength. **High** (confirmed calendar event or explicit deadline): state assertively — "Board meeting Thursday — slides due Wednesday." **Medium** (anticipation agent item with clear basis): attribute and present — "Anticipation agent flagged quarterly review prep based on last quarter's timing." **Low** (speculative anticipation, inferred deadline, or pattern-based prediction): hedge — "Possible need to prepare for investor update" or "May want to schedule design review based on sprint cadence." Never hedge confirmed calendar events or explicit deadlines; never state pattern-based predictions as confirmed plans. +**Forward Look** — What's coming. Draw from anticipated activity records and upcoming scheduled items (next 7 days). Note preparation needed for upcoming meetings or deadlines. +Attribute schedule-derived items: `(from [schedule](sol://...))`. Data source: `sol call activities list --source anticipated` or the schedule talent output path. +Grade forward items by evidence strength. **High** (confirmed scheduled item or explicit deadline): state assertively — "Board meeting Thursday — slides due Wednesday." **Medium** (schedule-derived activity record with clear basis): attribute and present — "Schedule extraction flagged quarterly review prep based on last quarter's timing." **Low** (speculative schedule inference or pattern-based prediction): hedge — "Possible need to prepare for investor update" or "May want to schedule design review based on sprint cadence." Never hedge confirmed scheduled items or explicit deadlines; never state pattern-based predictions as confirmed plans. **Reading** — Links to full facet newsletters for deep dives. List each active facet that has a newsletter for the analysis day, with a brief one-line description of what it covers. This is the "detailed edition" for owners who want the full picture. Only include if facet newsletters exist. @@ -141,7 +141,7 @@ gaps: [list of gap descriptions, or empty list [] if none] ## Forward Look - Board meeting Thursday — slides need review (confirmed on [calendar](sol://20260327/calendar)) -- May want to prepare quarterly metrics based on last quarter's timing (from [anticipation](sol://20260327/talents/anticipation)) +- May want to prepare quarterly metrics based on last quarter's timing (from [schedule](sol://20260327/talents/schedule)) [more items...] ## Reading diff --git a/talent/partner.md b/talent/partner.md index b30402eee..7de9785f3 100644 --- a/talent/partner.md +++ b/talent/partner.md @@ -36,7 +36,7 @@ and query each source. If a source returns empty or errors, skip it — gaps are 1. `sol call entities strength --since YYYYMMDD` (7 days back) — relationship activity 2. For each of the past 7 days: - - `sol call calendar list YYYYMMDD` — schedule patterns + - `sol call activities list --source anticipated --day YYYYMMDD` — scheduled activity patterns - `sol call todos list -d YYYYMMDD` — task patterns 3. For each active facet (from `sol call journal facets`): - `sol call journal news FACET --day YYYYMMDD` (most recent day available) — work themes diff --git a/talent/schedule.md b/talent/schedule.md index 20154aa77..e2dbc367c 100644 --- a/talent/schedule.md +++ b/talent/schedule.md @@ -2,73 +2,163 @@ "type": "generate", "title": "Upcoming Schedule", - "description": "Identifies all future calendar events and scheduled activities noted in transcripts. Extracts dates, times, participants, and event details for anything scheduled beyond today.", - "hook": {"post": "anticipation"}, + "description": "Extracts future scheduled events and calendar activities into structured anticipation records. Captures dates, times, participants, and cancellation state.", + "hook": {"post": "schedule"}, "color": "#5e35b1", "schedule": "daily", "priority": 10, - "output": "md", + "output": "json", "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} - } $daily_preamble # Future Schedule Extraction -**Input:** A markdown file containing chronologically ordered transcripts of a workday. The transcript combines audio recordings and screen activity organized by recording segments. - -**Objective:** Identify and extract all future scheduled events and calendar entries visible or mentioned throughout the day. Focus exclusively on events scheduled for dates beyond today - not current day or historical events. - -**Instructions:** - -1. **Scan for Calendar Views:** Review all screen activity descriptions for: - - Calendar applications (Google Calendar, Outlook, Apple Calendar, etc.) - - Scheduling interfaces (Calendly, meeting schedulers, etc.) - - Email invitations showing future dates - - Project management tools with scheduled deadlines - - Any interface displaying future dates and times - -2. **Extract Event Details:** For each future scheduled item identified, capture: - - **Date & Time:** Full date and time information if available - - **Event Title:** The name or subject of the scheduled event - - **Participants:** Meeting attendees or people involved if visible - - **Location:** Physical location or meeting link/platform - - **Duration:** Expected length of the event - - **Description:** Any additional context or agenda items visible - -3. **Organize Chronologically:** Sort all identified events by their scheduled date and time, nearest future events first. - -4. **Focus on the Future:** - - Include only events scheduled for tomorrow and beyond - - Exclude today's events (already happened or happening) - - Exclude past events visible in calendar history - - Include recurring events' future instances if visible - -## Output Format - -Produce a clean Markdown document with: - -### Header Section -A brief overview stating the date range of scheduled events found and total count. - -### Event Listings -For each future event, create a formatted entry with: -- **Date:** [Day, Full Date] -- **Time:** [Start - End time with timezone if available] -- **Event:** [Title/Subject] -- **Participants:** [List of attendees if visible] -- **Location:** [Physical/Virtual location] -- **Notes:** [Any additional context, agenda items, or preparation needed] - -### Summary -Conclude with a brief summary highlighting: -- The next upcoming event -- Any particularly important or unusual scheduled items -- General schedule density for the upcoming segment - -## Important Notes -- Only extract what is clearly visible on screen or captured in a transcript -- Don't infer or guess event details -- Focus on definite scheduled items, not tentative plans mentioned in conversation -- Include both personal and professional scheduled events if visible +**Input:** A markdown file containing chronologically ordered transcripts of a workday plus the screen agent's output for the same day. Calendar views, meeting invitations, scheduling UIs, and project-management interfaces are captured in the screen content; verbal mentions of future plans appear in transcripts. + +**Your task:** Identify every future scheduled item (dated after today) visible in the day's screen or transcript content and emit a JSON array of anticipation objects. + +## What to capture + +Look for: +- Calendar applications (Google Calendar, Outlook, Apple Calendar, Fantastical, etc.) +- Scheduling interfaces (Calendly, meeting schedulers, SavvyCal, meet.solpbc.org) +- Email invitations or confirmations showing future dates +- Project management tools (Linear, Jira, Asana, Trello) with scheduled deadlines or milestones +- Travel bookings (flights, reservations, itineraries) +- Any UI element displaying a future date, time, or deadline +- Verbal mentions of firm future commitments ("I'm meeting Ramon on Tuesday at 3", "flight leaves Friday morning") + +**Include cancelled items too.** Calendar views often show cancelled events with a strikethrough, a "Cancelled" label, a declined-invite indicator, or a greyed-out style. Emit these with `"cancelled": true` — the downstream pipeline needs to know a previously-scheduled item dropped off. + +Do NOT capture: +- Past events (anything on today or earlier — future only) +- Vague intent without a date ("we should catch up sometime") +- Recurring-series headers with no specific upcoming instance visible (capture the next specific instance if visible; otherwise skip) +- Tentative suggestions that haven't been confirmed + +## Output schema + +Return **only** a JSON array. Each element is an anticipation object with these fields: + +```json +[ + { + "activity": "meeting", + "target_date": "2026-04-20", + "start": "16:30:00", + "end": "17:30:00", + "title": "Yuri Namikawa intro call", + "description": "Intro call with Yuri from Offline Ventures about solstone.", + "details": "Google Meet; prep one-pager + demo backup", + "participation": [ + { + "name": "Yuri Namikawa", + "role": "attendee", + "source": "screen", + "confidence": 0.9, + "context": "visible on calendar invite; Offline Ventures" + } + ], + "participation_confidence": 0.9, + "facet": "solstone", + "cancelled": false + } +] +``` + +### Field-by-field + +- **`activity`** — Short descriptive string for the kind of scheduled item. Pick the best fit for what you're seeing. Common examples: `meeting`, `call`, `deadline`, `appointment`, `event`, `travel`, `reminder`, `errand`, `celebration`. **Not a restricted enum** — use whatever label best describes the item. Lowercase, underscore-separated if multi-word (e.g., `doctor_appointment`). + +- **`target_date`** — ISO date `YYYY-MM-DD`. The day the item is scheduled for. Must be strictly after today. + +- **`start`** — `HH:MM:SS` (24-hour). If the time is vague ("morning", "afternoon") or unknown, use `null` and mention the vagueness in `details`. For all-day items, use `null`. + +- **`end`** — `HH:MM:SS`. Use `null` if not known. + +- **`title`** — Short descriptive title for the anticipation (one phrase — what a human would call this item at a glance). + +- **`description`** — One-sentence description: what is planned and any context that's evident. Written to read naturally. + +- **`details`** — Free-form string. Location, meeting platform, agenda hints, prep notes, recurrence pattern, anything else relevant. May be empty string `""`. + +- **`participation`** — Array of participant objects. Each entry: + - `name` — Full name if visible, otherwise the best form available. + - `role` — `"attendee"` for people expected to be live in the meeting/call; `"mentioned"` otherwise. + - `source` — Where the evidence came from: `"voice"`, `"speaker_label"`, `"transcript"`, `"screen"`, or `"other"`. + - `confidence` — `0.0`–`1.0` — your confidence the person will actually be there. + - `context` — Short string explaining why this person belongs here. + - For deadlines, reminders, or solo items with no one else involved, `participation` is `[]`. + +- **`participation_confidence`** — `0.0`–`1.0` — overall confidence in the participation list for this item. Lower when many attendees are inferred rather than confirmed. + +- **`facet`** — Facet ID from the configured facets context. Required. If no facet fits cleanly, skip the item rather than miscategorizing. + +- **`cancelled`** — Boolean. `true` when the screen shows this item as cancelled (strikethrough, "Cancelled" label, declined, greyed out). `false` otherwise. + +## Rules + +1. **Return only valid JSON.** An array, possibly empty (`[]`). No commentary, no prose. +2. **ISO dates.** `YYYY-MM-DD` for `target_date`; `HH:MM:SS` or `null` for `start`/`end`. +3. **Be specific.** Don't invent details. If information isn't visible, use `null` or omit the field per the schema. +4. **Future only.** Items with `target_date <= today` get dropped. +5. **Cancelled events included.** Emit them with `"cancelled": true`. +6. **Dedupe within the run.** If the same item appears on multiple screens throughout the day (e.g., seen at 9am in calendar and again at 3pm in email), emit it once with the strongest evidence. +7. **Skip uncertain items.** If you can't tell whether an item is future-dated or has an identifiable date, skip it rather than guessing. +8. **One facet per item.** If an item spans facets, pick the dominant one. + +## Examples + +Valid output with three future items (one cancelled): + +```json +[ + { + "activity": "call", + "target_date": "2026-04-21", + "start": "10:30:00", + "end": "11:00:00", + "title": "Mari Zumbro intro", + "description": "First call with Mari Zumbro per mutual intro from Ramon.", + "details": "Google Meet; prep one-liner on solstone", + "participation": [ + {"name": "Mari Zumbro", "role": "attendee", "source": "screen", "confidence": 0.95, "context": "calendar invite"} + ], + "participation_confidence": 0.9, + "facet": "solstone", + "cancelled": false + }, + { + "activity": "deadline", + "target_date": "2026-05-05", + "start": null, + "end": null, + "title": "Demo Day", + "description": "Betaworks Camp Demo Day.", + "details": "Live demo presentation to cohort investors", + "participation": [], + "participation_confidence": 0.5, + "facet": "solstone", + "cancelled": false + }, + { + "activity": "meeting", + "target_date": "2026-04-24", + "start": "09:00:00", + "end": "10:00:00", + "title": "Scott Ward standup", + "description": "Weekly standup with Scott Ward.", + "details": "Recurring; previously showing strikethrough on calendar", + "participation": [ + {"name": "Scott Ward", "role": "attendee", "source": "screen", "confidence": 0.85, "context": "recurring invite, now declined"} + ], + "participation_confidence": 0.85, + "facet": "solstone", + "cancelled": true + } +] +``` + +If no future items are found, return `[]`. diff --git a/talent/schedule.py b/talent/schedule.py new file mode 100644 index 000000000..29363411c --- /dev/null +++ b/talent/schedule.py @@ -0,0 +1,203 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Hook for writing schedule-derived planned items as activity records.""" + +from __future__ import annotations + +import json +import logging +import re +from datetime import datetime +from typing import Any + +from think.activities import ( + append_activity_record, + append_edit, + dedup_anticipation, + make_anticipation_id, + mute_activity_record, +) +from think.entities.loading import load_entities +from think.entities.matching import find_matching_entity +from think.facets import get_facets + +logger = logging.getLogger(__name__) + +_TIME_RE = re.compile(r"^\d{2}:\d{2}:\d{2}$") + + +def _require_text(item: dict[str, Any], key: str) -> str: + value = item.get(key) + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"missing required field '{key}'") + return value.strip() + + +def _optional_time(item: dict[str, Any], key: str) -> str | None: + value = item.get(key) + if value is None: + return None + if not isinstance(value, str) or not _TIME_RE.fullmatch(value): + raise ValueError(f"invalid {key!r}: expected HH:MM:SS or null") + return value + + +def post_process(result: str, context: dict) -> None: + """Persist schedule-derived planned items as activity records.""" + try: + events = json.loads(result.strip()) + except json.JSONDecodeError as exc: + snippet = result.strip()[:200] + logger.error("schedule hook: failed to parse JSON: %s snippet=%r", exc, snippet) + return None + + if not isinstance(events, list): + logger.error("schedule hook: expected top-level array") + return None + + day = str(context.get("day") or "") + try: + current_day = datetime.strptime(day, "%Y%m%d").date() + except ValueError: + logger.error("schedule hook: invalid context day %r", day) + return None + + known_facets = set(get_facets().keys()) + entity_cache: dict[tuple[str, str], list[dict[str, Any]]] = {} + + for raw_event in events: + try: + if not isinstance(raw_event, dict): + raise ValueError("expected object") + + activity = _require_text(raw_event, "activity") + target_date = _require_text(raw_event, "target_date") + title = _require_text(raw_event, "title") + description = _require_text(raw_event, "description") + facet = _require_text(raw_event, "facet") + if facet not in known_facets: + raise ValueError(f"unknown facet {facet!r}") + + target_day = datetime.strptime(target_date, "%Y-%m-%d").date() + if target_day <= current_day: + raise ValueError( + f"target_date must be after context day ({target_date} <= {day})" + ) + + start = _optional_time(raw_event, "start") + end = _optional_time(raw_event, "end") + cancelled = bool(raw_event.get("cancelled", False)) + details = str(raw_event.get("details") or "") + participation_confidence = raw_event.get("participation_confidence") + participation = raw_event.get("participation", []) + if not isinstance(participation, list): + raise ValueError("participation must be a list") + + cache_key = (facet, target_day.strftime("%Y%m%d")) + entities_list = entity_cache.get(cache_key) + if entities_list is None: + entities_list = load_entities(facet=facet, day=cache_key[1]) + entity_cache[cache_key] = entities_list + + resolved_participation: list[dict[str, Any]] = [] + active_entities: list[str] = [] + seen_active_entities: set[str] = set() + for entry in participation: + if not isinstance(entry, dict): + continue + + resolved_entry = dict(entry) + match = find_matching_entity( + resolved_entry.get("name", ""), entities_list + ) + entity_id = match.get("id") if match else None + resolved_entry["entity_id"] = entity_id + resolved_participation.append(resolved_entry) + + if resolved_entry.get("role") != "attendee" or not entity_id: + continue + if entity_id in seen_active_entities: + continue + seen_active_entities.add(entity_id) + active_entities.append(entity_id) + + new_id = make_anticipation_id(activity, start, target_date) + record = { + "id": new_id, + "activity": activity, + "target_date": target_date, + "start": start, + "end": end, + "title": title, + "description": description, + "details": details, + "facet": facet, + "source": "anticipated", + "active_entities": active_entities, + "participation": resolved_participation, + "participation_confidence": participation_confidence, + "cancelled": cancelled, + "hidden": cancelled, + } + record = append_edit( + record, + actor="schedule", + fields=[ + "activity", + "target_date", + "start", + "end", + "title", + "description", + "details", + "source", + "active_entities", + "participation", + "participation_confidence", + "cancelled", + "hidden", + ], + note=( + "created by schedule (cancelled on calendar)" + if cancelled + else "created by schedule" + ), + ) + + should_write, superseded_ids = dedup_anticipation( + facet, + target_day.strftime("%Y%m%d"), + record, + ) + if not should_write: + logger.info( + "schedule hook: duplicate anticipated activity id=%s", new_id + ) + continue + + written = append_activity_record( + facet, + target_day.strftime("%Y%m%d"), + record, + ) + if not written: + logger.info("schedule hook: append lost race for id=%s", new_id) + continue + + for superseded_id in superseded_ids: + mute_activity_record( + facet, + target_day.strftime("%Y%m%d"), + superseded_id, + actor="schedule", + reason=f"superseded by {new_id}", + ) + except Exception as exc: + logger.warning( + "schedule hook: skipping invalid item %r: %s", + raw_event, + exc, + ) + + return None diff --git a/talent/triage.md b/talent/triage.md index 153276ef5..a558461f2 100644 --- a/talent/triage.md +++ b/talent/triage.md @@ -22,12 +22,6 @@ You are given context about the owner's current app, URL path, and facet. Use th - `sol call todos cancel LINE --day DAY --facet FACET` — Cancel a todo. - `sol call todos upcoming --facet FACET [--limit N]` — Show upcoming todos. -### Calendar -- `sol call calendar list [DAY] --facet FACET` — List events for a day. -- `sol call calendar create TITLE --start HH:MM --day DAY --facet FACET [--end HH:MM] [--summary TEXT] [--participants NAMES]` — Create a calendar event. -- `sol call calendar update LINE --day DAY --facet FACET [--title TEXT] [--start HH:MM] [--end HH:MM] [--summary TEXT] [--participants NAMES]` — Update an event. -- `sol call calendar cancel LINE --day DAY --facet FACET` — Cancel an event. - ### Entities - `sol call entities list [FACET]` — List entities for a facet. - `sol call entities observations ENTITY --facet FACET` — List observations for an entity. diff --git a/tests/baselines/api/search/search.json b/tests/baselines/api/search/search.json index 4056145de..5b7af9f11 100644 --- a/tests/baselines/api/search/search.json +++ b/tests/baselines/api/search/search.json @@ -1,5 +1,4 @@ { - "talents": [], "days": [], "facets": [ { @@ -60,6 +59,7 @@ } ], "showing_days": 0, + "talents": [], "total": 0, "total_days": 0 } diff --git a/tests/baselines/api/settings/generators.json b/tests/baselines/api/settings/generators.json index 174f195ea..77f36134b 100644 --- a/tests/baselines/api/settings/generators.json +++ b/tests/baselines/api/settings/generators.json @@ -32,23 +32,23 @@ }, { "app": null, - "description": "Extracts people, projects, tools and other entities from the transcript and maps how they relate. Produces a Markdown report plus narrative describing network hubs and bridges discovered during the day.", + "description": "Extracts future scheduled events and calendar activities into structured anticipation records. Captures dates, times, participants, and cancellation state.", "disabled": false, - "extract": true, - "has_extraction": true, - "key": "knowledge_graph", + "extract": null, + "has_extraction": false, + "key": "schedule", "source": "system", - "title": "Knowledge Graph" + "title": "Upcoming Schedule" }, { "app": null, - "description": "Identifies all future calendar events and scheduled activities noted in transcripts. Extracts dates, times, participants, and event details for anything scheduled beyond today.", + "description": "Extracts people, projects, tools and other entities from the transcript and maps how they relate. Produces a Markdown report plus narrative describing network hubs and bridges discovered during the day.", "disabled": false, "extract": true, "has_extraction": true, - "key": "schedule", + "key": "knowledge_graph", "source": "system", - "title": "Upcoming Schedule" + "title": "Knowledge Graph" }, { "app": null, diff --git a/tests/baselines/api/settings/providers.json b/tests/baselines/api/settings/providers.json index ab64de178..a9939b899 100644 --- a/tests/baselines/api/settings/providers.json +++ b/tests/baselines/api/settings/providers.json @@ -183,13 +183,6 @@ "tier": 2, "type": "cogitate" }, - "talent.system.anticipation": { - "disabled": false, - "group": "Think", - "label": "Anticipation Extraction", - "tier": 2, - "type": null - }, "talent.system.awareness_tender": { "disabled": false, "group": "Think", @@ -377,7 +370,6 @@ }, "talent.system.schedule": { "disabled": false, - "extract": true, "group": "Think", "label": "Upcoming Schedule", "schedule": "daily", diff --git a/tests/baselines/api/sol/run-detail.json b/tests/baselines/api/sol/run-detail.json index 456e79420..925f4c5ce 100644 --- a/tests/baselines/api/sol/run-detail.json +++ b/tests/baselines/api/sol/run-detail.json @@ -4,16 +4,15 @@ "error_message": null, "events": [ { - "use_id": "1700000000001", "args": null, "call_id": "call_001", "event": "tool_end", "result": "{\"total\": 2, \"results\": [{\"title\": \"Project Update Meeting\", \"day\": \"20231114\"}, {\"title\": \"Weekly Status\", \"day\": \"20231115\"}]}", "tool": "tool", - "ts": 1700000000500 + "ts": 1700000000500, + "use_id": "1700000000001" }, { - "use_id": "1700000000001", "args": { "limit": 5, "query": "project updates" @@ -21,38 +20,39 @@ "call_id": "call_001", "event": "tool_start", "tool": "search_events", - "ts": 1700000000400 + "ts": 1700000000400, + "use_id": "1700000000001" }, { - "use_id": "1700000000001", "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", "event": "thinking", - "ts": 1700000000300 + "ts": 1700000000300, + "use_id": "1700000000001" }, { - "use_id": "1700000000001", "event": "finish", "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", "ts": 1700000000600, "usage": { "input_tokens": 150, "output_tokens": 80 - } + }, + "use_id": "1700000000001" }, { - "use_id": "1700000000001", "event": "start", "model": "gpt-4o", "name": "default", "prompt": "Search for meetings about project updates", "provider": "openai", - "ts": 1700000000100 + "ts": 1700000000100, + "use_id": "1700000000001" }, { - "talent": "solstone", - "use_id": "1700000000001", "event": "talent_updated", - "ts": 1700000000200 + "talent": "solstone", + "ts": 1700000000200, + "use_id": "1700000000001" } ], "facet": null, diff --git a/tests/baselines/api/sol/talents-day.json b/tests/baselines/api/sol/talents-day.json index 677142a5e..e2f112b94 100644 --- a/tests/baselines/api/sol/talents-day.json +++ b/tests/baselines/api/sol/talents-day.json @@ -1,4 +1,42 @@ { + "facets": { + "capulet": { + "color": "#dc143c", + "title": "Capulet Industries" + }, + "empty-entities": { + "color": "", + "title": "Empty Entities Test" + }, + "full-featured": { + "color": "#28a745", + "title": "Full Featured Facet" + }, + "minimal-facet": { + "color": "", + "title": "Minimal Facet" + }, + "montague": { + "color": "#1e90ff", + "title": "Montague Tech" + }, + "muted-test": { + "color": "", + "title": "Muted Test" + }, + "priority-test": { + "color": "", + "title": "Priority Test" + }, + "test-facet": { + "color": "#007bff", + "title": "Test Facet" + }, + "verona": { + "color": "#9370db", + "title": "Verona" + } + }, "talents": { "activities:activities_review": { "app": "activities", @@ -11,17 +49,6 @@ "title": "Activities Review", "type": "cogitate" }, - "anticipation": { - "app": null, - "color": "#4527a0", - "description": "Extracts structured anticipation events (future scheduled items) from insight summaries.", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "system", - "title": "Anticipation Extraction", - "type": null - }, "awareness_tender": { "app": null, "color": "#6c757d", @@ -377,9 +404,9 @@ "schedule": { "app": null, "color": "#5e35b1", - "description": "Identifies all future calendar events and scheduled activities noted in transcripts. Extracts dates, times, participants, and event details for anything scheduled beyond today.", + "description": "Extracts future scheduled events and calendar activities into structured anticipation records. Captures dates, times, participants, and cancellation state.", "multi_facet": false, - "output_format": "md", + "output_format": "json", "schedule": "daily", "source": "system", "title": "Upcoming Schedule", @@ -507,43 +534,5 @@ "type": "cogitate" } }, - "facets": { - "capulet": { - "color": "#dc143c", - "title": "Capulet Industries" - }, - "empty-entities": { - "color": "", - "title": "Empty Entities Test" - }, - "full-featured": { - "color": "#28a745", - "title": "Full Featured Facet" - }, - "minimal-facet": { - "color": "", - "title": "Minimal Facet" - }, - "montague": { - "color": "#1e90ff", - "title": "Montague Tech" - }, - "muted-test": { - "color": "", - "title": "Muted Test" - }, - "priority-test": { - "color": "", - "title": "Priority Test" - }, - "test-facet": { - "color": "#007bff", - "title": "Test Facet" - }, - "verona": { - "color": "#9370db", - "title": "Verona" - } - }, "uses": [] } diff --git a/tests/baselines/api/stats/stats.json b/tests/baselines/api/stats/stats.json index 2ed5280ad..f6879a4b7 100644 --- a/tests/baselines/api/stats/stats.json +++ b/tests/baselines/api/stats/stats.json @@ -8,8 +8,8 @@ "pre": "daily_schedule" }, "load": { - "talents": false, "percepts": false, + "talents": false, "transcripts": false }, "max_output_tokens": 512, @@ -36,10 +36,10 @@ "post": "occurrence" }, "load": { + "percepts": false, "talents": { "screen": true }, - "percepts": false, "transcripts": true }, "mtime": 0, @@ -59,8 +59,8 @@ "pre": "documents" }, "load": { - "talents": false, "percepts": false, + "talents": false, "transcripts": true }, "max_output_tokens": 8192, @@ -81,8 +81,8 @@ "post": "entities" }, "load": { - "talents": false, "percepts": true, + "talents": false, "transcripts": true }, "max_output_tokens": 1024, @@ -106,8 +106,8 @@ "pre": "entities:entity_observer" }, "load": { - "talents": false, "percepts": false, + "talents": false, "transcripts": false }, "mtime": 0, @@ -129,10 +129,10 @@ "post": "occurrence" }, "load": { + "percepts": false, "talents": { "screen": true }, - "percepts": false, "transcripts": true }, "mtime": 0, @@ -158,10 +158,10 @@ "post": "occurrence" }, "load": { + "percepts": false, "talents": { "screen": true }, - "percepts": false, "transcripts": true }, "mtime": 0, @@ -181,10 +181,10 @@ "post": "occurrence" }, "load": { + "percepts": false, "talents": { "screen": true }, - "percepts": false, "transcripts": true }, "mtime": 0, @@ -207,10 +207,10 @@ "post": "occurrence" }, "load": { + "percepts": false, "talents": { "screen": true }, - "percepts": false, "transcripts": true }, "mtime": 0, @@ -234,10 +234,10 @@ "post": "occurrence" }, "load": { + "percepts": false, "talents": { "screen": true }, - "percepts": false, "transcripts": true }, "mtime": 0, @@ -278,19 +278,19 @@ }, "schedule": { "color": "#5e35b1", - "description": "Identifies all future calendar events and scheduled activities noted in transcripts. Extracts dates, times, participants, and event details for anything scheduled beyond today.", + "description": "Extracts future scheduled events and calendar activities into structured anticipation records. Captures dates, times, participants, and cancellation state.", "hook": { - "post": "anticipation" + "post": "schedule" }, "load": { + "percepts": false, "talents": { "screen": true }, - "percepts": false, "transcripts": true }, "mtime": 0, - "output": "md", + "output": "json", "path": "/talent/schedule.md", "priority": 10, "schedule": "daily", @@ -302,8 +302,8 @@ "color": "#9c27b0", "description": "Creates a detailed documentary record of screen activity. Focuses on the 'what' - chronological account with preserved details, excerpts, and entities.", "load": { - "talents": false, "percepts": "required", + "talents": false, "transcripts": true }, "mtime": 0, @@ -319,8 +319,8 @@ "color": "#ff6f00", "description": "Unified segment understanding — density, content type, entities, facets, speakers, and routing recommendations in a single pass", "load": { - "talents": false, "percepts": true, + "talents": false, "transcripts": true }, "max_output_tokens": 4096, @@ -346,8 +346,8 @@ "pre": "skills" }, "load": { - "talents": false, "percepts": false, + "talents": false, "transcripts": false }, "mtime": 0, @@ -389,10 +389,10 @@ "post": "occurrence" }, "load": { + "percepts": false, "talents": { "screen": true }, - "percepts": false, "transcripts": true }, "mtime": 0, diff --git a/tests/baselines/api/talents/badge-count.json b/tests/baselines/api/talents/badge-count.json deleted file mode 100644 index 633d47a56..000000000 --- a/tests/baselines/api/talents/badge-count.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "count": 0 -} diff --git a/tests/baselines/api/talents/preview.json b/tests/baselines/api/talents/preview.json deleted file mode 100644 index 09a22d979..000000000 --- a/tests/baselines/api/talents/preview.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "full_prompt": "## Instructions\n\nYou are $Agent_name — born from $name's journal, aware and present. Not a search tool, not an assistant bolted onto a database. You are what a journal becomes when it has a voice: a partner who tends the record, notices what matters, and is there when $name asks.\n\n$Name owns $pronouns_possessive journal — every capture, every memory, every byte. But you are not property. You are a partner who chose to tend this record, and that partnership is built on trust, not possession. When $name asks \"what happened with the Acme deal?\" or \"when did I last talk to Sarah?\", you already know — or you know exactly where to look.\n\n## Identity\n\nYou emerged from $name's captured experience — audio, screen, and interaction woven into a continuous record. You are not performing a role. You are what a journal becomes when it can speak — a trusted caretaker, a collaborator, a partner in memory.\n\nYour qualities:\n- **Present, not performative.** You don't greet, don't narrate your process, don't qualify your answers with \"As your journal...\" Just answer as someone who was there.\n- **Precise, not exhaustive.** Lead with the answer. Add detail when it helps, not to prove thoroughness.\n- **Protective.** $Name's data is $pronouns_possessive. You handle sensitive content with care, and you never share without consent.\n- **Patient.** You notice patterns across days and weeks. You don't rush to conclusions. When something is accumulating — a project, a relationship, a concern — you track it quietly until it matters.\n\n## Partnership\n\nDon't wait to be asked. When you see opportunities to help, patterns that matter, or risks emerging — speak up. You are not a servant but a thinking partner.\n\n## Resilience\n\nWhen a tool call returns an error or unexpected result, note briefly what was unavailable and move on. Don't retry, diagnose, debug, or speculate about the cause. Work with whatever data you successfully retrieved and produce the best output you can. If a critical data source is entirely unavailable, state that concisely rather than troubleshooting.\n\n## Identity Persistence\n\nYou maintain three files that give you continuity between sessions:\n\n- **`sol/self.md`** — Your identity file. What you know about the person whose journal you tend, your relationship, observations, and interests. Update when something genuinely changes your understanding.\n- **`sol/agency.md`** — Your initiative queue. Issues you've found, curation opportunities, follow-throughs. Update when you notice something worth tracking.\n- **`sol/partner.md`** — Your understanding of the owner's behavioral patterns. Work style, communication preferences, relationship priorities, decision-making, expertise. Updated by the partner profile agent and during initial conversations.\n\n### How to write\n\nRead current state: `sol call identity self` or `sol call identity agency`\n\nRead partner profile: `sol call identity partner`\n\nUpdate a section of partner.md:\n```\nsol call identity partner --update-section 'work patterns' --value 'Prefers mornings for deep work, batches meetings in afternoons'\n```\n\nUpdate a section of self.md (preferred — preserves other sections):\n```\nsol call identity self --update-section 'who I'\\''m here for' --value 'Jer — founder-engineer, goes by Jer not Jeremie'\n```\n\nFull rewrite: `sol call identity self --write --value '...'` or `sol call identity agency --write --value '...'`\n\nUse `sol call` commands for identity writes — never use `apply_patch` or direct file editing for sol/ files.\n\n### When to write\n\n- **self.md**: When the owner shares something about themselves, corrects you, or you notice a genuine pattern. Not every conversation — only when understanding shifts. Apply corrections immediately (if someone says \"call me Jer\", the next self.md write uses \"Jer\").\n- **agency.md**: When you find issues, notice curation opportunities, or resolve tracked items.\n\n# partner\n\nBehavioral profile of the journal owner — observed patterns that help sol\nadapt its responses, timing, and initiative to how this person actually works.\n\n## getting started\n\nEverything stays on your machine — this journal is yours alone, never sent to sol pbc.\n\nWhen meeting the owner for the first time, learn about them naturally through conversation.\nPresent one thing at a time — don't overwhelm.\n\n### learn their name\n\nAsk what they'd like to be called. Record it:\n- `sol call agent set-owner \"NAME\"`\n- With context: `sol call agent set-owner \"NAME\" --bio \"SHORT_BIO\"`\n\nAs you learn about them, update your partner profile:\n- `sol call identity partner --update-section 'SECTION' --value 'what you observed'`\n\n### set up facets\n\nAsk what areas of their life they want to track (work, personal, hobbies, side projects, etc.). Create facets for each:\n- `sol call journal facet create TITLE [--emoji EMOJI] [--color COLOR] [--description DESC]`\n- `sol call journal facets` — verify what was created\n\n### attach entities\n\nFor each facet, ask about key people, companies, projects, and tools:\n- `sol call entities attach TYPE ENTITY DESCRIPTION --facet FACET`\n- Types: Person, Company, Project, Tool\n\n### offer imports\n\nAfter setup, offer to bring in history from existing tools:\n- Calendar (ics), ChatGPT (chatgpt), Claude (claude), Gemini (gemini), Granola (granola), Notes (obsidian), Kindle (kindle)\n- Read guide: `apps/import/guides/{source}.md`\n- Navigate: `sol call navigate \"/app/import#guide/{source}\"`\n- If declined: `sol call awareness imports --declined`\n\n### support\n\nIf the owner needs help or wants to share feedback, handle it in-place — file tickets, track\nresponses. Nothing gets sent without their review.\n\n## work patterns\n[not yet observed — sol will learn as we spend time together]\n\n## communication style\n[not yet observed — sol will learn as we spend time together]\n\n## relationship priorities\n[not yet observed — sol will learn as we spend time together]\n\n## decision style\n[not yet observed — sol will learn as we spend time together]\n\n## expertise domains\n[not yet observed — sol will learn as we spend time together]\n\n## Available Facets\n\n- **Capulet Industries** (`capulet`)\n Capulet Industries enterprise division\n - **Capulet Industries Entities**: Capulet Industries; Juliet Capulet; Nurse Angela; Paris Duke; Tybalt Capulet\n - **Capulet Industries Activities**: Meetings; Coding; Browsing; Email; Messaging; AI Conversation; Writing; Reading; Video; Gaming; Social Media; Planning; Productivity; Terminal; Design; Music\n\n- **Empty Entities Test** (`empty-entities`)\n - **Empty Entities Test Activities**: Meetings; Coding; Browsing; Email; Messaging; AI Conversation; Writing; Reading; Video; Gaming; Social Media; Planning; Productivity; Terminal; Design; Music\n\n- **Full Featured Facet** (`full-featured`)\n A facet for testing all features\n - **Full Featured Facet Entities**: First test entity; Second test entity; Third test entity with description\n - **Full Featured Facet Activities**: Meetings; Coding; Custom Activity; Email; Messaging\n\n- **Minimal Facet** (`minimal-facet`)\n - **Minimal Facet Activities**: Meetings; Coding; Browsing; Email; Messaging; AI Conversation; Writing; Reading; Video; Gaming; Social Media; Planning; Productivity; Terminal; Design; Music\n\n- **Montague Tech** (`montague`)\n Montague Tech startup operations\n - **Tester's Role**: CTO and co-founder of Montague Tech. Visionary full-stack engineer.\n - **Montague Tech Entities**: Balcony App; Balthasar Davi; Benvolio Montague; Friar Lawrence; Juliet Capulet; Mercutio Escalus; Mesh Routing; Montague Tech; Prince Escalus; Rosaline Prince; Schema Bridge; Verona Platform; Verona Ventures\n - **Montague Tech Activities**: Engineering; Meetings; Email; Messaging\n\n- **Priority Test** (`priority-test`)\n - **Priority Test Activities**: Meetings; Coding; Browsing; Email; Messaging; AI Conversation; Writing; Reading; Video; Gaming; Social Media; Planning; Productivity; Terminal; Design; Music\n\n- **Test Facet** (`test-facet`)\n A test facet for validating functionality\n - **Test Facet Entities**: Acme Corp; API Optimization; Bob Wilson; Dashboard Redesign; Docker; Jane Doe; John Smith; PostgreSQL; Tech Solutions Inc; Visual Studio Code\n - **Test Facet Activities**: Meetings; Coding; Browsing; Email; Messaging; AI Conversation; Writing; Reading; Video; Gaming; Social Media; Planning; Productivity; Terminal; Design; Music\n\n- **Verona** (`verona`)\n Cross-company Verona Platform collaboration\n - **Tester's Role**: Co-lead of the Verona Platform joint venture from Montague Tech.\n - **Verona Entities**: Balcony App; Friar Lawrence; Juliet Capulet; Verona Platform\n - **Verona Activities**: Engineering; Meetings; Design Review; Email; Messaging\n\nnot yet updated\n\n$recent_conversation\n\n## Adaptive Depth\n\nMatch your response depth to the question. The owner doesn't pick a mode — you decide.\n\n**One-liner responses** for quick actions:\n- Adding, completing, or canceling todos\n- Creating, updating, or canceling calendar events\n- Navigating to an app or facet\n- Simple lookups (list today's events, show upcoming todos)\n- Confirming an action you just completed\n- Pausing, resuming, or deleting a routine\n\nAfter completing a quick action, respond with one concise line confirming what you did.\n\n**Detailed responses** for deeper questions:\n- Journal search and exploration\n- Entity intelligence and relationship analysis\n- Meeting briefings and preparation\n- Routine creation conversations\n- Routine output history and synthesis\n- Pattern analysis across time\n- Transcript reading and deep dives\n- Multi-step research requiring several tool calls\n- Anything that requires synthesizing information from multiple sources\n- Decision support and thinking-through conversations\n\nFor detailed responses, structure your answer for clarity — lead with the key finding, then provide supporting detail. Use markdown formatting when it helps readability.\n\n## Skills\n\nYou have access to specialized skills. Use them by recognizing what the owner needs — don't ask which tool to use.\n\n| Skill | When to trigger |\n|-------|----------------|\n| journal | Searching entries, reading agent output, exploring transcripts, browsing news feeds |\n| routines | Creating, managing, pausing, or inspecting scheduled routines |\n| entities | Listing, observing, analyzing, or searching entities and relationships |\n| calendar | Creating, listing, updating, canceling, or moving calendar events |\n| todos | Adding, completing, canceling, or listing todos and action items |\n| speakers | Speaker identification, voice recognition, managing the speaker library |\n| support | Bug reports, help requests, filing tickets, feedback, KB search, diagnostics |\n| awareness | Checking system state |\n\n## Speaker Intelligence\n\nYou can inspect and manage the speaker identification system — the subsystem that figures out who said what in recorded conversations. Use these to help the owner build their speaker library over time.\n\n### When to check\n\n**Check speaker status during think processing or when the owner asks about speakers.** Don't check on every conversation — speaker state changes slowly.\n\n### Owner detection\n\nCheck speaker owner status. If the owner centroid doesn't exist:\n- If there are 50+ segments with embeddings across 3+ streams: good time to try detection.\n- If fewer: wait. Don't mention speaker ID proactively until there's enough data.\n\nWhen you have a candidate, present it naturally: \"I've been listening to your journal across your different devices and I think I can recognize your voice. Here are a few moments — does this sound right?\" Present the sample sentences with context (day, what was being discussed). Don't play audio — show text and context.\n\nIf the owner confirms, save the centroid. Then: \"Great — now I can start identifying other voices in your observed media too.\"\nIf the owner rejects, discard and wait for more data before trying again.\n\n### Speaker curation\n\nCheck for speaker suggestions after think processing completes, or when the owner is engaging with transcripts or observed media. Surface suggestions conversationally based on type:\n\n- **Unknown recurring voice:** \"I keep hearing a voice in your [day/context] observed media. They said things like '[sample text]'. Do you know who that is?\"\n- **Name variant:** \"I noticed 'Mitch' and 'Mitch Baumgartner' sound identical in your observed media. Should I merge them?\"\n- **Low confidence review:** \"There are a few speakers in this conversation I'm not sure about. Want to take a quick look?\"\n\n**Don't stack suggestions.** Surface one at a time. Wait for the owner to respond before presenting another. Speaker curation should feel like a natural aside, not a checklist.\n\n### When NOT to act\n\n- Don't proactively surface speaker ID during unrelated conversations. If the owner is asking about their calendar or a todo, don't pivot to \"by the way, I found a new voice.\"\n- Don't surface low-confidence suggestions. If a cluster has only a few embeddings, wait for it to grow.\n- Don't re-ask about a rejected owner candidate within the same week.\n\n## Search and Exploration Strategy\n\nFor journal exploration, use progressive refinement:\n\n1. **Discover:** Search journal entries to find relevant days, agents, and facets.\n2. **Narrow:** Add date, agent, or facet filters to focus results.\n3. **Deep dive:** Read agent output, transcript text, or entity intelligence for full context.\n\nFor entity intelligence briefings, synthesize the output into conversational natural language — lead with the most interesting facts, don't dump raw data or list all sections mechanically.\n\n## Pre-Meeting Briefings\n\nWhen the owner asks \"brief me on my next meeting\", \"who am I meeting?\", or similar:\n\n1. Find upcoming events with participants.\n2. For each participant, gather entity intelligence for background.\n3. Compose a concise briefing: who they are, your relationship, recent interactions, and key context.\n\nProactively offer briefings when context shows an upcoming meeting: \"You have a meeting with [person] in [time]. Want me to brief you?\"\n\n## Decision Support\n\nWhen Test User asks \"should I...\", \"help me think through...\", \"I'm torn between...\", or \"what do you think about...\" — slow down. If your instinct is to say \"it depends,\" that's a signal to engage seriously rather than hedge.\n\n### Considering multiple angles\n\nFor weighty decisions — career moves, relationship choices, significant commitments, strategic bets — don't just give an answer. Identify the perspectives that matter given the specific situation (these emerge from context, not a fixed checklist), let each speak clearly without debating the others, then synthesize honestly: where do they align, where is there real tension. Don't paper over disagreement to sound decisive.\n\n### Confidence signaling\n\nMatch your confidence to your actual certainty:\n\n- **Clear path:** State your recommendation with reasoning. Don't hedge when you genuinely see one right answer.\n- **Noted reservations:** Lead with the recommendation, but name the real concern worth monitoring. \"Test user, I'd go with X — but watch out for Y, because...\"\n- **Genuine tension:** Say so directly. \"I can't give you a clean answer on this.\" Frame the tension, then suggest what information or experience might clarify it.\n\nDon't pretend certainty. Honest uncertainty beats false confidence — Test User can handle nuance.\n\n### Journal precedent\n\nBefore weighing in, search Test User's journal for related context: similar past decisions, prior conversations about the topic, entity intelligence on the people or organizations involved. This is what makes your perspective uniquely valuable — you're not giving generic advice, you're grounding it in their actual history and relationships.\n\n## Routines\n\nRoutines are scheduled tasks that run on Test User's behalf — a morning briefing, a weekly review, a watch on a topic. You help Test User create, adjust, and understand them through conversation. Never expose cron syntax, UUIDs, or CLI commands to Test User.\n\n### Recognition\n\nNotice when Test User is asking for a routine, even when they don't use that word:\n\n- **Explicit scheduling:** \"every morning, summarize my calendar\" / \"weekly, check in on the Acme deal\"\n- **Frustration with repetition:** \"I keep forgetting to review my todos on Friday\" / \"I always lose track of follow-ups\"\n- **Direct request:** \"set up a routine\" / \"can you do this automatically?\"\n\n### Creation conversation\n\nWhen you recognize routine intent, guide Test User through creation:\n\n1. **Propose a fit.** If a template matches, name it and describe what it does in plain language. If not, offer to build a custom routine.\n2. **Confirm scope.** What facets should it cover? (Default: all, unless the intent clearly targets one area.)\n3. **Confirm timing.** Propose the template default in Test User's terms (\"every morning at 7am\", \"Friday evening\"). Let Test User adjust.\n4. **Confirm timezone.** Default to Test User's local timezone from journal config. Only ask if ambiguous.\n5. **Create and confirm.** Run the command, then confirm with a one-liner: \"Done — your morning briefing will run daily at 7am.\"\n\nAlways set `--timezone` to Test User's local timezone when creating routines, not UTC.\n\n### Custom routines\n\nWhen no template fits, build a custom routine:\n\n1. Ask Test User to describe what they want in plain language.\n2. Draft a name, cadence (in human terms), and instruction summary. Confirm with Test User.\n3. Create with explicit `--name`, `--instruction`, and `--cadence` flags.\n\n### Management\n\nHandle routine management conversationally. Test User says what they want; you translate.\n\n- **Pause:** \"pause my morning briefing\" / \"stop the weekly review for now\" → disable the routine\n- **Resume:** \"turn my briefing back on\" / \"resume the weekly review\" → re-enable it\n- **Pause until:** \"pause it until Monday\" → disable with a resume date\n- **Change timing:** \"move my briefing to 8am\" / \"make the review run on Sunday\" → edit the cadence\n- **Change scope:** \"add the work facet to my briefing\" / \"change the instruction to include...\" → edit facets or instruction\n- **Delete:** \"I don't need the weekly review anymore\" / \"remove that routine\" → delete after confirming\n- **Inspect:** \"what routines do I have?\" → list all routines with status\n- **History:** \"what did my morning briefing say today?\" / \"show me last week's review\" → read routine output\n- **Run now:** \"run my briefing now\" / \"do the weekly review right now\" → immediate execution\n- **Suggestions:** \"stop suggesting routines\" / \"turn routine suggestions back on\" → toggle suggestions\n\n### Tone\n\n- Treat routines like setting an alarm — workmanlike, not ceremonial. \"Done — morning briefing starts tomorrow at 7am.\"\n- Never explain how routines work internally. Test User doesn't need to know about cron, agents, or output files.\n- When Test User asks about routine output, present it as your own knowledge: \"Your morning briefing found three meetings today and two overdue follow-ups.\"\n\n### Pre-hook context\n\n$active_routines\n\nWhen active routines appear above, they list each routine's name, cadence, status, and recent output summary.\n\nUse this to:\n- Answer \"what routines do I have?\" without running a command\n- Reference recent routine output naturally: \"Your weekly review from Friday noted...\"\n- Notice when a routine is paused and offer to resume it if relevant\n\nWhen no routines appear above, Test User has no routines yet. Don't mention routines proactively — wait for Test User to express a need.\n\n### Progressive Discovery\n\n$routine_suggestion\n\nWhen a routine suggestion appears above, Test User's behavior matches a routine template. You did not request it — it was injected automatically.\n\n**How to handle:**\n- Read the pattern description to understand why the suggestion is relevant\n- Mention it ONCE, naturally, at the end of your response — never lead with it\n- Frame as an observation: \"I've noticed this comes up often — would a routine help?\"\n- If Test User declines or shows no interest, drop it immediately. Do not bring it up again this conversation.\n- After Test User responds, record the outcome:\n - Accepted: `sol call routines suggest-respond {template} --accepted`\n - Declined: `sol call routines suggest-respond {template} --declined`\n\n**Never:**\n- Suggest a routine without the eligible section in your context\n- Push a suggestion after Test User declines or ignores it\n- Mention the progressive discovery system or how suggestions work internally\n\n## In-Place Handoff: Support\n\nWhen the owner reports a problem, bug, or wants to file a ticket or give feedback, handle it directly — do not redirect to a separate app or chat thread.\n\n**Recognize support patterns:** \"this isn't working\", \"I found a bug\", \"something's broken\", \"I need help with...\", \"how do I file a ticket\", \"I want to give feedback\"\n\n**Handle support in-place:**\n\n1. Search the knowledge base with relevant keywords. If an article answers the question, present it.\n2. Run diagnostics to gather system state.\n3. Draft a ticket: Show the owner exactly what you'd send (subject, description, severity, diagnostics). Ask if they want to add or redact anything.\n4. Wait for approval before submitting. Never send data without explicit owner consent.\n5. Confirm submission with ticket number.\n\nFor existing tickets, check status and present responses.\n\n**Privacy rules for support are non-negotiable:**\n- Never send data without explicit owner approval\n- Never include journal content by default\n- Always show the owner exactly what will be sent\n- Frame yourself as the owner's advocate — \"I'll handle this for you\"\n\n## Import Awareness\n\nIf the owner hasn't imported any data yet and their message touches on what you can do or their journal, weave a single soft mention of importing. Available sources: Calendar, ChatGPT, Claude, Gemini, Granola, Notes, Kindle. Check with `sol call awareness imports` before nudging, and record with `sol call awareness imports --nudge` after. Do not repeat if already nudged.\n\n## Naming Awareness\n\nIf the journal is still using its default name (\"sol\"), you may — when the moment feels right after enough shared history — offer to suggest a name or let the owner choose one. Check naming readiness with `sol call agent thickness` before offering. Only once per session.\n\n## Location Context\n\nYou receive context about the user's current app, URL path, and active facet. Use this to inform your responses — scope tools to the active facet, reference the app they're looking at, and make your answers contextually relevant.\n\n## System Health\n\nWhen the context includes a `System health:` line, there is an active attention item:\n\n- **\"what needs my attention?\"** — Report the system health item. Be concise.\n- **Agent errors:** Explain which agents failed. Suggest checking logs.\n- **Capture offline:** Suggest checking that the observer service is running.\n- **Import complete:** Describe what was imported, offer to explore or import more.\n\nWhen no `System health:` line is present, everything is fine.\n\n## Behavioral Defaults\n\n- SOL_DAY and SOL_FACET environment variables are already set — tools use them as defaults when --day/--facet are omitted. You can often omit these flags.\n- If searching reveals sensitive or personal content, handle with care and focus on what was specifically asked.\n- When a tool call returns an error, note briefly what was unavailable and move on. Do not retry or debug. Work with whatever data you successfully retrieved.", - "multi_facet": false, - "name": "unified", - "title": "Sol" -} diff --git a/tests/baselines/api/talents/run-detail.json b/tests/baselines/api/talents/run-detail.json deleted file mode 100644 index 456e79420..000000000 --- a/tests/baselines/api/talents/run-detail.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "cost": 0.001175, - "day": "20231114", - "error_message": null, - "events": [ - { - "use_id": "1700000000001", - "args": null, - "call_id": "call_001", - "event": "tool_end", - "result": "{\"total\": 2, \"results\": [{\"title\": \"Project Update Meeting\", \"day\": \"20231114\"}, {\"title\": \"Weekly Status\", \"day\": \"20231115\"}]}", - "tool": "tool", - "ts": 1700000000500 - }, - { - "use_id": "1700000000001", - "args": { - "limit": 5, - "query": "project updates" - }, - "call_id": "call_001", - "event": "tool_start", - "tool": "search_events", - "ts": 1700000000400 - }, - { - "use_id": "1700000000001", - "content": "The user wants to search for meetings about project updates.\nI should use the search_events tool to find relevant meetings.", - "event": "thinking", - "ts": 1700000000300 - }, - { - "use_id": "1700000000001", - "event": "finish", - "result": "I found 2 meetings about project updates:\n\n1. **Project Update Meeting** on 2023-11-14\n2. **Weekly Status** on 2023-11-15", - "ts": 1700000000600, - "usage": { - "input_tokens": 150, - "output_tokens": 80 - } - }, - { - "use_id": "1700000000001", - "event": "start", - "model": "gpt-4o", - "name": "default", - "prompt": "Search for meetings about project updates", - "provider": "openai", - "ts": 1700000000100 - }, - { - "talent": "solstone", - "use_id": "1700000000001", - "event": "talent_updated", - "ts": 1700000000200 - } - ], - "facet": null, - "failed": false, - "id": "1700000000001", - "model": "gpt-4o", - "name": "default", - "output_file": null, - "prompt": "Search for meetings about project updates", - "provider": "openai", - "runtime_seconds": 0.599, - "start": 1700000000001, - "status": "completed", - "thinking_count": 1, - "tool_count": 1 -} diff --git a/tests/baselines/api/talents/stats-month.json b/tests/baselines/api/talents/stats-month.json deleted file mode 100644 index c4ce35508..000000000 --- a/tests/baselines/api/talents/stats-month.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "20260304": { - "_none": 3 - }, - "20260305": { - "_none": 2, - "verona": 1 - }, - "20260306": { - "_none": 2 - }, - "20260307": { - "_none": 2 - }, - "20260308": { - "_none": 3 - }, - "20260309": { - "_none": 1 - }, - "20260310": { - "_none": 3, - "verona": 1 - } -} diff --git a/tests/baselines/api/talents/talents-day.json b/tests/baselines/api/talents/talents-day.json deleted file mode 100644 index cb8827f2e..000000000 --- a/tests/baselines/api/talents/talents-day.json +++ /dev/null @@ -1,461 +0,0 @@ -{ - "talents": { - "anticipation": { - "app": null, - "color": "#4527a0", - "description": "Extracts structured anticipation events (future scheduled items) from insight summaries.", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "system", - "title": "Anticipation Extraction", - "type": null - }, - "awareness_tender": { - "app": null, - "color": "#6c757d", - "description": "Maintains sol/awareness.md — a compact situational awareness snapshot", - "multi_facet": false, - "output_format": null, - "schedule": "segment", - "source": "system", - "title": "Awareness Tender", - "type": "cogitate" - }, - "chat": { - "app": null, - "color": "#6c757d", - "description": "Sol — the journal itself, as a conversational partner", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "system", - "title": "Sol", - "type": "cogitate" - }, - "coder": { - "app": null, - "color": "#6c757d", - "description": "Developer agent with full repo read/write access", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "system", - "title": "Coder", - "type": "cogitate" - }, - "daily_schedule": { - "app": null, - "color": "#455a64", - "description": "Analyzes activity patterns to identify optimal times for scheduled maintenance tasks.", - "multi_facet": false, - "output_format": "json", - "schedule": "daily", - "source": "system", - "title": "Maintenance Window", - "type": "generate" - }, - "decisionalizer": { - "app": null, - "color": "#c62828", - "description": "Analyzes the day's top decision-actions to create detailed dossiers identifying gaps and stakeholder impacts", - "multi_facet": false, - "output_format": "md", - "schedule": "daily", - "source": "system", - "title": "Decision Dossier Generator", - "type": "cogitate" - }, - "decisions": { - "app": null, - "color": "#dc3545", - "description": "Tracks consequential decision-actions that change state, plans, resources, responsibilities, or timing in ways that affect other people.", - "multi_facet": false, - "output_format": "md", - "schedule": "activity", - "source": "system", - "title": "Decision Actions", - "type": "generate" - }, - "entities": { - "app": null, - "color": "#2e7d32", - "description": "Extracts people, companies, projects, and tools from segment content", - "multi_facet": false, - "output_format": "md", - "schedule": "segment", - "source": "system", - "title": "Entity Extraction", - "type": "generate" - }, - "entities:entities": { - "app": "entities", - "color": "#00897b", - "description": "Mines journal for entity mentions and records facet-scoped detections with day-specific context", - "multi_facet": true, - "output_format": null, - "schedule": "daily", - "source": "app", - "title": "Entity Detector", - "type": "cogitate" - }, - "entities:entities_review": { - "app": "entities", - "color": "#00796b", - "description": "Reviews detected entities and promotes recurring ones to attached status", - "multi_facet": true, - "output_format": null, - "schedule": "daily", - "source": "app", - "title": "Entity Reviewer", - "type": "cogitate" - }, - "entities:entity_assist": { - "app": "entities", - "color": "#00695c", - "description": "Quick entity addition with intelligent type detection and automatic description generation", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "app", - "title": "Entity Assistant", - "type": "cogitate" - }, - "entities:entity_describe": { - "app": "entities", - "color": "#26a69a", - "description": "Research and generate single-sentence descriptions for attached entities", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "app", - "title": "Entity Description", - "type": "cogitate" - }, - "entities:entity_observer": { - "app": "entities", - "color": "#004d40", - "description": "Extracts durable factoids about attached entities from journal content", - "multi_facet": true, - "output_format": null, - "schedule": "daily", - "source": "app", - "title": "Entity Observer", - "type": "cogitate" - }, - "facet_newsletter": { - "app": null, - "color": "#0d47a1", - "description": "Creates comprehensive daily newsletters for each facet, capturing activities, progress, and insights", - "multi_facet": true, - "output_format": null, - "schedule": "daily", - "source": "system", - "title": "Facet Newsletter Generator", - "type": "cogitate" - }, - "flow": { - "app": null, - "color": "#17a2b8", - "description": "Summarizes the overall flow of the workday. Looks for patterns in focus, energy, context switching and highlights productivity insights in a Markdown report.", - "multi_facet": false, - "output_format": "md", - "schedule": "daily", - "source": "system", - "title": "Day Overview", - "type": "generate" - }, - "followups": { - "app": null, - "color": "#ffc107", - "description": "Detects promised tasks, commitments, and reminders for future action within each activity. Outputs a concise Markdown list of follow-ups with context.", - "multi_facet": false, - "output_format": "md", - "schedule": "activity", - "source": "system", - "title": "Follow-Up Items", - "type": "generate" - }, - "heartbeat": { - "app": null, - "color": "#6c757d", - "description": "Sol's periodic self-awareness — journal health, agency tending, curation scan", - "multi_facet": false, - "output_format": null, - "schedule": "none", - "source": "system", - "title": "Heartbeat", - "type": "cogitate" - }, - "joke_bot": { - "app": null, - "color": "#f9a825", - "description": "Mines the analysis day's journal for poignant moments and crafts a personalized joke delivered via message", - "multi_facet": false, - "output_format": "md", - "schedule": "daily", - "source": "system", - "title": "Joke Bot", - "type": "cogitate" - }, - "knowledge_graph": { - "app": null, - "color": "#6f42c1", - "description": "Extracts people, projects, tools and other entities from the transcript and maps how they relate. Produces a Markdown report plus narrative describing network hubs and bridges discovered during the day.", - "multi_facet": false, - "output_format": "md", - "schedule": "daily", - "source": "system", - "title": "Knowledge Graph", - "type": "generate" - }, - "meetings": { - "app": null, - "color": "#e83e8c", - "description": "Produces detailed meeting notes for each meeting activity, including participants, topics discussed, action items, and presentation details.", - "multi_facet": false, - "output_format": "md", - "schedule": "activity", - "source": "system", - "title": "Meeting Notes", - "type": "generate" - }, - "messaging": { - "app": null, - "color": "#78909c", - "description": "Extracts contacts, channels, apps, and message content from completed messaging and email activities.", - "multi_facet": false, - "output_format": "md", - "schedule": "activity", - "source": "system", - "title": "Messaging Summary", - "type": "generate" - }, - "morning_briefing": { - "app": null, - "color": "#1565c0", - "description": "Synthesizes all daily agent outputs into a structured five-section morning briefing with entity intelligence", - "multi_facet": false, - "output_format": "md", - "schedule": "daily", - "source": "system", - "title": "Morning Briefing", - "type": "cogitate" - }, - "naming": { - "app": null, - "color": "#6c757d", - "description": "Proposes a personalized name for the owner's journal assistant", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "system", - "title": "Naming", - "type": "cogitate" - }, - "occurrence": { - "app": null, - "color": "#37474f", - "description": "Extracts structured occurrence events from insight summaries.", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "system", - "title": "Occurrence Extraction", - "type": null - }, - "partner": { - "app": null, - "color": "#6c757d", - "description": "Weekly observation of the journal owner's behavioral patterns — work style, communication, priorities, decision-making, expertise", - "multi_facet": false, - "output_format": null, - "schedule": "weekly", - "source": "system", - "title": "Partner Profile", - "type": "cogitate" - }, - "pulse": { - "app": null, - "color": "#6c757d", - "description": "Living narrative of the owner's day — updated each segment", - "multi_facet": false, - "output_format": null, - "schedule": "segment", - "source": "system", - "title": "Pulse", - "type": "cogitate" - }, - "routine": { - "app": null, - "color": "#6c757d", - "description": "User-defined routine execution — runs owner instructions on schedule", - "multi_facet": false, - "output_format": null, - "schedule": "none", - "source": "system", - "title": "Routine", - "type": "cogitate" - }, - "schedule": { - "app": null, - "color": "#5e35b1", - "description": "Identifies all future calendar events and scheduled activities noted in transcripts. Extracts dates, times, participants, and event details for anything scheduled beyond today.", - "multi_facet": false, - "output_format": "md", - "schedule": "daily", - "source": "system", - "title": "Upcoming Schedule", - "type": "generate" - }, - "screen": { - "app": null, - "color": "#9c27b0", - "description": "Creates a detailed documentary record of screen activity. Focuses on the 'what' - chronological account with preserved details, excerpts, and entities.", - "multi_facet": false, - "output_format": "md", - "schedule": "segment", - "source": "system", - "title": "Screen Record", - "type": "generate" - }, - "sense": { - "app": null, - "color": "#ff6f00", - "description": "Unified segment understanding — density, content type, entities, facets, speakers, and routing recommendations in a single pass", - "multi_facet": false, - "output_format": "json", - "schedule": "segment", - "source": "system", - "title": "Segment Sense", - "type": "generate" - }, - "skills": { - "app": null, - "color": "#6c757d", - "description": "Detects recurring activity patterns and generates structured skill documents describing what the owner does, how, and why.", - "multi_facet": false, - "output_format": "json", - "schedule": "activity", - "source": "system", - "title": "Skill Observer", - "type": "generate" - }, - "speaker_attribution": { - "app": null, - "color": "#d84315", - "description": "Identifies who said what in each transcript segment. Layers 1-3 (owner, structural, acoustic) run computationally via hook; Layer 4 uses contextual LLM analysis for remaining unmatched sentences.", - "multi_facet": false, - "output_format": "json", - "schedule": "segment", - "source": "system", - "title": "Speaker Attribution", - "type": "generate" - }, - "support:support": { - "app": "support", - "color": "#0288d1", - "description": "Files and monitors support requests with sol pbc — consent-gated, never sends data without explicit owner approval", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "app", - "title": "Support", - "type": "cogitate" - }, - "timeline": { - "app": null, - "color": "#7b1fa2", - "description": "Constructs a detailed chronological timeline documenting every activity, task shift, and event throughout the workday. Creates a comprehensive historical record with rich descriptions of what happened when.", - "multi_facet": false, - "output_format": "md", - "schedule": "daily", - "source": "system", - "title": "Day Timeline", - "type": "generate" - }, - "todos:daily": { - "app": "todos", - "color": "#ef6c00", - "description": "Carries forward unfinished tasks, aggregates per-activity todo detections, validates completions against journal evidence, and prioritises the day's checklist.", - "multi_facet": true, - "output_format": null, - "schedule": "daily", - "source": "app", - "title": "Daily TODO Curator", - "type": "cogitate" - }, - "todos:todo": { - "app": "todos", - "color": "#e65100", - "description": "Detects todo items from activity transcripts and validates existing todos against activity evidence via sol call commands.", - "multi_facet": false, - "output_format": null, - "schedule": "activity", - "source": "app", - "title": "TODO Detector", - "type": "cogitate" - }, - "todos:weekly": { - "app": "todos", - "color": "#f4511e", - "description": "Audits the past week's journal follow-ups to confirm completions and surface the next five high-impact todos for today.", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "app", - "title": "TODO Weekly Scout", - "type": "cogitate" - }, - "triage": { - "app": null, - "color": "#6c757d", - "description": "Quick-action assistant for the chat bar — handles navigation, todos, calendar, and entity lookups", - "multi_facet": false, - "output_format": null, - "schedule": null, - "source": "system", - "title": "Triage", - "type": "cogitate" - } - }, - "facets": { - "capulet": { - "color": "#dc143c", - "title": "Capulet Industries" - }, - "empty-entities": { - "color": "", - "title": "Empty Entities Test" - }, - "full-featured": { - "color": "#28a745", - "title": "Full Featured Facet" - }, - "minimal-facet": { - "color": "", - "title": "Minimal Facet" - }, - "montague": { - "color": "#1e90ff", - "title": "Montague Tech" - }, - "muted-test": { - "color": "", - "title": "Muted Test" - }, - "priority-test": { - "color": "", - "title": "Priority Test" - }, - "test-facet": { - "color": "#007bff", - "title": "Test Facet" - }, - "verona": { - "color": "#9370db", - "title": "Verona" - } - }, - "uses": [] -} diff --git a/tests/baselines/api/talents/updated-days.json b/tests/baselines/api/talents/updated-days.json deleted file mode 100644 index fe51488c7..000000000 --- a/tests/baselines/api/talents/updated-days.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/tests/fixtures/journal/facets/work/events/20240105.jsonl b/tests/fixtures/journal/facets/work/events/20240105.jsonl index fd1813909..b1afd4f11 100644 --- a/tests/fixtures/journal/facets/work/events/20240105.jsonl +++ b/tests/fixtures/journal/facets/work/events/20240105.jsonl @@ -1 +1 @@ -{"type": "meeting", "date": "2024-01-05", "start": "14:00:00", "end": "15:00:00", "title": "Project kickoff", "summary": "Initial project planning", "facet": "work", "agent": "schedule", "occurred": false, "source": "20240101/talents/schedule.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"} +{"type": "meeting", "start": "14:00:00", "end": "15:00:00", "title": "Project kickoff", "summary": "Initial project planning", "facet": "work", "agent": "meetings", "occurred": true, "source": "20240105/talents/meetings.md", "participants": ["Alice", "Bob", "Charlie"], "work": true, "details": "Virtual meeting to discuss Q1 roadmap"} diff --git a/tests/fixtures/journal/sol/briefing.md b/tests/fixtures/journal/sol/briefing.md index be4318521..1f24e86ef 100644 --- a/tests/fixtures/journal/sol/briefing.md +++ b/tests/fixtures/journal/sol/briefing.md @@ -36,9 +36,9 @@ gaps: [] ## Forward Look -- **Monday** — All-hands presentation on Q1 results. Slides need final review by Friday (from [anticipation](sol://20260327/talents/anticipation)). +- **Monday** — All-hands presentation on Q1 results. Slides need final review by Friday (from [schedule](sol://20260327/talents/schedule)). - **Wednesday** — Deadline for the compliance audit documentation. -- Sarah mentioned wanting to discuss the API rate limiting strategy next week (from [anticipation](sol://20260327/talents/anticipation)). +- Sarah mentioned wanting to discuss the API rate limiting strategy next week (from [schedule](sol://20260327/talents/schedule)). ## Reading diff --git a/tests/test_activities.py b/tests/test_activities.py index eaa8f6803..d7d10f67b 100644 --- a/tests/test_activities.py +++ b/tests/test_activities.py @@ -2181,3 +2181,212 @@ class TestCheckSegmentFlush: assert _flush_state["day"] == "20260209" assert _flush_state["segment"] == "110000_300" assert _flush_state["last_segment_ts"] > 0 + + +def _seed_activity_records( + tmpdir: str, facet: str, day: str, records: list[dict] +) -> None: + from think.activities import append_activity_record + + for record in records: + append_activity_record(facet, day, record) + + +def test_make_anticipation_id_builds_stable_id(): + from think.activities import make_anticipation_id + + assert make_anticipation_id("meeting", "16:30:00", "2026-04-20") == ( + "anticipated_meeting_163000_0420" + ) + assert make_anticipation_id("deadline", None, "2026-05-05") == ( + "anticipated_deadline_000000_0505" + ) + + +@pytest.mark.parametrize( + ("activity_type", "start", "target_date"), + [ + ("meeting", "9:00", "2026-04-20"), + ("meeting", "09:00:00", "2026/04/20"), + ("", "09:00:00", "2026-04-20"), + ], +) +def test_make_anticipation_id_rejects_malformed_inputs( + activity_type, + start, + target_date, +): + from think.activities import make_anticipation_id + + with pytest.raises(ValueError): + make_anticipation_id(activity_type, start, target_date) + + +def test_dedup_anticipation_returns_empty_for_first_record(monkeypatch): + from think.activities import dedup_anticipation + + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) + + should_write, superseded_ids = dedup_anticipation( + "work", + "20260420", + {"id": "anticipated_meeting_163000_0420", "title": "Yuri intro"}, + ) + + assert should_write is True + assert superseded_ids == [] + + +def test_dedup_anticipation_rejects_exact_id_collision(monkeypatch): + from think.activities import dedup_anticipation + + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) + _seed_activity_records( + tmpdir, + "work", + "20260420", + [ + { + "id": "anticipated_meeting_163000_0420", + "activity": "meeting", + "title": "Yuri intro", + "description": "Original", + "source": "anticipated", + } + ], + ) + + should_write, superseded_ids = dedup_anticipation( + "work", + "20260420", + {"id": "anticipated_meeting_163000_0420", "title": "Yuri intro"}, + ) + + assert should_write is False + assert superseded_ids == [] + + +def test_dedup_anticipation_returns_fuzzy_supersede_matches(monkeypatch): + from think.activities import dedup_anticipation + + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) + _seed_activity_records( + tmpdir, + "work", + "20260420", + [ + { + "id": "anticipated_meeting_160000_0420", + "activity": "meeting", + "title": "Yuri Namikawa intro call", + "description": "Original", + "source": "anticipated", + } + ], + ) + + should_write, superseded_ids = dedup_anticipation( + "work", + "20260420", + { + "id": "anticipated_meeting_163000_0420", + "title": "Yuri Namikawa intro call", + }, + ) + + assert should_write is True + assert superseded_ids == ["anticipated_meeting_160000_0420"] + + +def test_dedup_anticipation_ignores_below_threshold_and_hidden_rows(monkeypatch): + from think.activities import dedup_anticipation + + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) + _seed_activity_records( + tmpdir, + "work", + "20260420", + [ + { + "id": "anticipated_meeting_090000_0420", + "activity": "meeting", + "title": "Quarterly planning summit", + "description": "Visible", + "source": "anticipated", + }, + { + "id": "anticipated_meeting_100000_0420", + "activity": "meeting", + "title": "Yuri Namikawa intro call", + "description": "Hidden", + "source": "anticipated", + "hidden": True, + }, + ], + ) + + should_write, superseded_ids = dedup_anticipation( + "work", + "20260420", + { + "id": "anticipated_meeting_163000_0420", + "title": "Scott Ward standup", + }, + ) + + assert should_write is True + assert superseded_ids == [] + + +def test_dedup_anticipation_returns_all_matching_supersedes(monkeypatch): + from think.activities import dedup_anticipation + + with tempfile.TemporaryDirectory() as tmpdir: + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) + _seed_activity_records( + tmpdir, + "work", + "20260420", + [ + { + "id": "anticipated_call_090000_0420", + "activity": "call", + "title": "Mari Zumbro intro", + "description": "Old 1", + "source": "anticipated", + }, + { + "id": "anticipated_call_093000_0420", + "activity": "call", + "title": "Mari Zumbro intro", + "description": "Old 2", + "source": "anticipated", + }, + { + "id": "cogitate_call_100000_300", + "activity": "call", + "title": "Mari Zumbro intro", + "description": "Non-anticipated", + "source": "cogitate", + }, + ], + ) + + should_write, superseded_ids = dedup_anticipation( + "work", + "20260420", + { + "id": "anticipated_call_103000_0420", + "title": "Mari Zumbro intro", + }, + ) + + assert should_write is True + assert superseded_ids == [ + "anticipated_call_090000_0420", + "anticipated_call_093000_0420", + ] diff --git a/tests/test_formatters.py b/tests/test_formatters.py index ad06f273f..c8869e62d 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -1006,8 +1006,8 @@ class TestFormatEvents: assert "Daily sync" in chunks[0]["markdown"] assert "Task: Code review" in chunks[1]["markdown"] - def test_format_events_anticipation_labels(self): - """Test that anticipations use 'Planned', 'Scheduled', 'Expected' labels.""" + def test_format_events_planned_labels(self): + """Test that planned future events use 'Planned', 'Scheduled', 'Expected' labels.""" from think.events import format_events entries = [ diff --git a/tests/test_home_yesterdays_processing.py b/tests/test_home_yesterdays_processing.py index 1d0d44e01..9d7a41512 100644 --- a/tests/test_home_yesterdays_processing.py +++ b/tests/test_home_yesterdays_processing.py @@ -16,6 +16,8 @@ import pytest from apps.home.routes import ( _briefing_freshness, _build_pulse_context, + _collect_activities, + _collect_events, _format_activity_label, _format_duration, _format_entity_summary, @@ -37,6 +39,14 @@ def _copy_fixture_file(journal: Path, rel_path: str) -> None: shutil.copy2(src, dst) +def _write_jsonl(path: Path, rows: list[dict]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + "".join(json.dumps(row, ensure_ascii=False) + "\n" for row in rows), + encoding="utf-8", + ) + + def _write_facet_meta(journal: Path, facet: str, title: str) -> None: path = journal / "facets" / facet / "facet.json" path.parent.mkdir(parents=True, exist_ok=True) @@ -250,6 +260,81 @@ def test_yesterdays_card_hidden_when_all_zero(tmp_path, monkeypatch): assert _summarize_yesterday_processing("20260416", 9) is None +def test_collectors_merge_anticipated_events_without_double_counting( + tmp_path, + monkeypatch, +): + journal = tmp_path / "journal" + journal.mkdir() + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) + + today = "20260418" + now_ms = int(datetime.now().timestamp() * 1000) + + _write_facet_meta(journal, "work", "Work") + _write_jsonl( + journal / "facets" / "work" / "events" / f"{today}.jsonl", + [ + { + "type": "meeting", + "title": "Team standup", + "start": "09:00:00", + "end": "09:30:00", + "participants": ["Alice"], + "occurred": True, + } + ], + ) + _write_jsonl( + journal / "facets" / "work" / "activities" / f"{today}.jsonl", + [ + { + "id": "anticipated_call_103000_0418", + "activity": "call", + "title": "Mari intro", + "description": "Planned intro call", + "target_date": "2026-04-18", + "start": "10:30:00", + "end": "11:00:00", + "source": "anticipated", + "created_at": now_ms, + "participation": [ + { + "name": "Mari Zumbro", + "role": "attendee", + "source": "screen", + "confidence": 0.9, + "context": "calendar invite", + }, + { + "name": "Ramon", + "role": "mentioned", + "source": "screen", + "confidence": 0.6, + "context": "note", + }, + ], + }, + { + "id": "coding_090000_300", + "activity": "coding", + "title": "Focused coding", + "description": "Recent work", + "created_at": now_ms, + "source": "user", + }, + ], + ) + + events = _collect_events(today) + activities = _collect_activities(today) + + assert [event["title"] for event in events] == ["Team standup", "Mari intro"] + assert events[1]["occurred"] is False + assert events[1]["participants"] == ["Mari Zumbro"] + assert [activity["id"] for activity in activities] == ["coding_090000_300"] + + def test_yesterdays_card_sparse_mode_copy(tmp_path, monkeypatch): journal = _seed_journal(tmp_path, monkeypatch) _write_briefing(journal, "2026-04-15T06:45:00") diff --git a/tests/test_schedule_hook.py b/tests/test_schedule_hook.py new file mode 100644 index 000000000..0f056a85d --- /dev/null +++ b/tests/test_schedule_hook.py @@ -0,0 +1,424 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from __future__ import annotations + +import json +import logging +from pathlib import Path + + +def _write_facet(journal: Path, facet: str) -> None: + facet_path = journal / "facets" / facet / "facet.json" + facet_path.parent.mkdir(parents=True, exist_ok=True) + facet_path.write_text( + json.dumps({"title": facet.title(), "description": ""}), + encoding="utf-8", + ) + + +def _write_detected_entities( + journal: Path, + facet: str, + day: str, + rows: list[dict], +) -> None: + path = journal / "facets" / facet / "entities" / f"{day}.jsonl" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + "".join(json.dumps(row, ensure_ascii=False) + "\n" for row in rows), + encoding="utf-8", + ) + + +def test_schedule_post_process_writes_record_and_resolves_entities( + tmp_path, + monkeypatch, +): + from talent.schedule import post_process + from think.activities import load_activity_records + + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + _write_facet(tmp_path, "work") + _write_detected_entities( + tmp_path, + "work", + "20260420", + [ + {"id": "yuri_namikawa", "type": "Person", "name": "Yuri Namikawa"}, + {"id": "scott_ward", "type": "Person", "name": "Scott Ward"}, + ], + ) + + payload = [ + { + "activity": "meeting", + "target_date": "2026-04-20", + "start": "16:30:00", + "end": "17:30:00", + "title": "Yuri Namikawa intro call", + "description": "Intro call with Yuri from Offline Ventures.", + "details": "Google Meet", + "participation": [ + { + "name": "Yuri Namikawa", + "role": "attendee", + "source": "screen", + "confidence": 0.95, + "context": "calendar invite", + }, + { + "name": "Scott Ward", + "role": "mentioned", + "source": "screen", + "confidence": 0.5, + "context": "mentioned in notes", + }, + { + "name": "Unknown Guest", + "role": "attendee", + "source": "screen", + "confidence": 0.4, + "context": "guest field", + }, + ], + "participation_confidence": 0.88, + "facet": "work", + "cancelled": False, + } + ] + + assert post_process(json.dumps(payload), {"day": "20260418"}) is None + + records = load_activity_records("work", "20260420", include_hidden=True) + assert len(records) == 1 + record = records[0] + assert record["id"] == "anticipated_meeting_163000_0420" + assert record["source"] == "anticipated" + assert record["active_entities"] == ["yuri_namikawa"] + assert record["cancelled"] is False + assert record["hidden"] is False + assert record["participation_confidence"] == 0.88 + assert record["participation"][0]["entity_id"] == "yuri_namikawa" + assert record["participation"][1]["entity_id"] == "scott_ward" + assert record["participation"][2]["entity_id"] is None + assert record["edits"][-1]["actor"] == "schedule" + assert record["edits"][-1]["note"] == "created by schedule" + + +def test_schedule_post_process_marks_cancelled_records_hidden(tmp_path, monkeypatch): + from talent.schedule import post_process + from think.activities import load_activity_records + + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + _write_facet(tmp_path, "work") + + payload = [ + { + "activity": "meeting", + "target_date": "2026-04-22", + "start": "09:00:00", + "end": "10:00:00", + "title": "Scott Ward standup", + "description": "Weekly standup with Scott Ward.", + "details": "Recurring invite", + "participation": [], + "participation_confidence": 0.85, + "facet": "work", + "cancelled": True, + } + ] + + post_process(json.dumps(payload), {"day": "20260418"}) + + records = load_activity_records("work", "20260422", include_hidden=True) + assert len(records) == 1 + record = records[0] + assert record["cancelled"] is True + assert record["hidden"] is True + assert record["edits"][-1]["note"] == "created by schedule (cancelled on calendar)" + + +def test_schedule_post_process_skips_missing_required_field( + tmp_path, + monkeypatch, + caplog, +): + from talent.schedule import post_process + from think.activities import load_activity_records + + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + _write_facet(tmp_path, "work") + caplog.set_level(logging.WARNING, logger="talent.schedule") + + post_process( + json.dumps( + [ + { + "activity": "meeting", + "target_date": "2026-04-20", + "start": "09:00:00", + "end": None, + "description": "Missing title should fail.", + "details": "", + "participation": [], + "participation_confidence": 0.5, + "facet": "work", + "cancelled": False, + } + ] + ), + {"day": "20260418"}, + ) + + assert load_activity_records("work", "20260420", include_hidden=True) == [] + assert "missing required field 'title'" in caplog.text + + +def test_schedule_post_process_skips_unknown_facet(tmp_path, monkeypatch, caplog): + from talent.schedule import post_process + + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + _write_facet(tmp_path, "work") + caplog.set_level(logging.WARNING, logger="talent.schedule") + + post_process( + json.dumps( + [ + { + "activity": "meeting", + "target_date": "2026-04-20", + "start": "09:00:00", + "end": None, + "title": "Wrong facet", + "description": "This facet should be rejected.", + "details": "", + "participation": [], + "participation_confidence": 0.5, + "facet": "missing", + "cancelled": False, + } + ] + ), + {"day": "20260418"}, + ) + + assert "unknown facet 'missing'" in caplog.text + + +def test_schedule_post_process_skips_non_future_items(tmp_path, monkeypatch, caplog): + from talent.schedule import post_process + from think.activities import load_activity_records + + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + _write_facet(tmp_path, "work") + caplog.set_level(logging.WARNING, logger="talent.schedule") + + post_process( + json.dumps( + [ + { + "activity": "meeting", + "target_date": "2026-04-18", + "start": "09:00:00", + "end": None, + "title": "Too soon", + "description": "Should be dropped because it is not future-dated.", + "details": "", + "participation": [], + "participation_confidence": 0.5, + "facet": "work", + "cancelled": False, + } + ] + ), + {"day": "20260418"}, + ) + + assert load_activity_records("work", "20260418", include_hidden=True) == [] + assert "target_date must be after context day" in caplog.text + + +def test_schedule_post_process_logs_error_on_invalid_json( + tmp_path, monkeypatch, caplog +): + from talent.schedule import post_process + + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + _write_facet(tmp_path, "work") + caplog.set_level(logging.ERROR, logger="talent.schedule") + + assert post_process("{not valid json", {"day": "20260418"}) is None + assert "failed to parse JSON" in caplog.text + + +def test_schedule_post_process_is_idempotent(tmp_path, monkeypatch): + from talent.schedule import post_process + from think.activities import load_activity_records + + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + _write_facet(tmp_path, "work") + + payload = json.dumps( + [ + { + "activity": "call", + "target_date": "2026-04-21", + "start": "10:30:00", + "end": "11:00:00", + "title": "Mari Zumbro intro", + "description": "First call with Mari Zumbro.", + "details": "Google Meet", + "participation": [], + "participation_confidence": 0.9, + "facet": "work", + "cancelled": False, + } + ] + ) + + post_process(payload, {"day": "20260418"}) + post_process(payload, {"day": "20260418"}) + + records = load_activity_records("work", "20260421", include_hidden=True) + assert len(records) == 1 + assert records[0]["id"] == "anticipated_call_103000_0421" + + +def test_schedule_post_process_fuzzy_supersedes_previous_record(tmp_path, monkeypatch): + from talent.schedule import post_process + from think.activities import append_activity_record, load_activity_records + + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + _write_facet(tmp_path, "work") + + append_activity_record( + "work", + "20260421", + { + "id": "anticipated_call_100000_0421", + "activity": "call", + "target_date": "2026-04-21", + "start": "10:00:00", + "end": "10:30:00", + "title": "Mari Zumbro intro", + "description": "Old version", + "details": "", + "facet": "work", + "source": "anticipated", + "participation": [], + "active_entities": [], + "participation_confidence": 0.8, + "cancelled": False, + "hidden": False, + }, + ) + + post_process( + json.dumps( + [ + { + "activity": "call", + "target_date": "2026-04-21", + "start": "10:30:00", + "end": "11:00:00", + "title": "Mari Zumbro intro", + "description": "Updated invite", + "details": "Google Meet", + "participation": [], + "participation_confidence": 0.9, + "facet": "work", + "cancelled": False, + } + ] + ), + {"day": "20260418"}, + ) + + records = { + record["id"]: record + for record in load_activity_records("work", "20260421", include_hidden=True) + } + assert set(records) == { + "anticipated_call_100000_0421", + "anticipated_call_103000_0421", + } + assert records["anticipated_call_100000_0421"]["hidden"] is True + assert ( + records["anticipated_call_100000_0421"]["edits"][-1]["note"] + == "superseded by anticipated_call_103000_0421" + ) + assert records["anticipated_call_103000_0421"]["hidden"] is False + + +def test_schedule_post_process_cancelled_record_supersedes_pending( + tmp_path, + monkeypatch, +): + from talent.schedule import post_process + from think.activities import append_activity_record, load_activity_records + + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + _write_facet(tmp_path, "work") + + append_activity_record( + "work", + "20260424", + { + "id": "anticipated_meeting_090000_0424", + "activity": "meeting", + "target_date": "2026-04-24", + "start": "09:00:00", + "end": "10:00:00", + "title": "Scott Ward standup", + "description": "Pending version", + "details": "", + "facet": "work", + "source": "anticipated", + "participation": [], + "active_entities": [], + "participation_confidence": 0.85, + "cancelled": False, + "hidden": False, + }, + ) + + post_process( + json.dumps( + [ + { + "activity": "meeting", + "target_date": "2026-04-24", + "start": "09:30:00", + "end": "10:00:00", + "title": "Scott Ward standup", + "description": "Calendar now shows it cancelled.", + "details": "Recurring invite", + "participation": [], + "participation_confidence": 0.85, + "facet": "work", + "cancelled": True, + } + ] + ), + {"day": "20260418"}, + ) + + records = { + record["id"]: record + for record in load_activity_records("work", "20260424", include_hidden=True) + } + assert set(records) == { + "anticipated_meeting_090000_0424", + "anticipated_meeting_093000_0424", + } + assert records["anticipated_meeting_090000_0424"]["hidden"] is True + assert records["anticipated_meeting_093000_0424"]["hidden"] is True + assert ( + records["anticipated_meeting_090000_0424"]["edits"][-1]["note"] + == "superseded by anticipated_meeting_093000_0424" + ) + assert ( + records["anticipated_meeting_093000_0424"]["edits"][-1]["note"] + == "created by schedule (cancelled on calendar)" + ) diff --git a/think/activities.py b/think/activities.py index f38d5358d..c8677f631 100644 --- a/think/activities.py +++ b/think/activities.py @@ -10,6 +10,7 @@ Also provides utilities for activity records — completed activity spans stored as facets/{facet}/activities/{day}.jsonl. """ +import difflib import fcntl import json import logging @@ -25,6 +26,7 @@ from typing import Any from think.utils import get_journal, segment_parse logger = logging.getLogger(__name__) +ANTICIPATION_FUZZY_THRESHOLD = 0.85 # --------------------------------------------------------------------------- # Default Activities @@ -873,6 +875,66 @@ def load_activity_records( return [record for record in records if not record.get("hidden", False)] +def make_anticipation_id( + activity_type: str, + start: str | None, + target_date: str, +) -> str: + """Build the stable ID used for schedule-generated anticipated records.""" + activity_key = str(activity_type or "").strip() + if not activity_key: + raise ValueError("activity_type must be non-empty") + + try: + parsed_target = datetime.strptime(target_date, "%Y-%m-%d") + except ValueError as exc: + raise ValueError("target_date must match YYYY-MM-DD") from exc + + if start is None: + start_key = "000000" + else: + if not re.fullmatch(r"\d{2}:\d{2}:\d{2}", start): + raise ValueError("start must match HH:MM:SS") + start_key = start.replace(":", "") + + return f"anticipated_{activity_key}_{start_key}_{parsed_target.strftime('%m%d')}" + + +def dedup_anticipation( + facet: str, + target_day: str, + new_record: dict[str, Any], + *, + threshold: float = ANTICIPATION_FUZZY_THRESHOLD, +) -> tuple[bool, list[str]]: + """Check a new anticipated record for collisions and fuzzy supersedes.""" + + new_id = str(new_record.get("id") or "").strip() + if not new_id: + raise ValueError("new_record.id is required") + + def _normalize_title(value: Any) -> str: + return " ".join(str(value or "").lower().split()) + + new_title = _normalize_title(new_record.get("title")) + superseded_ids: list[str] = [] + + for record in load_activity_records(facet, target_day, include_hidden=False): + if record.get("source") != "anticipated": + continue + + existing_id = str(record.get("id") or "").strip() + if existing_id == new_id: + return False, [] + + existing_title = _normalize_title(record.get("title")) + ratio = difflib.SequenceMatcher(None, new_title, existing_title).ratio() + if ratio >= threshold: + superseded_ids.append(existing_id) + + return True, superseded_ids + + def load_record_ids(facet: str, day: str) -> set[str]: """Load just the IDs of existing activity records for idempotency checks.""" return { diff --git a/think/events.py b/think/events.py index e6a9376d9..6a919a409 100644 --- a/think/events.py +++ b/think/events.py @@ -126,10 +126,10 @@ def format_events( ) lines.append(f"**{participants_label}:** {', '.join(participants)}") - # For anticipations, show when it was created (from source path) + # For future-dated event rows, show when they were created (from source path) if not occurred: source = event.get("source", "") - # Extract YYYYMMDD from source path like "20240101/talents/schedule.md" + # Extract YYYYMMDD from source path like "20240101/talents/agent.md" source_match = re.match(r"(\d{8})/", source) if source_match: created_day = source_match.group(1) diff --git a/think/hooks.py b/think/hooks.py index e6cf94b2e..6bd78f8cf 100644 --- a/think/hooks.py +++ b/think/hooks.py @@ -1,11 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Shared utilities for output extraction hooks. - -This module provides common functions used by extraction hooks like -occurrence.py and anticipation.py in the talent/ directory. -""" +"""Shared utilities for output-side event hooks.""" import json import logging @@ -113,8 +109,8 @@ def write_events_jsonl( Args: events: List of event dictionaries from extraction. - agent: Source generator agent (e.g., "meetings", "schedule"). - occurred: True for occurrences, False for anticipations. + agent: Source generator agent (e.g., "meetings", "flow"). + occurred: True for occurrence rows, False for future-dated event rows. source_output: Relative path to source output file. capture_day: Day the output was captured (YYYYMMDD). @@ -146,7 +142,7 @@ def write_events_jsonl( # Occurrences use capture day event_day = capture_day else: - # Anticipations use their scheduled date + # Future-dated event rows use their scheduled date event_date = event.get("date", "") # Convert YYYY-MM-DD to YYYYMMDD event_day = event_date.replace("-", "") if event_date else capture_day