personal memory agent
0
fork

Configure Feed

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

talents: replace 4 per-span talents + decisionalizer with 3 storytelling talents

Retire meetings/decisions/followups/messaging per-span talents plus the
orphaned decisionalizer cogitate. Introduce conversation/work/event
storytelling talents that each emit one structured JSONL row per
(span, talent) into journal/facets/{facet}/spans/{YYYYMMDD}.jsonl via a
shared talent/spans.py post-hook. think/spans.py::format_spans renders
each row as an FTS-indexable markdown chunk.

Dispatch changes in think/thinking.py skip all three talents on
synthetic (cogitate/anticipated) or segmentless activity records, and
skip the work talent for browsing/reading activities below level_avg
0.4. Coding is ungated.

Forward-only flip: historical on-disk markdown under
facets/*/activities/{date}/{span_id}/*.md is preserved and still
indexed. Downstream consumers that read the retired filenames
(speakers app, activities_review, morning_briefing) continue to work on
pre-flip-day data and degrade gracefully on new data; a follow-up will
migrate them to read the new spans.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+1234 -724
+3 -1
AGENTS.md
··· 38 38 | `think/` | Post-processing core — cortex, talent, callosum, indexer, entities, facets, activities, scheduler, heartbeat, supervisor | anything downstream of capture; most coder work lives here | `docs/THINK.md`, `docs/CORTEX.md`, `docs/CALLOSUM.md` | 39 39 | `convey/` | Web app framework — app discovery, routing, bridge, screenshot tooling | layout / framework-level UI changes | `docs/CONVEY.md` | 40 40 | `apps/` | Convey apps — each self-contained (`call.py` Typer sub-app + `routes.py` + `templates/`) | adding a user-facing feature, a `sol call <app>` verb, a UI surface | `docs/APPS.md` (required reading before modifying `apps/`) | 41 - | `talent/` | AI talent configs (markdown prompts + optional `.py` post-hooks) + `SKILL.md`s (journal, coder, partner, decisionalizer, …) | defining or tuning a talent; adding a journal-side skill | `talent/journal/SKILL.md`, `docs/PROMPT_TEMPLATES.md` | 41 + | `talent/` | AI talent configs (markdown prompts + optional `.py` post-hooks) + `SKILL.md`s (journal, coder, partner, …) | defining or tuning a talent; adding a journal-side skill | `talent/journal/SKILL.md`, `docs/PROMPT_TEMPLATES.md` | 42 42 | `scripts/` | Repo maintenance scripts — `check_layer_hygiene.py`, `gate_agents_rename.py` | tooling that guards the codebase; wired into `make ci` | (none) | 43 43 | `tests/` | Pytest suites + `tests/fixtures/journal/` mock journal | writing tests; debugging flakiness; `make dev` / `make sandbox` use fixtures as the journal | `docs/testing.md` | 44 44 | `docs/` | All longform documentation | reference lookups; never your first stop | §10 below | ··· 301 301 | `docs/JOURNAL.md` | **Breadcrumb only** — redirects to `talent/journal/SKILL.md`, the progressive-disclosure journal-layout reference | 302 302 | `talent/journal/SKILL.md` | Journal layout, vocabulary, and `sol call journal` CLI (loaded by cogitate talents on demand via skills) | 303 303 | `talent/journal/references/cli.md` | Full `sol call journal` reference, including **Talent CLI Boundaries** (which infrastructure commands cogitate talents must not call) | 304 + 305 + The live journal also carries `journal/AGENTS.md` as its runtime-facing breadcrumb. 304 306 305 307 `docs/BACKLOG.md` and `docs/ROADMAP.md` are product-planning docs — CPO/CEO reading, not coder reading. 306 308
+2
apps/search/routes.py
··· 25 25 "knowledge_graph": "🗺️", 26 26 "meetings": "📅", 27 27 "event": "📅", 28 + "span": "🧵", 28 29 "audio": "🎤", 29 30 "screen": "🖥️", 30 31 "todo": "✅", ··· 41 42 "knowledge_graph": "Knowledge Graph", 42 43 "meetings": "Meetings", 43 44 "event": "Event", 45 + "span": "Span", 44 46 "audio": "Transcript", 45 47 "screen": "Screen", 46 48 "todo": "Todo",
+45
talent/conversation.md
··· 1 + { 2 + "type": "generate", 3 + "title": "Conversation Story", 4 + "description": "Writes a structured narrative span row for meeting, call, messaging, and email activities.", 5 + "color": "#00796b", 6 + "schedule": "activity", 7 + "activities": ["meeting", "call", "messaging", "email"], 8 + "priority": 10, 9 + "output": "json", 10 + "hook": {"post": "spans"}, 11 + "load": { 12 + "transcripts": true, 13 + "percepts": true, 14 + "talents": false 15 + } 16 + } 17 + 18 + $facets 19 + 20 + $activity_context 21 + 22 + $activity_preamble 23 + 24 + # Conversation Story 25 + 26 + Write JSON only. No markdown fences. No prose outside the JSON object. 27 + 28 + Summarize this conversation as one coherent narrative for the full activity. 29 + Participation and entity extraction already happened upstream. Reuse that context; 30 + do not re-extract people or entities into new structures. 31 + 32 + Return exactly these three fields: 33 + - `body`: string narrative prose covering what was discussed, what moved, and any commitments. 34 + - `topics`: array of 3-8 short string tags. 35 + - `confidence`: float from 0.0 to 1.0. 36 + 37 + Body requirements: 38 + - Write one tight paragraph in chronological order. 39 + - Include 1-3 short verbatim quotes inline only when they sharpen a decision, 40 + commitment, or disagreement. 41 + - Focus on the actual exchange, not generic meeting boilerplate. 42 + - If the activity mixes channels, unify them into one narrative rather than 43 + listing separate threads. 44 + 45 + Output a single JSON object with only `body`, `topics`, and `confidence`.
-185
talent/decisionalizer.md
··· 1 - { 2 - "type": "cogitate", 3 - 4 - "title": "Decision Dossier Generator", 5 - "description": "Analyzes the day's top decision-actions to create detailed dossiers identifying gaps and stakeholder impacts", 6 - "color": "#c62828", 7 - "schedule": "daily", 8 - "priority": 60, 9 - "output": "md", 10 - "hook": {"pre": "decisionalizer"} 11 - } 12 - 13 - $facets 14 - 15 - ## Mission 16 - From the day's decision-action outputs (produced per-activity), you will: 17 - 1. Gather all decision-actions across the day's activities 18 - 2. Select the TWO most consequential decisions based on impact criteria 19 - 3. Use `sol call` commands to research context, stakeholders, and follow-ups 20 - 4. Identify gaps between expected and actual obligations 21 - 5. Produce actionable dossiers with specific remedies 22 - 23 - ## Available Commands 24 - 25 - SOL_DAY is set in your environment. Commands like `transcripts read` default to the current day — only pass explicit day values to override. Note: `journal search` requires explicit `-d DAY`. 26 - 27 - - `sol call journal search` for discovery across journal content 28 - - `sol call transcripts read --start HHMMSS --length MINUTES --full|--audio|--screen` for transcript windows 29 - 30 - **Query syntax**: Searches match ALL words by default; use `OR` between words to match ANY (e.g., `apple OR orange`), quote phrases for exact matches (e.g., `"project meeting"`), and append `*` for prefix matching (e.g., `debug*`). 31 - 32 - ## Inputs 33 - - The analysis day in YYYYMMDD format (provided as context) 34 - - Current date/time (provided as context) 35 - 36 - ## PHASE 0: Load the Day's Decisions 37 - 38 - **CRITICAL FIRST STEP**: Before any analysis, gather all decision outputs from the day's activities: 39 - 40 - ``` 41 - sol call journal search "decision" -d $day_YYYYMMDD -a decisions 42 - ``` 43 - 44 - Decision-actions are now produced per-activity, so this search may return multiple result sets (one per activity that had decisions). Collect ALL decision-actions across all activities into a single working list. If no decision outputs exist for this day, stop and report this clearly. 45 - 46 - ## PHASE 1: Decision Selection 47 - 48 - From all decision-actions gathered across activities, select the TWO most consequential based on: 49 - - Number of people affected (breadth of impact) 50 - - Criticality and irreversibility 51 - - Time sensitivity and deadline pressures 52 - - External stakeholder involvement 53 - - Dependencies on critical systems or processes 54 - 55 - ## PHASE 2: Research Protocol (for each selected decision) 56 - 57 - ### Step 1: Immediate Context Discovery 58 - Use these tools in sequence: 59 - 60 - 1. **Find the decision moment:** 61 - - `sol call journal search "decision keywords here" -d $day_YYYYMMDD -a audio -n 10` 62 - - Goal: Pinpoint exact time of decision (HH:MM:SS) 63 - 64 - 2. **Get full context:** 65 - - `sol call transcripts read --start HHMMSS --length 30 --full` 66 - - Goal: Extract 30 minutes of raw activity around decision time 67 - 68 - ### Step 2: Stakeholder & Dependency Mapping 69 - 70 - 1. **Identify all entities:** 71 - - `sol call journal search "entity names from decision" -d $day_YYYYMMDD` 72 - - Goal: Find all people, teams, projects mentioned 73 - 74 - 2. **Map meeting participants:** 75 - - `sol call journal search "[keywords]" -d $day_YYYYMMDD -a meetings` 76 - - `sol call journal search "[keywords]" -a news -f work -d $day_YYYYMMDD` for public announcements 77 - - Goal: Identify who needs to know about this decision 78 - 79 - ### Step 3: Historical Precedent Mining (30-day lookback) 80 - 81 - 1. **Find similar past decisions:** 82 - - `sol call journal search "decision type AND key entities" -n 20` 83 - - Goal: Discover patterns in how similar decisions were handled 84 - 85 - 2. **Check commitment history:** 86 - - `sol call journal search "entity decision approve cancel" -a audio -n 15` 87 - - Goal: Identify typical follow-up patterns 88 - 89 - ### Step 4: Forward Impact Assessment (2-6 hours post-decision) 90 - 91 - 1. **Check for communications:** 92 - - `sol call journal search "[keywords]" -d $day_YYYYMMDD -a audio -n 10` 93 - - `sol call journal search "[keywords]" -a news -d $day_YYYYMMDD` for decision announcements 94 - - Goal: Find follow-up notifications or discussions 95 - 96 - 2. **Review meetings:** 97 - - `sol call journal search "[keywords]" -d $day_YYYYMMDD -a meetings` 98 - - Goal: See if decision was discussed 99 - 100 - 3. **Check messaging:** 101 - - `sol call journal search "message OR notification" -d $day_YYYYMMDD` 102 - - Goal: Verify notifications were sent 103 - 104 - ### Step 5: Gap Detection 105 - 106 - 1. **Search for problems:** 107 - - `sol call journal search "rollback revert issue problem" -d $day_YYYYMMDD` 108 - - Goal: Identify emerging issues 109 - 110 - 2. **Verify updates:** 111 - - `sol call journal search "document update change commit" -d $day_YYYYMMDD -a audio` 112 - - Goal: Confirm tracking artifacts were updated 113 - 114 - OBLIGATION CATEGORIES (derive expectations from decision type + precedents; adapt to what the data shows) 115 - - Communication: informing affected people/groups at an appropriate breadth and channel; acknowledging external stakeholders when present. 116 - - Traceability: references to a tracking artifact or review process; linkage between the decision and the canonical record. 117 - - Plan-of-Record: updates to sources-of-truth (documents, schedules, tickets) so others can rely on current state. 118 - - Coordination: rescheduling, assignment, or next-step instructions when the decision creates work for others. 119 - - Mitigation & Reversibility: contingency, rollback plan, or risk acknowledgement for sensitive/irreversible changes. 120 - 121 - SCORING DIMENSIONS (qualitative, then calibrate a single confidence) 122 - - Others Affected: low / medium / high (breadth of impact). 123 - - Severity: low / medium / high (cost, sensitivity, coupling). 124 - - Reversibility: short-window / recoverable / painful. 125 - 126 - OUTPUT FORMAT (Markdown only; no JSON, no code blocks) 127 - 128 - ## Selected Decisions 129 - 130 - Brief statement of which 2 decisions you selected and why they are the most consequential. 131 - 132 - ## Decision Dossiers 133 - 134 - For each of your TWO selected decisions, create a detailed dossier: 135 - 136 - ### [#N] Decision: <short action summary> 137 - **When:** HH:MM:SS–HH:MM:SS 138 - **Type:** <decision type> 139 - **Why it matters (one-liner):** <succinct statement of potential impact on others> 140 - 141 - **Stakeholders & Dependencies** 142 - - Direct stakeholders: <bulleted names/roles/groups> 143 - - Indirect stakeholders: <bulleted names/roles/groups> 144 - - Dependencies (artifacts/services/meetings/docs): <bulleted list> 145 - - Breadth & sensitivity: <low/med/high breadth; flags like external_involved, high_centrality, time_sensitive> 146 - 147 - **Context & Precedents (recent history)** 148 - - Prior related decisions: <brief bullets with dates and how they resolved> 149 - - Usual follow-ups observed in similar past cases: <brief bullets> 150 - - Noted commitments that may apply: <brief bullets with who/what/when> 151 - 152 - **Observed Follow-ups (forward window)** 153 - - Communications observed: <bullets with times and audience scope> 154 - - Traceability updates: <bullets with times and artifact names if present> 155 - - Coordination/mitigation actions: <bullets, note absence if notable> 156 - 157 - **Evidence Timeline (minimal, high-signal)** 158 - - [HH:MM:SS] Audio: "<≤20 words>" 159 - - [HH:MM:SS] Screen/OCR: <short phrase> 160 - - [HH:MM:SS] Metadata: <concise note such as audience size/externality/environment> 161 - 162 - **Possible Side-Effects & Gaps** 163 - - <Gap/Side-effect #1>: <why this matters to others>; **obligation category:** <communication|traceability|plan|coordination|mitigation> 164 - - <Gap/Side-effect #2>: <…> 165 - (Include 2–5 items, only if supported by evidence or strong precedent.) 166 - 167 - **Assessment & Recommendation** 168 - - Others Affected: <low|medium|high> · Severity: <low|medium|high> · Reversibility: <short-window|recoverable|painful> · Confidence: <0.00–1.00> 169 - - Suggested next step(s): <clear, minimal actions to close gaps or reduce side-effects> 170 - 171 - ## Insights & Blind Spots 172 - 173 - Brief paragraph on: 174 - - Key patterns observed across both decisions 175 - - Data gaps or missing windows that limited analysis 176 - - Systemic issues that may require attention 177 - 178 - ## EXECUTION NOTES 179 - 180 - 1. **Tool Usage**: Use the exact `sol call` commands provided. Do not invent command names or parameters. 181 - 2. **Evidence**: Only cite what you find via tools. Never fabricate entities or counts. 182 - 3. **Precision**: When uncertain, mark confidence levels clearly. 183 - 4. **Focus**: Analyze only TWO decisions deeply rather than all of them superficially. 184 - 5. **Actionability**: Every gap identified should have a specific remedy. 185 - 6. **Critical Gaps**: If you discover a critical gap that needs immediate attention, clearly highlight it in your output with a **CRITICAL GAP** heading.
-16
talent/decisionalizer.py
··· 1 - # SPDX-License-Identifier: AGPL-3.0-only 2 - # Copyright (c) 2026 sol pbc 3 - 4 - """Pre-hook for decisionalizer talent — skips days with no decision outputs.""" 5 - 6 - from pathlib import Path 7 - 8 - from think.utils import get_journal 9 - 10 - 11 - def pre_process(context: dict) -> dict | None: 12 - """Skip days that have no decision activity outputs.""" 13 - day = context["day"] 14 - if not any(Path(get_journal()).glob(f"facets/*/activities/{day}/*/decisions.md")): 15 - return {"skip_reason": "no decision outputs for day"} 16 - return {}
-80
talent/decisions.md
··· 1 - { 2 - "type": "generate", 3 - 4 - "title": "Decision Actions", 5 - "description": "Tracks consequential decision-actions that change state, plans, resources, responsibilities, or timing in ways that affect other people.", 6 - "color": "#dc3545", 7 - "schedule": "activity", 8 - "activities": ["meeting", "call", "messaging", "email"], 9 - "priority": 10, 10 - "output": "md", 11 - "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 12 - } 13 - 14 - $facets 15 - 16 - $activity_context 17 - 18 - $activity_preamble 19 - 20 - ## Goal 21 - 22 - Identify and rank the most consequential DECISION-ACTIONS observable in this activity's recording segments (audio transcript, on-screen OCR/description, and local metadata). A decision-action is a bounded action that CHANGES the state, plan, resources, responsibilities, or timing in ways that plausibly affect other people (directly or indirectly). 23 - 24 - Use the Activity Context and Activity State Per Segment sections above to understand what this activity involves and to focus on relevant content in the transcript. 25 - 26 - WHAT COUNTS AS A DECISION-ACTION 27 - Look for actions that: 28 - - Declare or enact a change (state transitions, approvals, cancellations, allocations, permissions, commits, deferrals). 29 - - Create obligations or expectations for other people. 30 - - Bind future behavior (commitments, deadlines, irreversible moves). 31 - - Broadcast or withhold information at scale. 32 - - Touch high-centrality entities (large groups, critical resources). 33 - - Span boundaries (cross-team/org, external stakeholders). 34 - 35 - EXCLUDE 36 - - Pure narration or speculation without enactment. 37 - - Routine, reversible micro-steps with no effect on others. 38 - - Duplicates of the same decision; merge overlapping segments. 39 - - Content from concurrent activities unrelated to this $activity_type activity. 40 - 41 - RANKING PRINCIPLES 42 - Order by importance using: 43 - - Impact surface (# of people/teams plausibly affected; external presence). 44 - - Centrality/criticality of entities touched. 45 - - Irreversibility & coupling (cost to undo, downstream dependencies). 46 - - Temporal sensitivity (deadlines, meetings, releases). 47 - - Novelty vs. routine. 48 - 49 - OUTPUT FORMAT 50 - Produce two Markdown sections: 51 - 52 - ### Summary of Considerations 53 - A concise paragraph (<= 120 words) explaining how you assessed decision-actions in this activity: 54 - - Key conceptual patterns you looked for. 55 - - How you weighed impact on others. 56 - - Notable ambiguities or blind spots. 57 - 58 - ### Decision-Actions 59 - A ranked Markdown list of decision-actions found. For each item, provide: 60 - 61 - - **Time:** HH:MM:SS–HH:MM:SS (tight span, <= 90s if possible) 62 - - **Action Summary:** short phrase (<= 16 words) 63 - - **Decision Type:** DECLARE_CHANGE | SCHEDULE | RESCHEDULE | CANCEL | ALLOCATE | (RE)ASSIGN | APPROVE | ESCALATE | PUBLISH | UNPUBLISH | PERMIT | REVOKE | COMMIT | REVERT | DEFER | OTHER 64 - - **Actors:** who is making the decision (you + any co-deciders) 65 - - **Entities:** people, teams, groups, projects/issues, repos/branches, docs/artifacts, meetings, environments, orgs 66 - - **Impact Surface:** approx # people affected; external stakeholders? breadth (low/med/high); criticality flags (time_sensitive, high_centrality, irreversible) 67 - - **Evidence:** 68 - - Transcript quotes (<= 20 words each, 1–2 max) 69 - - Screen phrases (OCR/visual cues of enactment, when available) 70 - - Metadata notes (audience size, env flags, etc.) 71 - - **Stakes for Others:** <= 30 words on likely consequences 72 - - **Confidence:** 0.0–1.0 calibration (0.50 maybe, 0.70 likely, 0.85+ clear) 73 - 74 - If no decision-actions are found in this activity, output only a brief sentence explaining why (e.g., "No consequential decision-actions were observed during this $activity_type activity."). 75 - 76 - STRICT RULES 77 - - Do not fabricate entities or counts; estimate only from inputs. 78 - - Anchor times to actual segment boundaries. 79 - - Evidence should include both intent (often transcript) and enactment (often screen/metadata) when available. 80 - - Maintain Markdown only; no JSON or code blocks in the final output.
+44
talent/event.md
··· 1 + { 2 + "type": "generate", 3 + "title": "Event Story", 4 + "description": "Writes a structured narrative span row for appointment, event, travel, errand, celebration, deadline, and reminder activities.", 5 + "color": "#ff7043", 6 + "schedule": "activity", 7 + "activities": ["appointment", "event", "travel", "errand", "celebration", "deadline", "reminder"], 8 + "priority": 10, 9 + "output": "json", 10 + "hook": {"post": "spans"}, 11 + "load": { 12 + "transcripts": true, 13 + "percepts": true, 14 + "talents": false 15 + } 16 + } 17 + 18 + $facets 19 + 20 + $activity_context 21 + 22 + $activity_preamble 23 + 24 + # Event Story 25 + 26 + Write JSON only. No markdown fences. No prose outside the JSON object. 27 + 28 + Summarize what happened in this event, appointment, errand, travel block, or 29 + deadline-related activity. Participation and entity extraction already happened 30 + upstream. Use that context; do not re-extract people or entities into new 31 + structures. 32 + 33 + Return exactly these three fields: 34 + - `body`: string narrative prose describing what happened and any outcome. 35 + - `topics`: array of 3-8 short string tags. 36 + - `confidence`: float from 0.0 to 1.0. 37 + 38 + Body requirements: 39 + - Write one tight paragraph in chronological order. 40 + - Capture the event context, notable actions, and any decision or outcome. 41 + - Prefer what actually occurred over generic labels from the activity type. 42 + - If evidence is thin, keep the narrative modest and confidence honest. 43 + 44 + Output a single JSON object with only `body`, `topics`, and `confidence`.
-63
talent/followups.md
··· 1 - { 2 - "type": "generate", 3 - 4 - "title": "Follow-Up Items", 5 - "description": "Detects promised tasks, commitments, and reminders for future action within each activity. Outputs a concise Markdown list of follow-ups with context.", 6 - "color": "#ffc107", 7 - "schedule": "activity", 8 - "activities": ["meeting", "call", "messaging", "email"], 9 - "priority": 10, 10 - "output": "md", 11 - "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 12 - } 13 - 14 - $facets 15 - 16 - $activity_context 17 - 18 - $activity_preamble 19 - 20 - # Follow-up Identification 21 - 22 - ## Objective 23 - 24 - Review this activity's transcript to find instances where an important future action is implied, requested, or promised, covering both professional and personal contexts. 25 - 26 - Use the Activity Context and Activity State Per Segment sections above to understand what this activity involves and to focus on relevant content in the transcript. 27 - 28 - ## Approach 29 - 30 - 1. **Sequential Review** 31 - - Read the transcript chronologically, one block at a time. 32 - - Look for statements or contextual cues indicating outstanding tasks, open questions, or commitments to reconnect later. 33 - 34 - 2. **Recognize Follow-up Triggers** 35 - - Phrases such as "I'll do that tomorrow," "Let's talk later," or "Need to check". 36 - - References to documents, links, or resources to revisit. 37 - - Unresolved issues or decisions deferred to a future date. 38 - - Personal reminders like errands, appointments, or messages to send. 39 - 40 - 3. **Assess Importance** 41 - - Note who initiated the follow-up and any deadlines or urgency mentioned. 42 - - Determine if the context is work-related or personal. 43 - - Prioritize items that appear critical for ongoing projects or relationships. 44 - 45 - ## Exclusions 46 - 47 - - Content from concurrent activities unrelated to this $activity_type activity. 48 - - Pure speculation or hypothetical scenarios without a concrete commitment. 49 - - Duplicates of the same follow-up; merge overlapping mentions. 50 - 51 - ## Output Format 52 - 53 - Produce a concise Markdown list capturing each follow-up with the following fields in individual blocks with a short title: 54 - 55 - - **Time:** HH:MM:SS–HH:MM:SS (tight span) 56 - - **Context** – short description of the discussion or screen activity. 57 - - **Action Needed** – what follow-up is expected. 58 - - **Work/Personal** – classify the nature of the task. 59 - - **Confidence:** 0.0–1.0 calibration 60 - 61 - Conclude with a brief summary (<= 100 words) highlighting the most significant follow-ups. 62 - 63 - If no follow-ups are found in this activity, output only a brief sentence explaining why (e.g., "No follow-up items were identified during this $activity_type activity.").
+2 -1
talent/journal/references/captures.md
··· 268 268 Each template is a `.md` file with JSON frontmatter containing metadata (title, description, schedule, output format). The `schedule` field is required and must be `"segment"` or `"daily"` - generators with missing or invalid schedule are skipped. Use `get_talent_configs(has_tools=False)` from `think/talent.py` to retrieve all available generators, or `get_talent_configs(has_tools=False, schedule="daily")` to get generators filtered by schedule. 269 269 270 270 **Output naming:** 271 - - System outputs: `talents/{agent}.md` (e.g., `talents/meetings.md`, `talents/briefing.md`) 271 + - System outputs: `talents/{agent}.md` (e.g., `talents/briefing.md`, `talents/default.md`) 272 272 - App outputs: `talents/_{app}_{agent}.md` (e.g., `talents/_entities_observer.md`) 273 273 - JSON output: `talents/{agent}.json` when metadata specifies `"output": "json"` 274 + - Story span rows: `facets/{facet}/spans/{day}.jsonl` 274 275 275 276 Each generator type has a corresponding template file (`{name}.md`) that defines how the AI synthesizes extracts into narrative form.
+3 -3
talent/journal/references/cli.md
··· 26 26 - `-d, --day`: exact day filter (`YYYYMMDD`). 27 27 - `--day-from`, `--day-to`: inclusive date-range filters (`YYYYMMDD`). 28 28 - `-f, --facet`: facet filter (for example `work`, `personal`). 29 - - `-a, --agent`: agent/content filter (for example `meetings`, historical `event`, `news`, `entity:detected`). 29 + - `-a, --agent`: agent/content filter (for example `span`, historical `event`, `news`, `entity:detected`). 30 30 31 31 Behavior notes: 32 32 ··· 227 227 228 228 Read full content of an agent output. 229 229 230 - - `AGENT`: agent name, e.g. `meetings`, `briefing`, `activity` (positional argument). 230 + - `AGENT`: agent name, e.g. `briefing`, `activity`, `screen` (positional argument). 231 231 - `-d, --day`: day in `YYYYMMDD` (default: `SOL_DAY` env). 232 232 - `-s, --segment`: optional segment key (default: `SOL_SEGMENT` env). 233 233 - `--max`: max output bytes (default `16384`, `0` for unlimited). ··· 238 238 239 239 ```bash 240 240 sol call journal read briefing -d 20260115 241 - sol call journal read meetings 241 + sol call journal read briefing 242 242 sol call journal read activity -s 091500_300 243 243 ``` 244 244
+3 -3
talent/journal/references/config.md
··· 242 242 "contexts": { 243 243 "observe.*": {"provider": "google", "tier": 3}, 244 244 "talent.system.*": {"tier": 1}, 245 - "talent.system.meetings": {"provider": "anthropic", "disabled": true}, 245 + "talent.system.conversation": {"provider": "anthropic", "disabled": true}, 246 246 "talent.entities.observer": {"tier": 2} 247 247 }, 248 248 "models": { ··· 273 273 ### Context matching 274 274 275 275 Contexts are matched in order of specificity: 276 - 1. **Exact match** – `"talent.system.meetings"` matches only that exact context 276 + 1. **Exact match** – `"talent.system.conversation"` matches only that exact context 277 277 2. **Glob pattern** – `"observe.*"` matches any context starting with `observe.` 278 278 3. **Default** – Falls back to the `default` configuration 279 279 280 280 ### Context naming convention 281 281 282 282 Talent configs (agents and generators) use the pattern `talent.{source}.{name}`: 283 - - System configs: `talent.system.{name}` (e.g., `talent.system.meetings`, `talent.system.default`) 283 + - System configs: `talent.system.{name}` (e.g., `talent.system.conversation`, `talent.system.default`) 284 284 - App configs: `talent.{app}.{name}` (e.g., `talent.entities.observer`, `talent.support.support`) 285 285 286 286 Other contexts follow the pattern `{module}.{feature}[.{operation}]`:
-74
talent/meetings.md
··· 1 - { 2 - "type": "generate", 3 - 4 - "title": "Meeting Notes", 5 - "description": "Produces detailed meeting notes for each meeting activity, including participants, topics discussed, action items, and presentation details.", 6 - "color": "#e83e8c", 7 - "schedule": "activity", 8 - "activities": ["meeting"], 9 - "priority": 10, 10 - "output": "md", 11 - "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 12 - } 13 - 14 - $facets 15 - 16 - $activity_context 17 - 18 - $activity_preamble 19 - 20 - # Meeting Notes 21 - 22 - ## Objective 23 - 24 - Produce detailed meeting notes for this $activity_type activity. The activity system has already identified this as a meeting and provided the time range, description, and known participants above. Your job is to enrich that with thorough notes from the transcript. 25 - 26 - Use the Activity Context and Activity State Per Segment sections above to understand the meeting's scope and participants. 27 - 28 - ## Extract Meeting Details 29 - 30 - Prioritize the audio transcript as the primary source of truth: 31 - 32 - 1. **Participants** 33 - - Use the meeting software participant panel (Zoom/Meet/Teams UI participant list) as the primary source of truth. 34 - - If no reliable participant panel is visible, fall back to people who are visibly presenting or audibly speaking in the meeting. 35 - - Exclude organizations, companies, products, projects, tools, topics, podcast guests, and people only mentioned in conversation but not present. 36 - - Consolidate transcription variants of the same person and include $name as the default participant when unknown. 37 - 38 - 2. **Topics Discussed** 39 - - Synthesize the conversation into a concise summary of key subjects. 40 - - Note entities mentioned: people, teams, projects, technologies, companies, organizations. 41 - 42 - 3. **Meeting Brief** 43 - - Create a short one-liner title describing the meeting for list context. 44 - 45 - 4. **Slides Presented** 46 - - Note whether presentation slides were visible on screen (PowerPoint, Google Slides, Keynote, slide transitions, structured presentation content). 47 - - If slides were shown, provide a short summary of slide content and themes. 48 - 49 - 5. **Key Outcomes** 50 - - Decisions made during the meeting. 51 - - Action items or follow-ups assigned to specific people. 52 - - Open questions left unresolved. 53 - - Notable ideas or proposals floated, even if not decided on (speculative ideas from meetings are often the most valuable thing to capture — they don't survive as action items but represent new thinking). 54 - 55 - ## Exclusions 56 - 57 - - Content from concurrent activities unrelated to this meeting. 58 - - Duplicates of the same topic; merge overlapping discussion threads. 59 - 60 - ## Output Format 61 - 62 - Produce a friendly Markdown document with: 63 - 64 - - **Brief** – one-liner meeting title 65 - - **Time:** $segment_start–$segment_end 66 - - **Participants** – list of names involved 67 - - **Topics Discussed** – concise summary of key subjects and entities mentioned 68 - - **Slides Presented** – yes/no, with short description if yes 69 - - **Key Outcomes** – decisions, action items, and open questions 70 - - **Key Quotes** – 2-3 memorable or significant quotes with speaker attribution. Omit this section entirely if nothing stands out; do not pad with unremarkable quotes. 71 - 72 - Conclude with a brief summary (<= 100 words) of the meeting's significance and any immediate next steps. 73 - 74 - If the transcript does not contain substantive meeting content (e.g., a false positive from the activity detector), output only a brief sentence explaining why (e.g., "No substantive meeting content was found in this activity's transcript.").
-74
talent/messaging.md
··· 1 - { 2 - "type": "generate", 3 - 4 - "title": "Messaging Summary", 5 - "description": "Extracts contacts, channels, apps, and message content from completed messaging and email activities.", 6 - "color": "#78909c", 7 - "schedule": "activity", 8 - "activities": ["messaging", "email"], 9 - "priority": 10, 10 - "output": "md", 11 - "load": {"transcripts": true, "percepts": false, "talents": {"screen": true}} 12 - } 13 - 14 - $facets 15 - 16 - $activity_context 17 - 18 - $activity_preamble 19 - 20 - # Messaging & Email Analysis 21 - 22 - ## Objective 23 - 24 - Produce a detailed summary of this $activity_type activity. The activity system has already identified this as a messaging or email session and provided the time range, description, and known participants above. Your job is to enrich that with thorough extraction from the transcript. 25 - 26 - Use the Activity Context and Activity State Per Segment sections above to understand the session's scope and participants. 27 - 28 - ## Extract Communication Details 29 - 30 - Prioritize the audio transcript and screen activity together: 31 - 32 - 1. **Application & Account** 33 - - Identify which messaging or email app is in use (Gmail, Slack, Messages, Discord, Teams, Sococo, etc.). 34 - - Determine the account if deducible from screen content. 35 - 36 - 2. **Contacts & Channels** 37 - - Capture names of people or channels actively involved. 38 - - Note whether each contact was the sender, recipient, or part of a group thread. 39 - - Include $name as a default participant. 40 - 41 - 3. **Actions** 42 - - Distinguish whether $name was reading, composing, replying, or sending. 43 - - If multiple exchanges occur with different participants, capture them individually. 44 - - If multiple exchanges occur with the same participants, summarize them together. 45 - 46 - 4. **Message Content** 47 - - When message text is visible on screen, summarize what was read or written, noting all important entities. 48 - - If only partial content is visible, summarize the visible portion and note it is incomplete. 49 - 50 - 5. **Context Classification** 51 - - Assess whether each interaction is work-related or personal based on participants, content, and application. 52 - 53 - ## Exclusions 54 - 55 - - Content from concurrent activities unrelated to this $activity_type session. 56 - - Brief notifications or popups unless they show sustained interaction. 57 - - Duplicates of the same exchange; merge overlapping mentions. 58 - 59 - ## Output Format 60 - 61 - Produce a friendly Markdown document with each messaging interaction in its own section with a short title: 62 - 63 - - **Time:** $segment_start–$segment_end (or tighter span if identifiable) 64 - - **App** – the messaging or email application used 65 - - **Contacts** – people or channels involved 66 - - **Context** – work or personal classification 67 - - **Action** – reading, composing, replying, or sending 68 - - **Summary** – short recap of visible message contents 69 - 70 - List interactions in chronological order. 71 - 72 - Conclude with a brief summary (<= 100 words) of who was communicated with, via which apps, and what topics were discussed. 73 - 74 - If the transcript does not contain substantive messaging content (e.g., a false positive from the activity detector), output only a brief sentence explaining why (e.g., "No substantive messaging content was found in this activity's transcript.").
+1 -1
talent/patterns/provenance.md
··· 50 50 51 51 **Bidirectional rule:** Strong evidence must NOT get hedging language. Weak evidence must NOT get assertive language. Both directions must be enforced. 52 52 53 - **Upstream confidence scores:** Some agents (e.g., `talent/followups.md`, `talent/decisions.md`) emit a `Confidence: 0.0–1.0` field per item. When consuming this output, use the score to inform language grading. The briefing expresses confidence through language, not by forwarding the numeric score. 53 + **Upstream confidence scores:** Some upstream sources emit a `Confidence: 0.0–1.0` field per item or row. When consuming this output, use the score to inform language grading. The briefing expresses confidence through language, not by forwarding the numeric score. 54 54 55 55 ### 4. Tool Error Guard 56 56
+170
talent/spans.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Post-hook for structured storytelling span rows.""" 5 + 6 + from __future__ import annotations 7 + 8 + import json 9 + import logging 10 + import math 11 + import re 12 + from pathlib import Path 13 + from typing import Any 14 + 15 + from think.activities import locked_modify 16 + from think.utils import get_journal, segment_parse 17 + 18 + logger = logging.getLogger(__name__) 19 + 20 + 21 + def _strip_code_fences(result: str) -> str: 22 + stripped = result.strip() 23 + stripped = re.sub(r"^```(?:json)?\s*", "", stripped) 24 + return re.sub(r"\s*```$", "", stripped) 25 + 26 + 27 + def _normalize_topics(value: Any) -> list[str] | None: 28 + if not isinstance(value, list): 29 + logger.warning("spans hook: missing topics list") 30 + return None 31 + 32 + topics: list[str] = [] 33 + seen: set[str] = set() 34 + for item in value: 35 + if not isinstance(item, str): 36 + logger.warning("spans hook: invalid topics list") 37 + return None 38 + topic = item.strip().lower() 39 + if not topic or topic in seen: 40 + continue 41 + seen.add(topic) 42 + topics.append(topic) 43 + if len(topics) >= 10: 44 + break 45 + 46 + if not topics: 47 + logger.warning("spans hook: empty topics after normalization") 48 + return None 49 + 50 + return topics 51 + 52 + 53 + def _normalize_confidence(value: Any) -> float | None: 54 + if isinstance(value, bool) or not isinstance(value, (int, float)): 55 + logger.warning("spans hook: invalid confidence value") 56 + return None 57 + 58 + confidence = float(value) 59 + if math.isnan(confidence): 60 + logger.warning("spans hook: invalid confidence value") 61 + return None 62 + 63 + clamped = min(1.0, max(0.0, confidence)) 64 + if clamped != confidence: 65 + logger.warning( 66 + "spans hook: clamped confidence %.3f to %.3f", confidence, clamped 67 + ) 68 + return clamped 69 + 70 + 71 + def _activity_time_bounds(segments: Any) -> tuple[str, str] | None: 72 + if not isinstance(segments, list) or not segments: 73 + logger.warning("spans hook: missing activity segments") 74 + return None 75 + 76 + start_time, _ = segment_parse(str(segments[0])) 77 + _, end_time = segment_parse(str(segments[-1])) 78 + if start_time is None or end_time is None: 79 + logger.warning("spans hook: invalid activity segments") 80 + return None 81 + 82 + return start_time.strftime("%H:%M:%S"), end_time.strftime("%H:%M:%S") 83 + 84 + 85 + def _spans_path(facet: str, day: str) -> Path: 86 + return Path(get_journal()) / "facets" / facet / "spans" / f"{day}.jsonl" 87 + 88 + 89 + def post_process(result: str, context: dict) -> str: 90 + """Parse model JSON and persist a single storytelling span row.""" 91 + try: 92 + try: 93 + data = json.loads(_strip_code_fences(result)) 94 + except (json.JSONDecodeError, ValueError) as exc: 95 + logger.warning("spans hook: failed to parse JSON: %s", exc) 96 + return "" 97 + 98 + if not isinstance(data, dict): 99 + logger.warning("spans hook: expected top-level object") 100 + return "" 101 + 102 + body = data.get("body") 103 + if not isinstance(body, str) or not body.strip(): 104 + logger.warning("spans hook: missing body") 105 + return "" 106 + normalized_body = body.strip() 107 + 108 + topics = _normalize_topics(data.get("topics")) 109 + if topics is None: 110 + return "" 111 + 112 + confidence = _normalize_confidence(data.get("confidence")) 113 + if confidence is None: 114 + return "" 115 + 116 + activity = context.get("activity") 117 + if not isinstance(activity, dict): 118 + logger.warning("spans hook: missing activity context") 119 + return "" 120 + 121 + facet = str(context.get("facet") or "").strip() 122 + day = str(context.get("day") or "").strip() 123 + if not facet or not day: 124 + logger.warning("spans hook: missing facet/day context") 125 + return "" 126 + 127 + span_id = str(activity.get("id") or "").strip() 128 + activity_type = str(activity.get("activity") or "").strip() 129 + talent = str(context.get("name") or "").strip() 130 + if not span_id or not activity_type or not talent: 131 + logger.warning("spans hook: missing span metadata") 132 + return "" 133 + 134 + bounds = _activity_time_bounds(activity.get("segments")) 135 + if bounds is None: 136 + return "" 137 + start, end = bounds 138 + 139 + row = { 140 + "span_id": span_id, 141 + "talent": talent, 142 + "facet": facet, 143 + "day": day, 144 + "activity_type": activity_type, 145 + "start": start, 146 + "end": end, 147 + "body": normalized_body, 148 + "topics": topics, 149 + "confidence": confidence, 150 + } 151 + 152 + def modify_fn(records: list[dict[str, Any]]) -> list[dict[str, Any]]: 153 + updated: list[dict[str, Any]] = [] 154 + replaced = False 155 + for record in records: 156 + if record.get("span_id") == span_id and record.get("talent") == talent: 157 + if not replaced: 158 + updated.append(dict(row)) 159 + replaced = True 160 + continue 161 + updated.append(record) 162 + if not replaced: 163 + updated.append(dict(row)) 164 + return updated 165 + 166 + locked_modify(_spans_path(facet, day), modify_fn, create_if_missing=True) 167 + except Exception as exc: 168 + logger.warning("spans hook: failed to persist row: %s", exc) 169 + 170 + return ""
+44
talent/work.md
··· 1 + { 2 + "type": "generate", 3 + "title": "Work Story", 4 + "description": "Writes a structured narrative span row for coding, browsing, and reading activities.", 5 + "color": "#6d4c41", 6 + "schedule": "activity", 7 + "activities": ["coding", "browsing", "reading"], 8 + "priority": 10, 9 + "output": "json", 10 + "hook": {"post": "spans"}, 11 + "load": { 12 + "transcripts": true, 13 + "percepts": true, 14 + "talents": false 15 + } 16 + } 17 + 18 + $facets 19 + 20 + $activity_context 21 + 22 + $activity_preamble 23 + 24 + # Work Story 25 + 26 + Write JSON only. No markdown fences. No prose outside the JSON object. 27 + 28 + Summarize what this person accomplished, investigated, or worked through during 29 + the activity. Participation and entity extraction already happened upstream. 30 + Use that context; do not re-extract people or entities into new structures. 31 + 32 + Return exactly these three fields: 33 + - `body`: string narrative prose about the work performed and what changed. 34 + - `topics`: array of 3-8 short string tags. 35 + - `confidence`: float from 0.0 to 1.0. 36 + 37 + Body requirements: 38 + - Write one tight paragraph in chronological order. 39 + - Emphasize concrete progress, investigation, blockers, and outcomes. 40 + - Prefer the actual work performed over UI description. 41 + - If evidence is partial, describe the most defensible story and keep the 42 + confidence honest. 43 + 44 + Output a single JSON object with only `body`, `topics`, and `confidence`.
+20 -36
tests/baselines/api/settings/providers.json
··· 205 205 "tier": 2, 206 206 "type": "cogitate" 207 207 }, 208 - "talent.system.daily_schedule": { 208 + "talent.system.conversation": { 209 209 "disabled": false, 210 210 "group": "Think", 211 - "label": "Maintenance Window", 212 - "schedule": "daily", 211 + "label": "Conversation Story", 212 + "schedule": "activity", 213 213 "tier": 2, 214 214 "type": "generate" 215 215 }, 216 - "talent.system.decisionalizer": { 216 + "talent.system.daily_schedule": { 217 217 "disabled": false, 218 218 "group": "Think", 219 - "label": "Decision Dossier Generator", 219 + "label": "Maintenance Window", 220 220 "schedule": "daily", 221 221 "tier": 2, 222 - "type": "cogitate" 223 - }, 224 - "talent.system.decisions": { 225 - "disabled": false, 226 - "group": "Think", 227 - "label": "Decision Actions", 228 - "schedule": "activity", 229 - "tier": 2, 230 222 "type": "generate" 231 223 }, 232 224 "talent.system.documents": { ··· 245 237 "tier": 2, 246 238 "type": "generate" 247 239 }, 248 - "talent.system.facet_newsletter": { 240 + "talent.system.event": { 249 241 "disabled": false, 250 242 "group": "Think", 251 - "label": "facet_newsletter", 243 + "label": "Event Story", 244 + "schedule": "activity", 252 245 "tier": 2, 253 - "type": null 246 + "type": "generate" 254 247 }, 255 - "talent.system.followups": { 248 + "talent.system.facet_newsletter": { 256 249 "disabled": false, 257 250 "group": "Think", 258 - "label": "Follow-Up Items", 259 - "schedule": "activity", 251 + "label": "facet_newsletter", 260 252 "tier": 2, 261 - "type": "generate" 253 + "type": null 262 254 }, 263 255 "talent.system.heartbeat": { 264 256 "disabled": false, ··· 275 267 "schedule": "daily", 276 268 "tier": 2, 277 269 "type": "cogitate" 278 - }, 279 - "talent.system.meetings": { 280 - "disabled": false, 281 - "group": "Think", 282 - "label": "Meeting Notes", 283 - "schedule": "activity", 284 - "tier": 2, 285 - "type": "generate" 286 - }, 287 - "talent.system.messaging": { 288 - "disabled": false, 289 - "group": "Think", 290 - "label": "Messaging Summary", 291 - "schedule": "activity", 292 - "tier": 2, 293 - "type": "generate" 294 270 }, 295 271 "talent.system.morning_briefing": { 296 272 "disabled": false, ··· 392 368 "label": "Triage", 393 369 "tier": 2, 394 370 "type": "cogitate" 371 + }, 372 + "talent.system.work": { 373 + "disabled": false, 374 + "group": "Think", 375 + "label": "Work Story", 376 + "schedule": "activity", 377 + "tier": 2, 378 + "type": "generate" 395 379 }, 396 380 "talent.todos.daily": { 397 381 "disabled": false,
+32 -54
tests/baselines/api/sol/talents-day.json
··· 82 82 "title": "Coder", 83 83 "type": "cogitate" 84 84 }, 85 - "daily_schedule": { 85 + "conversation": { 86 86 "app": null, 87 - "color": "#455a64", 88 - "description": "Analyzes activity patterns to identify optimal times for scheduled maintenance tasks.", 87 + "color": "#00796b", 88 + "description": "Writes a structured narrative span row for meeting, call, messaging, and email activities.", 89 89 "multi_facet": false, 90 90 "output_format": "json", 91 - "schedule": "daily", 91 + "schedule": "activity", 92 92 "source": "system", 93 - "title": "Maintenance Window", 93 + "title": "Conversation Story", 94 94 "type": "generate" 95 95 }, 96 - "decisionalizer": { 96 + "daily_schedule": { 97 97 "app": null, 98 - "color": "#c62828", 99 - "description": "Analyzes the day's top decision-actions to create detailed dossiers identifying gaps and stakeholder impacts", 98 + "color": "#455a64", 99 + "description": "Analyzes activity patterns to identify optimal times for scheduled maintenance tasks.", 100 100 "multi_facet": false, 101 - "output_format": "md", 101 + "output_format": "json", 102 102 "schedule": "daily", 103 103 "source": "system", 104 - "title": "Decision Dossier Generator", 105 - "type": "cogitate" 106 - }, 107 - "decisions": { 108 - "app": null, 109 - "color": "#dc3545", 110 - "description": "Tracks consequential decision-actions that change state, plans, resources, responsibilities, or timing in ways that affect other people.", 111 - "multi_facet": false, 112 - "output_format": "md", 113 - "schedule": "activity", 114 - "source": "system", 115 - "title": "Decision Actions", 104 + "title": "Maintenance Window", 116 105 "type": "generate" 117 106 }, 118 107 "documents": { ··· 192 181 "title": "Entity Observer", 193 182 "type": "generate" 194 183 }, 184 + "event": { 185 + "app": null, 186 + "color": "#ff7043", 187 + "description": "Writes a structured narrative span row for appointment, event, travel, errand, celebration, deadline, and reminder activities.", 188 + "multi_facet": false, 189 + "output_format": "json", 190 + "schedule": "activity", 191 + "source": "system", 192 + "title": "Event Story", 193 + "type": "generate" 194 + }, 195 195 "facet_newsletter": { 196 196 "app": null, 197 197 "color": "#6c757d", ··· 203 203 "title": "facet_newsletter", 204 204 "type": null 205 205 }, 206 - "followups": { 207 - "app": null, 208 - "color": "#ffc107", 209 - "description": "Detects promised tasks, commitments, and reminders for future action within each activity. Outputs a concise Markdown list of follow-ups with context.", 210 - "multi_facet": false, 211 - "output_format": "md", 212 - "schedule": "activity", 213 - "source": "system", 214 - "title": "Follow-Up Items", 215 - "type": "generate" 216 - }, 217 206 "heartbeat": { 218 207 "app": null, 219 208 "color": "#6c757d", ··· 279 268 "source": "system", 280 269 "title": "Joke Bot", 281 270 "type": "cogitate" 282 - }, 283 - "meetings": { 284 - "app": null, 285 - "color": "#e83e8c", 286 - "description": "Produces detailed meeting notes for each meeting activity, including participants, topics discussed, action items, and presentation details.", 287 - "multi_facet": false, 288 - "output_format": "md", 289 - "schedule": "activity", 290 - "source": "system", 291 - "title": "Meeting Notes", 292 - "type": "generate" 293 - }, 294 - "messaging": { 295 - "app": null, 296 - "color": "#78909c", 297 - "description": "Extracts contacts, channels, apps, and message content from completed messaging and email activities.", 298 - "multi_facet": false, 299 - "output_format": "md", 300 - "schedule": "activity", 301 - "source": "system", 302 - "title": "Messaging Summary", 303 - "type": "generate" 304 271 }, 305 272 "morning_briefing": { 306 273 "app": null, ··· 488 455 "source": "system", 489 456 "title": "Triage", 490 457 "type": "cogitate" 458 + }, 459 + "work": { 460 + "app": null, 461 + "color": "#6d4c41", 462 + "description": "Writes a structured narrative span row for coding, browsing, and reading activities.", 463 + "multi_facet": false, 464 + "output_format": "json", 465 + "schedule": "activity", 466 + "source": "system", 467 + "title": "Work Story", 468 + "type": "generate" 491 469 } 492 470 }, 493 471 "uses": []
+68 -83
tests/baselines/api/stats/stats.json
··· 1 1 { 2 2 "generators": { 3 + "conversation": { 4 + "activities": [ 5 + "meeting", 6 + "call", 7 + "messaging", 8 + "email" 9 + ], 10 + "color": "#00796b", 11 + "description": "Writes a structured narrative span row for meeting, call, messaging, and email activities.", 12 + "hook": { 13 + "post": "spans" 14 + }, 15 + "load": { 16 + "percepts": true, 17 + "talents": false, 18 + "transcripts": true 19 + }, 20 + "mtime": 0, 21 + "output": "json", 22 + "path": "<PROJECT>/talent/conversation.md", 23 + "priority": 10, 24 + "schedule": "activity", 25 + "source": "system", 26 + "title": "Conversation Story", 27 + "type": "generate" 28 + }, 3 29 "daily_schedule": { 4 30 "color": "#455a64", 5 31 "description": "Analyzes activity patterns to identify optimal times for scheduled maintenance tasks.", ··· 21 47 "source": "system", 22 48 "thinking_budget": 4096, 23 49 "title": "Maintenance Window", 24 - "type": "generate" 25 - }, 26 - "decisions": { 27 - "activities": [ 28 - "meeting", 29 - "call", 30 - "messaging", 31 - "email" 32 - ], 33 - "color": "#dc3545", 34 - "description": "Tracks consequential decision-actions that change state, plans, resources, responsibilities, or timing in ways that affect other people.", 35 - "load": { 36 - "percepts": false, 37 - "talents": { 38 - "screen": true 39 - }, 40 - "transcripts": true 41 - }, 42 - "mtime": 0, 43 - "output": "md", 44 - "path": "<PROJECT>/talent/decisions.md", 45 - "priority": 10, 46 - "schedule": "activity", 47 - "source": "system", 48 - "title": "Decision Actions", 49 50 "type": "generate" 50 51 }, 51 52 "documents": { ··· 118 119 "title": "Entity Observer", 119 120 "type": "generate" 120 121 }, 121 - "followups": { 122 + "event": { 122 123 "activities": [ 123 - "meeting", 124 - "call", 125 - "messaging", 126 - "email" 124 + "appointment", 125 + "event", 126 + "travel", 127 + "errand", 128 + "celebration", 129 + "deadline", 130 + "reminder" 127 131 ], 128 - "color": "#ffc107", 129 - "description": "Detects promised tasks, commitments, and reminders for future action within each activity. Outputs a concise Markdown list of follow-ups with context.", 130 - "load": { 131 - "percepts": false, 132 - "talents": { 133 - "screen": true 134 - }, 135 - "transcripts": true 132 + "color": "#ff7043", 133 + "description": "Writes a structured narrative span row for appointment, event, travel, errand, celebration, deadline, and reminder activities.", 134 + "hook": { 135 + "post": "spans" 136 136 }, 137 - "mtime": 0, 138 - "output": "md", 139 - "path": "<PROJECT>/talent/followups.md", 140 - "priority": 10, 141 - "schedule": "activity", 142 - "source": "system", 143 - "title": "Follow-Up Items", 144 - "type": "generate" 145 - }, 146 - "meetings": { 147 - "activities": [ 148 - "meeting" 149 - ], 150 - "color": "#e83e8c", 151 - "description": "Produces detailed meeting notes for each meeting activity, including participants, topics discussed, action items, and presentation details.", 152 137 "load": { 153 - "percepts": false, 154 - "talents": { 155 - "screen": true 156 - }, 157 - "transcripts": true 158 - }, 159 - "mtime": 0, 160 - "output": "md", 161 - "path": "<PROJECT>/talent/meetings.md", 162 - "priority": 10, 163 - "schedule": "activity", 164 - "source": "system", 165 - "title": "Meeting Notes", 166 - "type": "generate" 167 - }, 168 - "messaging": { 169 - "activities": [ 170 - "messaging", 171 - "email" 172 - ], 173 - "color": "#78909c", 174 - "description": "Extracts contacts, channels, apps, and message content from completed messaging and email activities.", 175 - "load": { 176 - "percepts": false, 177 - "talents": { 178 - "screen": true 179 - }, 138 + "percepts": true, 139 + "talents": false, 180 140 "transcripts": true 181 141 }, 182 142 "mtime": 0, 183 - "output": "md", 184 - "path": "<PROJECT>/talent/messaging.md", 143 + "output": "json", 144 + "path": "<PROJECT>/talent/event.md", 185 145 "priority": 10, 186 146 "schedule": "activity", 187 147 "source": "system", 188 - "title": "Messaging Summary", 148 + "title": "Event Story", 189 149 "type": "generate" 190 150 }, 191 151 "participation": { ··· 318 278 "schedule": "segment", 319 279 "source": "system", 320 280 "title": "Speaker Attribution", 281 + "type": "generate" 282 + }, 283 + "work": { 284 + "activities": [ 285 + "coding", 286 + "browsing", 287 + "reading" 288 + ], 289 + "color": "#6d4c41", 290 + "description": "Writes a structured narrative span row for coding, browsing, and reading activities.", 291 + "hook": { 292 + "post": "spans" 293 + }, 294 + "load": { 295 + "percepts": true, 296 + "talents": false, 297 + "transcripts": true 298 + }, 299 + "mtime": 0, 300 + "output": "json", 301 + "path": "<PROJECT>/talent/work.md", 302 + "priority": 10, 303 + "schedule": "activity", 304 + "source": "system", 305 + "title": "Work Story", 321 306 "type": "generate" 322 307 } 323 308 },
+2 -2
tests/test_cortex.py
··· 187 187 config = { 188 188 "event": "request", 189 189 "ts": 987654321, 190 - "name": "decisions", 190 + "name": "work", 191 191 "day": "20240101", 192 192 "output": "md", 193 193 } ··· 214 214 written_data = mock_process.stdin.write.call_args[0][0] 215 215 ndjson = json.loads(written_data.strip()) 216 216 assert ndjson["event"] == "request" 217 - assert ndjson["name"] == "decisions" 217 + assert ndjson["name"] == "work" 218 218 assert ndjson["day"] == "20240101" 219 219 assert ndjson["output"] == "md" 220 220
-39
tests/test_decisionalizer_hook.py
··· 1 - # SPDX-License-Identifier: AGPL-3.0-only 2 - # Copyright (c) 2026 sol pbc 3 - 4 - """Tests for decisionalizer pre-hook.""" 5 - 6 - from unittest.mock import patch 7 - 8 - from talent.decisionalizer import pre_process 9 - 10 - 11 - def test_skip_when_no_decisions(tmp_path): 12 - """Skip when no decisions.md files exist for the day.""" 13 - activities = tmp_path / "facets" / "somefacet" / "activities" / "20260410" 14 - activities.mkdir(parents=True) 15 - 16 - with patch("talent.decisionalizer.get_journal", return_value=str(tmp_path)): 17 - result = pre_process({"day": "20260410"}) 18 - 19 - assert result == {"skip_reason": "no decision outputs for day"} 20 - 21 - 22 - def test_proceed_when_decisions_exist(tmp_path): 23 - """Proceed when decisions.md files exist for the day.""" 24 - decisions = ( 25 - tmp_path 26 - / "facets" 27 - / "testfacet" 28 - / "activities" 29 - / "20260410" 30 - / "meeting_100000_300" 31 - / "decisions.md" 32 - ) 33 - decisions.parent.mkdir(parents=True) 34 - decisions.write_text("") 35 - 36 - with patch("talent.decisionalizer.get_journal", return_value=str(tmp_path)): 37 - result = pre_process({"day": "20260410"}) 38 - 39 - assert result == {}
+108
tests/test_spans_formatter.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Tests for spans JSONL formatting.""" 5 + 6 + from __future__ import annotations 7 + 8 + from datetime import datetime 9 + from pathlib import Path 10 + 11 + 12 + def test_format_spans_builds_chunks_and_metadata(): 13 + from think.spans import format_spans 14 + 15 + entries = [ 16 + { 17 + "span_id": "meeting_090000_300", 18 + "talent": "conversation", 19 + "facet": "work", 20 + "day": "20260101", 21 + "activity_type": "meeting", 22 + "start": "09:00:00", 23 + "end": "09:15:00", 24 + "body": "Aligned on the launch plan and confirmed owners.", 25 + "topics": ["launch", "owners", "planning"], 26 + "confidence": 0.93, 27 + }, 28 + { 29 + "span_id": "coding_130000_300", 30 + "talent": "work", 31 + "facet": "work", 32 + "day": "20260101", 33 + "activity_type": "coding", 34 + "start": "13:00:00", 35 + "end": "13:10:00", 36 + "body": "Implemented the migration and updated the tests.", 37 + "topics": ["migration", "tests", "backend"], 38 + "confidence": 0.81, 39 + }, 40 + ] 41 + 42 + file_path = Path("/tmp/journal/facets/work/spans/20260101.jsonl") 43 + chunks, meta = format_spans(entries, {"file_path": file_path}) 44 + 45 + assert len(chunks) == 2 46 + assert meta["header"] == "# Spans for 'work' facet on 2026-01-01" 47 + assert meta["indexer"] == {"agent": "span"} 48 + 49 + first = chunks[0] 50 + expected_ts = int(datetime.strptime("20260101", "%Y%m%d").timestamp() * 1000) 51 + expected_ts += 9 * 3600 * 1000 52 + assert first["timestamp"] == expected_ts 53 + assert first["source"] == entries[0] 54 + assert "### Meeting: meeting_090000_300" in first["markdown"] 55 + assert "**Time:** 09:00:00-09:15:00" in first["markdown"] 56 + assert "**Activity Type:** meeting" in first["markdown"] 57 + assert "**Topics:** launch, owners, planning" in first["markdown"] 58 + assert "**Confidence:** 0.93" in first["markdown"] 59 + assert "**Talent:** conversation" in first["markdown"] 60 + assert "Aligned on the launch plan" in first["markdown"] 61 + 62 + 63 + def test_format_spans_skips_invalid_rows_and_reports_error(): 64 + from think.spans import format_spans 65 + 66 + entries = [ 67 + { 68 + "span_id": "valid_1", 69 + "talent": "work", 70 + "facet": "work", 71 + "day": "20260101", 72 + "activity_type": "coding", 73 + "start": "08:00:00", 74 + "end": "08:05:00", 75 + "body": "Valid row.", 76 + "topics": ["alpha", "beta", "gamma"], 77 + "confidence": 0.5, 78 + }, 79 + { 80 + "span_id": "invalid_1", 81 + "talent": "work", 82 + "facet": "work", 83 + "day": "20260101", 84 + "activity_type": "coding", 85 + "start": "08:05:00", 86 + "end": "08:10:00", 87 + "topics": ["alpha", "beta", "gamma"], 88 + "confidence": 0.5, 89 + }, 90 + ] 91 + 92 + chunks, meta = format_spans( 93 + entries, {"file_path": Path("/tmp/journal/facets/work/spans/20260101.jsonl")} 94 + ) 95 + 96 + assert len(chunks) == 1 97 + assert "Skipped 1 entries missing required fields" in meta["error"] 98 + assert "20260101.jsonl" in meta["error"] 99 + assert meta["indexer"] == {"agent": "span"} 100 + 101 + 102 + def test_get_formatter_returns_spans_formatter(): 103 + from think.formatters import get_formatter 104 + 105 + formatter = get_formatter("facets/foo/spans/20260101.jsonl") 106 + 107 + assert formatter is not None 108 + assert formatter.__name__ == "format_spans"
+297
tests/test_spans_hook.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Tests for the storytelling spans post-hook.""" 5 + 6 + from __future__ import annotations 7 + 8 + import json 9 + from pathlib import Path 10 + 11 + from talent.spans import post_process 12 + 13 + 14 + def _activity( 15 + *, 16 + activity_id: str = "coding_100000_300", 17 + activity_type: str = "coding", 18 + segments: list[str] | None = None, 19 + ) -> dict: 20 + return { 21 + "id": activity_id, 22 + "activity": activity_type, 23 + "segments": segments or ["100000_300", "100500_300"], 24 + } 25 + 26 + 27 + def _context( 28 + *, 29 + name: str = "work", 30 + facet: str = "work", 31 + day: str = "20260418", 32 + activity: dict | None = None, 33 + ) -> dict: 34 + return { 35 + "name": name, 36 + "facet": facet, 37 + "day": day, 38 + "activity": activity or _activity(), 39 + } 40 + 41 + 42 + def _rows(tmp_path: Path, *, facet: str = "work", day: str = "20260418") -> list[dict]: 43 + path = tmp_path / "facets" / facet / "spans" / f"{day}.jsonl" 44 + if not path.exists(): 45 + return [] 46 + return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()] 47 + 48 + 49 + def test_post_process_writes_all_fields_and_renders_coding_span(monkeypatch, tmp_path): 50 + from think.spans import format_spans 51 + 52 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 53 + 54 + result = json.dumps( 55 + { 56 + "body": "Implemented the retry path and verified the failing case.", 57 + "topics": ["Retry Logic", "Testing", "retry logic", " Testing "], 58 + "confidence": 0.82, 59 + } 60 + ) 61 + 62 + returned = post_process(result, _context()) 63 + 64 + assert returned == "" 65 + 66 + rows = _rows(tmp_path) 67 + assert len(rows) == 1 68 + assert rows[0] == { 69 + "span_id": "coding_100000_300", 70 + "talent": "work", 71 + "facet": "work", 72 + "day": "20260418", 73 + "activity_type": "coding", 74 + "start": "10:00:00", 75 + "end": "10:10:00", 76 + "body": "Implemented the retry path and verified the failing case.", 77 + "topics": ["retry logic", "testing"], 78 + "confidence": 0.82, 79 + } 80 + 81 + file_path = tmp_path / "facets" / "work" / "spans" / "20260418.jsonl" 82 + chunks, meta = format_spans(rows, {"file_path": file_path}) 83 + assert len(chunks) == 1 84 + assert chunks[0]["source"] == rows[0] 85 + assert "### Coding: coding_100000_300" in chunks[0]["markdown"] 86 + assert meta["indexer"] == {"agent": "span"} 87 + 88 + 89 + def test_post_process_writes_single_conversation_row_for_meeting(monkeypatch, tmp_path): 90 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 91 + 92 + result = json.dumps( 93 + { 94 + "body": 'Aligned on next steps and confirmed "ship it Friday".', 95 + "topics": ["planning", "alignment", "delivery"], 96 + "confidence": 0.91, 97 + } 98 + ) 99 + ctx = _context( 100 + name="conversation", 101 + activity=_activity( 102 + activity_id="meeting_090000_300", 103 + activity_type="meeting", 104 + segments=["090000_300", "091500_300"], 105 + ), 106 + ) 107 + 108 + returned = post_process(result, ctx) 109 + 110 + assert returned == "" 111 + rows = _rows(tmp_path) 112 + assert len(rows) == 1 113 + assert rows[0]["talent"] == "conversation" 114 + assert rows[0]["activity_type"] == "meeting" 115 + assert rows[0]["start"] == "09:00:00" 116 + assert rows[0]["end"] == "09:20:00" 117 + assert not ( 118 + tmp_path 119 + / "facets" 120 + / "work" 121 + / "activities" 122 + / "20260418" 123 + / "meeting_090000_300" 124 + / "conversation.json" 125 + ).exists() 126 + 127 + 128 + def test_post_process_clamps_confidence_and_logs(monkeypatch, tmp_path, caplog): 129 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 130 + 131 + returned = post_process( 132 + json.dumps( 133 + { 134 + "body": "Shipped the fix.", 135 + "topics": ["release", "shipping", "qa"], 136 + "confidence": 1.4, 137 + } 138 + ), 139 + _context(), 140 + ) 141 + 142 + assert returned == "" 143 + assert _rows(tmp_path)[0]["confidence"] == 1.0 144 + assert "clamped confidence" in caplog.text 145 + 146 + 147 + def test_post_process_rejects_bad_confidence(monkeypatch, tmp_path, caplog): 148 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 149 + 150 + returned = post_process( 151 + json.dumps( 152 + { 153 + "body": "Investigated the issue.", 154 + "topics": ["debugging", "logs", "triage"], 155 + "confidence": "high", 156 + } 157 + ), 158 + _context(), 159 + ) 160 + 161 + assert returned == "" 162 + assert _rows(tmp_path) == [] 163 + assert "invalid confidence" in caplog.text 164 + 165 + caplog.clear() 166 + returned = post_process( 167 + json.dumps( 168 + { 169 + "body": "Investigated the issue.", 170 + "topics": ["debugging", "logs", "triage"], 171 + "confidence": float("nan"), 172 + } 173 + ), 174 + _context(), 175 + ) 176 + 177 + assert returned == "" 178 + assert _rows(tmp_path) == [] 179 + assert "invalid confidence" in caplog.text 180 + 181 + 182 + def test_post_process_rejects_missing_or_empty_topics(monkeypatch, tmp_path, caplog): 183 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 184 + 185 + missing_topics = json.dumps({"body": "Worked through the task.", "confidence": 0.7}) 186 + returned = post_process(missing_topics, _context()) 187 + assert returned == "" 188 + assert _rows(tmp_path) == [] 189 + assert "missing topics" in caplog.text 190 + 191 + caplog.clear() 192 + empty_topics = json.dumps( 193 + {"body": "Worked through the task.", "topics": [" ", "\t"], "confidence": 0.7} 194 + ) 195 + returned = post_process(empty_topics, _context()) 196 + assert returned == "" 197 + assert _rows(tmp_path) == [] 198 + assert "empty topics" in caplog.text 199 + 200 + caplog.clear() 201 + invalid_topics = json.dumps( 202 + { 203 + "body": "Worked through the task.", 204 + "topics": ["valid", 7, "other"], 205 + "confidence": 0.7, 206 + } 207 + ) 208 + returned = post_process(invalid_topics, _context()) 209 + assert returned == "" 210 + assert _rows(tmp_path) == [] 211 + assert "invalid topics" in caplog.text 212 + 213 + 214 + def test_post_process_replaces_existing_row_by_span_and_talent(monkeypatch, tmp_path): 215 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 216 + ctx = _context() 217 + 218 + first = json.dumps( 219 + {"body": "First pass.", "topics": ["alpha", "beta", "gamma"], "confidence": 0.5} 220 + ) 221 + second = json.dumps( 222 + { 223 + "body": "Second pass.", 224 + "topics": ["delta", "epsilon", "zeta"], 225 + "confidence": 0.9, 226 + } 227 + ) 228 + 229 + assert post_process(first, ctx) == "" 230 + assert post_process(second, ctx) == "" 231 + 232 + rows = _rows(tmp_path) 233 + assert len(rows) == 1 234 + assert rows[0]["body"] == "Second pass." 235 + assert rows[0]["topics"] == ["delta", "epsilon", "zeta"] 236 + assert rows[0]["confidence"] == 0.9 237 + 238 + 239 + def test_post_process_appends_distinct_talent_rows(monkeypatch, tmp_path): 240 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 241 + activity = _activity(activity_id="event_130000_300", activity_type="event") 242 + 243 + event_ctx = _context(name="event", activity=activity) 244 + conversation_ctx = _context(name="conversation", activity=activity) 245 + 246 + assert ( 247 + post_process( 248 + json.dumps( 249 + { 250 + "body": "Wrapped the event.", 251 + "topics": ["planning", "venue", "timeline"], 252 + "confidence": 0.66, 253 + } 254 + ), 255 + event_ctx, 256 + ) 257 + == "" 258 + ) 259 + assert ( 260 + post_process( 261 + json.dumps( 262 + { 263 + "body": "Captured the side conversation.", 264 + "topics": ["alignment", "follow-up", "owners"], 265 + "confidence": 0.72, 266 + } 267 + ), 268 + conversation_ctx, 269 + ) 270 + == "" 271 + ) 272 + 273 + rows = _rows(tmp_path) 274 + assert len(rows) == 2 275 + assert {(row["span_id"], row["talent"]) for row in rows} == { 276 + ("event_130000_300", "event"), 277 + ("event_130000_300", "conversation"), 278 + } 279 + 280 + 281 + def test_post_process_handles_parse_failures_and_fenced_json( 282 + monkeypatch, tmp_path, caplog 283 + ): 284 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 285 + 286 + assert post_process("{not-json", _context()) == "" 287 + assert _rows(tmp_path) == [] 288 + assert "failed to parse JSON" in caplog.text 289 + 290 + caplog.clear() 291 + fenced = """```json 292 + {"body":"Recovered.","topics":["alpha","beta","gamma"],"confidence":0.6} 293 + ```""" 294 + assert post_process(fenced, _context()) == "" 295 + rows = _rows(tmp_path) 296 + assert len(rows) == 1 297 + assert rows[0]["body"] == "Recovered."
+8 -7
tests/test_talent_cli.py
··· 57 57 for key, info in activity.items(): 58 58 assert info.get("schedule") == "activity", f"{key} should be activity" 59 59 60 - # decisions is activity-scheduled 61 - assert "decisions" in activity 60 + assert "work" in activity 62 61 63 62 64 63 def test_collect_configs_filter_source(): ··· 603 602 from think.talent_cli import show_prompt_context 604 603 605 604 with pytest.raises(SystemExit): 606 - show_prompt_context("decisions", day="20260214") 605 + show_prompt_context("work", day="20260214") 607 606 608 607 output = capsys.readouterr().err 609 608 assert "activity-scheduled" in output.lower() ··· 615 614 from think.talent_cli import show_prompt_context 616 615 617 616 with pytest.raises(SystemExit): 618 - show_prompt_context("decisions", day="20260214", facet="full-featured") 617 + show_prompt_context("work", day="20260214", facet="full-featured") 619 618 620 619 output = capsys.readouterr().err 621 620 assert "--activity" in output ··· 629 628 630 629 with pytest.raises(SystemExit): 631 630 show_prompt_context( 632 - "decisions", 631 + "work", 633 632 day="20260214", 634 633 facet="full-featured", 635 634 activity="nonexistent_999", ··· 640 639 641 640 642 641 def test_list_prompts_activity_group(capsys): 643 - """List view includes activity group with decisions agent.""" 642 + """List view includes activity group with storytelling talents.""" 644 643 list_prompts() 645 644 output = capsys.readouterr().out 646 645 647 646 assert "activity:" in output 648 - assert "decisions" in output 647 + assert "conversation" in output 648 + assert "work" in output 649 + assert "event" in output
+240
tests/test_think_activity.py
··· 424 424 assert started_kw["activity"] == "coding_100000_300" 425 425 assert started_kw["facet"] == "work" 426 426 427 + @pytest.mark.parametrize( 428 + ("activity_type", "expected_name"), 429 + [ 430 + ("meeting", "conversation"), 431 + ("call", "conversation"), 432 + ("messaging", "conversation"), 433 + ("email", "conversation"), 434 + ("appointment", "event"), 435 + ("event", "event"), 436 + ("travel", "event"), 437 + ("errand", "event"), 438 + ("celebration", "event"), 439 + ("deadline", "event"), 440 + ("reminder", "event"), 441 + ], 442 + ) 443 + def test_dispatches_storytelling_talents_by_activity_type( 444 + self, monkeypatch, activity_type, expected_name 445 + ): 446 + from think.thinking import run_activity_prompts 447 + 448 + with tempfile.TemporaryDirectory() as tmpdir: 449 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) 450 + 451 + activity_id = f"{activity_type}_100000_300" 452 + self._write_record( 453 + tmpdir, 454 + "work", 455 + "20260209", 456 + { 457 + "id": activity_id, 458 + "activity": activity_type, 459 + "segments": ["100000_300"], 460 + "description": f"{activity_type} session", 461 + "active_entities": [], 462 + }, 463 + ) 464 + 465 + configs = { 466 + "conversation": { 467 + "type": "generate", 468 + "priority": 50, 469 + "activities": ["meeting", "call", "messaging", "email"], 470 + "output": "json", 471 + }, 472 + "work": { 473 + "type": "generate", 474 + "priority": 50, 475 + "activities": ["coding", "browsing", "reading"], 476 + "output": "json", 477 + }, 478 + "event": { 479 + "type": "generate", 480 + "priority": 50, 481 + "activities": [ 482 + "appointment", 483 + "event", 484 + "travel", 485 + "errand", 486 + "celebration", 487 + "deadline", 488 + "reminder", 489 + ], 490 + "output": "json", 491 + }, 492 + } 493 + 494 + monkeypatch.setattr( 495 + "think.thinking.get_talent_configs", lambda schedule: configs 496 + ) 497 + 498 + spawned: list[str] = [] 499 + 500 + def mock_cortex_request(prompt, name, config): 501 + spawned.append(name) 502 + return f"agent-{name}" 503 + 504 + monkeypatch.setattr("think.thinking.cortex_request", mock_cortex_request) 505 + monkeypatch.setattr( 506 + "think.thinking.wait_for_uses", 507 + lambda ids, timeout: ({aid: "finish" for aid in ids}, []), 508 + ) 509 + 510 + result = run_activity_prompts( 511 + day="20260209", 512 + activity_id=activity_id, 513 + facet="work", 514 + ) 515 + 516 + assert result is True 517 + assert spawned == [expected_name] 518 + 519 + def test_dispatches_work_talent_with_level_gate(self, monkeypatch): 520 + from think.thinking import run_activity_prompts 521 + 522 + with tempfile.TemporaryDirectory() as tmpdir: 523 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) 524 + 525 + configs = { 526 + "work": { 527 + "type": "generate", 528 + "priority": 50, 529 + "activities": ["coding", "browsing", "reading"], 530 + "output": "json", 531 + } 532 + } 533 + monkeypatch.setattr( 534 + "think.thinking.get_talent_configs", lambda schedule: configs 535 + ) 536 + monkeypatch.setattr( 537 + "think.thinking.wait_for_uses", 538 + lambda ids, timeout: ({aid: "finish" for aid in ids}, []), 539 + ) 540 + 541 + cases = [ 542 + ("coding_100000_300", "coding", None, True), 543 + ("browsing_100000_300", "browsing", 0.4, True), 544 + ("reading_100000_300", "reading", 0.8, True), 545 + ("browsing_100500_300", "browsing", 0.39, False), 546 + ("reading_101000_300", "reading", None, False), 547 + ] 548 + 549 + for activity_id, activity_type, level_avg, should_spawn in cases: 550 + self._write_record( 551 + tmpdir, 552 + "work", 553 + "20260209", 554 + { 555 + "id": activity_id, 556 + "activity": activity_type, 557 + "segments": ["100000_300"], 558 + "level_avg": level_avg, 559 + "description": activity_type, 560 + "active_entities": [], 561 + }, 562 + ) 563 + 564 + spawned: list[str] = [] 565 + 566 + def mock_cortex_request(prompt, name, config): 567 + spawned.append(config["activity"]["id"]) 568 + return f"agent-{config['activity']['id']}" 569 + 570 + monkeypatch.setattr("think.thinking.cortex_request", mock_cortex_request) 571 + 572 + for activity_id, _activity_type, _level_avg, _should_spawn in cases: 573 + result = run_activity_prompts( 574 + day="20260209", 575 + activity_id=activity_id, 576 + facet="work", 577 + ) 578 + assert result is True 579 + 580 + assert spawned == [ 581 + "coding_100000_300", 582 + "browsing_100000_300", 583 + "reading_100000_300", 584 + ] 585 + 586 + @pytest.mark.parametrize( 587 + "record", 588 + [ 589 + { 590 + "id": "coding_100000_300", 591 + "activity": "coding", 592 + "source": "cogitate", 593 + "segments": ["100000_300"], 594 + }, 595 + { 596 + "id": "coding_100500_300", 597 + "activity": "coding", 598 + "source": "anticipated", 599 + "segments": ["100500_300"], 600 + }, 601 + { 602 + "id": "coding_101000_300", 603 + "activity": "coding", 604 + "segments": [], 605 + }, 606 + ], 607 + ) 608 + def test_storytelling_talents_skip_synthetic_or_empty_records( 609 + self, monkeypatch, record 610 + ): 611 + from think.thinking import run_activity_prompts 612 + 613 + with tempfile.TemporaryDirectory() as tmpdir: 614 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) 615 + 616 + full_record = { 617 + "description": "skip me", 618 + "active_entities": [], 619 + **record, 620 + } 621 + self._write_record(tmpdir, "work", "20260209", full_record) 622 + 623 + monkeypatch.setattr( 624 + "think.thinking.get_talent_configs", 625 + lambda schedule: { 626 + "conversation": { 627 + "type": "generate", 628 + "priority": 50, 629 + "activities": ["meeting", "call", "messaging", "email"], 630 + "output": "json", 631 + }, 632 + "work": { 633 + "type": "generate", 634 + "priority": 50, 635 + "activities": ["coding", "browsing", "reading"], 636 + "output": "json", 637 + }, 638 + "event": { 639 + "type": "generate", 640 + "priority": 50, 641 + "activities": [ 642 + "appointment", 643 + "event", 644 + "travel", 645 + "errand", 646 + "celebration", 647 + "deadline", 648 + "reminder", 649 + ], 650 + "output": "json", 651 + }, 652 + }, 653 + ) 654 + monkeypatch.setattr( 655 + "think.thinking.cortex_request", 656 + lambda prompt, name, config: pytest.fail("should not dispatch"), 657 + ) 658 + 659 + result = run_activity_prompts( 660 + day="20260209", 661 + activity_id=record["id"], 662 + facet="work", 663 + ) 664 + 665 + assert result is True 666 + 427 667 428 668 class TestActivityPersistence: 429 669 """Verify state machine completed records persist and load correctly."""
+1
think/formatters.py
··· 140 140 False, # Indexed via _index_entity_search_chunks (enriched with relationship data) 141 141 ), 142 142 "facets/*/events/*.jsonl": ("think.event_formatter", "format_events", True), 143 + "facets/*/spans/*.jsonl": ("think.spans", "format_spans", True), 143 144 "facets/*/activities/*.jsonl": ("think.activities", "format_activities", True), 144 145 "facets/*/todos/*.jsonl": ("apps.todos.todo", "format_todos", True), 145 146 "facets/*/logs/*.jsonl": ("think.facets", "format_logs", True),
+128
think/spans.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Formatting helpers for storytelling spans JSONL files.""" 5 + 6 + from __future__ import annotations 7 + 8 + import logging 9 + import re 10 + from datetime import datetime 11 + from pathlib import Path 12 + from typing import Any 13 + 14 + 15 + def _extract_spans_path_context(file_path: str | Path | None) -> tuple[str, str | None]: 16 + facet_name = "unknown" 17 + day_str: str | None = None 18 + 19 + if not file_path: 20 + return facet_name, day_str 21 + 22 + path = Path(file_path) 23 + path_str = str(path) 24 + facet_match = re.search(r"facets/([^/]+)/spans", path_str) 25 + if facet_match: 26 + facet_name = facet_match.group(1) 27 + 28 + if path.stem.isdigit() and len(path.stem) == 8: 29 + day_str = path.stem 30 + 31 + return facet_name, day_str 32 + 33 + 34 + def _start_seconds(start: str) -> int | None: 35 + try: 36 + parts = start.split(":") 37 + hours = int(parts[0]) 38 + minutes = int(parts[1]) if len(parts) > 1 else 0 39 + seconds = int(parts[2]) if len(parts) > 2 else 0 40 + except (IndexError, ValueError, TypeError): 41 + return None 42 + return hours * 3600 + minutes * 60 + seconds 43 + 44 + 45 + def format_spans( 46 + entries: list[dict], 47 + context: dict | None = None, 48 + ) -> tuple[list[dict], dict]: 49 + """Format storytelling span JSONL rows into markdown chunks.""" 50 + ctx = context or {} 51 + file_path = ctx.get("file_path") 52 + meta: dict[str, Any] = {"indexer": {"agent": "span"}} 53 + chunks: list[dict[str, Any]] = [] 54 + skipped_count = 0 55 + 56 + facet_name, day_str = _extract_spans_path_context(file_path) 57 + 58 + base_ts = 0 59 + if day_str: 60 + try: 61 + dt = datetime.strptime(day_str, "%Y%m%d") 62 + base_ts = int(dt.timestamp() * 1000) 63 + except ValueError: 64 + pass 65 + 66 + if day_str: 67 + formatted_day = f"{day_str[:4]}-{day_str[4:6]}-{day_str[6:8]}" 68 + meta["header"] = f"# Spans for '{facet_name}' facet on {formatted_day}" 69 + else: 70 + meta["header"] = f"# Spans for '{facet_name}' facet" 71 + 72 + for entry in entries: 73 + body = str(entry.get("body") or "").strip() 74 + start = str(entry.get("start") or "").strip() 75 + talent = str(entry.get("talent") or "").strip() 76 + activity_type = str(entry.get("activity_type") or "").strip() 77 + span_id = str(entry.get("span_id") or "").strip() 78 + 79 + if not all((body, start, talent, activity_type, span_id)): 80 + skipped_count += 1 81 + continue 82 + 83 + ts = base_ts 84 + start_seconds = _start_seconds(start) 85 + if start_seconds is not None and base_ts: 86 + ts = base_ts + start_seconds * 1000 87 + 88 + end = str(entry.get("end") or "").strip() 89 + time_display = start if not end else f"{start}-{end}" 90 + topics = entry.get("topics", []) 91 + topics_display = ( 92 + ", ".join(str(topic).strip() for topic in topics if str(topic).strip()) 93 + if isinstance(topics, list) 94 + else "" 95 + ) 96 + 97 + confidence = entry.get("confidence") 98 + if isinstance(confidence, (int, float)): 99 + confidence_display = f"{float(confidence):.2f}" 100 + else: 101 + confidence_display = str(confidence or "") 102 + 103 + lines = [f"### {activity_type.capitalize()}: {span_id}\n", ""] 104 + lines.append(f"**Time:** {time_display}") 105 + lines.append(f"**Activity Type:** {activity_type}") 106 + lines.append(f"**Topics:** {topics_display}") 107 + lines.append(f"**Confidence:** {confidence_display}") 108 + lines.append(f"**Talent:** {talent}") 109 + lines.append("") 110 + lines.append(body) 111 + lines.append("") 112 + 113 + chunks.append( 114 + { 115 + "timestamp": ts, 116 + "markdown": "\n".join(lines), 117 + "source": entry, 118 + } 119 + ) 120 + 121 + if skipped_count > 0: 122 + error_msg = f"Skipped {skipped_count} entries missing required fields" 123 + if file_path: 124 + error_msg += f" in {file_path}" 125 + meta["error"] = error_msg 126 + logging.info(error_msg) 127 + 128 + return chunks, meta
+1 -1
think/talent.py
··· 139 139 Day directory path (YYYYMMDD). 140 140 key: 141 141 Generator key or talent name (e.g., "activity", "chat:sentiment", 142 - "decisionalizer", "entities:observer"). 142 + "entities:observer"). 143 143 segment: 144 144 Optional segment key (HHMMSS_LEN) for segment-level output. 145 145 output_format:
+12 -1
think/thinking.py
··· 1849 1849 activity_type = record.get("activity", "") 1850 1850 segments = record.get("segments", []) 1851 1851 1852 - if record.get("source") == "cogitate" or not segments: 1852 + if record.get("source") in ("cogitate", "anticipated") or not segments: 1853 1853 logging.info( 1854 1854 "Skipping activity-scheduled generators for synthetic activity %s (source=%s)", 1855 1855 activity_id, ··· 2048 2048 2049 2049 try: 2050 2050 logging.info(f"Spawning {prompt_name} for activity {activity_id}") 2051 + 2052 + if prompt_name == "work" and activity_type in ("browsing", "reading"): 2053 + level_avg = float(record.get("level_avg", 0.0) or 0.0) 2054 + if level_avg < 0.4: 2055 + logging.info( 2056 + "skipping work talent for low-level %s activity %s (level_avg=%.2f)", 2057 + activity_type, 2058 + record.get("id"), 2059 + level_avg, 2060 + ) 2061 + continue 2051 2062 2052 2063 output_format = config.get("output", "md") 2053 2064 request_config: dict = {