personal memory agent
0
fork

Configure Feed

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

chat: generalize dispatch to talents

+85 -74
+3 -3
apps/chat/tests/test_routes.py
··· 120 120 use_id="use-1", 121 121 text="sol reply", 122 122 notes="full note", 123 - requested_exec=False, 123 + requested_target=None, 124 124 requested_task=None, 125 125 ) 126 126 append_chat_event( ··· 182 182 use_id="use-5", 183 183 text="second", 184 184 notes="", 185 - requested_exec=False, 185 + requested_target=None, 186 186 requested_task=None, 187 187 ) 188 188 ··· 213 213 use_id="use-6", 214 214 text="later", 215 215 notes="", 216 - requested_exec=False, 216 + requested_target=None, 217 217 requested_task=None, 218 218 ) 219 219
+54 -47
convey/chat.py
··· 33 33 34 34 chat_bp = Blueprint("chat", __name__, url_prefix="/api/chat") 35 35 36 - MAX_ACTIVE_EXECS = 2 36 + MAX_ACTIVE_TALENTS = 2 37 37 MAX_LOOP_RETRIES = 3 38 38 DEFAULT_STREAM_LIMIT = 200 39 39 MAX_STREAM_LIMIT = 1000 ··· 46 46 _current_chat_use_id: str | None = None 47 47 _current_chat_state: dict[str, Any] | None = None 48 48 _queued_trigger: dict[str, Any] | None = None 49 - _active_execs: dict[str, dict[str, Any]] = {} 49 + _active_talents: dict[str, dict[str, Any]] = {} 50 50 _last_use_id = 0 51 51 _recovery_day: str | None = None 52 52 _runtime: "ChatRuntimeState | None" = None ··· 226 226 raw_chat_use_id = str(_current_chat_state.get("raw_use_id") or "") 227 227 if use_id == raw_chat_use_id: 228 228 logical_use_id = _current_chat_use_id 229 - elif use_id in _active_execs: 230 - logical_use_id = str(_active_execs[use_id]["chat_use_id"]) 229 + elif use_id in _active_talents: 230 + logical_use_id = str(_active_talents[use_id]["chat_use_id"]) 231 231 232 232 if logical_use_id is None: 233 233 return ··· 279 279 next_info = _clear_current_locked() 280 280 else: 281 281 message_text = parsed["message"] or "" 282 - requested_exec = parsed["talent_request"] is not None 282 + requested_target = "exec" if parsed["talent_request"] else None 283 283 requested_task = ( 284 284 parsed["talent_request"]["task"] 285 285 if parsed["talent_request"] ··· 290 290 use_id=logical_use_id, 291 291 text=message_text, 292 292 notes=parsed["notes"], 293 - requested_exec=requested_exec, 293 + requested_target=requested_target, 294 294 requested_task=requested_task, 295 295 ) 296 296 _current_chat_state["retry_count"] = 0 297 297 _current_chat_state["raw_use_id"] = None 298 - if requested_exec: 299 - active_exec_count = _active_exec_count_for_today_locked() 300 - if active_exec_count >= MAX_ACTIVE_EXECS: 298 + if requested_target: 299 + active_talent_count = _active_talent_count_for_today_locked() 300 + if active_talent_count >= MAX_ACTIVE_TALENTS: 301 301 _current_chat_state["trigger"] = { 302 302 "type": "synthetic-max-active", 303 303 "reason": MAX_ACTIVE_REASON, ··· 305 305 synthetic_use_id = _reserve_use_id_locked() 306 306 _current_chat_state["raw_use_id"] = synthetic_use_id 307 307 next_info = _build_spawn_info_locked(logical_use_id) 308 - elif _exec_loop_count_locked() >= MAX_LOOP_RETRIES: 308 + elif _talent_loop_count_locked() >= MAX_LOOP_RETRIES: 309 309 append_chat_event( 310 310 "chat_error", 311 311 reason=CHAT_TROUBLE_REASON, ··· 317 317 } 318 318 next_info = _clear_current_locked() 319 319 else: 320 - exec_use_id = _reserve_use_id_locked() 321 - _active_execs[exec_use_id] = { 320 + talent_use_id = _reserve_use_id_locked() 321 + _active_talents[talent_use_id] = { 322 322 "chat_use_id": logical_use_id, 323 + "target": requested_target, 323 324 "task": requested_task, 324 325 "location": dict(_current_chat_state["location"]), 325 326 } 326 327 append_chat_event( 327 328 "talent_spawned", 328 - use_id=exec_use_id, 329 - name="exec", 329 + use_id=talent_use_id, 330 + name=requested_target, 330 331 task=requested_task, 331 - started_at=int(exec_use_id), 332 + started_at=int(talent_use_id), 332 333 ) 333 334 next_info = { 334 - "kind": "exec", 335 + "kind": "talent", 335 336 "logical_use_id": logical_use_id, 336 - "use_id": exec_use_id, 337 + "target": requested_target, 338 + "use_id": talent_use_id, 337 339 "task": requested_task, 338 340 "context": parsed["talent_request"].get("context") or {}, 339 341 "location": dict(_current_chat_state["location"]), ··· 356 358 } 357 359 next_info = _clear_current_locked() 358 360 359 - elif use_id in _active_execs: 360 - exec_state = _active_execs.pop(use_id) 361 - logical_use_id = str(exec_state["chat_use_id"]) 361 + elif use_id in _active_talents: 362 + talent_state = _active_talents.pop(use_id) 363 + logical_use_id = str(talent_state["chat_use_id"]) 362 364 summary = str(message.get("result") or "").strip() 363 365 append_chat_event( 364 366 "talent_finished", 365 367 use_id=use_id, 366 - name="exec", 368 + name=str(talent_state["target"]), 367 369 summary=summary, 368 370 ) 369 371 if ( ··· 373 375 _current_chat_state["trigger"] = { 374 376 "type": "talent_finished", 375 377 "use_id": use_id, 376 - "name": "exec", 378 + "name": str(talent_state["target"]), 377 379 "summary": summary, 378 380 } 379 381 _current_chat_state["raw_use_id"] = _reserve_use_id_locked() ··· 407 409 ) 408 410 error_payload = {"use_id": logical_use_id, "reason": CHAT_TROUBLE_REASON} 409 411 next_info = _clear_current_locked() 410 - elif use_id in _active_execs: 411 - exec_state = _active_execs.pop(use_id) 412 - logical_use_id = str(exec_state["chat_use_id"]) 412 + elif use_id in _active_talents: 413 + talent_state = _active_talents.pop(use_id) 414 + logical_use_id = str(talent_state["chat_use_id"]) 413 415 reason = str(message.get("error") or CHAT_TROUBLE_REASON) 414 416 append_chat_event( 415 417 "talent_errored", 416 418 use_id=use_id, 417 - name="exec", 419 + name=str(talent_state["target"]), 418 420 reason=reason, 419 421 ) 420 422 if ( ··· 424 426 _current_chat_state["trigger"] = { 425 427 "type": "talent_errored", 426 428 "use_id": use_id, 427 - "name": "exec", 429 + "name": str(talent_state["target"]), 428 430 "reason": reason, 429 431 } 430 432 _current_chat_state["raw_use_id"] = _reserve_use_id_locked() ··· 443 445 if not _spawn_chat_generate(action): 444 446 _handle_chat_failure(action["logical_use_id"], CHAT_TROUBLE_REASON) 445 447 return 446 - if action.get("kind") == "exec": 447 - if not _spawn_exec(action): 448 - _handle_exec_spawn_failure(action) 448 + if action.get("kind") == "talent": 449 + if not _spawn_talent(action): 450 + _handle_talent_spawn_failure(action) 449 451 450 452 451 453 def _spawn_chat_generate(action: dict[str, Any]) -> bool: ··· 477 479 return True 478 480 479 481 480 - def _spawn_exec(action: dict[str, Any]) -> bool: 482 + def _spawn_talent(action: dict[str, Any]) -> bool: 481 483 from convey.utils import spawn_agent 482 484 483 - prompt = _build_exec_prompt( 485 + prompt = _build_talent_prompt( 486 + action["target"], 484 487 action["task"], 485 488 action["context"], 486 489 action["location"], ··· 493 496 } 494 497 use_id = spawn_agent( 495 498 prompt=prompt, 496 - name="exec", 499 + name=action["target"], 497 500 provider=None, 498 501 config=config, 499 502 use_id=action["use_id"], ··· 504 507 return True 505 508 506 509 507 - def _handle_exec_spawn_failure(action: dict[str, Any]) -> None: 510 + def _handle_talent_spawn_failure(action: dict[str, Any]) -> None: 508 511 next_info: dict[str, Any] | None = None 509 512 with _state_lock: 510 - _active_execs.pop(str(action["use_id"]), None) 513 + _active_talents.pop(str(action["use_id"]), None) 511 514 append_chat_event( 512 515 "talent_errored", 513 516 use_id=action["use_id"], 514 - name="exec", 517 + name=action["target"], 515 518 reason=CHAT_TROUBLE_REASON, 516 519 ) 517 520 if _current_chat_use_id == action["logical_use_id"] and _current_chat_state: 518 521 _current_chat_state["trigger"] = { 519 522 "type": "talent_errored", 520 523 "use_id": action["use_id"], 521 - "name": "exec", 524 + "name": action["target"], 522 525 "reason": CHAT_TROUBLE_REASON, 523 526 } 524 527 _current_chat_state["raw_use_id"] = _reserve_use_id_locked() ··· 616 619 ) 617 620 618 621 619 - def _active_exec_count_for_today_locked() -> int: 622 + def _active_talent_count_for_today_locked() -> int: 620 623 return len(reduce_chat_state(_today_day())["active_talents"]) 621 624 622 625 623 - def _exec_loop_count_locked() -> int: 626 + def _talent_loop_count_locked() -> int: 624 627 events = read_chat_events(_today_day()) 625 628 count = 0 626 629 for index in range(len(events) - 1, -1, -1): ··· 630 633 break 631 634 if kind != "sol_message": 632 635 continue 633 - if not event.get("requested_exec"): 636 + if not event.get("requested_target"): 634 637 continue 635 638 636 639 previous = events[index - 1] if index > 0 else None ··· 679 682 } 680 683 681 684 682 - def _build_exec_prompt( 685 + def _build_talent_prompt( 686 + target: str, 683 687 task: str, 684 688 context_hints: dict[str, Any], 685 689 location: dict[str, str], ··· 703 707 history_lines.append(f"**Sol**: {event['text']}") 704 708 if history_lines: 705 709 parts.append("Recent chat:\n" + "\n".join(history_lines[-6:])) 710 + 711 + if target != "exec": 712 + parts.append(f"Target: {target}") 706 713 707 714 return "\n\n".join(parts) 708 715 ··· 784 791 return None 785 792 786 793 latest_sol: dict[str, Any] | None = None 787 - exec_state: dict[str, Any] | None = None 794 + talent_state: dict[str, Any] | None = None 788 795 chat_error: dict[str, Any] | None = None 789 796 spawned_task: str | None = None 790 797 ··· 796 803 chat_error = event 797 804 elif kind == "talent_spawned" and str(event.get("use_id")) == use_id: 798 805 spawned_task = event.get("task") 799 - exec_state = {"state": "active", "task": spawned_task} 806 + talent_state = {"state": "active", "task": spawned_task} 800 807 elif kind == "talent_finished" and str(event.get("use_id")) == use_id: 801 - exec_state = { 808 + talent_state = { 802 809 "state": "finished", 803 810 "summary": event.get("summary", ""), 804 811 "task": spawned_task, 805 812 } 806 813 elif kind == "talent_errored" and str(event.get("use_id")) == use_id: 807 - exec_state = { 814 + talent_state = { 808 815 "state": "errored", 809 816 "reason": event.get("reason", ""), 810 817 "task": spawned_task, ··· 813 820 with _state_lock: 814 821 if _current_chat_use_id == use_id: 815 822 task = None 816 - if latest_sol and latest_sol.get("requested_exec"): 823 + if latest_sol and latest_sol.get("requested_target"): 817 824 task = latest_sol.get("requested_task") 818 825 return {"state": "active", "task": task} 819 826 ··· 827 834 "state": "finished", 828 835 "summary": latest_sol.get("text", ""), 829 836 } 830 - return exec_state 837 + return talent_state 831 838 832 839 833 840 def _read_talent_log(use_id: str) -> dict[str, Any] | None:
+2 -2
convey/chat_stream.py
··· 28 28 "use_id", 29 29 "text", 30 30 "notes", 31 - "requested_exec", 31 + "requested_target", 32 32 "requested_task", 33 33 ), 34 34 "talent_spawned": ("use_id", "name", "task", "started_at"), ··· 122 122 "use_id": event["use_id"], 123 123 "text": event["text"], 124 124 "notes": event["notes"], 125 - "requested_exec": event["requested_exec"], 125 + "requested_target": event["requested_target"], 126 126 "requested_task": event["requested_task"], 127 127 } 128 128 continue
+1 -1
talent/chat.md
··· 49 49 - If the owner is asking about imports, naming, or system readiness, answer plainly from the supplied context. 50 50 - Request exec only when answering well requires deeper lookup, synthesis, or tool use. 51 51 52 - ## When To Dispatch Exec 52 + ## When To Dispatch Talents 53 53 54 54 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. 55 55
+1 -1
talent/chat_context.py
··· 435 435 if not active_talents: 436 436 return "" 437 437 438 - lines = ["## Active Execs\n"] 438 + lines = ["## Active Talents\n"] 439 439 for talent in active_talents: 440 440 started_at = _format_started_at(talent.get("started_at")) 441 441 line = f"- **{talent.get('name', 'exec')}** — {talent.get('task', '')}"
+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\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 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.", 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\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.", 3 3 "multi_facet": false, 4 4 "name": "chat", 5 5 "title": "Chat"
+3 -3
tests/test_chat_context.py
··· 84 84 use_id="use-chat-1", 85 85 text="I can help with that.", 86 86 notes="Responded directly.", 87 - requested_exec=False, 87 + requested_target=None, 88 88 requested_task=None, 89 89 ) 90 90 append_chat_event( ··· 150 150 {"role": "assistant", "content": "I can help with that."}, 151 151 ] 152 152 assert all("exec spawned" not in msg["content"] for msg in result["messages"]) 153 - assert "## Active Execs" in template_vars["active_talents"] 153 + assert "## Active Talents" in template_vars["active_talents"] 154 154 assert "Prepare the meeting brief" in template_vars["active_talents"] 155 155 assert "## Trigger Context" in template_vars["trigger_context"] 156 156 assert "Type: owner_message" in template_vars["trigger_context"] ··· 229 229 use_id="use-chat-2", 230 230 text="Looking into it.", 231 231 notes="Acknowledged request.", 232 - requested_exec=False, 232 + requested_target=None, 233 233 requested_task=None, 234 234 ) 235 235 append_chat_event(
+8 -6
tests/test_chat_runtime.py
··· 14 14 chat_module._current_chat_use_id = None 15 15 chat_module._current_chat_state = None 16 16 chat_module._queued_trigger = None 17 - chat_module._active_execs.clear() 17 + chat_module._active_talents.clear() 18 18 chat_module._recovery_day = None 19 19 chat_module._last_use_id = 0 20 20 ··· 26 26 return journal 27 27 28 28 29 - def test_chat_result_with_two_active_execs_retriggers_with_max_active_reason( 29 + def test_chat_result_with_two_active_talents_retriggers_with_max_active_reason( 30 30 tmp_path, monkeypatch 31 31 ): 32 32 import convey.chat as chat ··· 85 85 sol_messages = [ 86 86 e for e in read_chat_events(chat._today_day()) if e["kind"] == "sol_message" 87 87 ] 88 - assert sol_messages[-1]["requested_exec"] is True 88 + assert sol_messages[-1]["requested_target"] == "exec" 89 89 assert sol_messages[-1]["requested_task"] == "research it" 90 90 91 91 ··· 117 117 use_id="1713621999999", 118 118 text=f"follow up {index}", 119 119 notes="retrying", 120 - requested_exec=True, 120 + requested_target="exec", 121 121 requested_task=f"task {index}", 122 122 ) 123 123 ··· 182 182 "location": {"app": "sol", "path": "/app/sol", "facet": "work"}, 183 183 "retry_count": 0, 184 184 } 185 - chat._active_execs["1713623000001"] = { 185 + chat._active_talents["1713623000001"] = { 186 186 "chat_use_id": "1713623000000", 187 + "target": "exec", 187 188 "task": "summarize", 188 189 "location": {"app": "sol", "path": "/app/sol", "facet": "work"}, 189 190 } ··· 205 206 "location": {"app": "sol", "path": "/app/sol", "facet": "work"}, 206 207 "retry_count": 0, 207 208 } 208 - chat._active_execs["1713624000001"] = { 209 + chat._active_talents["1713624000001"] = { 209 210 "chat_use_id": "1713624000000", 211 + "target": "exec", 210 212 "task": "summarize", 211 213 "location": {"app": "sol", "path": "/app/sol", "facet": "work"}, 212 214 }
+5 -5
tests/test_chat_stream.py
··· 99 99 use_id="1713626000000", 100 100 text="hello", 101 101 notes="ready", 102 - requested_exec=False, 102 + requested_target=None, 103 103 requested_task=None, 104 104 ) 105 105 ··· 296 296 use_id="chat-1", 297 297 text="dispatching", 298 298 notes="planning", 299 - requested_exec=True, 299 + requested_target="exec", 300 300 requested_task="compare drafts", 301 301 ) 302 302 append_chat_event( ··· 343 343 "use_id": "chat-1", 344 344 "text": "dispatching", 345 345 "notes": "planning", 346 - "requested_exec": True, 346 + "requested_target": "exec", 347 347 "requested_task": "compare drafts", 348 348 } 349 349 assert reduced["active_talents"] == [ ··· 402 402 use_id="chat-1", 403 403 text="working", 404 404 notes="", 405 - requested_exec=False, 405 + requested_target=None, 406 406 requested_task=None, 407 407 ) 408 408 append_chat_event( ··· 436 436 use_id="chat-1", 437 437 text="thanks", 438 438 notes="", 439 - requested_exec=False, 439 + requested_target=None, 440 440 requested_task=None, 441 441 ) 442 442
+6 -4
tests/test_convey_chat.py
··· 26 26 chat_module._current_chat_use_id = None 27 27 chat_module._current_chat_state = None 28 28 chat_module._queued_trigger = None 29 - chat_module._active_execs.clear() 29 + chat_module._active_talents.clear() 30 30 chat_module._recovery_day = None 31 31 chat_module._last_use_id = 0 32 32 ··· 86 86 assert starts and starts[-1]["logical_use_id"] == payload["use_id"] 87 87 88 88 89 - def test_session_endpoint_reduces_from_chat_stream(chat_client): 89 + def test_session_endpoint_reduces_from_chat_stream(chat_client, monkeypatch): 90 90 day = "20260420" 91 + monkeypatch.setattr("convey.chat._today_day", lambda: day) 91 92 append_chat_event( 92 93 "sol_message", 93 94 ts=_ms(2026, 4, 20, 12, 0, 0), 94 95 use_id="1713626000000", 95 96 text="hello", 96 97 notes="ready", 97 - requested_exec=False, 98 + requested_target=None, 98 99 requested_task=None, 99 100 ) 100 101 append_chat_event( ··· 136 137 use_id = str(_ms(2026, 4, 20, 12, 0, 0)) 137 138 append_chat_event( 138 139 "sol_message", 140 + ts=_ms(2026, 4, 20, 12, 0, 0), 139 141 use_id=use_id, 140 142 text="stream reply", 141 143 notes="done", 142 - requested_exec=False, 144 + requested_target=None, 143 145 requested_task=None, 144 146 ) 145 147
+1 -1
tests/test_journal_index.py
··· 1691 1691 use_id="1713628000000", 1692 1692 text="The unique nebula phrase is now in chat history.", 1693 1693 notes="done", 1694 - requested_exec=False, 1694 + requested_target=None, 1695 1695 requested_task=None, 1696 1696 ) 1697 1697