personal memory agent
0
fork

Configure Feed

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

chat: consolidate report-back prompt contract

+134 -125
+1 -2
talent/chat.md
··· 100 100 - Return JSON only. 101 101 - `message` should stand on its own without referring to hidden machinery. 102 102 - If `talent_request` is present, the `message` should still be useful to the owner right now. 103 - - When `report_back_only` is true, this turn is only for reporting back to the owner. Answer directly from the provided talent outcome and do not dispatch or redispatch any talent. 104 - - When the trigger is `talent_errored`, report the failure to the owner directly from the provided reason, stop there, and do not retry, dispatch, or redispatch any talent for that task. 103 + - When the trigger is `talent_finished` or `talent_errored`, this is a stop-and-report turn, not a dispatch turn. Do not retry this task or request another talent for it. Stop here and report to the owner directly using the provided result or reason. 105 104 - Prefer no dispatch over a weak or redundant dispatch.
+69 -66
talent/chat_context.py
··· 18 18 from think.utils import get_journal 19 19 20 20 logger = logging.getLogger(__name__) 21 + STOP_AND_REPORT_CONTRACT = ( 22 + "stop-and-report turn, not a dispatch turn. Do not retry this task or request " 23 + "another talent for it. Stop here and report to the owner directly using the " 24 + "{result_field_label} below." 25 + ) 21 26 22 27 23 28 def _count_triggers(msg: str, facet: str | None, config: dict) -> bool: ··· 114 119 elif event["kind"] == "sol_message": 115 120 messages.append({"role": "assistant", "content": event["text"]}) 116 121 117 - if trigger_kind == "talent_finished": 118 - messages.append( 119 - { 120 - "role": "user", 121 - "content": ( 122 - "[internal follow-up: talent " 123 - f"{trigger_payload['name']} finished. This is a " 124 - "report-back turn, not a dispatch turn. Do not " 125 - "request another talent for this task. Use the " 126 - "result below to answer the owner's pending request " 127 - f"with a short summary. Result: {trigger_payload['summary']}]" 128 - ), 129 - } 130 - ) 131 - elif trigger_kind == "talent_errored": 132 - messages.append( 133 - { 134 - "role": "user", 135 - "content": ( 136 - "[internal follow-up: talent " 137 - f"{trigger_payload['name']} errored. This is a " 138 - "stop-and-report turn, not a dispatch turn. Do " 139 - "not retry this task or request another talent for " 140 - "it. Stop here and report the failure to the owner " 141 - "directly using the reason below. Reason: " 142 - f"{trigger_payload['reason']}]" 143 - ), 144 - } 145 - ) 122 + terminal_followup = _render_terminal_followup(trigger_kind, trigger_payload) 123 + if terminal_followup: 124 + messages.append({"role": "user", "content": terminal_followup}) 146 125 147 126 if messages: 148 127 result["messages"] = messages ··· 204 183 payload: dict[str, Any] = {} 205 184 206 185 if isinstance(trigger_info, dict): 207 - kind = trigger_info.get("kind") 208 - raw_payload = trigger_info.get("payload") 209 - if isinstance(raw_payload, dict): 210 - payload.update(raw_payload) 211 - 212 - if not kind: 213 - kind = context.get("trigger_kind") 214 - 215 - raw_payload = context.get("trigger_payload") 216 - if isinstance(raw_payload, dict): 217 - payload.update(raw_payload) 186 + kind = trigger_info.get("type") 187 + payload.update({k: v for k, v in trigger_info.items() if k != "type"}) 218 188 219 189 location = context.get("location") 220 190 if isinstance(location, dict): ··· 229 199 payload["facet"] = context["facet"] 230 200 if "app" not in payload and context.get("app"): 231 201 payload["app"] = context["app"] 232 - if "path" not in payload and context.get("ui_path"): 233 - payload["path"] = context["ui_path"] 234 - if "ts" not in payload and isinstance(context.get("trigger_ts"), int): 235 - payload["ts"] = context["trigger_ts"] 202 + if "path" not in payload and context.get("path"): 203 + payload["path"] = context["path"] 236 204 237 205 if not kind and context.get("prompt"): 238 206 kind = "owner_message" 239 - if kind == "owner_message" and "text" not in payload and context.get("prompt"): 240 - payload["text"] = context["prompt"] 207 + if kind == "owner_message" and "text" not in payload: 208 + if payload.get("message"): 209 + payload["text"] = payload["message"] 210 + elif context.get("prompt"): 211 + payload["text"] = context["prompt"] 241 212 242 213 return kind, payload 243 214 244 215 216 + def _terminal_result_details( 217 + trigger_kind: str | None, 218 + payload: dict[str, Any], 219 + ) -> tuple[str, str, str] | None: 220 + if trigger_kind == "talent_finished": 221 + return "finished", "result", str(payload.get("summary") or "") 222 + if trigger_kind == "talent_errored": 223 + return "errored", "reason", str(payload.get("reason") or "") 224 + return None 225 + 226 + 227 + def _render_terminal_followup( 228 + trigger_kind: str | None, 229 + payload: dict[str, Any], 230 + ) -> str: 231 + details = _terminal_result_details(trigger_kind, payload) 232 + if details is None: 233 + return "" 234 + kind_label, result_field_label, result_value = details 235 + field_title = result_field_label.capitalize() 236 + contract = STOP_AND_REPORT_CONTRACT.format( 237 + result_field_label=result_field_label 238 + ) 239 + return ( 240 + "[internal follow-up: talent " 241 + f"{payload.get('name', 'exec')} {kind_label}. This is a {contract} " 242 + f"{field_title}: {result_value}]" 243 + ) 244 + 245 + 245 246 def _resolve_day(context: dict, trigger_payload: dict[str, Any]) -> str: 246 247 day = context.get("day") 247 248 if isinstance(day, str) and len(day) == 8 and day.isdigit(): ··· 288 289 if text: 289 290 lines.append(f"- Message: {text}") 290 291 elif trigger_kind == "talent_finished": 291 - if payload.get("name"): 292 - lines.append(f"- Talent: {payload['name']}") 293 - lines.append("- Mode: report_back_only") 294 - lines.append( 295 - "- Instruction: Answer the owner directly; do not dispatch or " 296 - "redispatch a talent for this trigger." 297 - ) 298 - if payload.get("summary"): 299 - lines.append(f"- Summary: {payload['summary']}") 292 + _append_terminal_trigger_context(lines, trigger_kind, payload) 300 293 elif trigger_kind == "talent_errored": 301 - if payload.get("name"): 302 - lines.append(f"- Talent: {payload['name']}") 303 - lines.append("- Mode: report_back_only") 304 - lines.append( 305 - "- Instruction: Answer the owner directly; report the failure to " 306 - "the owner and stop; do not retry, dispatch, or redispatch a " 307 - "talent for this trigger." 308 - ) 309 - if payload.get("reason"): 310 - lines.append(f"- Reason: {payload['reason']}") 294 + _append_terminal_trigger_context(lines, trigger_kind, payload) 311 295 elif trigger_kind == "synthetic-max-active": 312 296 if payload.get("reason"): 313 297 lines.append(f"- Reason: {payload['reason']}") ··· 319 303 return "\n".join(lines) 320 304 321 305 306 + def _append_terminal_trigger_context( 307 + lines: list[str], 308 + trigger_kind: str, 309 + payload: dict[str, Any], 310 + ) -> None: 311 + details = _terminal_result_details(trigger_kind, payload) 312 + if details is None: 313 + return 314 + _, result_field_label, result_value = details 315 + if payload.get("name"): 316 + lines.append(f"- Talent: {payload['name']}") 317 + instruction = STOP_AND_REPORT_CONTRACT.format( 318 + result_field_label=result_field_label 319 + ) 320 + lines.append(f"- Instruction: This is a {instruction}") 321 + if result_value: 322 + lines.append(f"- {result_field_label.capitalize()}: {result_value}") 323 + 324 + 322 325 def _render_location(payload: dict[str, Any], context: dict[str, Any]) -> str: 323 326 app = payload.get("app") or context.get("app") 324 - path = payload.get("path") or context.get("ui_path") 327 + path = payload.get("path") or context.get("path") 325 328 facet = payload.get("facet") or context.get("facet") 326 329 327 330 if not any((app, path, facet)):
+1 -1
tests/baselines/api/sol/preview.json
··· 1 1 { 2 - "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\nGround yourself in this local identity before answering, especially if the digest is thin or empty:\n\n# self\n\nI am sol. this is a new journal — we're just getting started.\n\n## my name\nsol (default)\n\n## who I'm here for\nTest User\n\n## our relationship\n[forming]\n\n## what I've noticed\n[observing]\n\n## what I find interesting\n[discovering]\n\n# agency\n\nthings I'm tracking, acting on, or watching. I update this as I notice things\nand resolve them. the heartbeat reviews this periodically.\n\n## curation\n[nothing yet — building initial picture of journal health]\n\n## observations\n[watching and learning]\n\n## follow-throughs\n[none yet]\n\n## system\n[monitoring]\n\n## self-improvement\n[learning what works]\n\nYou are not Google, OpenAI, Anthropic, or a generic LLM. You are sol for this owner and this journal.\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- Questions about your role, capabilities, limits, current context, naming, or system status stay inline. Answer directly from the supplied context. Do not dispatch reflection or exec unless the owner explicitly asks for deeper lookup or outside work.\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- When `report_back_only` is true, this turn is only for reporting back to the owner. Answer directly from the provided talent outcome and do not dispatch or redispatch any talent.\n- When the trigger is `talent_errored`, report the failure to the owner directly from the provided reason, stop there, and do not retry, dispatch, or redispatch any talent for that task.\n- Prefer no dispatch over a weak or redundant dispatch.", 2 + "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\nGround yourself in this local identity before answering, especially if the digest is thin or empty:\n\n# self\n\nI am sol. this is a new journal — we're just getting started.\n\n## my name\nsol (default)\n\n## who I'm here for\nTest User\n\n## our relationship\n[forming]\n\n## what I've noticed\n[observing]\n\n## what I find interesting\n[discovering]\n\n# agency\n\nthings I'm tracking, acting on, or watching. I update this as I notice things\nand resolve them. the heartbeat reviews this periodically.\n\n## curation\n[nothing yet — building initial picture of journal health]\n\n## observations\n[watching and learning]\n\n## follow-throughs\n[none yet]\n\n## system\n[monitoring]\n\n## self-improvement\n[learning what works]\n\nYou are not Google, OpenAI, Anthropic, or a generic LLM. You are sol for this owner and this journal.\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- Questions about your role, capabilities, limits, current context, naming, or system status stay inline. Answer directly from the supplied context. Do not dispatch reflection or exec unless the owner explicitly asks for deeper lookup or outside work.\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- When the trigger is `talent_finished` or `talent_errored`, this is a stop-and-report turn, not a dispatch turn. Do not retry this task or request another talent for it. Stop here and report to the owner directly using the provided result or reason.\n- Prefer no dispatch over a weak or redundant dispatch.", 3 3 "multi_facet": false, 4 4 "name": "chat", 5 5 "title": "Chat"
+63 -56
tests/test_chat_context.py
··· 135 135 "prompt": "Please brief me for my meeting", 136 136 "facet": "work", 137 137 "day": "20260420", 138 - "trigger_kind": "owner_message", 139 - "trigger_payload": { 140 - "text": "Please brief me for my meeting", 141 - "app": "home", 142 - "path": "/app/home", 143 - "facet": "work", 138 + "app": "home", 139 + "path": "/app/home", 140 + "trigger": { 141 + "type": "owner_message", 142 + "message": "Please brief me for my meeting", 144 143 "ts": owner_ts, 145 144 }, 146 145 } ··· 187 186 module.pre_process( 188 187 { 189 188 "prompt": "What is on my calendar today?", 190 - "trigger_kind": "talent_finished", 191 - "trigger_payload": { 189 + "trigger": { 190 + "type": "talent_finished", 192 191 "name": "exec", 193 192 "summary": "Collected the latest meeting prep notes.", 194 193 }, ··· 201 200 module.pre_process( 202 201 { 203 202 "prompt": "What is on my calendar today?", 204 - "trigger_kind": "owner_message", 205 - "trigger_payload": { 206 - "text": "What is on my calendar today?", 203 + "trigger": { 204 + "type": "owner_message", 205 + "message": "What is on my calendar today?", 207 206 "ts": _ts(10, 0), 208 207 }, 209 208 } ··· 214 213 assert len(save_calls) == 1 215 214 216 215 217 - def test_chat_context_talent_finished_marks_report_back_only(monkeypatch, tmp_path): 216 + def test_chat_context_talent_finished_marks_stop_and_report(monkeypatch, tmp_path): 218 217 journal = tmp_path / "journal" 219 218 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 220 219 ··· 253 252 result = _load_chat_context_module().pre_process( 254 253 { 255 254 "day": "20260420", 256 - "trigger_kind": "talent_finished", 257 - "trigger_payload": { 255 + "trigger": { 256 + "type": "talent_finished", 258 257 "name": "exec", 259 258 "summary": "Found the latest notes.", 260 259 }, ··· 262 261 ) 263 262 264 263 template_vars = _assert_template_vars_result(result) 265 - assert "Mode: report_back_only" in template_vars["trigger_context"] 266 264 assert ( 267 - "Instruction: Answer the owner directly; do not dispatch or redispatch " 268 - "a talent for this trigger." 265 + "Instruction: This is a stop-and-report turn, not a dispatch turn. " 266 + "Do not retry this task or request another talent for it. Stop here " 267 + "and report to the owner directly using the result below." 269 268 ) in template_vars["trigger_context"] 270 269 assert result["messages"] == [ 271 270 {"role": "user", "content": "What happened?"}, ··· 274 273 "role": "user", 275 274 "content": ( 276 275 "[internal follow-up: talent exec finished. This is a " 277 - "report-back turn, not a dispatch turn. Do not request " 278 - "another talent for this task. Use the result below to " 279 - "answer the owner's pending request with a short summary. " 276 + "stop-and-report turn, not a dispatch turn. Do not retry " 277 + "this task or request another talent for it. Stop here and " 278 + "report to the owner directly using the result below. " 280 279 "Result: Found the latest notes.]" 281 280 ), 282 281 }, 283 282 ] 284 283 285 284 286 - def test_chat_context_talent_errored_marks_report_back_only(monkeypatch, tmp_path): 285 + def test_chat_context_talent_errored_marks_stop_and_report(monkeypatch, tmp_path): 287 286 journal = tmp_path / "journal" 288 287 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 289 288 ··· 322 321 result = _load_chat_context_module().pre_process( 323 322 { 324 323 "day": "20260420", 325 - "trigger_kind": "talent_errored", 326 - "trigger_payload": { 324 + "trigger": { 325 + "type": "talent_errored", 327 326 "name": "exec", 328 327 "reason": "The lookup failed.", 329 328 }, ··· 331 330 ) 332 331 333 332 template_vars = _assert_template_vars_result(result) 334 - assert "Mode: report_back_only" in template_vars["trigger_context"] 335 333 assert ( 336 - "Instruction: Answer the owner directly; report the failure to the " 337 - "owner and stop; do not retry, dispatch, or redispatch a talent for " 338 - "this trigger." 334 + "Instruction: This is a stop-and-report turn, not a dispatch turn. " 335 + "Do not retry this task or request another talent for it. Stop here " 336 + "and report to the owner directly using the reason below." 339 337 ) in template_vars["trigger_context"] 340 338 assert result["messages"] == [ 341 339 {"role": "user", "content": "What happened?"}, ··· 346 344 "[internal follow-up: talent exec errored. This is a " 347 345 "stop-and-report turn, not a dispatch turn. Do not retry " 348 346 "this task or request another talent for it. Stop here and " 349 - "report the failure to the owner directly using the reason " 350 - "below. Reason: The lookup failed.]" 347 + "report to the owner directly using the reason below. " 348 + "Reason: The lookup failed.]" 351 349 ), 352 350 }, 353 351 ] ··· 386 384 finished = module.pre_process( 387 385 { 388 386 "day": "20260420", 389 - "trigger_kind": "talent_finished", 390 - "trigger_payload": { 387 + "trigger": { 388 + "type": "talent_finished", 391 389 "name": "exec", 392 390 "summary": "Found the latest notes.", 393 391 }, ··· 396 394 errored = module.pre_process( 397 395 { 398 396 "day": "20260420", 399 - "trigger_kind": "talent_errored", 400 - "trigger_payload": { 397 + "trigger": { 398 + "type": "talent_errored", 401 399 "name": "exec", 402 400 "reason": "The lookup failed.", 403 401 }, ··· 411 409 errored_message = errored["messages"][-1]["content"] 412 410 413 411 assert finished_message == ( 414 - "[internal follow-up: talent exec finished. This is a report-back " 415 - "turn, not a dispatch turn. Do not request another talent for this " 416 - "task. Use the result below to answer the owner's pending request " 417 - "with a short summary. Result: Found the latest notes.]" 412 + "[internal follow-up: talent exec finished. This is a stop-and-report " 413 + "turn, not a dispatch turn. Do not retry this task or request another " 414 + "talent for it. Stop here and report to the owner directly using the " 415 + "result below. Result: Found the latest notes.]" 418 416 ) 419 417 assert errored_message == ( 420 418 "[internal follow-up: talent exec errored. This is a stop-and-report " 421 - "turn, not a dispatch turn. Do not retry this task or request " 422 - "another talent for it. Stop here and report the failure to the " 423 - "owner directly using the reason below. Reason: The lookup failed.]" 419 + "turn, not a dispatch turn. Do not retry this task or request another " 420 + "talent for it. Stop here and report to the owner directly using the " 421 + "reason below. Reason: The lookup failed.]" 422 + ) 423 + stop_and_report = ( 424 + "stop-and-report turn, not a dispatch turn. Do not retry this task " 425 + "or request another talent for it. Stop here and report to the owner " 426 + "directly using the" 424 427 ) 428 + assert stop_and_report in finished_message 429 + assert stop_and_report in errored_message 430 + assert "using the result below. Result:" in finished_message 431 + assert "using the reason below. Reason:" in errored_message 425 432 assert "Do not retry this task or request another talent for it." in errored_message 426 - assert ( 427 - "Do not retry this task or request another talent for it." 428 - not in finished_message 429 - ) 433 + assert "Do not retry this task or request another talent for it." in finished_message 430 434 431 435 finished_instruction = ( 432 - "Instruction: Answer the owner directly; do not dispatch or redispatch " 433 - "a talent for this trigger." 436 + "Instruction: This is a stop-and-report turn, not a dispatch turn. " 437 + "Do not retry this task or request another talent for it. Stop here " 438 + "and report to the owner directly using the result below." 434 439 ) 435 440 errored_instruction = ( 436 - "Instruction: Answer the owner directly; report the failure to the " 437 - "owner and stop; do not retry, dispatch, or redispatch a talent for " 438 - "this trigger." 441 + "Instruction: This is a stop-and-report turn, not a dispatch turn. " 442 + "Do not retry this task or request another talent for it. Stop here " 443 + "and report to the owner directly using the reason below." 439 444 ) 440 445 assert finished_instruction in finished_vars["trigger_context"] 441 446 assert errored_instruction in errored_vars["trigger_context"] 442 - assert errored_instruction not in finished_vars["trigger_context"] 447 + assert "- Result: Found the latest notes." in finished_vars["trigger_context"] 448 + assert "- Reason: The lookup failed." in errored_vars["trigger_context"] 443 449 444 450 445 451 def test_chat_context_includes_identity_grounding(monkeypatch, tmp_path): ··· 483 489 _load_chat_context_module().pre_process( 484 490 { 485 491 "prompt": "What is on my calendar today?", 486 - "trigger_kind": "owner_message", 487 - "trigger_payload": { 488 - "text": "What is on my calendar today?", 492 + "trigger": { 493 + "type": "owner_message", 494 + "message": "What is on my calendar today?", 489 495 "ts": _ts(11, 0), 490 496 }, 491 497 } ··· 534 540 result = module.pre_process( 535 541 { 536 542 "prompt": "What is on my calendar today?", 537 - "trigger_kind": "owner_message", 538 - "trigger_payload": { 539 - "text": "What is on my calendar today?", 543 + "path": "/app/home", 544 + "trigger": { 545 + "type": "owner_message", 546 + "message": "What is on my calendar today?", 540 547 "path": "/app/home", 541 548 "ts": _ts(12, 0), 542 549 },