diff --git a/docs/APPS.md b/docs/APPS.md index 44c7dfda3..8cd3aa1b2 100644 --- a/docs/APPS.md +++ b/docs/APPS.md @@ -374,7 +374,7 @@ Define custom agents and generator templates that integrate with solstone's Cort - Agents have a `tools` field, generators have `schedule` but no `tools` - App agents/generators are automatically discovered alongside system ones - Keys are namespaced as `{app}:{name}` (e.g., `my_app:helper`) -- Agents inherit all system agent capabilities (tools, scheduling, handoffs, multi-facet) +- Agents inherit all system agent capabilities (tools, scheduling, multi-facet) **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. diff --git a/docs/CORTEX.md b/docs/CORTEX.md index 728bfbbd8..ba7901e97 100644 --- a/docs/CORTEX.md +++ b/docs/CORTEX.md @@ -49,13 +49,7 @@ Requests are created via `cortex_request()` from `think.cortex_client`, which br "env": { // Optional: environment variables for subprocess "API_KEY": "secret", "DEBUG": "true" - }, - "handoff": { // Optional: chain to another agent on completion - "name": "reviewer", - "prompt": "Review the analysis", - "provider": "openai" - }, - "handoff_from": "1234567890122" // Optional: present when spawned via handoff + } } ``` @@ -120,9 +114,7 @@ The initial spawn request (first line of file, written by client). "provider": "openai", "name": "default", "output": "md", - "day": "20250109", - "handoff": {}, - "handoff_from": "1234567890122" + "day": "20250109" } ``` @@ -197,12 +189,7 @@ Emitted when the agent run completes successfully. "event": "finish", "ts": 1234567890123, "agent_id": "1234567890123", - "result": "Final response text to the user", - "handoff": { // Optional: triggers next agent - "prompt": "Continue with next task", - "name": "specialist", - "provider": "openai" - } + "result": "Final response text to the user" } ``` @@ -247,19 +234,10 @@ When an agent completes successfully, its result can be automatically written to - Output path is derived from agent name + format + schedule: - Daily agents: `YYYYMMDD/agents/{name}.{ext}` - Segment agents: `YYYYMMDD/{segment}/{name}.{ext}` -- Writing occurs before any handoff processing +- Writing occurs before completion - Write failures are logged but don't interrupt the agent flow - Commonly used for scheduled agents that generate daily reports -## Agent Handoff - -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. - -- The `finish` event may include a `handoff` field specifying the next agent -- The subsequent request includes `handoff_from` with the originating agent ID -- Handoff agents automatically inherit the parent agent's configuration (provider, model, etc.) unless explicitly overridden -- This enables multi-step workflows and agent specialization with consistent configuration - ## Agent Configuration Agents use configurations stored in the `muse/` directory. Each agent is a `.md` file containing: @@ -298,7 +276,6 @@ The JSON frontmatter for an agent can include: - `env`: Environment variables to set for the agent subprocess (object) - Keys are variable names, values are coerced to strings - Request-level `env` overrides agent defaults - - Inherited by handoff agents unless explicitly overridden - Note: `_SOLSTONE_JOURNAL_OVERRIDE` is set by Cortex for child processes ### Model Resolution diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index 0955d2a46..f1f953c6c 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -817,7 +817,6 @@ Each line is a JSON object with an `event` field indicating the event type: - `text` – streaming text output from the agent - `tool_call` – agent invoked a tool - `tool_result` – result returned from tool execution -- `handoff` – agent delegated to another agent - `error` – error occurred during execution - `finish` – agent session completed, includes final result diff --git a/tests/test_cortex.py b/tests/test_cortex.py index 2339f0b51..94b143292 100644 --- a/tests/test_cortex.py +++ b/tests/test_cortex.py @@ -234,39 +234,6 @@ def test_spawn_generator_via_subprocess( mock_timer_instance.start.assert_called_once() -@patch("think.cortex.subprocess.Popen") -def test_spawn_subprocess_with_handoff_from(mock_popen, cortex_service, mock_journal): - """Test spawning an agent with handoff_from parameter.""" - mock_process = MagicMock() - mock_process.pid = 12345 - mock_process.stdin = MagicMock() - mock_process.stdout = MagicMock() - mock_process.stderr = MagicMock() - mock_popen.return_value = mock_process - - agent_id = "123456789" - file_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" - - request = { - "event": "request", - "ts": 123456789, - "prompt": "Test", - "provider": "openai", - "name": "unified", - "handoff_from": "parent123", - } - - with patch("think.cortex.threading.Thread"): - cortex_service._spawn_subprocess( - agent_id, file_path, request, ["sol", "agents"], "agent" - ) - - # Check handoff_from was included in NDJSON - written_data = mock_process.stdin.write.call_args[0][0] - ndjson = json.loads(written_data.strip()) - assert ndjson["handoff_from"] == "parent123" - - def test_monitor_stdout_json_events(cortex_service, mock_journal): """Test monitoring stdout with JSON events.""" from io import StringIO @@ -334,41 +301,6 @@ def test_monitor_stdout_non_json_output(cortex_service, mock_journal): assert "ts" in info_event -def test_monitor_stdout_with_handoff(cortex_service, mock_journal): - """Test monitoring stdout with handoff in finish event.""" - from io import StringIO - - from think.cortex import AgentProcess - - agent_id = "123456789" - log_path = mock_journal / "agents" / f"{agent_id}_active.jsonl" - - # Handoff config is now in the finish event itself - finish_event = { - "event": "finish", - "ts": 1234567890, - "result": "Create matter", - "handoff": {"name": "matter_editor", "facet": "test"}, - } - - mock_process = MagicMock() - mock_process.poll.return_value = 0 - mock_process.stdout = StringIO(json.dumps(finish_event) + "\n") - - agent = AgentProcess(agent_id, mock_process, log_path) - cortex_service.running_agents[agent_id] = agent - - with patch.object(cortex_service, "_spawn_handoff") as mock_handoff: - with patch.object(cortex_service, "_complete_agent_file"): - cortex_service._monitor_stdout(agent) - - mock_handoff.assert_called_once_with( - agent_id, - "Create matter", - {"name": "matter_editor", "facet": "test"}, - ) - - def test_monitor_stdout_no_finish_event(cortex_service, mock_journal): """Test monitoring stdout when process exits without finish event.""" from io import StringIO @@ -547,56 +479,6 @@ def test_write_error_and_complete(cortex_service, mock_journal): assert "ts" in error_event -def test_spawn_handoff(cortex_service, mock_journal): - """Test spawning a handoff agent.""" - parent_id = "parent123" - result = "Create a new matter for AI research" - handoff = { - "name": "matter_editor", - "provider": "anthropic", - "facet": "test", - "max_turns": 5, - } - - with patch("think.cortex_client.cortex_request") as mock_request: - mock_request.return_value = ( - mock_journal / "agents" / "987654321000_active.jsonl" - ) - cortex_service._spawn_handoff(parent_id, result, handoff) - - # Check cortex_request was called with correct parameters - mock_request.assert_called_once_with( - prompt=result, - name="matter_editor", - provider="anthropic", - handoff_from=parent_id, - config={"facet": "test", "max_turns": 5}, - ) - - -def test_spawn_handoff_with_explicit_prompt(cortex_service, mock_journal): - """Test spawning handoff with explicit prompt in config.""" - parent_id = "parent123" - result = "Parent result" - handoff = { - "name": "reviewer", - "prompt": "Review this analysis", # Explicit prompt - } - - with patch("think.cortex_client.cortex_request") as mock_request: - cortex_service._spawn_handoff(parent_id, result, handoff) - - # Check cortex_request was called with explicit prompt - # Provider is None when not explicitly set - let the agent resolve its own - mock_request.assert_called_once_with( - prompt="Review this analysis", # Uses explicit prompt - name="reviewer", - provider=None, - handoff_from=parent_id, - config=None, - ) - - def test_get_status(cortex_service): """Test getting service status.""" from think.cortex import AgentProcess diff --git a/tests/test_cortex_client.py b/tests/test_cortex_client.py index 299a99908..3982ae076 100644 --- a/tests/test_cortex_client.py +++ b/tests/test_cortex_client.py @@ -118,24 +118,6 @@ def test_cortex_request_returns_agent_id(callosum_server): assert len(agent_id) == 13 # Millisecond timestamp -def test_cortex_request_with_handoff(callosum_listener): - """Test cortex_request with handoff_from parameter.""" - messages = callosum_listener - - cortex_request( - prompt="Continue analysis", - name="reviewer", - provider="anthropic", - handoff_from="1234567890000", - ) - - time.sleep(0.2) - - msg = messages[0] - assert msg["handoff_from"] == "1234567890000" - assert msg["name"] == "reviewer" - - def test_cortex_request_unique_agent_ids(callosum_server): """Test that cortex_request generates unique agent IDs.""" _ = callosum_server # Needed for side effects only diff --git a/think/agents.py b/think/agents.py index 4246df0f1..e9de9f158 100644 --- a/think/agents.py +++ b/think/agents.py @@ -813,8 +813,6 @@ async def _execute_with_tools( data = {**data, "result": result} if output_path and result: _write_output(output_path, result) - if config.get("handoff"): - data = {**data, "handoff": config["handoff"]} # Filter out start events from providers (we already emitted ours) if data.get("event") == "start": @@ -1023,8 +1021,6 @@ async def _execute_generate( } if usage_data: finish_event["usage"] = usage_data - if config.get("handoff"): - finish_event["handoff"] = config["handoff"] emit_event(finish_event) diff --git a/think/cortex.py b/think/cortex.py index 4636aef72..af7610b38 100644 --- a/think/cortex.py +++ b/think/cortex.py @@ -17,7 +17,6 @@ real-time event distribution to all interested services. from __future__ import annotations -import copy import json import logging import os @@ -224,7 +223,7 @@ class CortexService: self.logger.info(f"Processing agent request: {agent_id}") - # Store request for later use (handoffs, output writing) + # Store request for later use (output writing) with self.lock: self.agent_requests[agent_id] = request @@ -409,7 +408,7 @@ class CortexService: # Handle finish or error event if event.get("event") in ["finish", "error"]: - # Check for output and handoff (only on finish) + # Check for output (only on finish) if event.get("event") == "finish": result = event.get("result", "") @@ -458,12 +457,6 @@ class CortexService: original_request, ) - # Handle handoff from finish event - handoff_config = event.get("handoff") - if handoff_config: - self._spawn_handoff( - agent.agent_id, result, handoff_config - ) # Break to trigger cleanup break @@ -698,63 +691,6 @@ class CortexService: except Exception as e: self.logger.error(f"Failed to write agent {agent_id} output: {e}") - def _spawn_handoff( - self, parent_id: str, result: str, handoff: Dict[str, Any] - ) -> None: - """Spawn a handoff agent from a completed agent's result.""" - try: - from think.cortex_client import cortex_request - - if not handoff: - self.logger.debug( - "No handoff configuration provided for agent %s", parent_id - ) - return - - # Operate on a copy so callers keep their original config untouched. - handoff_config = copy.deepcopy(handoff) - - # Determine prompt/provider/name before pruning extra keys. - prompt = handoff_config.pop("prompt", None) or result - name = handoff_config.pop("name", None) or "unified" - - # Provider can be explicitly set in handoff config, otherwise let - # the handoff agent resolve its own provider from context - provider = handoff_config.pop("provider", None) - - # Ensure we do not propagate parent handoff metadata. - handoff_config.pop("handoff", None) - handoff_config.pop("handoff_from", None) - handoff_config.pop("model", None) - - # Inherit env from parent if not explicitly set in handoff config - if "env" not in handoff_config: - with self.lock: - parent_env = self.agent_requests.get(parent_id, {}).get("env") - if parent_env: - handoff_config["env"] = parent_env - - # Only pass through additional overrides if any remain. - extra_config = handoff_config or None - - # Use cortex_request to create the handoff agent - agent_id = cortex_request( - prompt=prompt, - name=name, - provider=provider, - handoff_from=parent_id, - config=extra_config, - ) - - if agent_id is None: - self.logger.error(f"Failed to send handoff request from {parent_id}") - return - - self.logger.info(f"Spawned handoff agent {agent_id} from {parent_id}") - - except Exception as e: - self.logger.error(f"Failed to spawn handoff agent: {e}") - def stop(self) -> None: """Stop the Cortex service.""" self.stop_event.set() diff --git a/think/cortex_client.py b/think/cortex_client.py index 5729d17d2..26429c6c0 100644 --- a/think/cortex_client.py +++ b/think/cortex_client.py @@ -36,7 +36,6 @@ def cortex_request( prompt: str, name: str, provider: Optional[str] = None, - handoff_from: Optional[str] = None, config: Optional[Dict[str, Any]] = None, ) -> str | None: """Create a Cortex agent request via Callosum broadcast. @@ -45,7 +44,6 @@ def cortex_request( prompt: The task or question for the agent name: Agent name - system (e.g., "unified") or app-qualified (e.g., "entities:entity_assist") provider: AI provider - openai, google, or anthropic - handoff_from: Previous agent ID if this is a handoff request config: Provider-specific configuration (model, max_output_tokens, thinking_budget, etc.) Returns: @@ -86,9 +84,6 @@ def cortex_request( # Merge config overrides directly into the request for a flat schema request.update(config) - if handoff_from: - request["handoff_from"] = handoff_from - # Broadcast request to Callosum # Note: callosum_send() signature is send(tract, event, **fields) # Remove "event" from request dict to avoid conflict