personal memory agent
0
fork

Configure Feed

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

Remove dead cortex handoff mechanism

Handoff agent-chaining was built and tested but never wired into any
agent workflow. Removes _spawn_handoff(), handoff_from parameter,
handoff propagation in agent events, 5 handoff tests, and all
documentation references. Observer "atomic handoff" (directory renames)
and CALLOSUM "event-driven handoffs" (observer pipeline) are unrelated
and left intact.

+7 -240
+1 -1
docs/APPS.md
··· 374 374 - Agents have a `tools` field, generators have `schedule` but no `tools` 375 375 - App agents/generators are automatically discovered alongside system ones 376 376 - Keys are namespaced as `{app}:{name}` (e.g., `my_app:helper`) 377 - - Agents inherit all system agent capabilities (tools, scheduling, handoffs, multi-facet) 377 + - Agents inherit all system agent capabilities (tools, scheduling, multi-facet) 378 378 379 379 **Metadata format:** Same schema as system agents in `muse/*.md` - JSON frontmatter includes `title`, `provider`, `model`, `tools`, `schedule`, `priority`, `multi_facet`, `max_output_tokens`, and `thinking_budget` fields. The `priority` field is **required** for all scheduled prompts - prompts without explicit priority will fail validation. See the priority bands documentation in [THINK.md](THINK.md#unified-priority-execution). Optional `max_output_tokens` sets the maximum response length; `thinking_budget` sets the model's thinking token budget (provider-specific defaults apply if omitted; OpenAI uses fixed reasoning and ignores this field). See [CORTEX.md](CORTEX.md) for agent configuration details. 380 380
+4 -27
docs/CORTEX.md
··· 49 49 "env": { // Optional: environment variables for subprocess 50 50 "API_KEY": "secret", 51 51 "DEBUG": "true" 52 - }, 53 - "handoff": { // Optional: chain to another agent on completion 54 - "name": "reviewer", 55 - "prompt": "Review the analysis", 56 - "provider": "openai" 57 - }, 58 - "handoff_from": "1234567890122" // Optional: present when spawned via handoff 52 + } 59 53 } 60 54 ``` 61 55 ··· 120 114 "provider": "openai", 121 115 "name": "default", 122 116 "output": "md", 123 - "day": "20250109", 124 - "handoff": {}, 125 - "handoff_from": "1234567890122" 117 + "day": "20250109" 126 118 } 127 119 ``` 128 120 ··· 197 189 "event": "finish", 198 190 "ts": 1234567890123, 199 191 "agent_id": "1234567890123", 200 - "result": "Final response text to the user", 201 - "handoff": { // Optional: triggers next agent 202 - "prompt": "Continue with next task", 203 - "name": "specialist", 204 - "provider": "openai" 205 - } 192 + "result": "Final response text to the user" 206 193 } 207 194 ``` 208 195 ··· 247 234 - Output path is derived from agent name + format + schedule: 248 235 - Daily agents: `YYYYMMDD/agents/{name}.{ext}` 249 236 - Segment agents: `YYYYMMDD/{segment}/{name}.{ext}` 250 - - Writing occurs before any handoff processing 237 + - Writing occurs before completion 251 238 - Write failures are logged but don't interrupt the agent flow 252 239 - Commonly used for scheduled agents that generate daily reports 253 240 254 - ## Agent Handoff 255 - 256 - Agents can transfer control to other agents for specialized tasks. When an agent completes with a handoff configuration, Cortex automatically spawns the next agent in the chain. 257 - 258 - - The `finish` event may include a `handoff` field specifying the next agent 259 - - The subsequent request includes `handoff_from` with the originating agent ID 260 - - Handoff agents automatically inherit the parent agent's configuration (provider, model, etc.) unless explicitly overridden 261 - - This enables multi-step workflows and agent specialization with consistent configuration 262 - 263 241 ## Agent Configuration 264 242 265 243 Agents use configurations stored in the `muse/` directory. Each agent is a `.md` file containing: ··· 298 276 - `env`: Environment variables to set for the agent subprocess (object) 299 277 - Keys are variable names, values are coerced to strings 300 278 - Request-level `env` overrides agent defaults 301 - - Inherited by handoff agents unless explicitly overridden 302 279 - Note: `_SOLSTONE_JOURNAL_OVERRIDE` is set by Cortex for child processes 303 280 304 281 ### Model Resolution
-1
docs/JOURNAL.md
··· 817 817 - `text` – streaming text output from the agent 818 818 - `tool_call` – agent invoked a tool 819 819 - `tool_result` – result returned from tool execution 820 - - `handoff` – agent delegated to another agent 821 820 - `error` – error occurred during execution 822 821 - `finish` – agent session completed, includes final result 823 822
-118
tests/test_cortex.py
··· 234 234 mock_timer_instance.start.assert_called_once() 235 235 236 236 237 - @patch("think.cortex.subprocess.Popen") 238 - def test_spawn_subprocess_with_handoff_from(mock_popen, cortex_service, mock_journal): 239 - """Test spawning an agent with handoff_from parameter.""" 240 - mock_process = MagicMock() 241 - mock_process.pid = 12345 242 - mock_process.stdin = MagicMock() 243 - mock_process.stdout = MagicMock() 244 - mock_process.stderr = MagicMock() 245 - mock_popen.return_value = mock_process 246 - 247 - agent_id = "123456789" 248 - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 249 - 250 - request = { 251 - "event": "request", 252 - "ts": 123456789, 253 - "prompt": "Test", 254 - "provider": "openai", 255 - "name": "unified", 256 - "handoff_from": "parent123", 257 - } 258 - 259 - with patch("think.cortex.threading.Thread"): 260 - cortex_service._spawn_subprocess( 261 - agent_id, file_path, request, ["sol", "agents"], "agent" 262 - ) 263 - 264 - # Check handoff_from was included in NDJSON 265 - written_data = mock_process.stdin.write.call_args[0][0] 266 - ndjson = json.loads(written_data.strip()) 267 - assert ndjson["handoff_from"] == "parent123" 268 - 269 - 270 237 def test_monitor_stdout_json_events(cortex_service, mock_journal): 271 238 """Test monitoring stdout with JSON events.""" 272 239 from io import StringIO ··· 334 301 assert "ts" in info_event 335 302 336 303 337 - def test_monitor_stdout_with_handoff(cortex_service, mock_journal): 338 - """Test monitoring stdout with handoff in finish event.""" 339 - from io import StringIO 340 - 341 - from think.cortex import AgentProcess 342 - 343 - agent_id = "123456789" 344 - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" 345 - 346 - # Handoff config is now in the finish event itself 347 - finish_event = { 348 - "event": "finish", 349 - "ts": 1234567890, 350 - "result": "Create matter", 351 - "handoff": {"name": "matter_editor", "facet": "test"}, 352 - } 353 - 354 - mock_process = MagicMock() 355 - mock_process.poll.return_value = 0 356 - mock_process.stdout = StringIO(json.dumps(finish_event) + "\n") 357 - 358 - agent = AgentProcess(agent_id, mock_process, log_path) 359 - cortex_service.running_agents[agent_id] = agent 360 - 361 - with patch.object(cortex_service, "_spawn_handoff") as mock_handoff: 362 - with patch.object(cortex_service, "_complete_agent_file"): 363 - cortex_service._monitor_stdout(agent) 364 - 365 - mock_handoff.assert_called_once_with( 366 - agent_id, 367 - "Create matter", 368 - {"name": "matter_editor", "facet": "test"}, 369 - ) 370 - 371 - 372 304 def test_monitor_stdout_no_finish_event(cortex_service, mock_journal): 373 305 """Test monitoring stdout when process exits without finish event.""" 374 306 from io import StringIO ··· 545 477 assert error_event["event"] == "error" 546 478 assert error_event["error"] == "Test error message" 547 479 assert "ts" in error_event 548 - 549 - 550 - def test_spawn_handoff(cortex_service, mock_journal): 551 - """Test spawning a handoff agent.""" 552 - parent_id = "parent123" 553 - result = "Create a new matter for AI research" 554 - handoff = { 555 - "name": "matter_editor", 556 - "provider": "anthropic", 557 - "facet": "test", 558 - "max_turns": 5, 559 - } 560 - 561 - with patch("think.cortex_client.cortex_request") as mock_request: 562 - mock_request.return_value = ( 563 - mock_journal / "agents" / "987654321000_active.jsonl" 564 - ) 565 - cortex_service._spawn_handoff(parent_id, result, handoff) 566 - 567 - # Check cortex_request was called with correct parameters 568 - mock_request.assert_called_once_with( 569 - prompt=result, 570 - name="matter_editor", 571 - provider="anthropic", 572 - handoff_from=parent_id, 573 - config={"facet": "test", "max_turns": 5}, 574 - ) 575 - 576 - 577 - def test_spawn_handoff_with_explicit_prompt(cortex_service, mock_journal): 578 - """Test spawning handoff with explicit prompt in config.""" 579 - parent_id = "parent123" 580 - result = "Parent result" 581 - handoff = { 582 - "name": "reviewer", 583 - "prompt": "Review this analysis", # Explicit prompt 584 - } 585 - 586 - with patch("think.cortex_client.cortex_request") as mock_request: 587 - cortex_service._spawn_handoff(parent_id, result, handoff) 588 - 589 - # Check cortex_request was called with explicit prompt 590 - # Provider is None when not explicitly set - let the agent resolve its own 591 - mock_request.assert_called_once_with( 592 - prompt="Review this analysis", # Uses explicit prompt 593 - name="reviewer", 594 - provider=None, 595 - handoff_from=parent_id, 596 - config=None, 597 - ) 598 480 599 481 600 482 def test_get_status(cortex_service):
-18
tests/test_cortex_client.py
··· 118 118 assert len(agent_id) == 13 # Millisecond timestamp 119 119 120 120 121 - def test_cortex_request_with_handoff(callosum_listener): 122 - """Test cortex_request with handoff_from parameter.""" 123 - messages = callosum_listener 124 - 125 - cortex_request( 126 - prompt="Continue analysis", 127 - name="reviewer", 128 - provider="anthropic", 129 - handoff_from="1234567890000", 130 - ) 131 - 132 - time.sleep(0.2) 133 - 134 - msg = messages[0] 135 - assert msg["handoff_from"] == "1234567890000" 136 - assert msg["name"] == "reviewer" 137 - 138 - 139 121 def test_cortex_request_unique_agent_ids(callosum_server): 140 122 """Test that cortex_request generates unique agent IDs.""" 141 123 _ = callosum_server # Needed for side effects only
-4
think/agents.py
··· 813 813 data = {**data, "result": result} 814 814 if output_path and result: 815 815 _write_output(output_path, result) 816 - if config.get("handoff"): 817 - data = {**data, "handoff": config["handoff"]} 818 816 819 817 # Filter out start events from providers (we already emitted ours) 820 818 if data.get("event") == "start": ··· 1023 1021 } 1024 1022 if usage_data: 1025 1023 finish_event["usage"] = usage_data 1026 - if config.get("handoff"): 1027 - finish_event["handoff"] = config["handoff"] 1028 1024 emit_event(finish_event) 1029 1025 1030 1026
+2 -66
think/cortex.py
··· 17 17 18 18 from __future__ import annotations 19 19 20 - import copy 21 20 import json 22 21 import logging 23 22 import os ··· 224 223 225 224 self.logger.info(f"Processing agent request: {agent_id}") 226 225 227 - # Store request for later use (handoffs, output writing) 226 + # Store request for later use (output writing) 228 227 with self.lock: 229 228 self.agent_requests[agent_id] = request 230 229 ··· 409 408 410 409 # Handle finish or error event 411 410 if event.get("event") in ["finish", "error"]: 412 - # Check for output and handoff (only on finish) 411 + # Check for output (only on finish) 413 412 if event.get("event") == "finish": 414 413 result = event.get("result", "") 415 414 ··· 458 457 original_request, 459 458 ) 460 459 461 - # Handle handoff from finish event 462 - handoff_config = event.get("handoff") 463 - if handoff_config: 464 - self._spawn_handoff( 465 - agent.agent_id, result, handoff_config 466 - ) 467 460 # Break to trigger cleanup 468 461 break 469 462 ··· 697 690 698 691 except Exception as e: 699 692 self.logger.error(f"Failed to write agent {agent_id} output: {e}") 700 - 701 - def _spawn_handoff( 702 - self, parent_id: str, result: str, handoff: Dict[str, Any] 703 - ) -> None: 704 - """Spawn a handoff agent from a completed agent's result.""" 705 - try: 706 - from think.cortex_client import cortex_request 707 - 708 - if not handoff: 709 - self.logger.debug( 710 - "No handoff configuration provided for agent %s", parent_id 711 - ) 712 - return 713 - 714 - # Operate on a copy so callers keep their original config untouched. 715 - handoff_config = copy.deepcopy(handoff) 716 - 717 - # Determine prompt/provider/name before pruning extra keys. 718 - prompt = handoff_config.pop("prompt", None) or result 719 - name = handoff_config.pop("name", None) or "unified" 720 - 721 - # Provider can be explicitly set in handoff config, otherwise let 722 - # the handoff agent resolve its own provider from context 723 - provider = handoff_config.pop("provider", None) 724 - 725 - # Ensure we do not propagate parent handoff metadata. 726 - handoff_config.pop("handoff", None) 727 - handoff_config.pop("handoff_from", None) 728 - handoff_config.pop("model", None) 729 - 730 - # Inherit env from parent if not explicitly set in handoff config 731 - if "env" not in handoff_config: 732 - with self.lock: 733 - parent_env = self.agent_requests.get(parent_id, {}).get("env") 734 - if parent_env: 735 - handoff_config["env"] = parent_env 736 - 737 - # Only pass through additional overrides if any remain. 738 - extra_config = handoff_config or None 739 - 740 - # Use cortex_request to create the handoff agent 741 - agent_id = cortex_request( 742 - prompt=prompt, 743 - name=name, 744 - provider=provider, 745 - handoff_from=parent_id, 746 - config=extra_config, 747 - ) 748 - 749 - if agent_id is None: 750 - self.logger.error(f"Failed to send handoff request from {parent_id}") 751 - return 752 - 753 - self.logger.info(f"Spawned handoff agent {agent_id} from {parent_id}") 754 - 755 - except Exception as e: 756 - self.logger.error(f"Failed to spawn handoff agent: {e}") 757 693 758 694 def stop(self) -> None: 759 695 """Stop the Cortex service."""
-5
think/cortex_client.py
··· 36 36 prompt: str, 37 37 name: str, 38 38 provider: Optional[str] = None, 39 - handoff_from: Optional[str] = None, 40 39 config: Optional[Dict[str, Any]] = None, 41 40 ) -> str | None: 42 41 """Create a Cortex agent request via Callosum broadcast. ··· 45 44 prompt: The task or question for the agent 46 45 name: Agent name - system (e.g., "unified") or app-qualified (e.g., "entities:entity_assist") 47 46 provider: AI provider - openai, google, or anthropic 48 - handoff_from: Previous agent ID if this is a handoff request 49 47 config: Provider-specific configuration (model, max_output_tokens, thinking_budget, etc.) 50 48 51 49 Returns: ··· 85 83 raise ValueError("config must be a dictionary") 86 84 # Merge config overrides directly into the request for a flat schema 87 85 request.update(config) 88 - 89 - if handoff_from: 90 - request["handoff_from"] = handoff_from 91 86 92 87 # Broadcast request to Callosum 93 88 # Note: callosum_send() signature is send(tract, event, **fields)