diff --git a/codex/hooks/setup.py b/codex/hooks/setup.py index bc9db706..abfecafb 100644 --- a/codex/hooks/setup.py +++ b/codex/hooks/setup.py @@ -464,6 +464,30 @@ def configure_codex_hooks() -> bool: } ] } + ], + "SubagentStart": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": hook_command, + "timeout": 60 + } + ] + } + ], + "SubagentStop": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": hook_command, + "timeout": 60 + } + ] + } ] } diff --git a/codex/hooks/unbound.py b/codex/hooks/unbound.py index c49db28c..deb96ad5 100644 --- a/codex/hooks/unbound.py +++ b/codex/hooks/unbound.py @@ -1083,6 +1083,28 @@ def process_stop_event(event: Dict, api_key: str): # Parse tool uses from Codex transcript (function_call/function_call_output pairs) assistant_tool_uses = parse_codex_transcript_for_tools(transcript_path, user_prompt_timestamp) + # Fold in subagent invocations from SubagentStop hook events. Codex subagents + # run in their own transcript and don't appear as function_calls in the parent + # rollout, so we recover them from the accumulated hook events (same session, + # after the user prompt) and forward them as Task tool_uses for the gateway. + for log in logs: + log_session_id = log.get('session_id') or log.get('event', {}).get('session_id') + if log_session_id != session_id: + continue + log_event = log.get('event', {}) if 'event' in log else log + if log_event.get('hook_event_name') != 'SubagentStop': + continue + if user_prompt_timestamp and log.get('timestamp') and log.get('timestamp') <= user_prompt_timestamp: + continue + assistant_tool_uses.append({ + 'type': 'SubagentStop', + 'tool_name': 'Task', + 'tool_input': {'subagent_type': log_event.get('agent_type')}, + 'tool_response': { + 'last_assistant_message': log_event.get('last_assistant_message'), + }, + }) + assistant_msg = { 'role': 'assistant', 'content': last_assistant_message or '' @@ -1427,13 +1449,13 @@ def main(): input_data = sys.stdin.read().strip() if not input_data: - print('{"suppressOutput": true}', flush=True) + print("{}", flush=True) return try: event = json.loads(input_data) except json.JSONDecodeError: - print('{"suppressOutput": true}', flush=True) + print("{}", flush=True) return hook_event_name = event.get('hook_event_name') @@ -1465,7 +1487,8 @@ def main(): 'session_id': event.get('session_id'), 'event': event }) - response["suppressOutput"] = True + # Codex does not support suppressOutput — emit the plain response. + response.pop("suppressOutput", None) print(json.dumps(response), flush=True) return @@ -1485,12 +1508,12 @@ def main(): cleanup_old_logs() - print('{"suppressOutput": true}', flush=True) + print("{}", flush=True) except Exception as e: # Still return empty JSON object to Codex to indicate completion log_error(f"Exception in main: {str(e)}", 'general') - print('{"suppressOutput": true}', flush=True) + print("{}", flush=True) if __name__ == '__main__': diff --git a/cursor/hooks.json b/cursor/hooks.json index 3b88b096..3902a930 100644 --- a/cursor/hooks.json +++ b/cursor/hooks.json @@ -54,6 +54,16 @@ "command": "./hooks/unbound.py" } ], + "subagentStart": [ + { + "command": "./hooks/unbound.py" + } + ], + "subagentStop": [ + { + "command": "./hooks/unbound.py" + } + ], "stop": [ { "command": "./hooks/unbound.py" diff --git a/cursor/unbound.py b/cursor/unbound.py index bbb848c5..432dd248 100644 --- a/cursor/unbound.py +++ b/cursor/unbound.py @@ -1003,12 +1003,28 @@ def build_llm_exchange(events, api_key=None): elif hook_event_name == 'afterAgentResponse': assistant_response = event.get('text') - + + elif hook_event_name == 'subagentStop': + assistant_tool_uses.append({ + 'type': 'subagentStop', + 'tool_name': 'Task', + 'tool_input': { + 'subagent_type': event.get('subagent_type'), + 'task': event.get('task'), + }, + 'tool_response': { + 'status': event.get('status'), + 'summary': event.get('summary'), + 'duration_ms': event.get('duration_ms'), + 'tool_call_count': event.get('tool_call_count'), + }, + }) + if user_prompt: messages.append({'role': 'user', 'content': user_prompt}) - - if assistant_response: - assistant_msg = {'role': 'assistant', 'content': assistant_response} + + if assistant_response or assistant_tool_uses: + assistant_msg = {'role': 'assistant', 'content': assistant_response or ''} if assistant_tool_uses: assistant_msg['tool_use'] = assistant_tool_uses messages.append(assistant_msg) @@ -1157,7 +1173,16 @@ def process_stop_event(generation_id, api_key=None): ) if has_stop: - exchange = build_llm_exchange(events, api_key) + turn_start = min((l.get('timestamp', '') for l in events), default='') + subagent_events = [ + log + for gen_events in generations.values() + for log in gen_events + if log.get('event', {}).get('hook_event_name') + in ('subagentStart', 'subagentStop') + and (not turn_start or log.get('timestamp', '') >= turn_start) + ] + exchange = build_llm_exchange(events + subagent_events, api_key) if exchange: send_to_api(exchange, api_key) break