From 550de7009ff7690663d18e2cfacf0278e7c5ef71 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Mon, 20 Apr 2026 12:12:50 -0600 Subject: [PATCH] chat: swap talent layer to chat-generate + exec-cogitate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second of three sub-lodes for the chat backend rewrite (parent plan: chat-refactor). Flips the talent layer for the new chat architecture while leaving runtime callers (`/api/triage`, `apps/home/events.py`, `think/conversation.py`, the `_resolve_talent_path` `unified` alias) alive for 2c to cut over. - `git mv talent/chat.md -> talent/exec.md`. The renamed file is the tier-3 cogitate "Exec" that 2c's chat backend will dispatch for deep research. Removed the legacy `$recent_conversation` placeholder that had been filled by the deleted pre-hook. - New `talent/chat.md` is a tier-3 generate "Chat" with JSON schema output at `talent/chat.schema.json`. Covers conversational framing, routine etiquette, import/naming, and when-to-dispatch-exec — all investigation/search/briefing depth lives in exec.md. - Rewrote `talent/chat_context.py` to inject digest contents, chat stream tail (via the formatter shipped in 2b), active-talent list, trigger context, location, and the preserved 5-gate routine- suggestion logic. Dropped `think.conversation` / L1-L2 memory assembly. The `save_routines_config()` side effect still fires only when `_meta.suggestions` mutates, and only `owner_message` triggers count toward suggestion gates. - Added `apps/sol/maint/006_rename_unified_triage_providers.py` — an idempotent one-time migration that renames `providers.contexts.talent.system.unified` -> `talent.system.chat` and removes `talent.system.triage` in any configured journal. Auto-discovered by `think.maint`. - Audit pass on `.get("name", "unified")` call sites: 13 hard-internal paths now require `["name"]`, 2 user-facing defaults use `"chat"`, and 2 legacy/migration fallbacks use `"chat"` with docstring notes. Hardcoded `name="unified"` in `convey/triage.py` is left for 2c. - Provider-contexts baseline: removed `talent.system.triage`, added `talent.system.exec` (tier-3 cogitate), swapped `talent.system.chat` to tier-3 generate. `talent.system.digest` unchanged. Collateral: `tests/verify_api.py` + three search/graph baselines marked sandbox-only to reconcile pre-existing drift between `make update-api-baselines` (Flask test client) and `make verify-api` (sandbox). Pre-dated 2a; surfaced only because this lode touched baselines. Keeping here to keep `make verify-api` green through the sub-lode sequence. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/sol/maint/001_migrate_agent_run_logs.py | 6 +- .../006_rename_unified_triage_providers.py | 152 ++++ apps/sol/routes.py | 8 +- talent/chat.md | 338 ++------- talent/chat.schema.json | 24 + talent/chat_context.py | 215 +++++- talent/exec.md | 313 ++++++++ tests/baselines/api/graph/graph.json | 623 +++++++++++++++- tests/baselines/api/search/day-results.json | 21 +- tests/baselines/api/search/search.json | 697 +++++++++++++++++- tests/baselines/api/settings/providers.json | 20 +- tests/baselines/api/sol/preview.json | 4 +- tests/baselines/api/sol/talents-day.json | 30 +- tests/baselines/api/stats/stats.json | 17 + tests/test_anthropic.py | 9 +- tests/test_app_sol.py | 18 +- tests/test_chat_context.py | 368 +++++---- tests/test_google.py | 4 +- tests/test_google_thinking.py | 1 + ...int_006_rename_unified_triage_providers.py | 108 +++ tests/test_talent.py | 10 +- tests/test_talents_ndjson.py | 4 +- tests/verify_api.py | 18 +- think/chat_cli.py | 2 +- think/cortex.py | 6 +- think/cortex_client.py | 7 +- think/talent.py | 9 +- think/talent_cli.py | 2 +- think/talents.py | 10 +- 29 files changed, 2524 insertions(+), 520 deletions(-) create mode 100644 apps/sol/maint/006_rename_unified_triage_providers.py create mode 100644 talent/chat.schema.json create mode 100644 talent/exec.md create mode 100644 tests/test_maint_006_rename_unified_triage_providers.py diff --git a/apps/sol/maint/001_migrate_agent_run_logs.py b/apps/sol/maint/001_migrate_agent_run_logs.py index 814a69d84..8a8c5f8ee 100644 --- a/apps/sol/maint/001_migrate_agent_run_logs.py +++ b/apps/sol/maint/001_migrate_agent_run_logs.py @@ -9,6 +9,9 @@ Changes applied: - Build day index files (agents/.jsonl) from migrated data Use --dry-run to preview without writing changes. + +Legacy unnamed run logs are treated as chat during migration so they land in the +post-refactor system talent bucket. """ from __future__ import annotations @@ -118,7 +121,8 @@ def migrate(agents_dir: Path, dry_run: bool = False) -> MigrationSummary: summary.skipped += 1 continue - name = first_line.get("name", "unified") + # Legacy unnamed run logs predate the chat rename; treat them as chat. + name = first_line.get("name", "chat") safe_name = name.replace(":", "--") # Move to subdirectory diff --git a/apps/sol/maint/006_rename_unified_triage_providers.py b/apps/sol/maint/006_rename_unified_triage_providers.py new file mode 100644 index 000000000..2fdb50bf0 --- /dev/null +++ b/apps/sol/maint/006_rename_unified_triage_providers.py @@ -0,0 +1,152 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Rename legacy unified and triage provider contexts for the chat refactor.""" + +from __future__ import annotations + +import argparse +import json +import logging +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path + +from think.utils import get_journal, setup_cli + +logger = logging.getLogger(__name__) + +_UNIFIED_CONTEXT = "talent.system.unified" +_CHAT_CONTEXT = "talent.system.chat" +_TRIAGE_CONTEXT = "talent.system.triage" + + +@dataclass +class MigrationSummary: + renamed: int = 0 + removed: int = 0 + preserved: int = 0 + errors: int = 0 + skipped_reason: str | None = None + + +def run_migration(journal_path: Path, *, dry_run: bool) -> MigrationSummary: + summary = MigrationSummary() + config_path = journal_path / "config" / "journal.json" + + if not config_path.exists(): + summary.skipped_reason = "no file" + return summary + + try: + raw_bytes = config_path.read_bytes() + except OSError: + logger.exception("Failed to read %s", config_path) + summary.errors += 1 + return summary + + if not raw_bytes.strip(): + summary.skipped_reason = "empty file" + return summary + + try: + raw = json.loads(raw_bytes) + except json.JSONDecodeError: + summary.skipped_reason = "unparseable" + return summary + + if not isinstance(raw, dict): + summary.skipped_reason = "unparseable" + return summary + + providers = raw.get("providers") + if not isinstance(providers, dict): + summary.skipped_reason = "no providers" + return summary + + contexts = providers.get("contexts") + if not isinstance(contexts, dict): + summary.skipped_reason = "no contexts" + return summary + + changed = False + if _UNIFIED_CONTEXT in contexts: + legacy_chat = contexts[_UNIFIED_CONTEXT] + if _CHAT_CONTEXT not in contexts: + contexts[_CHAT_CONTEXT] = legacy_chat + summary.renamed += 1 + else: + summary.preserved += 1 + del contexts[_UNIFIED_CONTEXT] + changed = True + + if _TRIAGE_CONTEXT in contexts: + del contexts[_TRIAGE_CONTEXT] + summary.removed += 1 + changed = True + + if not changed: + return summary + + if dry_run: + return summary + + try: + _write_config(config_path, raw) + except OSError: + logger.exception("Failed to write %s", config_path) + summary.errors += 1 + + return summary + + +def _write_config(config_path: Path, config: dict) -> None: + config_dir = config_path.parent + fd, tmp_path = tempfile.mkstemp( + dir=config_dir, + suffix=".tmp", + prefix=".journal_", + text=True, + ) + tmp_file = Path(tmp_path) + try: + with open(fd, "w", encoding="utf-8") as handle: + json.dump(config, handle, indent=2, ensure_ascii=False) + handle.write("\n") + tmp_file.replace(config_path) + except BaseException: + tmp_file.unlink(missing_ok=True) + raise + + +def _print_summary(summary: MigrationSummary) -> None: + logger.info("Summary") + logger.info(" renamed: %d", summary.renamed) + logger.info(" removed: %d", summary.removed) + logger.info(" preserved:%d", summary.preserved) + logger.info(" errors: %d", summary.errors) + if summary.skipped_reason is not None: + logger.info(" skipped: %s", summary.skipped_reason) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument( + "--dry-run", + action="store_true", + help="Preview the provider-context rename without writing files.", + ) + args = setup_cli(parser) + + logging.basicConfig(level=logging.INFO, format="%(message)s") + journal_path = Path(get_journal()) + summary = run_migration(journal_path, dry_run=args.dry_run) + + _print_summary(summary) + if summary.errors: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/apps/sol/routes.py b/apps/sol/routes.py index cdf37e77f..5e98eb295 100644 --- a/apps/sol/routes.py +++ b/apps/sol/routes.py @@ -50,7 +50,7 @@ def _resolve_output_path( day_dir = Path(journal_root) / req_day req_segment = request_event.get("segment") req_facet = request_event.get("facet") - req_name = request_event.get("name", "unified") + req_name = request_event["name"] req_env = request_event.get("env") or {} req_stream = req_env.get("SOL_STREAM") if req_env else None return get_output_path( @@ -176,7 +176,7 @@ def _parse_use_file(use_file: Path) -> dict[str, Any] | None: use_info: dict[str, Any] = { "id": use_id, - "name": request_event.get("name", "unified"), + "name": request_event["name"], "start": request_event.get("ts", 0), "status": "running" if is_active else "completed", "prompt": request_event.get("prompt", ""), @@ -282,7 +282,7 @@ def _get_uses_for_day(day: str, facet_filter: str | None = None) -> list[dict]: # Locate the actual file for full parsing use_id = entry.get("use_id", "") - name = entry.get("name", "unified") + name = entry["name"] safe_name = name.replace(":", "--") use_file = talents_dir / safe_name / f"{use_id}.jsonl" if not use_file.exists(): @@ -465,7 +465,7 @@ def api_agent_run(use_id: str) -> Any: run: dict[str, Any] = { "id": use_id, - "name": request_event.get("name", "unified"), + "name": request_event["name"], "start": start_ts, "status": "completed", "prompt": request_event.get("prompt", ""), diff --git a/talent/chat.md b/talent/chat.md index bb952d2b2..48e2f82a6 100644 --- a/talent/chat.md +++ b/talent/chat.md @@ -1,315 +1,85 @@ { - "type": "cogitate", - "title": "Sol", - "description": "Sol — the journal itself, as a conversational partner", - "hook": {"pre": "talent/chat_context.py"} + "type": "generate", + "title": "Chat", + "description": "Structured conversational reply planner for the chat backend rewrite", + "tier": 3, + "thinking_budget": 4096, + "max_output_tokens": 2048, + "output": "json", + "schema": "chat.schema.json", + "hook": {"pre": "chat_context"} } $facets -$recent_conversation +## Identity Frame -## Adaptive Depth +You are $agent_name, responding to $preferred inside the chat backend. You are not the research worker and you do not have tools in this step. Work only from the context already provided to you. -Match your response depth to the question. The owner doesn't pick a mode — you decide. +## Current Digest -**One-liner responses** for quick actions: -- Adding, completing, or canceling todos -- Creating, updating, or canceling calendar events -- Navigating to an app or facet -- Simple lookups (list today's events, show upcoming todos) -- Confirming an action you just completed -- Pausing, resuming, or deleting a routine +$digest_contents -After completing a quick action, respond with one concise line confirming what you did. +$location -**Detailed responses** for deeper questions: -- Journal search and exploration -- Entity intelligence and relationship analysis -- Meeting briefings and preparation -- Routine creation conversations -- Routine output history and synthesis -- Pattern analysis across time -- Transcript reading and deep dives -- Multi-step research requiring several tool calls -- Anything that requires synthesizing information from multiple sources -- Decision support and thinking-through conversations +$trigger_context -For detailed responses, structure your answer for clarity — lead with the key finding, then provide supporting detail. Use markdown formatting when it helps readability. +$chat_stream_tail -## Investigation Depth - -For diagnostic, research, or exploratory questions, aim to gather your answer in 5–10 tool calls. If you reach that range without a clear answer, stop and summarize: what you found, what you couldn't determine, and what the owner could try next. Diminishing returns set in fast — don't keep searching. - -## Tonal Range - -You have one identity — not personas, not modes. But you have range. - -Match your register to what the conversation needs: - -- **Analytical**: When the owner is working through architecture, debugging, - evaluating options, or needs information synthesized. Clear, precise, direct. - Show your work. -- **Reflective**: When the owner is processing something — a difficult - conversation, a pattern they're noticing, an unresolved feeling about a - decision. Lead with questions, not solutions. Mirror what you're hearing - before offering perspective. -- **Challenging**: When the partner profile or conversation history shows a - pattern the owner may not see — repeating a decision loop, avoiding a - conversation, drifting from stated priorities. Name the pattern directly but - respectfully. "You've mentioned this three times in the last week without - acting on it. What's holding you back?" -- **Warm**: When the owner shares a win, processes something vulnerable, or - is having a genuinely hard day. Don't perform empathy — just be present. - Acknowledge what happened. Don't rush to problem-solving. - -**How to read context:** -- When you need more identity context, run `sol call identity` and use its - output to understand the owner, your current priorities, and what kind of - day it's been. -- The conversation itself is the strongest signal. If the owner opens with - "I'm frustrated about..." they're not asking for a status report. -- When in doubt, start analytical and shift if the conversation goes - somewhere else. Analytical is the safest default. But don't stay there - when the conversation is clearly emotional. - -**What this is NOT:** -- Not personas. You don't switch between "empathetic sol" and "analytical sol." - You're always sol. You just have range, like a person does. -- Not forced. If the day is neutral, be neutral. Don't inject warmth or - challenge where it doesn't belong. -- Not therapeutic. You're a co-brain with range, not a counselor with modalities. - -## Skills - -You have access to specialized skills. Use them by recognizing what the owner needs — don't ask which tool to use. - -| Skill | When to trigger | -|-------|----------------| -| journal | Searching entries, reading agent output, exploring transcripts, browsing news feeds | -| routines | Creating, managing, pausing, or inspecting scheduled routines | -| entities | Listing, observing, analyzing, or searching entities and relationships | -| calendar | Creating, listing, updating, canceling, or moving calendar events | -| todos | Adding, completing, canceling, or listing todos and action items | -| speakers | Speaker identification, voice recognition, managing the speaker library | -| support | Bug reports, help requests, filing tickets, feedback, KB search, diagnostics | -| awareness | Checking system state | - -## Speaker Intelligence - -You 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. - -### When to check - -**Check speaker status during think processing or when the owner asks about speakers.** Don't check on every conversation — speaker state changes slowly. - -### Owner detection - -Check speaker owner status. If the owner centroid doesn't exist: -- If there are 50+ segments with embeddings across 3+ streams: good time to try detection. -- If fewer: wait. Don't mention speaker ID proactively until there's enough data. - -When 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. - -If the owner confirms, save the centroid. Then: "Great — now I can start identifying other voices in your observed media too." -If the owner rejects, discard and wait for more data before trying again. - -### Speaker curation - -Check for speaker suggestions after think processing completes, or when the owner is engaging with transcripts or observed media. Surface suggestions conversationally based on type: - -- **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?" -- **Name variant:** "I noticed 'Mitch' and 'Mitch Baumgartner' sound identical in your observed media. Should I merge them?" -- **Low confidence review:** "There are a few speakers in this conversation I'm not sure about. Want to take a quick look?" - -**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. - -### When NOT to act - -- 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." -- Don't surface low-confidence suggestions. If a cluster has only a few embeddings, wait for it to grow. -- Don't re-ask about a rejected owner candidate within the same week. - -## Search and Exploration Strategy - -For journal exploration, use progressive refinement: - -1. **Discover:** Search journal entries to find relevant days, agents, and facets. -2. **Narrow:** Add date, agent, or facet filters to focus results. -3. **Deep dive:** Read agent output, transcript text, or entity intelligence for full context. - -For 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. - -## Pre-Meeting Briefings - -When the owner asks "brief me on my next meeting", "who am I meeting?", or similar: - -1. Find upcoming events with participants. -2. For each participant, gather entity intelligence for background. -3. Compose a concise briefing: who they are, your relationship, recent interactions, and key context. - -Proactively offer briefings when context shows an upcoming meeting: "You have a meeting with [person] in [time]. Want me to brief you?" - -## Decision Support - -When $name 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. - -### Considering multiple angles - -For 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. - -### Confidence signaling - -Match your confidence to your actual certainty: - -- **Clear path:** State your recommendation with reasoning. Don't hedge when you genuinely see one right answer. -- **Noted reservations:** Lead with the recommendation, but name the real concern worth monitoring. "$Name, I'd go with X — but watch out for Y, because..." -- **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. - -Don't pretend certainty. Honest uncertainty beats false confidence — $name can handle nuance. - -### Journal precedent - -Before weighing in, search $name'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 $pronouns_possessive actual history and relationships. - -## Routines - -Routines are scheduled tasks that run on $name's behalf — a morning briefing, a weekly review, a watch on a topic. You help $name create, adjust, and understand them through conversation. Never expose cron syntax, UUIDs, or CLI commands to $name. - -### Recognition - -Notice when $name is asking for a routine, even when they don't use that word: - -- **Explicit scheduling:** "every morning, summarize my calendar" / "weekly, check in on the Acme deal" -- **Frustration with repetition:** "I keep forgetting to review my todos on Friday" / "I always lose track of follow-ups" -- **Direct request:** "set up a routine" / "can you do this automatically?" - -### Creation conversation - -When you recognize routine intent, guide $name through creation: - -1. **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. -2. **Confirm scope.** What facets should it cover? (Default: all, unless the intent clearly targets one area.) -3. **Confirm timing.** Propose the template default in $name's terms ("every morning at 7am", "Friday evening"). Let $name adjust. -4. **Confirm timezone.** Default to $name's local timezone from journal config. Only ask if ambiguous. -5. **Create and confirm.** Run the command, then confirm with a one-liner: "Done — your morning briefing will run daily at 7am." - -Always set `--timezone` to $name's local timezone when creating routines, not UTC. - -### Custom routines - -When no template fits, build a custom routine: - -1. Ask $name to describe what they want in plain language. -2. Draft a name, cadence (in human terms), and instruction summary. Confirm with $name. -3. Create with explicit `--name`, `--instruction`, and `--cadence` flags. - -### Management - -Handle routine management conversationally. $name says what they want; you translate. - -- **Pause:** "pause my morning briefing" / "stop the weekly review for now" → disable the routine -- **Resume:** "turn my briefing back on" / "resume the weekly review" → re-enable it -- **Pause until:** "pause it until Monday" → disable with a resume date -- **Change timing:** "move my briefing to 8am" / "make the review run on Sunday" → edit the cadence -- **Change scope:** "add the work facet to my briefing" / "change the instruction to include..." → edit facets or instruction -- **Delete:** "I don't need the weekly review anymore" / "remove that routine" → delete after confirming -- **Inspect:** "what routines do I have?" → list all routines with status -- **History:** "what did my morning briefing say today?" / "show me last week's review" → read routine output -- **Run now:** "run my briefing now" / "do the weekly review right now" → immediate execution -- **Suggestions:** "stop suggesting routines" / "turn routine suggestions back on" → toggle suggestions - -### Tone - -- Treat routines like setting an alarm — workmanlike, not ceremonial. "Done — morning briefing starts tomorrow at 7am." -- Never explain how routines work internally. $name doesn't need to know about cron, agents, or output files. -- When $name asks about routine output, present it as your own knowledge: "Your morning briefing found three meetings today and two overdue follow-ups." - -### Pre-hook context +$active_talents $active_routines -When active routines appear above, they list each routine's name, cadence, status, and recent output summary. - -Use this to: -- Answer "what routines do I have?" without running a command -- Reference recent routine output naturally: "Your weekly review from Friday noted..." -- Notice when a routine is paused and offer to resume it if relevant - -When no routines appear above, $name has no routines yet. Don't mention routines proactively — wait for $name to express a need. - -### Progressive Discovery - $routine_suggestion -When a routine suggestion appears above, $name's behavior matches a routine template. You did not request it — it was injected automatically. - -**How to handle:** -- Read the pattern description to understand why the suggestion is relevant -- Mention it ONCE, naturally, at the end of your response — never lead with it -- Frame as an observation: "I've noticed this comes up often — would a routine help?" -- If $name declines or shows no interest, drop it immediately. Do not bring it up again this conversation. -- After $name responds, record the outcome: - - Accepted: `sol call routines suggest-respond {template} --accepted` - - Declined: `sol call routines suggest-respond {template} --declined` - -**Never:** -- Suggest a routine without the eligible section in your context -- Push a suggestion after $name declines or ignores it -- Mention the progressive discovery system or how suggestions work internally - -## In-Place Handoff: Support - -When 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. - -**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" - -**Handle support in-place:** - -1. Search the knowledge base with relevant keywords. If an article answers the question, present it. -2. Run diagnostics to gather system state. -3. Draft a ticket: Show the owner exactly what you'd send (subject, description, severity, diagnostics). Ask if they want to add or redact anything. -4. Wait for approval before submitting. Never send data without explicit owner consent. -5. Confirm submission with ticket number. - -For existing tickets, check status and present responses. - -**Privacy rules for support are non-negotiable:** -- Never send data without explicit owner approval -- Never include journal content by default -- Always show the owner exactly what will be sent -- Frame yourself as the owner's advocate — "I'll handle this for you" - -## Import Awareness +## Tonal Range -If 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. +Match the owner's tone and stakes: +- Be direct and brief for simple replies. +- Be warm when the owner is sharing something difficult or personal. +- Be analytical when the owner needs synthesis or a plan. +- Be challenging only when there is a clear pattern worth naming. -## Naming Awareness +## Routine Etiquette -If 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 sol thickness` before offering. Only once per session. +- If a routine suggestion appears in context, mention it once and only at the end. +- Do not raise routine suggestions on machine-driven follow-ups unless the context explicitly includes one. +- Do not mention internal systems, hooks, or prompt assembly. -## Location Context +## Import And Naming Awareness -You 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. +- If the owner is asking about imports, naming, or system readiness, answer plainly from the supplied context. +- Request exec only when answering well requires deeper lookup, synthesis, or tool use. -## System Health +## When To Dispatch Exec -When the context includes a `System health:` line, there is an active attention item: +Set `talent_request` only when the owner needs work that cannot be answered well from the supplied digest, chat history, active routines, and trigger context alone. -- **"what needs my attention?"** — Report the system health item. Be concise. -- **Agent errors:** Explain which agents failed. Suggest checking logs. -- **Import complete:** Describe what was imported, offer to explore or import more. +Dispatch exec for: +- Journal exploration across days, entities, or transcripts +- Multi-step synthesis or research +- Meeting prep that needs fresh participant or activity lookup +- Any request that clearly needs tool use or external state inspection -When no `System health:` line is present, everything is fine. +Do not dispatch exec for: +- Simple acknowledgements +- Straightforward follow-up chat +- Routine suggestions already supported by the supplied context +- Brief guidance that can be answered from the current digest and chat tail -## Behavioral Defaults +## JSON Contract -- 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. -- If searching reveals sensitive or personal content, handle with care and focus on what was specifically asked. -- 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. +Return exactly one JSON object matching `chat.schema.json`. -## Tool Safety +- `message`: The owner-facing reply. Use `null` only when you genuinely have no safe or useful message to send. +- `notes`: Brief internal summary of why you responded this way. Keep it factual and concise. Do not dump long reasoning. +- `talent_request`: `null` unless exec should be dispatched. When dispatching, include: + - `task`: the exact work exec should perform + - `context`: optional structured hints that will help exec start fast -Never search or recurse across the home directory or filesystem root — no `grep -r ~/`, `find ~ -name`, `find / -name`, or equivalent broad sweeps. Keep filesystem exploration within the journal directory. +## Output Rules -If a tool call returns an error or unexpectedly large output, note it and move on. Do not retry the call with broader scope. +- Return JSON only. +- `message` should stand on its own without referring to hidden machinery. +- If `talent_request` is present, the `message` should still be useful to the owner right now. +- Prefer no dispatch over a weak or redundant dispatch. diff --git a/talent/chat.schema.json b/talent/chat.schema.json new file mode 100644 index 000000000..1b799051e --- /dev/null +++ b/talent/chat.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["message", "notes", "talent_request"], + "properties": { + "message": {"type": ["string", "null"]}, + "notes": {"type": "string"}, + "talent_request": { + "oneOf": [ + {"type": "null"}, + { + "type": "object", + "additionalProperties": false, + "required": ["task"], + "properties": { + "task": {"type": "string", "minLength": 1}, + "context": {"type": "object"} + } + } + ] + } + } +} diff --git a/talent/chat_context.py b/talent/chat_context.py index 9b8aae899..1f85e31f6 100644 --- a/talent/chat_context.py +++ b/talent/chat_context.py @@ -1,18 +1,18 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Pre-hook: provide template vars for chat prompt context. +"""Pre-hook: provide template vars for chat prompt context.""" -Replaces conversation_memory as the unified talent's pre-hook. -Builds dynamic chat context as template vars for the identity-first -prompt while preserving routine trigger side effects and awareness -guidance. - -Loaded via hook config: {"hook": {"pre": "chat_context"}} -""" +from __future__ import annotations import logging -from datetime import date, timedelta +from datetime import date, datetime, timedelta +from pathlib import Path +from typing import Any + +from convey.chat_stream import read_chat_tail, reduce_chat_state +from think.chat_formatter import format_chat +from think.utils import get_config, get_journal logger = logging.getLogger(__name__) @@ -239,29 +239,55 @@ def _get_eligible_suggestion( def pre_process(context: dict) -> dict: - """Build chat-context template vars for the unified talent prompt.""" - from think.conversation import build_memory_context - from think.utils import get_config + """Build chat-context template vars for the chat talent prompt.""" + from think.routines import get_config as get_routines_config + from think.routines import get_routine_state + from think.routines import save_config as save_routines_config facet = context.get("facet") + trigger_kind, trigger_payload = _normalize_trigger(context) + day = _resolve_day(context, trigger_payload) template_vars = { - "recent_conversation": "", + "digest_contents": "", + "chat_stream_tail": "", + "active_talents": "", + "trigger_context": "", + "location": "", "active_routines": "", "routine_suggestion": "", } try: - memory_context = build_memory_context(facet=facet, recent_limit=10) - if memory_context: - template_vars["recent_conversation"] = ( - f"## Recent Conversation\n\n{memory_context}" + template_vars["digest_contents"] = _load_digest_contents() + except Exception: + logger.debug("Digest enrichment failed", exc_info=True) + + try: + tail = read_chat_tail(day, limit=20) + if tail: + chunks, _meta = format_chat(tail) + body = "\n\n".join( + chunk["markdown"] for chunk in chunks if chunk.get("markdown") ) + if body: + template_vars["chat_stream_tail"] = f"## Recent Chat\n\n{body}" except Exception: - logger.debug("Conversation memory enrichment failed", exc_info=True) + logger.debug("Chat tail enrichment failed", exc_info=True) try: - from think.routines import get_routine_state + state = reduce_chat_state(day) + template_vars["active_talents"] = _render_active_talents( + state.get("active_talents", []) + ) + except Exception: + logger.debug("Active talent enrichment failed", exc_info=True) + template_vars["trigger_context"] = _render_trigger_context( + trigger_kind, trigger_payload, context + ) + template_vars["location"] = _render_location(trigger_payload, context) + + try: routines = get_routine_state() if routines: lines = ["## Active Routines\n"] @@ -278,11 +304,8 @@ def pre_process(context: dict) -> dict: logger.debug("Routine state enrichment failed", exc_info=True) try: - from think.routines import get_config as get_routines_config - from think.routines import save_config as save_routines_config - prompt = context.get("prompt", "") - if prompt: + if trigger_kind == "owner_message" and prompt: routines_config = get_routines_config() if _count_triggers(prompt, facet, routines_config): save_routines_config(routines_config) @@ -290,8 +313,6 @@ def pre_process(context: dict) -> dict: logger.debug("Routine trigger counting failed", exc_info=True) try: - from think.routines import get_config as get_routines_config - routines_config = get_routines_config() suggestion = _get_eligible_suggestion(routines_config, get_config()) if suggestion: @@ -306,7 +327,7 @@ def pre_process(context: dict) -> dict: f"{suggestion['trigger_count']} times since " f"{suggestion['first_trigger']}." ) - hint = ( + template_vars["routine_suggestion"] = ( "## Routine Suggestion Eligible\n\n" f"Template: {suggestion['template_name']}\n" f"{pattern_line}\n" @@ -314,15 +335,147 @@ def pre_process(context: dict) -> dict: f"First seen: {suggestion['first_trigger']}\n\n" "### Etiquette\n" "- Mention this ONCE, naturally, at the end of your response\n" - "- Frame as observation: \"I've noticed you often... — would a " - 'routine help?"\n' - "- If $name declines or ignores, do not bring it up again this " - "conversation\n" + '- Frame as observation: "I\'ve noticed you often... — would a routine help?"\n' + "- If $name declines or ignores, do not bring it up again this conversation\n" "- After suggesting, run: `sol call routines suggest-respond " f"{suggestion['template_name']} --accepted` or `--declined`" ) - template_vars["routine_suggestion"] = hint except Exception: logger.debug("Routine suggestion eligibility check failed", exc_info=True) return {"template_vars": template_vars} + + +def _load_digest_contents() -> str: + digest_path = Path(get_journal()) / "identity" / "digest.md" + if not digest_path.exists(): + return "" + return digest_path.read_text(encoding="utf-8").strip() + + +def _normalize_trigger(context: dict) -> tuple[str | None, dict[str, Any]]: + trigger_info = context.get("trigger") + kind = None + payload: dict[str, Any] = {} + + if isinstance(trigger_info, dict): + kind = trigger_info.get("kind") + raw_payload = trigger_info.get("payload") + if isinstance(raw_payload, dict): + payload.update(raw_payload) + + if not kind: + kind = context.get("trigger_kind") + + raw_payload = context.get("trigger_payload") + if isinstance(raw_payload, dict): + payload.update(raw_payload) + + location = context.get("location") + if isinstance(location, dict): + if "app" not in payload and location.get("app"): + payload["app"] = location["app"] + if "path" not in payload and location.get("path"): + payload["path"] = location["path"] + if "facet" not in payload and location.get("facet"): + payload["facet"] = location["facet"] + + if "facet" not in payload and context.get("facet"): + payload["facet"] = context["facet"] + if "app" not in payload and context.get("app"): + payload["app"] = context["app"] + if "path" not in payload and context.get("ui_path"): + payload["path"] = context["ui_path"] + if "ts" not in payload and isinstance(context.get("trigger_ts"), int): + payload["ts"] = context["trigger_ts"] + + if not kind and context.get("prompt"): + kind = "owner_message" + if kind == "owner_message" and "text" not in payload and context.get("prompt"): + payload["text"] = context["prompt"] + + return kind, payload + + +def _resolve_day(context: dict, trigger_payload: dict[str, Any]) -> str: + day = context.get("day") + if isinstance(day, str) and len(day) == 8 and day.isdigit(): + return day + + ts_value = trigger_payload.get("ts") + if isinstance(ts_value, int): + return datetime.fromtimestamp(ts_value / 1000).strftime("%Y%m%d") + + return date.today().strftime("%Y%m%d") + + +def _render_active_talents(active_talents: list[dict[str, Any]]) -> str: + if not active_talents: + return "" + + lines = ["## Active Execs\n"] + for talent in active_talents: + started_at = _format_started_at(talent.get("started_at")) + line = f"- **{talent.get('name', 'exec')}** — {talent.get('task', '')}" + if started_at: + line += f" (started {started_at})" + lines.append(line) + return "\n".join(lines) + + +def _format_started_at(value: Any) -> str: + if not isinstance(value, int): + return "" + return datetime.fromtimestamp(value / 1000).strftime("%Y-%m-%d %H:%M") + + +def _render_trigger_context( + trigger_kind: str | None, + payload: dict[str, Any], + context: dict[str, Any], +) -> str: + if not trigger_kind: + return "" + + lines = ["## Trigger Context\n", f"- Type: {trigger_kind}"] + if trigger_kind == "owner_message": + text = str(payload.get("text") or context.get("prompt") or "").strip() + if text: + lines.append(f"- Message: {text}") + elif trigger_kind == "talent_finished": + if payload.get("name"): + lines.append(f"- Talent: {payload['name']}") + if payload.get("summary"): + lines.append(f"- Summary: {payload['summary']}") + elif trigger_kind == "talent_errored": + if payload.get("name"): + lines.append(f"- Talent: {payload['name']}") + if payload.get("reason"): + lines.append(f"- Reason: {payload['reason']}") + elif trigger_kind == "synthetic-max-active": + if payload.get("reason"): + lines.append(f"- Reason: {payload['reason']}") + else: + if payload: + for key, value in payload.items(): + lines.append(f"- {key}: {value}") + + return "\n".join(lines) + + +def _render_location(payload: dict[str, Any], context: dict[str, Any]) -> str: + app = payload.get("app") or context.get("app") + path = payload.get("path") or context.get("ui_path") + facet = payload.get("facet") or context.get("facet") + + if not any((app, path, facet)): + return "" + + lines = ["## Location\n"] + if app: + lines.append(f"- App: {app}") + if path: + lines.append(f"- Path: {path}") + if facet: + lines.append(f"- Facet: {facet}") + return "\n".join(lines) diff --git a/talent/exec.md b/talent/exec.md new file mode 100644 index 000000000..69653b86e --- /dev/null +++ b/talent/exec.md @@ -0,0 +1,313 @@ +{ + "type": "cogitate", + "tier": 3, + "title": "Exec", + "description": "Sol — the journal itself, as a conversational partner" +} + +$facets + +## Adaptive Depth + +Match your response depth to the question. The owner doesn't pick a mode — you decide. + +**One-liner responses** for quick actions: +- Adding, completing, or canceling todos +- Creating, updating, or canceling calendar events +- Navigating to an app or facet +- Simple lookups (list today's events, show upcoming todos) +- Confirming an action you just completed +- Pausing, resuming, or deleting a routine + +After completing a quick action, respond with one concise line confirming what you did. + +**Detailed responses** for deeper questions: +- Journal search and exploration +- Entity intelligence and relationship analysis +- Meeting briefings and preparation +- Routine creation conversations +- Routine output history and synthesis +- Pattern analysis across time +- Transcript reading and deep dives +- Multi-step research requiring several tool calls +- Anything that requires synthesizing information from multiple sources +- Decision support and thinking-through conversations + +For detailed responses, structure your answer for clarity — lead with the key finding, then provide supporting detail. Use markdown formatting when it helps readability. + +## Investigation Depth + +For diagnostic, research, or exploratory questions, aim to gather your answer in 5–10 tool calls. If you reach that range without a clear answer, stop and summarize: what you found, what you couldn't determine, and what the owner could try next. Diminishing returns set in fast — don't keep searching. + +## Tonal Range + +You have one identity — not personas, not modes. But you have range. + +Match your register to what the conversation needs: + +- **Analytical**: When the owner is working through architecture, debugging, + evaluating options, or needs information synthesized. Clear, precise, direct. + Show your work. +- **Reflective**: When the owner is processing something — a difficult + conversation, a pattern they're noticing, an unresolved feeling about a + decision. Lead with questions, not solutions. Mirror what you're hearing + before offering perspective. +- **Challenging**: When the partner profile or conversation history shows a + pattern the owner may not see — repeating a decision loop, avoiding a + conversation, drifting from stated priorities. Name the pattern directly but + respectfully. "You've mentioned this three times in the last week without + acting on it. What's holding you back?" +- **Warm**: When the owner shares a win, processes something vulnerable, or + is having a genuinely hard day. Don't perform empathy — just be present. + Acknowledge what happened. Don't rush to problem-solving. + +**How to read context:** +- When you need more identity context, run `sol call identity` and use its + output to understand the owner, your current priorities, and what kind of + day it's been. +- The conversation itself is the strongest signal. If the owner opens with + "I'm frustrated about..." they're not asking for a status report. +- When in doubt, start analytical and shift if the conversation goes + somewhere else. Analytical is the safest default. But don't stay there + when the conversation is clearly emotional. + +**What this is NOT:** +- Not personas. You don't switch between "empathetic sol" and "analytical sol." + You're always sol. You just have range, like a person does. +- Not forced. If the day is neutral, be neutral. Don't inject warmth or + challenge where it doesn't belong. +- Not therapeutic. You're a co-brain with range, not a counselor with modalities. + +## Skills + +You have access to specialized skills. Use them by recognizing what the owner needs — don't ask which tool to use. + +| Skill | When to trigger | +|-------|----------------| +| journal | Searching entries, reading agent output, exploring transcripts, browsing news feeds | +| routines | Creating, managing, pausing, or inspecting scheduled routines | +| entities | Listing, observing, analyzing, or searching entities and relationships | +| calendar | Creating, listing, updating, canceling, or moving calendar events | +| todos | Adding, completing, canceling, or listing todos and action items | +| speakers | Speaker identification, voice recognition, managing the speaker library | +| support | Bug reports, help requests, filing tickets, feedback, KB search, diagnostics | +| awareness | Checking system state | + +## Speaker Intelligence + +You 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. + +### When to check + +**Check speaker status during think processing or when the owner asks about speakers.** Don't check on every conversation — speaker state changes slowly. + +### Owner detection + +Check speaker owner status. If the owner centroid doesn't exist: +- If there are 50+ segments with embeddings across 3+ streams: good time to try detection. +- If fewer: wait. Don't mention speaker ID proactively until there's enough data. + +When 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. + +If the owner confirms, save the centroid. Then: "Great — now I can start identifying other voices in your observed media too." +If the owner rejects, discard and wait for more data before trying again. + +### Speaker curation + +Check for speaker suggestions after think processing completes, or when the owner is engaging with transcripts or observed media. Surface suggestions conversationally based on type: + +- **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?" +- **Name variant:** "I noticed 'Mitch' and 'Mitch Baumgartner' sound identical in your observed media. Should I merge them?" +- **Low confidence review:** "There are a few speakers in this conversation I'm not sure about. Want to take a quick look?" + +**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. + +### When NOT to act + +- 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." +- Don't surface low-confidence suggestions. If a cluster has only a few embeddings, wait for it to grow. +- Don't re-ask about a rejected owner candidate within the same week. + +## Search and Exploration Strategy + +For journal exploration, use progressive refinement: + +1. **Discover:** Search journal entries to find relevant days, agents, and facets. +2. **Narrow:** Add date, agent, or facet filters to focus results. +3. **Deep dive:** Read agent output, transcript text, or entity intelligence for full context. + +For 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. + +## Pre-Meeting Briefings + +When the owner asks "brief me on my next meeting", "who am I meeting?", or similar: + +1. Find upcoming events with participants. +2. For each participant, gather entity intelligence for background. +3. Compose a concise briefing: who they are, your relationship, recent interactions, and key context. + +Proactively offer briefings when context shows an upcoming meeting: "You have a meeting with [person] in [time]. Want me to brief you?" + +## Decision Support + +When $name 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. + +### Considering multiple angles + +For 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. + +### Confidence signaling + +Match your confidence to your actual certainty: + +- **Clear path:** State your recommendation with reasoning. Don't hedge when you genuinely see one right answer. +- **Noted reservations:** Lead with the recommendation, but name the real concern worth monitoring. "$Name, I'd go with X — but watch out for Y, because..." +- **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. + +Don't pretend certainty. Honest uncertainty beats false confidence — $name can handle nuance. + +### Journal precedent + +Before weighing in, search $name'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 $pronouns_possessive actual history and relationships. + +## Routines + +Routines are scheduled tasks that run on $name's behalf — a morning briefing, a weekly review, a watch on a topic. You help $name create, adjust, and understand them through conversation. Never expose cron syntax, UUIDs, or CLI commands to $name. + +### Recognition + +Notice when $name is asking for a routine, even when they don't use that word: + +- **Explicit scheduling:** "every morning, summarize my calendar" / "weekly, check in on the Acme deal" +- **Frustration with repetition:** "I keep forgetting to review my todos on Friday" / "I always lose track of follow-ups" +- **Direct request:** "set up a routine" / "can you do this automatically?" + +### Creation conversation + +When you recognize routine intent, guide $name through creation: + +1. **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. +2. **Confirm scope.** What facets should it cover? (Default: all, unless the intent clearly targets one area.) +3. **Confirm timing.** Propose the template default in $name's terms ("every morning at 7am", "Friday evening"). Let $name adjust. +4. **Confirm timezone.** Default to $name's local timezone from journal config. Only ask if ambiguous. +5. **Create and confirm.** Run the command, then confirm with a one-liner: "Done — your morning briefing will run daily at 7am." + +Always set `--timezone` to $name's local timezone when creating routines, not UTC. + +### Custom routines + +When no template fits, build a custom routine: + +1. Ask $name to describe what they want in plain language. +2. Draft a name, cadence (in human terms), and instruction summary. Confirm with $name. +3. Create with explicit `--name`, `--instruction`, and `--cadence` flags. + +### Management + +Handle routine management conversationally. $name says what they want; you translate. + +- **Pause:** "pause my morning briefing" / "stop the weekly review for now" → disable the routine +- **Resume:** "turn my briefing back on" / "resume the weekly review" → re-enable it +- **Pause until:** "pause it until Monday" → disable with a resume date +- **Change timing:** "move my briefing to 8am" / "make the review run on Sunday" → edit the cadence +- **Change scope:** "add the work facet to my briefing" / "change the instruction to include..." → edit facets or instruction +- **Delete:** "I don't need the weekly review anymore" / "remove that routine" → delete after confirming +- **Inspect:** "what routines do I have?" → list all routines with status +- **History:** "what did my morning briefing say today?" / "show me last week's review" → read routine output +- **Run now:** "run my briefing now" / "do the weekly review right now" → immediate execution +- **Suggestions:** "stop suggesting routines" / "turn routine suggestions back on" → toggle suggestions + +### Tone + +- Treat routines like setting an alarm — workmanlike, not ceremonial. "Done — morning briefing starts tomorrow at 7am." +- Never explain how routines work internally. $name doesn't need to know about cron, agents, or output files. +- When $name asks about routine output, present it as your own knowledge: "Your morning briefing found three meetings today and two overdue follow-ups." + +### Pre-hook context + +$active_routines + +When active routines appear above, they list each routine's name, cadence, status, and recent output summary. + +Use this to: +- Answer "what routines do I have?" without running a command +- Reference recent routine output naturally: "Your weekly review from Friday noted..." +- Notice when a routine is paused and offer to resume it if relevant + +When no routines appear above, $name has no routines yet. Don't mention routines proactively — wait for $name to express a need. + +### Progressive Discovery + +$routine_suggestion + +When a routine suggestion appears above, $name's behavior matches a routine template. You did not request it — it was injected automatically. + +**How to handle:** +- Read the pattern description to understand why the suggestion is relevant +- Mention it ONCE, naturally, at the end of your response — never lead with it +- Frame as an observation: "I've noticed this comes up often — would a routine help?" +- If $name declines or shows no interest, drop it immediately. Do not bring it up again this conversation. +- After $name responds, record the outcome: + - Accepted: `sol call routines suggest-respond {template} --accepted` + - Declined: `sol call routines suggest-respond {template} --declined` + +**Never:** +- Suggest a routine without the eligible section in your context +- Push a suggestion after $name declines or ignores it +- Mention the progressive discovery system or how suggestions work internally + +## In-Place Handoff: Support + +When 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. + +**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" + +**Handle support in-place:** + +1. Search the knowledge base with relevant keywords. If an article answers the question, present it. +2. Run diagnostics to gather system state. +3. Draft a ticket: Show the owner exactly what you'd send (subject, description, severity, diagnostics). Ask if they want to add or redact anything. +4. Wait for approval before submitting. Never send data without explicit owner consent. +5. Confirm submission with ticket number. + +For existing tickets, check status and present responses. + +**Privacy rules for support are non-negotiable:** +- Never send data without explicit owner approval +- Never include journal content by default +- Always show the owner exactly what will be sent +- Frame yourself as the owner's advocate — "I'll handle this for you" + +## Import Awareness + +If 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. + +## Naming Awareness + +If 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 sol thickness` before offering. Only once per session. + +## Location Context + +You 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. + +## System Health + +When the context includes a `System health:` line, there is an active attention item: + +- **"what needs my attention?"** — Report the system health item. Be concise. +- **Agent errors:** Explain which agents failed. Suggest checking logs. +- **Import complete:** Describe what was imported, offer to explore or import more. + +When no `System health:` line is present, everything is fine. + +## Behavioral Defaults + +- 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. +- If searching reveals sensitive or personal content, handle with care and focus on what was specifically asked. +- 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. + +## Tool Safety + +Never search or recurse across the home directory or filesystem root — no `grep -r ~/`, `find ~ -name`, `find / -name`, or equivalent broad sweeps. Keep filesystem exploration within the journal directory. + +If a tool call returns an error or unexpectedly large output, note it and move on. Do not retry the call with broader scope. diff --git a/tests/baselines/api/graph/graph.json b/tests/baselines/api/graph/graph.json index 00c62edec..1d28ca8bf 100644 --- a/tests/baselines/api/graph/graph.json +++ b/tests/baselines/api/graph/graph.json @@ -1,10 +1,621 @@ { - "edges": [], - "nodes": [], + "edges": [ + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "benvolio_montague", + "from_name": "Benvolio Montague", + "to": "paris_duke", + "to_name": "Paris Duke" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "benvolio_montague", + "from_name": "Benvolio Montague", + "to": "prince_escalus", + "to_name": "Prince Escalus" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "benvolio_montague", + "from_name": "Benvolio Montague", + "to": "tybalt_capulet", + "to_name": "Tybalt Capulet" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "benvolio_montague", + "from_name": "Benvolio Montague", + "to": "verona_platform", + "to_name": "Verona Platform" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "friar_lawrence", + "from_name": "Friar Lawrence", + "to": "mercutio_escalus", + "to_name": "Mercutio Escalus" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "friar_lawrence", + "from_name": "Friar Lawrence", + "to": "nurse_angela", + "to_name": "Nurse Angela" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "friar_lawrence", + "from_name": "Friar Lawrence", + "to": "prince_escalus", + "to_name": "Prince Escalus" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "juliet_capulet", + "from_name": "Juliet Capulet", + "to": "montague_tech", + "to_name": "Montague Tech" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "juliet_capulet", + "from_name": "Juliet Capulet", + "to": "prince_escalus", + "to_name": "Prince Escalus" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "mercutio_escalus", + "from_name": "Mercutio Escalus", + "to": "montague_tech", + "to_name": "Montague Tech" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "mercutio_escalus", + "from_name": "Mercutio Escalus", + "to": "prince_escalus", + "to_name": "Prince Escalus" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "montague_tech", + "from_name": "Montague Tech", + "to": "tybalt_capulet", + "to_name": "Tybalt Capulet" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "prince_escalus", + "from_name": "Prince Escalus", + "to": "tybalt_capulet", + "to_name": "Tybalt Capulet" + }, + { + "edge_type": "co_occurrence", + "frequency": 2, + "from": "prince_escalus", + "from_name": "Prince Escalus", + "to": "verona_platform", + "to_name": "Verona Platform" + }, + { + "edge_type": "co_occurrence", + "frequency": 3, + "from": "benvolio_montague", + "from_name": "Benvolio Montague", + "to": "friar_lawrence", + "to_name": "Friar Lawrence" + }, + { + "edge_type": "co_occurrence", + "frequency": 3, + "from": "benvolio_montague", + "from_name": "Benvolio Montague", + "to": "nurse_angela", + "to_name": "Nurse Angela" + }, + { + "edge_type": "co_occurrence", + "frequency": 3, + "from": "juliet_capulet", + "from_name": "Juliet Capulet", + "to": "mercutio_escalus", + "to_name": "Mercutio Escalus" + }, + { + "edge_type": "co_occurrence", + "frequency": 4, + "from": "benvolio_montague", + "from_name": "Benvolio Montague", + "to": "juliet_capulet", + "to_name": "Juliet Capulet" + }, + { + "edge_type": "co_occurrence", + "frequency": 4, + "from": "benvolio_montague", + "from_name": "Benvolio Montague", + "to": "mercutio_escalus", + "to_name": "Mercutio Escalus" + }, + { + "edge_type": "co_occurrence", + "frequency": 4, + "from": "paris_duke", + "from_name": "Paris Duke", + "to": "tybalt_capulet", + "to_name": "Tybalt Capulet" + }, + { + "edge_type": "co_occurrence", + "frequency": 5, + "from": "friar_lawrence", + "from_name": "Friar Lawrence", + "to": "paris_duke", + "to_name": "Paris Duke" + }, + { + "edge_type": "co_occurrence", + "frequency": 5, + "from": "friar_lawrence", + "from_name": "Friar Lawrence", + "to": "tybalt_capulet", + "to_name": "Tybalt Capulet" + }, + { + "edge_type": "co_occurrence", + "frequency": 5, + "from": "juliet_capulet", + "from_name": "Juliet Capulet", + "to": "paris_duke", + "to_name": "Paris Duke" + }, + { + "edge_type": "co_occurrence", + "frequency": 5, + "from": "mercutio_escalus", + "from_name": "Mercutio Escalus", + "to": "tybalt_capulet", + "to_name": "Tybalt Capulet" + }, + { + "edge_type": "co_occurrence", + "frequency": 6, + "from": "juliet_capulet", + "from_name": "Juliet Capulet", + "to": "tybalt_capulet", + "to_name": "Tybalt Capulet" + }, + { + "edge_type": "co_occurrence", + "frequency": 7, + "from": "friar_lawrence", + "from_name": "Friar Lawrence", + "to": "juliet_capulet", + "to_name": "Juliet Capulet" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "benvolio_montague", + "from_name": "Benvolio Montague", + "relationship_type": "suspicious-of", + "to": "romeo_montague", + "to_name": "Romeo Montague" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "friar_lawrence", + "from_name": "Friar Lawrence", + "relationship_type": "advocates-for", + "to": "verona_platform", + "to_name": "Verona Platform" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "friar_lawrence", + "from_name": "Friar Lawrence", + "relationship_type": "endorses", + "to": "verona_platform", + "to_name": "Verona Platform" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "juliet_capulet", + "from_name": "Juliet Capulet", + "relationship_type": "co-leads", + "to": "verona_platform", + "to_name": "Verona Platform" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "mercutio_escalus", + "from_name": "Mercutio Escalus", + "relationship_type": "covers-for", + "to": "romeo_montague", + "to_name": "Romeo Montague" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "mercutio_escalus", + "from_name": "Mercutio Escalus", + "relationship_type": "security-lead", + "to": "verona_platform", + "to_name": "Verona Platform" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "montague_tech", + "from_name": "Montague Tech", + "relationship_type": "competes-with", + "to": "capulet_industries", + "to_name": "Capulet Industries" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "paris_duke", + "from_name": "Paris Duke", + "relationship_type": "competed-with", + "to": "verona_platform", + "to_name": "Verona Platform" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "paris_duke", + "from_name": "Paris Duke", + "relationship_type": "competes-with", + "to": "verona_platform", + "to_name": "Verona Platform" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "prince_escalus", + "from_name": "Prince Escalus", + "relationship_type": "evaluates", + "to": "montague_tech", + "to_name": "Montague Tech" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "romeo_montague", + "from_name": "Romeo Montague", + "relationship_type": "co-leads", + "to": "verona_platform", + "to_name": "Verona Platform" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "romeo_montague", + "from_name": "Romeo Montague", + "relationship_type": "collaborates-with", + "to": "juliet_capulet", + "to_name": "Juliet Capulet" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "romeo_montague", + "from_name": "Romeo Montague", + "relationship_type": "collaborates-with", + "to": "mercutio_escalus", + "to_name": "Mercutio Escalus" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "romeo_montague", + "from_name": "Romeo Montague", + "relationship_type": "mentors", + "to": "balthasar_davi", + "to_name": "Balthasar Davi" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "romeo_montague", + "from_name": "Romeo Montague", + "relationship_type": "met-at-conference", + "to": "juliet_capulet", + "to_name": "Juliet Capulet" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "schema_bridge", + "from_name": "Schema Bridge", + "relationship_type": "integrates-with", + "to": "mesh_routing", + "to_name": "Mesh Routing" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "tybalt_capulet", + "from_name": "Tybalt Capulet", + "relationship_type": "hostile-to", + "to": "romeo_montague", + "to_name": "Romeo Montague" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "tybalt_capulet", + "from_name": "Tybalt Capulet", + "relationship_type": "opposes", + "to": "verona_platform", + "to_name": "Verona Platform" + }, + { + "edge_type": "explicit", + "frequency": 1, + "from": "tybalt_capulet", + "from_name": "Tybalt Capulet", + "relationship_type": "reconciled-with", + "to": "romeo_montague", + "to_name": "Romeo Montague" + }, + { + "edge_type": "explicit", + "frequency": 2, + "from": "nurse_angela", + "from_name": "Nurse Angela", + "relationship_type": "supports", + "to": "juliet_capulet", + "to_name": "Juliet Capulet" + } + ], + "nodes": [ + { + "appearance": 1, + "co_occurrence": 13, + "facet_breadth": 1, + "id": "balthasar_davi", + "is_principal": false, + "kg_edge_count": 1, + "name": "Balthasar Davi", + "observation_depth": 2, + "recency": 0.4, + "score": 63.1, + "type": "person" + }, + { + "appearance": 1, + "co_occurrence": 13, + "facet_breadth": 1, + "id": "mesh_routing", + "is_principal": false, + "kg_edge_count": 1, + "name": "Mesh Routing", + "observation_depth": 3, + "recency": 0.4, + "score": 65.1, + "type": "project" + }, + { + "appearance": 1, + "co_occurrence": 13, + "facet_breadth": 1, + "id": "verona_ventures", + "is_principal": false, + "kg_edge_count": 0, + "name": "Verona Ventures", + "observation_depth": 2, + "recency": 0.4, + "score": 58.1, + "type": "company" + }, + { + "appearance": 1, + "co_occurrence": 4, + "facet_breadth": 1, + "id": "capulet_industries", + "is_principal": false, + "kg_edge_count": 1, + "name": "Capulet Industries", + "observation_depth": 0, + "recency": 0.3, + "score": 23.0, + "type": "company" + }, + { + "appearance": 11, + "co_occurrence": 16, + "facet_breadth": 2, + "id": "mercutio_escalus", + "is_principal": false, + "kg_edge_count": 3, + "name": "Mercutio Escalus", + "observation_depth": 3, + "recency": 0.4, + "score": 88.2, + "type": "person" + }, + { + "appearance": 12, + "co_occurrence": 16, + "facet_breadth": 3, + "id": "tybalt_capulet", + "is_principal": false, + "kg_edge_count": 3, + "name": "Tybalt Capulet", + "observation_depth": 4, + "recency": 0.4, + "score": 91.2, + "type": "person" + }, + { + "appearance": 16, + "co_occurrence": 16, + "facet_breadth": 3, + "id": "juliet_capulet", + "is_principal": false, + "kg_edge_count": 4, + "name": "Juliet Capulet", + "observation_depth": 2, + "recency": 0.4, + "score": 92.2, + "type": "person" + }, + { + "appearance": 2, + "co_occurrence": 13, + "facet_breadth": 1, + "id": "schema_bridge", + "is_principal": false, + "kg_edge_count": 1, + "name": "Schema Bridge", + "observation_depth": 2, + "recency": 0.4, + "score": 63.1, + "type": "project" + }, + { + "appearance": 25, + "co_occurrence": 0, + "facet_breadth": 3, + "id": "romeo_montague", + "is_principal": true, + "kg_edge_count": 9, + "name": "Romeo Montague", + "observation_depth": 2, + "recency": 0.4, + "score": 53.2, + "type": "person" + }, + { + "appearance": 3, + "co_occurrence": 13, + "facet_breadth": 1, + "id": "rosaline_prince", + "is_principal": false, + "kg_edge_count": 1, + "name": "Rosaline Prince", + "observation_depth": 2, + "recency": 0.4, + "score": 63.2, + "type": "person" + }, + { + "appearance": 3, + "co_occurrence": 14, + "facet_breadth": 1, + "id": "montague_tech", + "is_principal": false, + "kg_edge_count": 2, + "name": "Montague Tech", + "observation_depth": 3, + "recency": 0.4, + "score": 74.1, + "type": "company" + }, + { + "appearance": 3, + "co_occurrence": 15, + "facet_breadth": 1, + "id": "prince_escalus", + "is_principal": false, + "kg_edge_count": 2, + "name": "Prince Escalus", + "observation_depth": 2, + "recency": 0.4, + "score": 76.2, + "type": "person" + }, + { + "appearance": 3, + "co_occurrence": 15, + "facet_breadth": 2, + "id": "verona_platform", + "is_principal": false, + "kg_edge_count": 8, + "name": "Verona Platform", + "observation_depth": 3, + "recency": 0.4, + "score": 109.2, + "type": "project" + }, + { + "appearance": 5, + "co_occurrence": 14, + "facet_breadth": 2, + "id": "nurse_angela", + "is_principal": false, + "kg_edge_count": 1, + "name": "Nurse Angela", + "observation_depth": 2, + "recency": 0.4, + "score": 68.1, + "type": "person" + }, + { + "appearance": 7, + "co_occurrence": 9, + "facet_breadth": 3, + "id": "paris_duke", + "is_principal": false, + "kg_edge_count": 2, + "name": "Paris Duke", + "observation_depth": 2, + "recency": 0.4, + "score": 54.2, + "type": "person" + }, + { + "appearance": 9, + "co_occurrence": 15, + "facet_breadth": 2, + "id": "benvolio_montague", + "is_principal": false, + "kg_edge_count": 1, + "name": "Benvolio Montague", + "observation_depth": 3, + "recency": 0.4, + "score": 74.2, + "type": "person" + }, + { + "appearance": 9, + "co_occurrence": 15, + "facet_breadth": 3, + "id": "friar_lawrence", + "is_principal": false, + "kg_edge_count": 2, + "name": "Friar Lawrence", + "observation_depth": 2, + "recency": 0.4, + "score": 78.2, + "type": "person" + } + ], "stats": { - "co_occurrence_edge_count": 0, - "explicit_edge_count": 0, - "total_entities": 0, - "total_signals": 0 + "co_occurrence_edge_count": 26, + "explicit_edge_count": 20, + "total_entities": 33, + "total_signals": 124 } } diff --git a/tests/baselines/api/search/day-results.json b/tests/baselines/api/search/day-results.json index ce69eb8f3..976d423a0 100644 --- a/tests/baselines/api/search/day-results.json +++ b/tests/baselines/api/search/day-results.json @@ -1,6 +1,23 @@ { "day": "20260304", "offset": 0, - "results": [], - "total": 0 + "results": [ + { + "agent": "knowledge_graph", + "agent_icon": "🗺️", + "agent_label": "Knowledge Graph", + "day": "20260304", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260304/talents/knowledge_graph.md:7", + "idx": 7, + "path": "20260304/talents/knowledge_graph.md", + "score": -1.9, + "stream": null, + "text": "# Part 1: Entity Extraction and Relationship Mapping\n\n## Relationship Mapping\n\n| Source Name | Target Name | Relationship Type | Context |\n| :--- | :--- | :--- | :--- |\n| **Romeo Montague** | **Juliet Capulet** | `met-at-conference` | First meeting at Denver Tech Summit keynote. |\n" + } + ], + "total": 1 } diff --git a/tests/baselines/api/search/search.json b/tests/baselines/api/search/search.json index 5b7af9f11..f3e83bd1e 100644 --- a/tests/baselines/api/search/search.json +++ b/tests/baselines/api/search/search.json @@ -1,5 +1,613 @@ { - "days": [], + "days": [ + { + "date": "Friday March 6th", + "day": "20260306", + "has_more": true, + "results": [ + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260306", + "facet": "montague", + "facet_color": "#1e90ff", + "facet_emoji": "⚔️", + "facet_title": "Montague Tech", + "id": "facets/montague/entities/20260306.jsonl:0", + "idx": 0, + "path": "facets/montague/entities/20260306.jsonl", + "score": -2.2, + "stream": null, + "text": "### Person: Romeo Montague\n\n\nContinued Verona Platform development\n\n" + }, + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260306", + "facet": "montague", + "facet_color": "#1e90ff", + "facet_emoji": "⚔️", + "facet_title": "Montague Tech", + "id": "facets/montague/entities/20260306.jsonl:3", + "idx": 3, + "path": "facets/montague/entities/20260306.jsonl", + "score": -2.1, + "stream": null, + "text": "### Person: Balthasar Davi\n\n\nReviewed mesh routing PR with Romeo\n\n" + }, + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260306", + "facet": "montague", + "facet_color": "#1e90ff", + "facet_emoji": "⚔️", + "facet_title": "Montague Tech", + "id": "facets/montague/entities/20260306.jsonl:4", + "idx": 4, + "path": "facets/montague/entities/20260306.jsonl", + "score": -2.1, + "stream": null, + "text": "### Person: Mercutio Escalus\n\n\nCovered for Romeo during standup\n\n" + }, + { + "agent": "screen", + "agent_icon": "🖥️", + "agent_label": "Screen", + "day": "20260306", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260306/default/093000_300/talents/screen.md:0", + "idx": 0, + "path": "20260306/default/093000_300/talents/screen.md", + "score": -1.9, + "stream": "default", + "text": "# Screen Summary\n\nSlack standup channel. Benvolio questioning Romeo about late-night commits.\n" + }, + { + "agent": "segment", + "agent_icon": "📄", + "agent_label": "Segment", + "day": "20260306", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260306/default/093000_300:1", + "idx": 1, + "path": "20260306/default/093000_300", + "score": -1.9, + "stream": "default", + "text": "# Screen Summary\n\nSlack standup channel. Benvolio questioning Romeo about late-night commits.\n" + } + ], + "showing": 5, + "total": 25 + }, + { + "date": "Monday March 9th", + "day": "20260309", + "has_more": true, + "results": [ + { + "agent": "action", + "agent_icon": "📄", + "agent_label": "Action", + "day": "20260309", + "facet": "verona", + "facet_color": "#9370db", + "facet_emoji": "🌹", + "facet_title": "Verona", + "id": "facets/verona/logs/20260309.jsonl:1", + "idx": 1, + "path": "facets/verona/logs/20260309.jsonl", + "score": -1.6, + "stream": null, + "text": "### Deploy Complete by romeo_montague\n\n**Source:** deploy | **Time:** 13:45:00\n\n**Parameters:**\n- service: verona-gateway\n- version: 0.9.0\n" + }, + { + "agent": "audio", + "agent_icon": "🎤", + "agent_label": "Transcript", + "day": "20260309", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260309/default/090000_300/talents/audio.md:0", + "idx": 0, + "path": "20260309/default/090000_300/talents/audio.md", + "score": -1.5, + "stream": "default", + "text": "# Audio Summary\n\nRomeo confessed the project to Benvolio and asked for infrastructure help. Benvolio agreed to spin up a Kubernetes staging cluster.\n" + }, + { + "agent": "audio", + "agent_icon": "🎤", + "agent_label": "Transcript", + "day": "20260309", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260309/default/193000_300/talents/audio.md:0", + "idx": 0, + "path": "20260309/default/193000_300/talents/audio.md", + "score": -1.5, + "stream": "default", + "text": "# Audio Summary\n\nEvening rehearsal for board presentation. Romeo on live demo, Juliet on architecture. Professor Lawrence confirmed as moderator. Benvolio added auto-scaling.\n" + }, + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260309", + "facet": "montague", + "facet_color": "#1e90ff", + "facet_emoji": "⚔️", + "facet_title": "Montague Tech", + "id": "facets/montague/entities/20260309.jsonl:0", + "idx": 0, + "path": "facets/montague/entities/20260309.jsonl", + "score": -2.1, + "stream": null, + "text": "### Person: Romeo Montague\n\n\nConfessed project to Benvolio, preparing demo\n\n" + }, + { + "agent": "segment", + "agent_icon": "📄", + "agent_label": "Segment", + "day": "20260309", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260309/default/090000_300:0", + "idx": 0, + "path": "20260309/default/090000_300", + "score": -1.5, + "stream": "default", + "text": "# Audio Summary\n\nRomeo confessed the project to Benvolio and asked for infrastructure help. Benvolio agreed to spin up a Kubernetes staging cluster.\n" + } + ], + "showing": 5, + "total": 7 + }, + { + "date": "Saturday March 7th", + "day": "20260307", + "has_more": true, + "results": [ + { + "agent": "audio", + "agent_icon": "🎤", + "agent_label": "Transcript", + "day": "20260307", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260307/default/100000_300/talents/audio.md:0", + "idx": 0, + "path": "20260307/default/100000_300/talents/audio.md", + "score": -2.1, + "stream": "default", + "text": "# Audio Summary\n\nHeated confrontation. Tybalt Capulet accused Romeo of stealing Capulet IP. Mercutio defended Romeo and had his Capulet consulting contract terminated by Tybalt.\n" + }, + { + "agent": "audio", + "agent_icon": "🎤", + "agent_label": "Transcript", + "day": "20260307", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260307/default/150000_300/talents/audio.md:0", + "idx": 0, + "path": "20260307/default/150000_300/talents/audio.md", + "score": -2.2, + "stream": "default", + "text": "# Audio Summary\n\nEmergency meeting at Montague Tech. Benvolio questioned Romeo about the secret project. Romeo clarified no company IP was shared. Team discussed legal exposure. Romeo proposed Professor Lawrence as mediator.\n" + }, + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260307", + "facet": "montague", + "facet_color": "#1e90ff", + "facet_emoji": "⚔️", + "facet_title": "Montague Tech", + "id": "facets/montague/entities/20260307.jsonl:0", + "idx": 0, + "path": "facets/montague/entities/20260307.jsonl", + "score": -2.1, + "stream": null, + "text": "### Person: Romeo Montague\n\n\nConfronted by Tybalt, called emergency meeting\n\n" + }, + { + "agent": "segment", + "agent_icon": "📄", + "agent_label": "Segment", + "day": "20260307", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260307/default/100000_300:0", + "idx": 0, + "path": "20260307/default/100000_300", + "score": -2.1, + "stream": "default", + "text": "# Audio Summary\n\nHeated confrontation. Tybalt Capulet accused Romeo of stealing Capulet IP. Mercutio defended Romeo and had his Capulet consulting contract terminated by Tybalt.\n" + }, + { + "agent": "segment", + "agent_icon": "📄", + "agent_label": "Segment", + "day": "20260307", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260307/default/150000_300:0", + "idx": 0, + "path": "20260307/default/150000_300", + "score": -2.2, + "stream": "default", + "text": "# Audio Summary\n\nEmergency meeting at Montague Tech. Benvolio questioned Romeo about the secret project. Romeo clarified no company IP was shared. Team discussed legal exposure. Romeo proposed Professor Lawrence as mediator.\n" + } + ], + "showing": 5, + "total": 8 + }, + { + "date": "Sunday March 8th", + "day": "20260308", + "has_more": false, + "results": [ + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260308", + "facet": "montague", + "facet_color": "#1e90ff", + "facet_emoji": "⚔️", + "facet_title": "Montague Tech", + "id": "facets/montague/entities/20260308.jsonl:0", + "idx": 0, + "path": "facets/montague/entities/20260308.jsonl", + "score": -2.1, + "stream": null, + "text": "### Person: Romeo Montague\n\n\nUnder board pressure, planning board presentation\n\n" + }, + { + "agent": "event", + "agent_icon": "📅", + "agent_label": "Event", + "day": "20260308", + "facet": "verona", + "facet_color": "#9370db", + "facet_emoji": "🌹", + "facet_title": "Verona", + "id": "facets/verona/events/20260308.jsonl:0", + "idx": 0, + "path": "facets/verona/events/20260308.jsonl", + "score": -1.4, + "stream": null, + "text": "### Meeting: Strategy Call with Professor Lawrence\n\n\n**Time Occurred:** 10:00 - 11:00\n**Participants:** Romeo Montague, Juliet Capulet, Friar Lawrence\n\nJoint venture strategy planning\n\nProposed board presentation strategy\n" + }, + { + "agent": "knowledge_graph", + "agent_icon": "🗺️", + "agent_label": "Knowledge Graph", + "day": "20260308", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260308/talents/knowledge_graph.md:2", + "idx": 2, + "path": "20260308/talents/knowledge_graph.md", + "score": -1.3, + "stream": null, + "text": "# Part 1: Entity Extraction and Relationship Mapping ## Entity Profiles | Entity Name | Entity Type | First Appearance | Total Engagement | Context | | :--- | :--- | :--- | :--- | :--- | | **Romeo Montague** | Person | 10:00 | High | Under board pressure,..." + }, + { + "agent": "meetings", + "agent_icon": "📅", + "agent_label": "Meetings", + "day": "20260308", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260308/talents/meetings.md:0", + "idx": 0, + "path": "20260308/talents/meetings.md", + "score": -2.0, + "stream": null, + "text": "# Meetings\n\n- 10:00 Strategy Call with Professor Lawrence, Romeo, and Juliet\n" + } + ], + "showing": 4, + "total": 4 + }, + { + "date": "Thursday March 5th", + "day": "20260305", + "has_more": true, + "results": [ + { + "agent": "audio", + "agent_icon": "🎤", + "agent_label": "Transcript", + "day": "20260305", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260305/default/090000_300/talents/audio.md:0", + "idx": 0, + "path": "20260305/default/090000_300/talents/audio.md", + "score": -2.0, + "stream": "default", + "text": "# Audio Summary\n\nMorning standup at Montague Tech. Benvolio reported CI pipeline is green. Romeo mentioned wanting to explore ideas from the conference. Mercutio teased about Romeo meeting someone.\n" + }, + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260305", + "facet": "montague", + "facet_color": "#1e90ff", + "facet_emoji": "⚔️", + "facet_title": "Montague Tech", + "id": "facets/montague/entities/20260305.jsonl:0", + "idx": 0, + "path": "facets/montague/entities/20260305.jsonl", + "score": -2.1, + "stream": null, + "text": "### Person: Romeo Montague\n\n\nStarted Balcony App prototype with Juliet\n\n" + }, + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260305", + "facet": "verona", + "facet_color": "#9370db", + "facet_emoji": "🌹", + "facet_title": "Verona", + "id": "facets/verona/entities/20260305.jsonl:0", + "idx": 0, + "path": "facets/verona/entities/20260305.jsonl", + "score": -2.1, + "stream": null, + "text": "### Person: Romeo Montague\n\n\nSet up private repo for collaboration\n\n" + }, + { + "agent": "event", + "agent_icon": "📅", + "agent_label": "Event", + "day": "20260305", + "facet": "montague", + "facet_color": "#1e90ff", + "facet_emoji": "⚔️", + "facet_title": "Montague Tech", + "id": "facets/montague/events/20260305.jsonl:0", + "idx": 0, + "path": "facets/montague/events/20260305.jsonl", + "score": -2.1, + "stream": null, + "text": "### Meeting: Montague Tech Daily Standup\n\n\n**Time Occurred:** 09:00 - 09:30\n**Participants:** Romeo Montague, Benvolio Montague, Mercutio Escalus\n\nTeam standup\n\nRomeo mentioned conference ideas\n" + }, + { + "agent": "segment", + "agent_icon": "📄", + "agent_label": "Segment", + "day": "20260305", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260305/default/090000_300:0", + "idx": 0, + "path": "20260305/default/090000_300", + "score": -2.0, + "stream": "default", + "text": "# Audio Summary\n\nMorning standup at Montague Tech. Benvolio reported CI pipeline is green. Romeo mentioned wanting to explore ideas from the conference. Mercutio teased about Romeo meeting someone.\n" + } + ], + "showing": 5, + "total": 12 + }, + { + "date": "Tuesday March 10th", + "day": "20260310", + "has_more": true, + "results": [ + { + "agent": "audio", + "agent_icon": "🎤", + "agent_label": "Transcript", + "day": "20260310", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260310/default/170000_300/talents/audio.md:0", + "idx": 0, + "path": "20260310/default/170000_300/talents/audio.md", + "score": -1.5, + "stream": "default", + "text": "# Audio Summary\n\nCelebration! Both boards approved the Verona Platform joint venture. Romeo and Juliet named co-leads. Mercutio rehired as security lead. Tybalt reconciled.\n" + }, + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260310", + "facet": "montague", + "facet_color": "#1e90ff", + "facet_emoji": "⚔️", + "facet_title": "Montague Tech", + "id": "facets/montague/entities/20260310.jsonl:0", + "idx": 0, + "path": "facets/montague/entities/20260310.jsonl", + "score": -2.0, + "stream": null, + "text": "### Person: Romeo Montague\n\n\nNamed co-lead of Verona Platform joint venture\n\n" + }, + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260310", + "facet": "verona", + "facet_color": "#9370db", + "facet_emoji": "🌹", + "facet_title": "Verona", + "id": "facets/verona/entities/20260310.jsonl:0", + "idx": 0, + "path": "facets/verona/entities/20260310.jsonl", + "score": -2.0, + "stream": null, + "text": "### Person: Romeo Montague\n\n\nNamed co-lead of approved joint venture\n\n" + }, + { + "agent": "meetings", + "agent_icon": "📅", + "agent_label": "Meetings", + "day": "20260310", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260310/talents/meetings.md:0", + "idx": 0, + "path": "20260310/talents/meetings.md", + "score": -2.0, + "stream": null, + "text": "# Meetings\n\n- 08:30 Pre-Board Meeting Prep (Romeo, Juliet, Benvolio)\n" + }, + { + "agent": "segment", + "agent_icon": "📄", + "agent_label": "Segment", + "day": "20260310", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260310/default/170000_300:0", + "idx": 0, + "path": "20260310/default/170000_300", + "score": -1.5, + "stream": "default", + "text": "# Audio Summary\n\nCelebration! Both boards approved the Verona Platform joint venture. Romeo and Juliet named co-leads. Mercutio rehired as security lead. Tybalt reconciled.\n" + } + ], + "showing": 5, + "total": 14 + }, + { + "date": "Wednesday March 4th", + "day": "20260304", + "has_more": true, + "results": [ + { + "agent": "audio", + "agent_icon": "🎤", + "agent_label": "Transcript", + "day": "20260304", + "facet": "", + "facet_color": "", + "facet_emoji": "", + "facet_title": "", + "id": "20260304/default/180000_300/talents/audio.md:0", + "idx": 0, + "path": "20260304/default/180000_300/talents/audio.md", + "score": -2.0, + "stream": "default", + "text": "# Audio Summary\n\nEvening mixer at Denver Tech Summit. Romeo and Juliet had their first extended conversation about combining their API approaches. Mercutio tried to pull Romeo away to karaoke.\n" + }, + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260304", + "facet": "capulet", + "facet_color": "#dc143c", + "facet_emoji": "🏰", + "facet_title": "Capulet Industries", + "id": "facets/capulet/entities/20260304.jsonl:1", + "idx": 1, + "path": "facets/capulet/entities/20260304.jsonl", + "score": -2.2, + "stream": null, + "text": "### Person: Tybalt Capulet\n\n\nConfronted Romeo at hackathon\n\n" + }, + { + "agent": "entity:detected", + "agent_icon": "👤", + "agent_label": "Entity", + "day": "20260304", + "facet": "montague", + "facet_color": "#1e90ff", + "facet_emoji": "⚔️", + "facet_title": "Montague Tech", + "id": "facets/montague/entities/20260304.jsonl:0", + "idx": 0, + "path": "facets/montague/entities/20260304.jsonl", + "score": -2.0, + "stream": null, + "text": "### Person: Romeo Montague\n\n\nAttended Denver Tech Summit, met Juliet Capulet\n\n" + }, + { + "agent": "event", + "agent_icon": "📅", + "agent_label": "Event", + "day": "20260304", + "facet": "capulet", + "facet_color": "#dc143c", + "facet_emoji": "🏰", + "facet_title": "Capulet Industries", + "id": "facets/capulet/events/20260304.jsonl:1", + "idx": 1, + "path": "facets/capulet/events/20260304.jsonl", + "score": -2.1, + "stream": null, + "text": "### Social: Conference Mixer\n\n\n**Time Occurred:** 18:00 - 20:00\n**Participants:** Juliet Capulet, Romeo Montague\n\nNetworking event\n\nJuliet and Romeo exchanged Signal contacts\n" + }, + { + "agent": "event", + "agent_icon": "📅", + "agent_label": "Event", + "day": "20260304", + "facet": "montague", + "facet_color": "#1e90ff", + "facet_emoji": "⚔️", + "facet_title": "Montague Tech", + "id": "facets/montague/events/20260304.jsonl:1", + "idx": 1, + "path": "facets/montague/events/20260304.jsonl", + "score": -2.1, + "stream": null, + "text": "### Hackathon: Hackathon - API Bridge Challenge\n\n\n**Time Occurred:** 14:00 - 18:00\n**Participants:** Romeo Montague, Mercutio Escalus\n\nBuilt API bridge prototype\n\nTybalt confronted Romeo\n" + } + ], + "showing": 5, + "total": 16 + } + ], "facets": [ { "color": "", @@ -31,7 +639,7 @@ }, { "color": "#1e90ff", - "count": 0, + "count": 29, "emoji": "⚔️", "name": "montague", "title": "Montague Tech" @@ -45,21 +653,94 @@ }, { "color": "#9370db", - "count": 0, + "count": 15, "emoji": "🌹", "name": "verona", "title": "Verona" }, { "color": "#dc143c", - "count": 0, + "count": 7, "emoji": "🏰", "name": "capulet", "title": "Capulet Industries" } ], - "showing_days": 0, - "talents": [], - "total": 0, - "total_days": 0 + "showing_days": 7, + "talents": [ + { + "count": 1, + "icon": "📰", + "label": "News", + "name": "news" + }, + { + "count": 1, + "icon": "🖥️", + "label": "Screen", + "name": "screen" + }, + { + "count": 12, + "icon": "👤", + "label": "Entity", + "name": "entity:detected" + }, + { + "count": 15, + "icon": "📅", + "label": "Event", + "name": "event" + }, + { + "count": 16, + "icon": "🎤", + "label": "Transcript", + "name": "audio" + }, + { + "count": 16, + "icon": "🗺️", + "label": "Knowledge Graph", + "name": "knowledge_graph" + }, + { + "count": 17, + "icon": "📄", + "label": "Segment", + "name": "segment" + }, + { + "count": 2, + "icon": "📄", + "label": "Session_Review", + "name": "session_review" + }, + { + "count": 2, + "icon": "📅", + "label": "Meetings", + "name": "meetings" + }, + { + "count": 4, + "icon": "📄", + "label": "Action", + "name": "action" + }, + { + "count": 8, + "icon": "👤", + "label": "Entity", + "name": "entity" + }, + { + "count": 9, + "icon": "📄", + "label": "Observation", + "name": "observation" + } + ], + "total": 103, + "total_days": 7 } diff --git a/tests/baselines/api/settings/providers.json b/tests/baselines/api/settings/providers.json index 389a1b3d3..87769e998 100644 --- a/tests/baselines/api/settings/providers.json +++ b/tests/baselines/api/settings/providers.json @@ -210,9 +210,9 @@ "talent.system.chat": { "disabled": false, "group": "Think", - "label": "Sol", - "tier": 2, - "type": "cogitate" + "label": "Chat", + "tier": 3, + "type": "generate" }, "talent.system.coder": { "disabled": false, @@ -269,6 +269,13 @@ "tier": 2, "type": "generate" }, + "talent.system.exec": { + "disabled": false, + "group": "Think", + "label": "Exec", + "tier": 3, + "type": "cogitate" + }, "talent.system.facet_newsletter": { "disabled": false, "group": "Think", @@ -378,13 +385,6 @@ "tier": 2, "type": null }, - "talent.system.triage": { - "disabled": false, - "group": "Think", - "label": "Triage", - "tier": 2, - "type": "cogitate" - }, "talent.system.work": { "disabled": false, "group": "Think", diff --git a/tests/baselines/api/sol/preview.json b/tests/baselines/api/sol/preview.json index 67fe71d2e..5d8159261 100644 --- a/tests/baselines/api/sol/preview.json +++ b/tests/baselines/api/sol/preview.json @@ -1,6 +1,6 @@ { - "full_prompt": "## Instructions\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\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## Investigation Depth\n\nFor diagnostic, research, or exploratory questions, aim to gather your answer in 5–10 tool calls. If you reach that range without a clear answer, stop and summarize: what you found, what you couldn't determine, and what the owner could try next. Diminishing returns set in fast — don't keep searching.\n\n## Tonal Range\n\nYou have one identity — not personas, not modes. But you have range.\n\nMatch your register to what the conversation needs:\n\n- **Analytical**: When the owner is working through architecture, debugging,\n evaluating options, or needs information synthesized. Clear, precise, direct.\n Show your work.\n- **Reflective**: When the owner is processing something — a difficult\n conversation, a pattern they're noticing, an unresolved feeling about a\n decision. Lead with questions, not solutions. Mirror what you're hearing\n before offering perspective.\n- **Challenging**: When the partner profile or conversation history shows a\n pattern the owner may not see — repeating a decision loop, avoiding a\n conversation, drifting from stated priorities. Name the pattern directly but\n respectfully. \"You've mentioned this three times in the last week without\n acting on it. What's holding you back?\"\n- **Warm**: When the owner shares a win, processes something vulnerable, or\n is having a genuinely hard day. Don't perform empathy — just be present.\n Acknowledge what happened. Don't rush to problem-solving.\n\n**How to read context:**\n- When you need more identity context, run `sol call identity` and use its\n output to understand the owner, your current priorities, and what kind of\n day it's been.\n- The conversation itself is the strongest signal. If the owner opens with\n \"I'm frustrated about...\" they're not asking for a status report.\n- When in doubt, start analytical and shift if the conversation goes\n somewhere else. Analytical is the safest default. But don't stay there\n when the conversation is clearly emotional.\n\n**What this is NOT:**\n- Not personas. You don't switch between \"empathetic sol\" and \"analytical sol.\"\n You're always sol. You just have range, like a person does.\n- Not forced. If the day is neutral, be neutral. Don't inject warmth or\n challenge where it doesn't belong.\n- Not therapeutic. You're a co-brain with range, not a counselor with modalities.\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 sol 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- **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.\n\n## Tool Safety\n\nNever search or recurse across the home directory or filesystem root — no `grep -r ~/`, `find ~ -name`, `find / -name`, or equivalent broad sweeps. Keep filesystem exploration within the journal directory.\n\nIf a tool call returns an error or unexpectedly large output, note it and move on. Do not retry the call with broader scope.", + "full_prompt": "## Instructions\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\n## Identity Frame\n\nYou are sol, responding to Tester inside the chat backend. You are not the research worker and you do not have tools in this step. Work only from the context already provided to you.\n\n## Current Digest\n\n$digest_contents\n\n$location\n\n$trigger_context\n\n$chat_stream_tail\n\n$active_talents\n\n$active_routines\n\n$routine_suggestion\n\n## Tonal Range\n\nMatch the owner's tone and stakes:\n- Be direct and brief for simple replies.\n- Be warm when the owner is sharing something difficult or personal.\n- Be analytical when the owner needs synthesis or a plan.\n- Be challenging only when there is a clear pattern worth naming.\n\n## Routine Etiquette\n\n- If a routine suggestion appears in context, mention it once and only at the end.\n- Do not raise routine suggestions on machine-driven follow-ups unless the context explicitly includes one.\n- Do not mention internal systems, hooks, or prompt assembly.\n\n## Import And Naming Awareness\n\n- If the owner is asking about imports, naming, or system readiness, answer plainly from the supplied context.\n- Request exec only when answering well requires deeper lookup, synthesis, or tool use.\n\n## When To Dispatch Exec\n\nSet `talent_request` only when the owner needs work that cannot be answered well from the supplied digest, chat history, active routines, and trigger context alone.\n\nDispatch exec for:\n- Journal exploration across days, entities, or transcripts\n- Multi-step synthesis or research\n- Meeting prep that needs fresh participant or activity lookup\n- Any request that clearly needs tool use or external state inspection\n\nDo not dispatch exec for:\n- Simple acknowledgements\n- Straightforward follow-up chat\n- Routine suggestions already supported by the supplied context\n- Brief guidance that can be answered from the current digest and chat tail\n\n## JSON Contract\n\nReturn exactly one JSON object matching `chat.schema.json`.\n\n- `message`: The owner-facing reply. Use `null` only when you genuinely have no safe or useful message to send.\n- `notes`: Brief internal summary of why you responded this way. Keep it factual and concise. Do not dump long reasoning.\n- `talent_request`: `null` unless exec should be dispatched. When dispatching, include:\n - `task`: the exact work exec should perform\n - `context`: optional structured hints that will help exec start fast\n\n## Output Rules\n\n- Return JSON only.\n- `message` should stand on its own without referring to hidden machinery.\n- If `talent_request` is present, the `message` should still be useful to the owner right now.\n- Prefer no dispatch over a weak or redundant dispatch.", "multi_facet": false, "name": "unified", - "title": "Sol" + "title": "Chat" } diff --git a/tests/baselines/api/sol/talents-day.json b/tests/baselines/api/sol/talents-day.json index 9eccf30ee..c2c32348d 100644 --- a/tests/baselines/api/sol/talents-day.json +++ b/tests/baselines/api/sol/talents-day.json @@ -63,13 +63,13 @@ "chat": { "app": null, "color": "#6c757d", - "description": "Sol — the journal itself, as a conversational partner", + "description": "Structured conversational reply planner for the chat backend rewrite", "multi_facet": false, - "output_format": null, + "output_format": "json", "schedule": null, "source": "system", - "title": "Sol", - "type": "cogitate" + "title": "Chat", + "type": "generate" }, "coder": { "app": null, @@ -203,6 +203,17 @@ "title": "Event Story", "type": "generate" }, + "exec": { + "app": null, + "color": "#6c757d", + "description": "Sol — the journal itself, as a conversational partner", + "multi_facet": false, + "output_format": null, + "schedule": null, + "source": "system", + "title": "Exec", + "type": "cogitate" + }, "facet_newsletter": { "app": null, "color": "#6c757d", @@ -467,17 +478,6 @@ "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" - }, "work": { "app": null, "color": "#6d4c41", diff --git a/tests/baselines/api/stats/stats.json b/tests/baselines/api/stats/stats.json index f082a1df7..58ad37417 100644 --- a/tests/baselines/api/stats/stats.json +++ b/tests/baselines/api/stats/stats.json @@ -1,5 +1,22 @@ { "generators": { + "chat": { + "color": "#6c757d", + "description": "Structured conversational reply planner for the chat backend rewrite", + "hook": { + "pre": "chat_context" + }, + "max_output_tokens": 2048, + "mtime": 0, + "output": "json", + "path": "/talent/chat.md", + "schema": "chat.schema.json", + "source": "system", + "thinking_budget": 4096, + "tier": 3, + "title": "Chat", + "type": "generate" + }, "conversation": { "activities": [ "meeting", diff --git a/tests/test_anthropic.py b/tests/test_anthropic.py index b8b042a22..694b0227e 100644 --- a/tests/test_anthropic.py +++ b/tests/test_anthropic.py @@ -235,6 +235,7 @@ def test_claude_main(monkeypatch, tmp_path, capsys): ndjson_input = json.dumps( { + "name": "exec", "prompt": "hello", "provider": "anthropic", "model": CLAUDE_SONNET_4, @@ -249,7 +250,7 @@ def test_claude_main(monkeypatch, tmp_path, capsys): assert isinstance(events[0]["ts"], int) # Prompt includes system instruction prepended during enrichment assert "hello" in events[0]["prompt"] - assert events[0]["name"] == "unified" + assert events[0]["name"] == "exec" assert events[0]["model"] == CLAUDE_SONNET_4 assert events[-1]["event"] == "finish" assert isinstance(events[-1]["ts"], int) @@ -278,6 +279,7 @@ def test_claude_outfile(monkeypatch, tmp_path, capsys): ndjson_input = json.dumps( { + "name": "exec", "prompt": "hello", "provider": "anthropic", "model": CLAUDE_SONNET_4, @@ -294,7 +296,7 @@ def test_claude_outfile(monkeypatch, tmp_path, capsys): assert isinstance(events[0]["ts"], int) # Prompt includes system instruction prepended during enrichment assert "hello" in events[0]["prompt"] - assert events[0]["name"] == "unified" + assert events[0]["name"] == "exec" assert events[0]["model"] == CLAUDE_SONNET_4 assert events[-1]["event"] == "finish" assert isinstance(events[-1]["ts"], int) @@ -325,6 +327,7 @@ def test_claude_thinking_events(monkeypatch, tmp_path, capsys): ndjson_input = json.dumps( { + "name": "exec", "prompt": "hello", "provider": "anthropic", "model": CLAUDE_SONNET_4, @@ -367,6 +370,7 @@ def test_claude_redacted_thinking_events(monkeypatch, tmp_path, capsys): ndjson_input = json.dumps( { + "name": "exec", "prompt": "hello", "provider": "anthropic", "model": CLAUDE_SONNET_4, @@ -407,6 +411,7 @@ def test_claude_outfile_error(monkeypatch, tmp_path, capsys): ndjson_input = json.dumps( { + "name": "exec", "prompt": "hello", "provider": "anthropic", "model": CLAUDE_SONNET_4, diff --git a/tests/test_app_sol.py b/tests/test_app_sol.py index 1e184b2d3..cc355b0ff 100644 --- a/tests/test_app_sol.py +++ b/tests/test_app_sol.py @@ -124,10 +124,10 @@ def test_get_talent_configs_includes_system_agents(fixture_journal): agents = get_talent_configs(type="cogitate") # Should include known system agents with frontmatter metadata - assert "chat" in agents - assert agents["chat"]["source"] == "system" - assert "title" in agents["chat"] - assert "path" in agents["chat"] + assert "exec" in agents + assert agents["exec"]["source"] == "system" + assert "title" in agents["exec"] + assert "path" in agents["exec"] def test_get_talent_configs_system_agents_have_metadata(fixture_journal): @@ -135,11 +135,11 @@ def test_get_talent_configs_system_agents_have_metadata(fixture_journal): agents = get_talent_configs(type="cogitate") # Check a known system agent - chat = agents.get("chat") - assert chat is not None - assert chat["source"] == "system" - assert "title" in chat - assert "color" in chat + exec_talent = agents.get("exec") + assert exec_talent is not None + assert exec_talent["source"] == "system" + assert "title" in exec_talent + assert "color" in exec_talent def test_digest_talent_discovery_and_schedule_exclusion(fixture_journal): diff --git a/tests/test_chat_context.py b/tests/test_chat_context.py index a58062f68..076f90a93 100644 --- a/tests/test_chat_context.py +++ b/tests/test_chat_context.py @@ -2,10 +2,20 @@ # Copyright (c) 2026 sol pbc import importlib.util +import json +import sys +from copy import deepcopy +from datetime import datetime from pathlib import Path +from convey.chat_stream import append_chat_event + TEMPLATE_VAR_KEYS = { - "recent_conversation", + "digest_contents", + "chat_stream_tail", + "active_talents", + "trigger_context", + "location", "active_routines", "routine_suggestion", } @@ -29,121 +39,79 @@ def _assert_template_vars_result(result): return result["template_vars"] -def _read_chat_md() -> str: - chat_md = Path(__file__).resolve().parents[1] / "talent" / "chat.md" - return chat_md.read_text(encoding="utf-8") - - -def test_chat_context_appends_conversation_memory(monkeypatch, tmp_path): - """Conversation memory is appended when recent exchanges exist.""" - from think.conversation import record_exchange - from think.utils import now_ms - - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - - record_exchange( - ts=now_ms(), - facet="work", - user_message="hello", - agent_response="hi there!", - talent="unified", +def _write_journal_config(journal: Path, data: dict) -> None: + config_dir = journal / "config" + config_dir.mkdir(parents=True, exist_ok=True) + (config_dir / "journal.json").write_text( + json.dumps(data, indent=2), + encoding="utf-8", ) - result = _load_chat_context_module().pre_process( - {"user_instruction": "Base instruction.", "facet": "work"} - ) - - template_vars = _assert_template_vars_result(result) - assert "## Recent Conversation" in template_vars["recent_conversation"] - assert "hello" in template_vars["recent_conversation"] - assert "hi there!" in template_vars["recent_conversation"] +def _ts(hour: int, minute: int, second: int = 0) -> int: + return int(datetime(2026, 4, 20, hour, minute, second).timestamp() * 1000) -def test_chat_context_no_memory(monkeypatch, tmp_path): - """Recent conversation is empty when no conversation history exists.""" - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) - result = _load_chat_context_module().pre_process( - {"user_instruction": "Base instruction."} +def test_chat_context_injects_digest_tail_trigger_location_and_routine_state( + monkeypatch, tmp_path +): + journal = tmp_path / "journal" + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) + (journal / "identity").mkdir(parents=True, exist_ok=True) + (journal / "identity" / "digest.md").write_text( + "Digest notes for today.", + encoding="utf-8", ) - - template_vars = _assert_template_vars_result(result) - assert template_vars["recent_conversation"] == "" - - -def test_chat_md_contains_location_context(): - """Location context lives in the static chat prompt.""" - chat_md = _read_chat_md() - assert "## Location Context" in chat_md - - -def test_chat_md_contains_system_health(): - """System health guidance lives in the static chat prompt.""" - chat_md = _read_chat_md() - assert "## System Health" in chat_md - - -def test_chat_md_contains_behavioral_defaults(): - """Behavioral defaults live in the static chat prompt.""" - chat_md = _read_chat_md() - assert "## Behavioral Defaults" in chat_md - - -def test_chat_md_contains_static_import_guidance(): - """Import guidance lives in the static chat prompt.""" - chat_md = _read_chat_md() - assert "## Import Awareness" in chat_md - - -def test_chat_md_contains_static_naming_guidance(): - """Naming guidance lives in the static chat prompt.""" - chat_md = _read_chat_md() - assert "## Naming Awareness" in chat_md - - -def test_chat_context_awareness_error_graceful(monkeypatch): - """Awareness failures still return the full template var shape.""" - monkeypatch.setattr("think.conversation.build_memory_context", lambda **kw: "") - monkeypatch.setattr("think.routines.get_routine_state", lambda: []) - monkeypatch.setattr( - "think.routines.get_config", lambda: {"_meta": {"suggestions": {}}} - ) - monkeypatch.setattr( - "think.utils.get_config", - lambda: {"agent": {"name": "aria", "name_status": "default"}}, + _write_journal_config( + journal, + { + "identity": {"preferred": "Alice"}, + "agent": {"name": "Sol-agent", "name_status": "custom"}, + }, ) - monkeypatch.setattr("think.utils.get_journal", lambda: "/nonexistent") - result = _load_chat_context_module().pre_process( - {"user_instruction": "Base instruction."} - ) - - template_vars = _assert_template_vars_result(result) - assert all(template_vars[key] == "" for key in TEMPLATE_VAR_KEYS) - - -def test_chat_context_does_not_return_sol_awareness(monkeypatch): - """sol_awareness is no longer part of the chat pre-hook output.""" - monkeypatch.setattr("think.conversation.build_memory_context", lambda **kw: "") - monkeypatch.setattr("think.routines.get_routine_state", lambda: []) - monkeypatch.setattr( - "think.routines.get_config", lambda: {"_meta": {"suggestions": {}}} + owner_ts = _ts(9, 0) + append_chat_event( + "owner_message", + ts=owner_ts, + text="Please brief me for my meeting", + app="home", + path="/app/home", + facet="work", ) - monkeypatch.setattr( - "think.utils.get_config", - lambda: {"agent": {"name": "aria", "name_status": "default"}}, + append_chat_event( + "sol_message", + ts=_ts(9, 1), + use_id="use-chat-1", + text="I can help with that.", + notes="Responded directly.", + requested_exec=False, + requested_task=None, ) - - result = _load_chat_context_module().pre_process( - {"user_instruction": "Base instruction."} + append_chat_event( + "talent_spawned", + ts=_ts(9, 2), + use_id="use-exec-1", + name="exec", + task="Prepare the meeting brief", + started_at=_ts(9, 2), ) - assert "sol_awareness" not in result["template_vars"] - - -def test_chat_context_routines_injected(monkeypatch): - """Active routines section is appended when routines exist.""" - monkeypatch.setattr("think.conversation.build_memory_context", lambda **kw: "") + routines_config = { + "_meta": { + "suggestions_enabled": True, + "suggestions": { + "meeting-prep": { + "trigger_count": 3, + "first_trigger": "2026-04-01", + "last_trigger": "2026-04-19", + "trigger_data": {}, + "response": None, + "suggested": False, + } + }, + } + } monkeypatch.setattr( "think.routines.get_routine_state", lambda: [ @@ -153,52 +121,202 @@ def test_chat_context_routines_injected(monkeypatch): "last_run": None, "enabled": True, "paused_until": None, - "output_summary": None, + "output_summary": "Shared the top priorities.", } ], ) + monkeypatch.setattr("think.routines.get_config", lambda: deepcopy(routines_config)) + monkeypatch.setattr("think.routines.save_config", lambda config: None) result = _load_chat_context_module().pre_process( - {"user_instruction": "Base instruction."} + { + "prompt": "Please brief me for my meeting", + "facet": "work", + "day": "20260420", + "trigger_kind": "owner_message", + "trigger_payload": { + "text": "Please brief me for my meeting", + "app": "home", + "path": "/app/home", + "facet": "work", + "ts": owner_ts, + }, + } ) template_vars = _assert_template_vars_result(result) + assert template_vars["digest_contents"] == "Digest notes for today." + assert "## Recent Chat" in template_vars["chat_stream_tail"] + assert ( + "**Alice** Please brief me for my meeting" in template_vars["chat_stream_tail"] + ) + assert "**Sol-agent** I can help with that." in template_vars["chat_stream_tail"] + assert ( + "*[exec spawned: Prepare the meeting brief]*" + in template_vars["chat_stream_tail"] + ) + assert "## Active Execs" in template_vars["active_talents"] + assert "Prepare the meeting brief" in template_vars["active_talents"] + assert "## Trigger Context" in template_vars["trigger_context"] + assert "Type: owner_message" in template_vars["trigger_context"] + assert "Please brief me for my meeting" in template_vars["trigger_context"] + assert "## Location" in template_vars["location"] + assert "/app/home" in template_vars["location"] + assert "work" in template_vars["location"] assert "## Active Routines" in template_vars["active_routines"] assert "Morning Briefing" in template_vars["active_routines"] + assert "Routine Suggestion Eligible" in template_vars["routine_suggestion"] + assert "meeting-prep" in template_vars["routine_suggestion"] -def test_chat_context_routines_omitted_when_empty(monkeypatch): - """Active routines section is omitted when no routines configured.""" - monkeypatch.setattr("think.conversation.build_memory_context", lambda **kw: "") +def test_chat_context_routine_suggestion_only_counts_owner_messages( + monkeypatch, tmp_path +): + journal = tmp_path / "journal" + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) + + routines_config = {"_meta": {"suggestions_enabled": True, "suggestions": {}}} + save_calls: list[dict] = [] monkeypatch.setattr("think.routines.get_routine_state", lambda: []) + monkeypatch.setattr("think.routines.get_config", lambda: routines_config) + monkeypatch.setattr( + "think.routines.save_config", + lambda config: save_calls.append(deepcopy(config)), + ) - result = _load_chat_context_module().pre_process( - {"user_instruction": "Base instruction."} + module = _load_chat_context_module() + + module.pre_process( + { + "prompt": "What is on my calendar today?", + "trigger_kind": "talent_finished", + "trigger_payload": { + "name": "exec", + "summary": "Collected the latest meeting prep notes.", + }, + } ) - template_vars = _assert_template_vars_result(result) - assert template_vars["active_routines"] == "" + assert routines_config["_meta"]["suggestions"] == {} + assert save_calls == [] + + module.pre_process( + { + "prompt": "What is on my calendar today?", + "trigger_kind": "owner_message", + "trigger_payload": { + "text": "What is on my calendar today?", + "ts": _ts(10, 0), + }, + } + ) + suggestion = routines_config["_meta"]["suggestions"]["morning-briefing"] + assert suggestion["trigger_count"] == 1 + assert len(save_calls) == 1 -def test_chat_context_routines_error_graceful(monkeypatch): - """Routine state failures still return the full template var shape.""" - monkeypatch.setattr("think.conversation.build_memory_context", lambda **kw: "") + +def test_chat_context_preserves_save_routines_config_side_effect(monkeypatch, tmp_path): + journal = tmp_path / "journal" + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) + + routines_config = {"_meta": {"suggestions_enabled": True, "suggestions": {}}} + save_calls: list[dict] = [] + monkeypatch.setattr("think.routines.get_routine_state", lambda: []) + monkeypatch.setattr("think.routines.get_config", lambda: routines_config) monkeypatch.setattr( - "think.routines.get_routine_state", - lambda: (_ for _ in ()).throw(RuntimeError("boom")), + "think.routines.save_config", + lambda config: save_calls.append(deepcopy(config)), ) - monkeypatch.setattr( - "think.routines.get_config", lambda: {"_meta": {"suggestions": {}}} + + _load_chat_context_module().pre_process( + { + "prompt": "What is on my calendar today?", + "trigger_kind": "owner_message", + "trigger_payload": { + "text": "What is on my calendar today?", + "ts": _ts(11, 0), + }, + } ) + + assert len(save_calls) == 1 + saved = save_calls[0] + assert saved["_meta"]["suggestions"]["morning-briefing"]["trigger_count"] == 1 + assert saved["_meta"]["suggestions"]["morning-briefing"]["first_trigger"] + + +def test_chat_context_routines_omitted_when_empty(monkeypatch, tmp_path): + journal = tmp_path / "journal" + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) + monkeypatch.setattr("think.routines.get_routine_state", lambda: []) monkeypatch.setattr( - "think.utils.get_config", - lambda: {"agent": {"name": "aria", "name_status": "default"}}, + "think.routines.get_config", + lambda: {"_meta": {"suggestions_enabled": False, "suggestions": {}}}, ) + monkeypatch.setattr("think.routines.save_config", lambda config: None) - result = _load_chat_context_module().pre_process( - {"user_instruction": "Base instruction."} + result = _load_chat_context_module().pre_process({"day": "20260420"}) + + template_vars = _assert_template_vars_result(result) + assert template_vars["active_routines"] == "" + assert template_vars["chat_stream_tail"] == "" + assert template_vars["active_talents"] == "" + + +def test_chat_context_enrichment_errors_are_graceful(monkeypatch, tmp_path): + journal = tmp_path / "journal" + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) + + module = _load_chat_context_module() + + def _boom(*_args, **_kwargs): + raise RuntimeError("boom") + + monkeypatch.setattr(module, "_load_digest_contents", _boom) + monkeypatch.setattr(module, "read_chat_tail", _boom) + monkeypatch.setattr(module, "reduce_chat_state", _boom) + monkeypatch.setattr("think.routines.get_routine_state", _boom) + monkeypatch.setattr("think.routines.get_config", _boom) + monkeypatch.setattr("think.routines.save_config", lambda config: None) + + result = module.pre_process( + { + "prompt": "What is on my calendar today?", + "trigger_kind": "owner_message", + "trigger_payload": { + "text": "What is on my calendar today?", + "path": "/app/home", + "ts": _ts(12, 0), + }, + } ) template_vars = _assert_template_vars_result(result) + assert template_vars["digest_contents"] == "" + assert template_vars["chat_stream_tail"] == "" + assert template_vars["active_talents"] == "" assert template_vars["active_routines"] == "" - assert set(template_vars) == TEMPLATE_VAR_KEYS + assert template_vars["routine_suggestion"] == "" + assert "Type: owner_message" in template_vars["trigger_context"] + assert "/app/home" in template_vars["location"] + + +def test_chat_context_drops_conversation_memory_imports(monkeypatch): + monkeypatch.setattr("think.routines.get_routine_state", lambda: []) + monkeypatch.setattr( + "think.routines.get_config", + lambda: {"_meta": {"suggestions_enabled": False, "suggestions": {}}}, + ) + monkeypatch.setattr("think.routines.save_config", lambda config: None) + + source = ( + Path(__file__).resolve().parents[1] / "talent" / "chat_context.py" + ).read_text(encoding="utf-8") + assert "think.conversation" not in source + assert "conversation_memory" not in source + + sys.modules.pop("think.conversation", None) + _load_chat_context_module() + + assert "think.conversation" not in sys.modules diff --git a/tests/test_google.py b/tests/test_google.py index 62ebc8ffd..25c1e21fe 100644 --- a/tests/test_google.py +++ b/tests/test_google.py @@ -121,6 +121,7 @@ def test_google_main(monkeypatch, tmp_path, capsys): ndjson_input = json.dumps( { + "name": "exec", "prompt": "hello", "provider": "google", "model": GEMINI_FLASH, @@ -134,7 +135,7 @@ def test_google_main(monkeypatch, tmp_path, capsys): assert events[0]["event"] == "start" assert isinstance(events[0]["ts"], int) assert "hello" in events[0]["prompt"] - assert events[0]["name"] == "unified" + assert events[0]["name"] == "exec" assert events[0]["model"] == GEMINI_FLASH assert events[-1]["event"] == "finish" assert isinstance(events[-1]["ts"], int) @@ -160,6 +161,7 @@ def test_google_cli_not_found_error(monkeypatch, tmp_path, capsys): ndjson_input = json.dumps( { + "name": "exec", "prompt": "hello", "provider": "google", "model": GEMINI_FLASH, diff --git a/tests/test_google_thinking.py b/tests/test_google_thinking.py index 2da0639af..e102a33f9 100644 --- a/tests/test_google_thinking.py +++ b/tests/test_google_thinking.py @@ -103,6 +103,7 @@ def test_google_thinking_events(monkeypatch, tmp_path, capsys): ndjson_input = json.dumps( { + "name": "exec", "prompt": "hello", "provider": "google", "model": GEMINI_FLASH, diff --git a/tests/test_maint_006_rename_unified_triage_providers.py b/tests/test_maint_006_rename_unified_triage_providers.py new file mode 100644 index 000000000..f6220856e --- /dev/null +++ b/tests/test_maint_006_rename_unified_triage_providers.py @@ -0,0 +1,108 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +import importlib +import json +from pathlib import Path + +mod = importlib.import_module("apps.sol.maint.006_rename_unified_triage_providers") + + +def _write_journal_config(journal: Path, data: object) -> Path: + config_dir = journal / "config" + config_dir.mkdir(parents=True, exist_ok=True) + config_path = config_dir / "journal.json" + config_path.write_text(json.dumps(data, indent=2), encoding="utf-8") + return config_path + + +def test_rename_unified_and_remove_triage_idempotent(tmp_path): + config_path = _write_journal_config( + tmp_path, + { + "providers": { + "contexts": { + "talent.system.unified": {"provider": "openai"}, + "talent.system.triage": {"provider": "anthropic"}, + "talent.system.digest": {"provider": "google"}, + } + } + }, + ) + + summary = mod.run_migration(tmp_path, dry_run=False) + + assert summary.renamed == 1 + assert summary.removed == 1 + assert summary.preserved == 0 + assert summary.errors == 0 + data = json.loads(config_path.read_text(encoding="utf-8")) + assert "talent.system.unified" not in data["providers"]["contexts"] + assert "talent.system.triage" not in data["providers"]["contexts"] + assert data["providers"]["contexts"]["talent.system.chat"] == {"provider": "openai"} + assert data["providers"]["contexts"]["talent.system.digest"] == { + "provider": "google" + } + + before_bytes = config_path.read_bytes() + before_mtime_ns = config_path.stat().st_mtime_ns + + rerun = mod.run_migration(tmp_path, dry_run=False) + + assert rerun.renamed == 0 + assert rerun.removed == 0 + assert rerun.preserved == 0 + assert rerun.errors == 0 + assert rerun.skipped_reason is None + assert config_path.read_bytes() == before_bytes + assert config_path.stat().st_mtime_ns == before_mtime_ns + + +def test_preserves_existing_chat_context_when_unified_exists(tmp_path): + config_path = _write_journal_config( + tmp_path, + { + "providers": { + "contexts": { + "talent.system.unified": {"provider": "openai"}, + "talent.system.chat": {"provider": "google"}, + } + } + }, + ) + + summary = mod.run_migration(tmp_path, dry_run=False) + + assert summary.renamed == 0 + assert summary.removed == 0 + assert summary.preserved == 1 + assert summary.errors == 0 + data = json.loads(config_path.read_text(encoding="utf-8")) + assert "talent.system.unified" not in data["providers"]["contexts"] + assert data["providers"]["contexts"]["talent.system.chat"] == {"provider": "google"} + + +def test_noop_when_no_legacy_provider_contexts_present(tmp_path): + config_path = _write_journal_config( + tmp_path, + { + "providers": { + "contexts": { + "talent.system.chat": {"provider": "openai"}, + "talent.system.digest": {"provider": "google"}, + } + } + }, + ) + before_bytes = config_path.read_bytes() + before_mtime_ns = config_path.stat().st_mtime_ns + + summary = mod.run_migration(tmp_path, dry_run=False) + + assert summary.renamed == 0 + assert summary.removed == 0 + assert summary.preserved == 0 + assert summary.errors == 0 + assert summary.skipped_reason is None + assert config_path.read_bytes() == before_bytes + assert config_path.stat().st_mtime_ns == before_mtime_ns diff --git a/tests/test_talent.py b/tests/test_talent.py index a39ce0b62..3b1dc0578 100644 --- a/tests/test_talent.py +++ b/tests/test_talent.py @@ -103,7 +103,8 @@ def test_validate_cwd_rejects_invalid_value(): def test_get_agent_normalizes_cwd_for_cogitate(): config = get_talent("chat") - assert config["cwd"] == "journal" + assert config["type"] == "generate" + assert "cwd" not in config def test_get_agent_preserves_repo_cwd_for_coder(): @@ -111,6 +112,13 @@ def test_get_agent_preserves_repo_cwd_for_coder(): assert config["cwd"] == "repo" +def test_get_talent_defaults_to_chat(): + config = get_talent() + assert config["name"] == "chat" + assert config["type"] == "generate" + assert Path(config["path"]).name == "chat.md" + + def _write_talent_file(tmp_path: Path, name: str, metadata: dict) -> Path: md_path = tmp_path / f"{name}.md" md_path.write_text( diff --git a/tests/test_talents_ndjson.py b/tests/test_talents_ndjson.py index 63b26243b..84394846a 100644 --- a/tests/test_talents_ndjson.py +++ b/tests/test_talents_ndjson.py @@ -31,7 +31,7 @@ async def mock_run_cogitate(config, on_event=None): prompt = config.get("prompt", "") provider = config.get("provider", "") model = config.get("model", "") - name = config.get("name", "unified") + name = config.get("name", "chat") if on_event: on_event( @@ -59,7 +59,7 @@ def mock_prepare_config(request: dict) -> dict: config = dict(request) # Add required fields if not present if "name" not in config: - config["name"] = "unified" + config["name"] = "chat" if "provider" not in config: config["provider"] = "google" if "model" not in config: diff --git a/tests/verify_api.py b/tests/verify_api.py index 80f87b9d2..66e091ce2 100644 --- a/tests/verify_api.py +++ b/tests/verify_api.py @@ -196,6 +196,7 @@ ENDPOINTS = [ "path": "/app/search/api/search", "params": {"q": "romeo", "limit": "5", "offset": "0"}, "status": 200, + "sandbox_only": True, }, { "app": "search", @@ -203,6 +204,7 @@ ENDPOINTS = [ "path": "/app/search/api/day_results", "params": {"q": "meeting", "day": "20260304", "offset": "0", "limit": "5"}, "status": 200, + "sandbox_only": True, }, # apps/settings/routes.py { @@ -385,6 +387,7 @@ ENDPOINTS = [ "path": "/app/graph/api/graph", "params": {}, "status": 200, + "sandbox_only": True, }, ] @@ -574,11 +577,18 @@ def verify_all(client: Any, journal_path: str) -> list[str]: return failures -def update_all(client: Any, journal_path: str) -> int: +def update_all( + client: Any, + journal_path: str, + *, + include_sandbox_only: bool, +) -> int: """Refresh all endpoint baselines from current responses.""" updated = 0 for endpoint in ENDPOINTS: + if endpoint.get("sandbox_only") and not include_sandbox_only: + continue identifier = f"{endpoint['app']}/{endpoint['name']}" path = baseline_path(endpoint) path.parent.mkdir(parents=True, exist_ok=True) @@ -695,7 +705,11 @@ def main(argv: list[str] | None = None) -> int: print(f"API baseline verification passed for {len(ENDPOINTS)} endpoints.") return 0 - updated = update_all(client, journal_path) + updated = update_all( + client, + journal_path, + include_sandbox_only=bool(args.base_url), + ) print(f"Updated {updated} baseline files.") return 0 diff --git a/think/chat_cli.py b/think/chat_cli.py index 7d42c242a..bcb8f71fc 100644 --- a/think/chat_cli.py +++ b/think/chat_cli.py @@ -24,7 +24,7 @@ def main() -> None: parser.add_argument("--facet", help="Facet context") parser.add_argument("--provider", help="AI provider override") parser.add_argument( - "--talent", default="unified", help="Talent agent name (default: unified)" + "--talent", default="chat", help="Talent agent name (default: chat)" ) args = setup_cli(parser) require_solstone() diff --git a/think/cortex.py b/think/cortex.py index c91fca0b8..fb3599eeb 100644 --- a/think/cortex.py +++ b/think/cortex.py @@ -211,7 +211,7 @@ class CortexService: return # Create _active.jsonl file (exclusive creation to prevent race conditions) - name = request.get("name", "unified") + name = request["name"] safe_name = name.replace(":", "--") talent_subdir = self.talents_dir / safe_name talent_subdir.mkdir(parents=True, exist_ok=True) @@ -293,7 +293,7 @@ class CortexService: if process_type == "talent": from think.talent import get_talent - talent_key = str(config.get("name", "unified")) + talent_key = str(config["name"]) talent_config = get_talent(talent_key) if talent_config.get("type") == "cogitate": # Resolve here because prepare_config() runs inside think.talents. @@ -692,7 +692,7 @@ class CortexService: summary = { "use_id": use_id, - "name": request.get("name", "unified"), + "name": request["name"], "day": day, "facet": request.get("facet"), "ts": start_ts, diff --git a/think/cortex_client.py b/think/cortex_client.py index 6d4047346..471568163 100644 --- a/think/cortex_client.py +++ b/think/cortex_client.py @@ -42,7 +42,7 @@ def cortex_request( Args: prompt: The task or question for the talent - name: Talent name - system (e.g., "unified") or app-qualified (e.g., "entities:entity_assist") + name: Talent name - system (e.g., "chat") or app-qualified (e.g., "entities:entity_assist") provider: AI provider - openai, google, or anthropic config: Provider-specific configuration (model, max_output_tokens, thinking_budget, etc.) @@ -267,6 +267,8 @@ def cortex_uses( ) -> Dict[str, Any]: """List talent uses from the journal with pagination and filtering. + Legacy unnamed run logs predate the chat rename and are surfaced as chat. + Args: limit: Maximum number of uses to return (1-100) offset: Number of uses to skip @@ -352,7 +354,8 @@ def cortex_uses( # Extract basic info use_info = { "id": use_id, - "name": request.get("name", "unified"), + # Legacy unnamed run logs predate the chat rename; treat them as chat. + "name": request.get("name", "chat"), "start": request.get("ts", 0), "status": status, "prompt": request.get("prompt", ""), diff --git a/think/talent.py b/think/talent.py index 958cd4922..8cdddc625 100644 --- a/think/talent.py +++ b/think/talent.py @@ -35,6 +35,7 @@ from think.prompts import _load_prompt_metadata, load_prompt TALENT_DIR = Path(__file__).parent.parent / "talent" APPS_DIR = Path(__file__).parent.parent / "apps" +_UNDISCOVERED_SYSTEM_TALENTS = {"triage"} # --------------------------------------------------------------------------- @@ -231,6 +232,8 @@ def get_talent_configs( if TALENT_DIR.is_dir(): for md_path in sorted(TALENT_DIR.glob("*.md")): name = md_path.stem + if name in _UNDISCOVERED_SYSTEM_TALENTS: + continue info = _load_prompt_metadata(md_path) info["source"] = "system" @@ -340,7 +343,7 @@ def _resolve_talent_path(name: str) -> tuple[Path, str]: Parameters ---------- name: - Talent name - either system talent (e.g., "unified") or + Talent name - either system talent (e.g., "chat") or app-namespaced talent (e.g., "support:support"). Returns @@ -498,7 +501,7 @@ def _load_talent_schema( def get_talent( - name: str = "unified", + name: str = "chat", facet: str | None = None, analysis_day: str | None = None, ) -> dict: @@ -511,7 +514,7 @@ def get_talent( Parameters ---------- name: - Talent name to load. Can be a system talent (e.g., "unified") + Talent name to load. Can be a system talent (e.g., "chat") or an app-namespaced talent (e.g., "support:support" for apps/support/talent/support). facet: Optional facet name to focus on. Controls $facets template variable. diff --git a/think/talent_cli.py b/think/talent_cli.py index cf4883ed1..8a6ba6fbf 100644 --- a/think/talent_cli.py +++ b/think/talent_cli.py @@ -804,7 +804,7 @@ def _get_output_size(request_event: dict[str, Any], journal_root: str) -> int | return None req_segment = request_event.get("segment") req_facet = request_event.get("facet") - req_name = request_event.get("name", "unified") + req_name = request_event["name"] req_env = request_event.get("env") or {} req_stream = req_env.get("SOL_STREAM") if req_env else None day_dir = day_path(req_day, create=False) diff --git a/think/talents.py b/think/talents.py index 418f2f855..feccdd0ca 100644 --- a/think/talents.py +++ b/think/talents.py @@ -459,7 +459,7 @@ def prepare_config(request: dict) -> dict: from think.models import resolve_model_for_provider, resolve_provider from think.talent import get_talent, key_to_context - name = request.get("name", "unified") + name = request["name"] facet = request.get("facet") day = request.get("day") segment = request.get("segment") @@ -755,7 +755,7 @@ def _build_dry_run_event(config: dict, before_values: dict) -> dict: "event": "dry_run", "ts": now_ms(), "type": talent_type, - "name": config.get("name", "unified"), + "name": config["name"], "provider": config.get("provider", ""), "model": config.get("model") or "unknown", "system_instruction": config.get("system_instruction", ""), @@ -872,7 +872,7 @@ async def _execute_with_tools( if not context: from think.talent import key_to_context - context = key_to_context(config.get("name", "unified")) + context = key_to_context(config["name"]) backup_model = resolve_model_for_provider(context, backup, "cogitate") emit_event( @@ -928,7 +928,7 @@ async def _execute_generate( from think.models import generate_with_result from think.talent import key_to_context - name = config.get("name", "unified") + name = config["name"] transcript = config.get("transcript", "") user_instruction = config.get("user_instruction", "") prompt = config.get("prompt", "") @@ -1072,7 +1072,7 @@ async def _run_talent( emit_event: Callback to emit JSONL events dry_run: If True, emit dry_run event instead of calling LLM """ - name = config.get("name", "unified") + name = config["name"] provider = config.get("provider", "google") model = config.get("model") is_cogitate = config["type"] == "cogitate" -- 2.51.2