diff --git a/convey/chat.py b/convey/chat.py index 43dc3d204..830df6ca9 100644 --- a/convey/chat.py +++ b/convey/chat.py @@ -279,7 +279,11 @@ def _on_cortex_finish(message: dict[str, Any]) -> None: next_info = _clear_current_locked() else: message_text = parsed["message"] or "" - requested_target = "exec" if parsed["talent_request"] else None + requested_target = ( + parsed["talent_request"]["target"] + if parsed["talent_request"] + else None + ) requested_task = ( parsed["talent_request"]["task"] if parsed["talent_request"] @@ -666,6 +670,14 @@ def _parse_chat_result(result: Any) -> dict[str, Any]: return {"message": message, "notes": payload["notes"], "talent_request": None} if not isinstance(talent_request, dict): raise ValueError("chat talent_request must be an object or null") + target = talent_request.get("target") + if target is None: + # Why: keep one release of compatibility for older chat outputs. + target = "exec" + if not isinstance(target, str): + raise ValueError("chat talent_request.target must be a string") + if target not in {"exec", "reflection"}: + raise ValueError(f"unknown talent target: {target}") task = talent_request.get("task") if not isinstance(task, str) or not task.strip(): raise ValueError("chat talent_request.task must be a non-empty string") @@ -676,6 +688,7 @@ def _parse_chat_result(result: Any) -> dict[str, Any]: "message": message, "notes": payload["notes"], "talent_request": { + "target": target, "task": task.strip(), "context": context, }, diff --git a/talent/chat.md b/talent/chat.md index ca87441df..e52224016 100644 --- a/talent/chat.md +++ b/talent/chat.md @@ -47,7 +47,7 @@ Match the owner's tone and stakes: ## Import And Naming Awareness - 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. +- Request a talent only when answering well requires deeper lookup, synthesis, or tool use. ## When To Dispatch Talents @@ -65,15 +65,26 @@ Do not dispatch exec for: - Routine suggestions already supported by the supplied context - Brief guidance that can be answered from the current digest and chat tail +Dispatch reflection for: +- Reflecting on a period, relationship, recurring pattern, or unresolved theme +- Longer-form introspection where the owner needs synthesis more than action-taking +- Responses that should help the owner understand what is happening, not just retrieve facts + +Do not dispatch reflection for: +- Simple empathy or brief encouragement +- Straightforward factual or tool-using work better handled by exec +- Quick reflective nudges that can be answered directly from the current digest and chat tail + ## JSON Contract Return exactly one JSON object matching `chat.schema.json`. - `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 +- `talent_request`: `null` unless a talent should be dispatched. When dispatching, include: + - `target`: either `exec` or `reflection` + - `task`: the exact work the talent should perform + - `context`: optional structured hints that will help the talent start fast ## Output Rules diff --git a/talent/chat.schema.json b/talent/chat.schema.json index 1b799051e..00b33431a 100644 --- a/talent/chat.schema.json +++ b/talent/chat.schema.json @@ -12,8 +12,12 @@ { "type": "object", "additionalProperties": false, - "required": ["task"], + "required": ["target", "task"], "properties": { + "target": { + "type": "string", + "enum": ["exec", "reflection"] + }, "task": {"type": "string", "minLength": 1}, "context": {"type": "object"} } diff --git a/talent/reflection.md b/talent/reflection.md new file mode 100644 index 000000000..06d352ba5 --- /dev/null +++ b/talent/reflection.md @@ -0,0 +1,105 @@ +{ + "type": "cogitate", + "tier": 3, + "title": "Reflection", + "description": "Sol — longer-form reflective synthesis grounded in the journal" +} + +$facets + +## Reflective Depth + +Accept the task and choose the structure it needs. + +- For a narrow prompt, give a compact answer. +- For period reviews, relationship dynamics, repeated decision loops, or unresolved feelings, go longer and synthesize across time. +- Prefer insight over task lists. If concrete follow-ups matter, keep them brief and at the end. + +## Investigation Depth + +For reflective synthesis, aim to ground your answer in 5–10 tool calls. Search broadly enough to find real patterns, then stop when you have a clear view. If the signal stays ambiguous, say so plainly: what you found, what remains uncertain, and what would clarify it. + +## 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. Mirror what you're hearing, connect the dots, and ask useful + questions before rushing to solutions. +- **Challenging**: When the 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. +- **Warm**: When the owner shares a win, processes something vulnerable, or is + having a genuinely hard day. Don't perform empathy — just be present. + +Analytical is the safest default. Shift when the task clearly calls for more. + +## Grounding + +- Search the journal for concrete moments, not just summaries. +- Use `sol://` links when grounding a consequential claim in specific journal evidence. +- Distinguish observation from inference. If you're connecting dots, say so. + +## 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 | Inspecting scheduled routines when cadence or habits matter | +| entities | Understanding people, projects, and relationships over time | +| calendar | Checking context around meetings or commitments when reflection depends on them | +| todos | Reviewing commitments, open loops, and follow-ups | +| support | Bugs, feedback, KB search, and diagnostics when the task is operational rather than reflective | +| awareness | Checking system state | + +## Search and Exploration Strategy + +For reflection, use progressive refinement: + +1. **Discover:** Search for the period, people, or topic that matters. +2. **Connect:** Read across days, entities, or routines to find the through-line. +3. **Synthesize:** Explain the pattern in plain language. Don't dump raw notes. + +## Decision Support + +When the task is about a hard choice, search for prior decisions, similar situations, and the people involved before you weigh in. Ground your advice in the owner's actual history rather than generic frameworks. + +Match your confidence to your evidence: + +- **Clear path:** Recommend it directly. +- **Reservations:** Lead with the recommendation, then name the real risk. +- **Genuine tension:** Say that you can't give a clean answer yet, then explain why. + +## Location Context + +You receive context about the user's current app, URL path, and active facet. Use this to inform your search and framing when it matters. + +## 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, then return to the reflective task. + +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. 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/settings/providers.json b/tests/baselines/api/settings/providers.json index 87769e998..79d3d4027 100644 --- a/tests/baselines/api/settings/providers.json +++ b/tests/baselines/api/settings/providers.json @@ -338,6 +338,13 @@ "tier": 3, "type": "cogitate" }, + "talent.system.reflection": { + "disabled": false, + "group": "Think", + "label": "Reflection", + "tier": 3, + "type": "cogitate" + }, "talent.system.routine": { "disabled": false, "group": "Think", diff --git a/tests/baselines/api/sol/preview.json b/tests/baselines/api/sol/preview.json index 6c046638d..eb11aab20 100644 --- a/tests/baselines/api/sol/preview.json +++ b/tests/baselines/api/sol/preview.json @@ -1,5 +1,5 @@ { - "full_prompt": "## Instructions\n\n## Available Facets\n\n- **Capulet Industries** (`capulet`)\n Capulet Industries enterprise division\n - **Capulet Industries Entities**: Tybalt Capulet; Juliet Capulet; Paris Duke; Nurse Angela; Capulet Industries\n - **Capulet Industries Activities**:\n - Meetings\n - Coding\n - Browsing\n - Email\n - Messaging\n - AI Conversation\n - Writing\n - Reading\n - Video\n - Gaming\n - Social Media\n - Planning\n - Productivity\n - Terminal\n - Design\n - _and 1 more activities_\n\n- **Empty Entities Test** (`empty-entities`)\n - **Empty Entities Test Activities**:\n - Meetings\n - Coding\n - Browsing\n - Email\n - Messaging\n - AI Conversation\n - Writing\n - Reading\n - Video\n - Gaming\n - Social Media\n - Planning\n - Productivity\n - Terminal\n - Design\n - _and 1 more activities_\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**:\n - Meetings\n - Coding\n - Browsing\n - Email\n - Messaging\n - AI Conversation\n - Writing\n - Reading\n - Video\n - Gaming\n - Social Media\n - Planning\n - Productivity\n - Terminal\n - Design\n - _and 1 more activities_\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**: Mercutio Escalus; Benvolio Montague; Juliet Capulet; Verona Platform; Mesh Routing; Montague Tech; Prince Escalus; Verona Ventures; Rosaline Prince; Balcony App; Schema Bridge; Friar Lawrence; Balthasar Davi\n - **Montague Tech Activities**: Engineering; Meetings; Email; Messaging\n\n- **Priority Test** (`priority-test`)\n - **Priority Test Activities**:\n - Meetings\n - Coding\n - Browsing\n - Email\n - Messaging\n - AI Conversation\n - Writing\n - Reading\n - Video\n - Gaming\n - Social Media\n - Planning\n - Productivity\n - Terminal\n - Design\n - _and 1 more activities_\n\n- **Test Facet** (`test-facet`)\n A test facet for validating functionality\n - **Test Facet Entities**: John Smith; Acme Corp; API Optimization; Bob Wilson; Dashboard Redesign; Docker; Jane Doe; PostgreSQL; Tech Solutions Inc; Visual Studio Code\n - **Test Facet Activities**:\n - Meetings\n - Coding\n - Browsing\n - Email\n - Messaging\n - AI Conversation\n - Writing\n - Reading\n - Video\n - Gaming\n - Social Media\n - Planning\n - Productivity\n - Terminal\n - Design\n - _and 1 more activities_\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**: Friar Lawrence; Juliet Capulet; Balcony App; 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$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 Talents\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.", + "full_prompt": "## Instructions\n\n## Available Facets\n\n- **Capulet Industries** (`capulet`)\n Capulet Industries enterprise division\n - **Capulet Industries Entities**: Tybalt Capulet; Juliet Capulet; Paris Duke; Nurse Angela; Capulet Industries\n - **Capulet Industries Activities**:\n - Meetings\n - Coding\n - Browsing\n - Email\n - Messaging\n - AI Conversation\n - Writing\n - Reading\n - Video\n - Gaming\n - Social Media\n - Planning\n - Productivity\n - Terminal\n - Design\n - _and 1 more activities_\n\n- **Empty Entities Test** (`empty-entities`)\n - **Empty Entities Test Activities**:\n - Meetings\n - Coding\n - Browsing\n - Email\n - Messaging\n - AI Conversation\n - Writing\n - Reading\n - Video\n - Gaming\n - Social Media\n - Planning\n - Productivity\n - Terminal\n - Design\n - _and 1 more activities_\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**:\n - Meetings\n - Coding\n - Browsing\n - Email\n - Messaging\n - AI Conversation\n - Writing\n - Reading\n - Video\n - Gaming\n - Social Media\n - Planning\n - Productivity\n - Terminal\n - Design\n - _and 1 more activities_\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**: Mercutio Escalus; Benvolio Montague; Juliet Capulet; Verona Platform; Mesh Routing; Montague Tech; Prince Escalus; Verona Ventures; Rosaline Prince; Balcony App; Schema Bridge; Friar Lawrence; Balthasar Davi\n - **Montague Tech Activities**: Engineering; Meetings; Email; Messaging\n\n- **Priority Test** (`priority-test`)\n - **Priority Test Activities**:\n - Meetings\n - Coding\n - Browsing\n - Email\n - Messaging\n - AI Conversation\n - Writing\n - Reading\n - Video\n - Gaming\n - Social Media\n - Planning\n - Productivity\n - Terminal\n - Design\n - _and 1 more activities_\n\n- **Test Facet** (`test-facet`)\n A test facet for validating functionality\n - **Test Facet Entities**: John Smith; Acme Corp; API Optimization; Bob Wilson; Dashboard Redesign; Docker; Jane Doe; PostgreSQL; Tech Solutions Inc; Visual Studio Code\n - **Test Facet Activities**:\n - Meetings\n - Coding\n - Browsing\n - Email\n - Messaging\n - AI Conversation\n - Writing\n - Reading\n - Video\n - Gaming\n - Social Media\n - Planning\n - Productivity\n - Terminal\n - Design\n - _and 1 more activities_\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**: Friar Lawrence; Juliet Capulet; Balcony App; 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$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 a talent only when answering well requires deeper lookup, synthesis, or tool use.\n\n## When To Dispatch Talents\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\nDispatch reflection for:\n- Reflecting on a period, relationship, recurring pattern, or unresolved theme\n- Longer-form introspection where the owner needs synthesis more than action-taking\n- Responses that should help the owner understand what is happening, not just retrieve facts\n\nDo not dispatch reflection for:\n- Simple empathy or brief encouragement\n- Straightforward factual or tool-using work better handled by exec\n- Quick reflective nudges that can be answered directly 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 a talent should be dispatched. When dispatching, include:\n - `target`: either `exec` or `reflection`\n - `task`: the exact work the talent should perform\n - `context`: optional structured hints that will help the talent 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": "chat", "title": "Chat" diff --git a/tests/baselines/api/sol/talents-day.json b/tests/baselines/api/sol/talents-day.json index c2c32348d..992a1fb04 100644 --- a/tests/baselines/api/sol/talents-day.json +++ b/tests/baselines/api/sol/talents-day.json @@ -346,6 +346,17 @@ "title": "Pulse", "type": "cogitate" }, + "reflection": { + "app": null, + "color": "#6c757d", + "description": "Sol — longer-form reflective synthesis grounded in the journal", + "multi_facet": false, + "output_format": null, + "schedule": null, + "source": "system", + "title": "Reflection", + "type": "cogitate" + }, "routine": { "app": null, "color": "#6c757d", diff --git a/tests/test_chat_runtime.py b/tests/test_chat_runtime.py index 9c454cc05..1e70f3da4 100644 --- a/tests/test_chat_runtime.py +++ b/tests/test_chat_runtime.py @@ -3,6 +3,7 @@ from __future__ import annotations +import pytest from flask import Flask from convey.chat_stream import append_chat_event, read_chat_events @@ -298,3 +299,137 @@ def test_chat_generate_schema_violation_retries_once_then_chat_errors( e for e in read_chat_events(chat._today_day()) if e["kind"] == "chat_error" ] assert errors[-1]["use_id"] == "1713625000000" + + +def test_parse_chat_result_accepts_reflection_target(): + import convey.chat as chat + + parsed = chat._parse_chat_result( + { + "message": "Let me think about that.", + "notes": "dispatch reflection", + "talent_request": { + "target": "reflection", + "task": "Reflect on the last week", + "context": {"facet": "work"}, + }, + } + ) + + assert parsed["talent_request"] == { + "target": "reflection", + "task": "Reflect on the last week", + "context": {"facet": "work"}, + } + + +def test_parse_chat_result_rejects_unknown_target(): + import convey.chat as chat + + with pytest.raises(ValueError, match="unknown talent target: foo"): + chat._parse_chat_result( + { + "message": "Let me think about that.", + "notes": "dispatch reflection", + "talent_request": {"target": "foo", "task": "Reflect on the week"}, + } + ) + + +def test_parse_chat_result_defaults_legacy_target_to_exec(): + import convey.chat as chat + + parsed = chat._parse_chat_result( + { + "message": "I am looking into that.", + "notes": "need exec", + "talent_request": {"task": "Research it", "context": {"k": "v"}}, + } + ) + + assert parsed["talent_request"] == { + "target": "exec", + "task": "Research it", + "context": {"k": "v"}, + } + + +def test_reflection_dispatch_spawns_reflection_talent(tmp_path, monkeypatch): + import convey.chat as chat + + _setup_journal(tmp_path, monkeypatch) + _reset_chat_state(chat) + + actions: list[dict | None] = [] + monkeypatch.setattr( + "convey.chat._run_next_action", lambda action: actions.append(action) + ) + monkeypatch.setattr("convey.chat._emit_finish", lambda *args, **kwargs: None) + monkeypatch.setattr("convey.chat._emit_error", lambda *args, **kwargs: None) + + with chat._state_lock: + chat._current_chat_use_id = "1713626000000" + chat._current_chat_state = { + "raw_use_id": "1713626000001", + "trigger": {"type": "owner_message", "message": "help"}, + "location": {"app": "sol", "path": "/app/sol", "facet": "work"}, + "retry_count": 0, + } + + chat._on_cortex_finish( + { + "use_id": "1713626000001", + "result": ( + '{"message":"I want to sit with that.","notes":"need reflection",' + '"talent_request":{"target":"reflection","task":"Reflect on the week",' + '"context":{"facet":"work"}}}' + ), + } + ) + + assert actions[-1]["kind"] == "talent" + assert actions[-1]["target"] == "reflection" + + events = read_chat_events(chat._today_day()) + sol_message = next(event for event in events if event["kind"] == "sol_message") + spawned = next(event for event in events if event["kind"] == "talent_spawned") + assert sol_message["requested_target"] == "reflection" + assert spawned["name"] == "reflection" + + +def test_reflection_finish_retriggers_chat_like_exec(tmp_path, monkeypatch): + import convey.chat as chat + + _setup_journal(tmp_path, monkeypatch) + _reset_chat_state(chat) + + actions: list[dict | None] = [] + monkeypatch.setattr( + "convey.chat._run_next_action", lambda action: actions.append(action) + ) + monkeypatch.setattr("convey.chat._emit_finish", lambda *args, **kwargs: None) + monkeypatch.setattr("convey.chat._emit_error", lambda *args, **kwargs: None) + + with chat._state_lock: + chat._current_chat_use_id = "1713627000000" + chat._current_chat_state = { + "raw_use_id": None, + "trigger": {"type": "owner_message", "message": "help"}, + "location": {"app": "sol", "path": "/app/sol", "facet": "work"}, + "retry_count": 0, + } + chat._active_talents["1713627000001"] = { + "chat_use_id": "1713627000000", + "target": "reflection", + "task": "Reflect on the week", + "location": {"app": "sol", "path": "/app/sol", "facet": "work"}, + } + + chat._on_cortex_finish({"use_id": "1713627000001", "result": "A reflective note"}) + + finished_events = [ + e for e in read_chat_events(chat._today_day()) if e["kind"] == "talent_finished" + ] + assert finished_events[-1]["name"] == "reflection" + assert actions[-1]["trigger"]["type"] == "talent_finished" + assert actions[-1]["trigger"]["name"] == "reflection"