From 635306d35083d06418c21499357c4fce468465e6 Mon Sep 17 00:00:00 2001 From: Tucker Date: Thu, 9 Mar 2023 17:10:30 -0500 Subject: [PATCH 1/8] Fix SMARTS ignores social agent start time. --- scenarios/sumo/zoo_intersection/scenario.py | 16 ++-- smarts/core/agent_manager.py | 66 +++++++++----- smarts/core/trap_manager.py | 97 ++++++++++++++++----- smarts/env/tests/test_social_agent.py | 55 ++++++------ 4 files changed, 156 insertions(+), 78 deletions(-) diff --git a/scenarios/sumo/zoo_intersection/scenario.py b/scenarios/sumo/zoo_intersection/scenario.py index b10fd43587..c31b810948 100644 --- a/scenarios/sumo/zoo_intersection/scenario.py +++ b/scenarios/sumo/zoo_intersection/scenario.py @@ -86,17 +86,19 @@ social_agent_missions={ f"s-agent-{social_agent2.name}": ( [social_agent2], - [Mission(RandomRoute())], - ), - f"s-agent-{social_agent1.name}": ( - [social_agent1], [ - EndlessMission(begin=("edge-south-SN", 0, 30)), Mission( Route( - begin=("edge-west-WE", 0, 10), end=("edge-east-WE", 0, 10) - ) + begin=("edge-south-SN", 0, 30), end=("edge-east-WE", 0, 10) + ), ), + ], + ), + f"s-agent-{social_agent1.name}": ( + [social_agent1], + [ + EndlessMission(begin=("edge-south-SN", 0, 10), start_time=0.7), + ], ), }, diff --git a/smarts/core/agent_manager.py b/smarts/core/agent_manager.py index 1299ecd0c7..5759093953 100644 --- a/smarts/core/agent_manager.py +++ b/smarts/core/agent_manager.py @@ -61,6 +61,7 @@ def __init__(self, sim, interfaces, zoo_addrs=None): # would not be included self._initial_interfaces = interfaces self._pending_agent_ids = set() + self._pending_social_agent_ids = set() # Agent interfaces are interfaces for _all_ active agents self._agent_interfaces = {} @@ -118,6 +119,11 @@ def pending_agent_ids(self) -> Set[str]: """The IDs of agents that are waiting to enter the simulation""" return self._pending_agent_ids + @property + def pending_social_agent_ids(self) -> Set[str]: + """The IDs of social agents that are waiting to enter the simulation""" + return self._pending_social_agent_ids + @property def active_agents(self) -> Set[str]: """A list of all active agents in the simulation (agents that have a vehicle.)""" @@ -460,28 +466,7 @@ def _setup_social_agents(self): sim = self._sim() assert sim social_agents = sim.scenario.social_agents - if social_agents: - self._setup_agent_buffer() - else: - return - - self._remote_social_agents = { - agent_id: self._agent_buffer.acquire_agent() for agent_id in social_agents - } - - for agent_id, (social_agent, social_agent_model) in social_agents.items(): - self._add_agent( - agent_id, - social_agent.interface, - social_agent_model, - trainable=False, - # XXX: Currently boids can only be run from bubbles - boid=False, - ) - self._social_agent_ids.add(agent_id) - - for social_agent_id, remote_social_agent in self._remote_social_agents.items(): - remote_social_agent.start(social_agents[social_agent_id][0]) + self._pending_social_agent_ids.update(social_agents.keys()) def _start_keep_alive_boid_agents(self): """Configures and adds boid agents to the sim.""" @@ -510,6 +495,43 @@ def _start_keep_alive_boid_agents(self): ) self.start_social_agent(agent_id, social_agent, social_agent_data_model) + def add_and_emit_social_agent( + self, agent_id: str, agent_spec, agent_model: SocialAgent + ): + """Generates an entirely new social agent and emits a vehicle for it immediately. + + This + + Args: + agent_id (str): The agent id for the new agent. + agent_spec (AgentSpec): The agent spec of the new agent + agent_model (SocialAgent): The agent configuration of the new vehicle. + Returns: + bool: + If the agent is added. False if the agent id is already reserved + by a pending ego agent or current social/ego agent. + """ + if agent_id in self.agent_ids or agent_id in self.pending_agent_ids: + return False + + self._setup_agent_buffer() + remote_agent = self._agent_buffer.acquire_agent() + self._add_agent( + agent_id=agent_id, + agent_interface=agent_spec.interface, + agent_model=agent_model, + trainable=False, + boid=False, + ) + if agent_id in self._pending_social_agent_ids: + self._pending_social_agent_ids.remove(agent_id) + remote_agent.start(agent_spec=agent_spec) + self._remote_social_agents[agent_id] = remote_agent + self._agent_interfaces[agent_id] = agent_spec.interface + self._social_agent_ids.add(agent_id) + self._social_agent_data_models[agent_id] = agent_model + return True + def _add_agent( self, agent_id, agent_interface, agent_model, boid=False, trainable=True ): diff --git a/smarts/core/trap_manager.py b/smarts/core/trap_manager.py index 5d42aa0f9a..936de8b2d7 100644 --- a/smarts/core/trap_manager.py +++ b/smarts/core/trap_manager.py @@ -164,7 +164,10 @@ def step(self, sim): captures_by_agent_id = defaultdict(list) # Do an optimization to only check if there are pending agents. - if not sim.agent_manager.pending_agent_ids: + if ( + not sim.agent_manager.pending_agent_ids + | sim.agent_manager.pending_social_agent_ids + ): return social_vehicle_ids: List[str] = [ @@ -184,7 +187,10 @@ def largest_vehicle_plane_dimension(vehicle: Vehicle): for v in vehicles.values() ] - for agent_id in sim.agent_manager.pending_agent_ids: + for agent_id in ( + sim.agent_manager.pending_agent_ids + | sim.agent_manager.pending_social_agent_ids + ): trap = self._traps.get(agent_id) if trap is None: @@ -224,29 +230,32 @@ def largest_vehicle_plane_dimension(vehicle: Vehicle): ), ) ) - # TODO: Resolve overlap using a tree instead of just removing. - social_vehicle_ids.remove(v_id) break - # Use fed in trapped vehicles. - agents_given_vehicle = set() used_traps = [] - for agent_id in sim._agent_manager.pending_agent_ids: - if agent_id not in self._traps: - continue - - trap = self._traps[agent_id] + for agent_id in ( + sim.agent_manager.pending_agent_ids + | sim.agent_manager.pending_social_agent_ids + ): + trap = self._traps.get(agent_id) - captures = captures_by_agent_id[agent_id] + if trap is None: + continue if not trap.ready(sim.elapsed_sim_time): continue + captures = captures_by_agent_id[agent_id] + vehicle = None if len(captures) > 0: vehicle_id, trap, mission = rand.choice(captures) - vehicle = sim.switch_control_to_agent( - vehicle_id, agent_id, mission, recreate=True, is_hijacked=False + vehicle = self._take_existing_vehicle( + sim, + vehicle_id, + agent_id, + mission, + social=agent_id in sim.agent_manager.pending_social_agent_ids, ) elif trap.patience_expired(sim.elapsed_sim_time): # Make sure there is not a vehicle in the same location @@ -265,20 +274,21 @@ def largest_vehicle_plane_dimension(vehicle: Vehicle): if overlapping: continue - vehicle = TrapManager._make_vehicle( - sim, agent_id, mission, trap.default_entry_speed + vehicle = TrapManager._make_new_vehicle( + sim, + agent_id, + mission, + trap.default_entry_speed, + social=agent_id in sim.agent_manager.pending_social_agent_ids, ) else: continue - if vehicle == None: + if vehicle is None: continue - sim.create_vehicle_in_providers(vehicle, agent_id, True) - agents_given_vehicle.add(agent_id) used_traps.append((agent_id, trap)) - if len(agents_given_vehicle) > 0: + if len(used_traps) > 0: self.remove_traps(used_traps) - sim.agent_manager.remove_pending_agent_ids(agents_given_vehicle) @property def traps(self) -> Dict[str, Trap]: @@ -286,7 +296,28 @@ def traps(self) -> Dict[str, Trap]: return self._traps @staticmethod - def _make_vehicle(sim, agent_id, mission, initial_speed): + def _take_existing_vehicle(sim, vehicle_id, agent_id, mission, social=False): + from smarts.core.smarts import SMARTS + + assert isinstance(sim, SMARTS) + if social: + # Not supported + return None + vehicle = sim.switch_control_to_agent( + vehicle_id, agent_id, mission, recreate=True, is_hijacked=False + ) + if vehicle is not None: + sim.agent_manager.remove_pending_agent_ids({agent_id}) + sim.create_vehicle_in_providers(vehicle, agent_id, True) + return vehicle + + @staticmethod + def _make_new_vehicle(sim, agent_id, mission, initial_speed, social=False): + from smarts.core.smarts import SMARTS + + assert isinstance(sim, SMARTS) + if social: + return TrapManager._make_new_social_vehicle(sim, agent_id, initial_speed) agent_interface = sim.agent_manager.agent_interface_for_agent_id(agent_id) plan = Plan(sim.road_map, mission) # 3. Apply agent vehicle association. @@ -302,11 +333,31 @@ def _make_vehicle(sim, agent_id, mission, initial_speed): initial_speed=initial_speed, boid=False, ) + if vehicle is not None: + sim.agent_manager.remove_pending_agent_ids({agent_id}) + sim.create_vehicle_in_providers(vehicle, agent_id, True) return vehicle + @staticmethod + def _make_new_social_vehicle(sim, agent_id, initial_speed): + from smarts.core.smarts import SMARTS + + sim: SMARTS = sim + social_agent_spec, social_agent_model = sim.scenario.social_agents[agent_id] + + social_agent_model = replace(social_agent_model, initial_speed=initial_speed) + sim.agent_manager.add_and_emit_social_agent( + agent_id, + social_agent_spec, + social_agent_model, + ) + vehicles = sim.vehicle_index.vehicles_by_actor_id(agent_id) + + return vehicles[0] if len(vehicles) else None + def reset(self): """Resets to a pre-initialized state.""" - self.captures_by_agent_id = defaultdict(list) + pass def teardown(self): """Clear internal state""" diff --git a/smarts/env/tests/test_social_agent.py b/smarts/env/tests/test_social_agent.py index f897c3de5b..8ca7ce0f47 100644 --- a/smarts/env/tests/test_social_agent.py +++ b/smarts/env/tests/test_social_agent.py @@ -25,28 +25,25 @@ from smarts.core.agent import Agent from smarts.core.agent_interface import AgentInterface, AgentType from smarts.core.utils.episodes import episodes -from smarts.zoo.agent_spec import AgentSpec +from smarts.env.hiway_env import HiWayEnv AGENT_ID = "Agent-007" SOCIAL_AGENT_ID = "Alec Trevelyan" -MAX_EPISODES = 3 +MAX_EPISODES = 1 @pytest.fixture -def agent_spec(): - return AgentSpec( - interface=AgentInterface.from_type(AgentType.Laner, max_episode_steps=100), - agent_builder=lambda: Agent.from_function(lambda _: "keep_lane"), - ) +def agent_interface(): + return AgentInterface.from_type(AgentType.Laner, max_episode_steps=100, neighborhood_vehicle_states=True) @pytest.fixture -def env(agent_spec): +def env(agent_interface: AgentInterface): env = gym.make( "smarts.env:hiway-v0", scenarios=["scenarios/sumo/zoo_intersection"], - agent_specs={AGENT_ID: agent_spec}, + agent_interfaces={AGENT_ID: agent_interface}, headless=True, visdom=False, fixed_timestep_sec=0.01, @@ -56,29 +53,35 @@ def env(agent_spec): env.close() -def test_social_agents(env, agent_spec): - episode = None - for episode in episodes(n=MAX_EPISODES): - agent = agent_spec.build_agent() +def test_social_agents_not_in_env_obs_keys(env: HiWayEnv): + for _ in range(MAX_EPISODES): observations = env.reset() - episode.record_scenario(env.scenario_log) dones = {"__all__": False} while not dones["__all__"]: - obs = observations[AGENT_ID] - observations, rewards, dones, infos = env.step({AGENT_ID: agent.act(obs)}) - episode.record_step(observations, rewards, dones, infos) + observations, rewards, dones, infos = env.step({AGENT_ID: "keep_lane"}) assert SOCIAL_AGENT_ID not in observations assert SOCIAL_AGENT_ID not in dones - # Reward is currently the delta in distance travelled by this agent. - # We want to make sure that this is infact a delta and not total distance - # travelled since this bug has appeared a few times. - # - # The way to verify this is by making sure the reward does not grow without bounds - assert -3 < rewards[AGENT_ID] < 3 - assert episode is not None and episode.index == ( - MAX_EPISODES - 1 - ), "Simulation must cycle through to the final episode" +def test_social_agents_in_env_neighborhood_vehicle_obs(env: HiWayEnv, agent_interface: AgentInterface): + first_seen_vehicles = {} + for _ in range(MAX_EPISODES): + observations = env.reset() + + dones = {"__all__": False} + while not dones["__all__"]: + observations, rewards, dones, infos = env.step({AGENT_ID: "keep_lane"}) + + new_nvs_ids = [nvs.id for nvs in observations[AGENT_ID].neighborhood_vehicle_states if nvs.id not in first_seen_vehicles] + for v_id in new_nvs_ids: + first_seen_vehicles[v_id] = observations[AGENT_ID].step_count + 1 + print(first_seen_vehicles) + print() + + seen_zoo_social_vehicles = [v_id for v_id in first_seen_vehicles if "zoo" in v_id] + assert len(seen_zoo_social_vehicles) == 2 + late_entry = next((v_id for v_id in seen_zoo_social_vehicles if "zoo-car1" in v_id), None) + assert late_entry is not None, seen_zoo_social_vehicles + assert first_seen_vehicles[late_entry] == 70 \ No newline at end of file From c1b8a5f81a673d70d8503c7d109cf157d6a25994 Mon Sep 17 00:00:00 2001 From: Tucker Date: Thu, 9 Mar 2023 17:12:31 -0500 Subject: [PATCH 2/8] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e12bb34f..309e2cdafc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Copy and pasting the git commit messages is __NOT__ enough. ### Changed ### Deprecated ### Fixed +- Fixed an issue with SMARTS where the social vehicles started instantly regardless of what mission start time they were given. ### Removed ### Security From e0153fac61b83fd6cfcdbec1244194711c840ab6 Mon Sep 17 00:00:00 2001 From: Tucker Date: Thu, 9 Mar 2023 17:14:33 -0500 Subject: [PATCH 3/8] Fix formatting. --- smarts/core/agent_manager.py | 2 -- smarts/env/tests/test_social_agent.py | 22 +++++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/smarts/core/agent_manager.py b/smarts/core/agent_manager.py index 5759093953..ba05996ec2 100644 --- a/smarts/core/agent_manager.py +++ b/smarts/core/agent_manager.py @@ -500,8 +500,6 @@ def add_and_emit_social_agent( ): """Generates an entirely new social agent and emits a vehicle for it immediately. - This - Args: agent_id (str): The agent id for the new agent. agent_spec (AgentSpec): The agent spec of the new agent diff --git a/smarts/env/tests/test_social_agent.py b/smarts/env/tests/test_social_agent.py index 8ca7ce0f47..087b330b28 100644 --- a/smarts/env/tests/test_social_agent.py +++ b/smarts/env/tests/test_social_agent.py @@ -35,7 +35,9 @@ @pytest.fixture def agent_interface(): - return AgentInterface.from_type(AgentType.Laner, max_episode_steps=100, neighborhood_vehicle_states=True) + return AgentInterface.from_type( + AgentType.Laner, max_episode_steps=100, neighborhood_vehicle_states=True + ) @pytest.fixture @@ -65,7 +67,9 @@ def test_social_agents_not_in_env_obs_keys(env: HiWayEnv): assert SOCIAL_AGENT_ID not in dones -def test_social_agents_in_env_neighborhood_vehicle_obs(env: HiWayEnv, agent_interface: AgentInterface): +def test_social_agents_in_env_neighborhood_vehicle_obs( + env: HiWayEnv, agent_interface: AgentInterface +): first_seen_vehicles = {} for _ in range(MAX_EPISODES): observations = env.reset() @@ -74,14 +78,18 @@ def test_social_agents_in_env_neighborhood_vehicle_obs(env: HiWayEnv, agent_inte while not dones["__all__"]: observations, rewards, dones, infos = env.step({AGENT_ID: "keep_lane"}) - new_nvs_ids = [nvs.id for nvs in observations[AGENT_ID].neighborhood_vehicle_states if nvs.id not in first_seen_vehicles] + new_nvs_ids = [ + nvs.id + for nvs in observations[AGENT_ID].neighborhood_vehicle_states + if nvs.id not in first_seen_vehicles + ] for v_id in new_nvs_ids: first_seen_vehicles[v_id] = observations[AGENT_ID].step_count + 1 - print(first_seen_vehicles) - print() seen_zoo_social_vehicles = [v_id for v_id in first_seen_vehicles if "zoo" in v_id] assert len(seen_zoo_social_vehicles) == 2 - late_entry = next((v_id for v_id in seen_zoo_social_vehicles if "zoo-car1" in v_id), None) + late_entry = next( + (v_id for v_id in seen_zoo_social_vehicles if "zoo-car1" in v_id), None + ) assert late_entry is not None, seen_zoo_social_vehicles - assert first_seen_vehicles[late_entry] == 70 \ No newline at end of file + assert first_seen_vehicles[late_entry] == 70 From e4dc8bebfb146ed33fe5377dafcaeae66b78d9ea Mon Sep 17 00:00:00 2001 From: Tucker Date: Thu, 9 Mar 2023 17:21:27 -0500 Subject: [PATCH 4/8] Format scenarios. --- scenarios/sumo/zoo_intersection/scenario.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scenarios/sumo/zoo_intersection/scenario.py b/scenarios/sumo/zoo_intersection/scenario.py index c31b810948..fbab61c2b7 100644 --- a/scenarios/sumo/zoo_intersection/scenario.py +++ b/scenarios/sumo/zoo_intersection/scenario.py @@ -92,13 +92,12 @@ begin=("edge-south-SN", 0, 30), end=("edge-east-WE", 0, 10) ), ), - ], + ], ), f"s-agent-{social_agent1.name}": ( [social_agent1], [ EndlessMission(begin=("edge-south-SN", 0, 10), start_time=0.7), - ], ), }, From 2a15bbac666b92ae987523f3db601d9b7cb08e24 Mon Sep 17 00:00:00 2001 From: Tucker Date: Thu, 9 Mar 2023 17:44:36 -0500 Subject: [PATCH 5/8] Fix tests. --- smarts/core/smarts.py | 5 ++++- smarts/core/trap_manager.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/smarts/core/smarts.py b/smarts/core/smarts.py index dda381bb97..a2540d0cfd 100644 --- a/smarts/core/smarts.py +++ b/smarts/core/smarts.py @@ -562,7 +562,10 @@ def switch_control_to_agent( ), f"Vehicle has already been hijacked: {vehicle_id}" assert ( not vehicle_id in self.vehicle_index.agent_vehicle_ids() - ), f"Can't hijack vehicle that is already controlled by an agent: {vehicle_id}" + ), ( + f"`{agent_id}` can't hijack vehicle that is already controlled by an agent" + f" `{self.vehicle_index.actor_id_from_vehicle_id(vehicle_id)}`: {vehicle_id}" + ) # Switch control to agent plan = Plan(self.road_map, mission) diff --git a/smarts/core/trap_manager.py b/smarts/core/trap_manager.py index 936de8b2d7..0159c7d5ee 100644 --- a/smarts/core/trap_manager.py +++ b/smarts/core/trap_manager.py @@ -230,6 +230,7 @@ def largest_vehicle_plane_dimension(vehicle: Vehicle): ), ) ) + social_vehicle_ids.remove(v_id) break used_traps = [] From b89ed67ddae4ec56b234e7d70356fd3276c51d7e Mon Sep 17 00:00:00 2001 From: Tucker Date: Thu, 9 Mar 2023 17:44:58 -0500 Subject: [PATCH 6/8] Format smarts. --- smarts/core/smarts.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/smarts/core/smarts.py b/smarts/core/smarts.py index a2540d0cfd..bb05836e47 100644 --- a/smarts/core/smarts.py +++ b/smarts/core/smarts.py @@ -560,9 +560,7 @@ def switch_control_to_agent( assert not self.vehicle_index.vehicle_is_hijacked( vehicle_id ), f"Vehicle has already been hijacked: {vehicle_id}" - assert ( - not vehicle_id in self.vehicle_index.agent_vehicle_ids() - ), ( + assert not vehicle_id in self.vehicle_index.agent_vehicle_ids(), ( f"`{agent_id}` can't hijack vehicle that is already controlled by an agent" f" `{self.vehicle_index.actor_id_from_vehicle_id(vehicle_id)}`: {vehicle_id}" ) From 6affe4507a7c20bda216f214e99e7020cda09f52 Mon Sep 17 00:00:00 2001 From: Montgomery Alban Date: Fri, 10 Mar 2023 02:09:32 +0000 Subject: [PATCH 7/8] Fix typing. --- smarts/core/scenario.py | 2 +- smarts/core/trap_manager.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/smarts/core/scenario.py b/smarts/core/scenario.py index b96fd22fc7..b1079c8297 100644 --- a/smarts/core/scenario.py +++ b/smarts/core/scenario.py @@ -1025,7 +1025,7 @@ def missions(self) -> Dict[str, Mission]: return self._missions @property - def social_agents(self) -> Dict[str, SocialAgent]: + def social_agents(self) -> Dict[str, Tuple[Any, SocialAgent]]: """Managed social agents within this scenario.""" return self._social_agents diff --git a/smarts/core/trap_manager.py b/smarts/core/trap_manager.py index 0159c7d5ee..a3f6d01314 100644 --- a/smarts/core/trap_manager.py +++ b/smarts/core/trap_manager.py @@ -22,7 +22,7 @@ import random as rand from collections import defaultdict from dataclasses import dataclass -from typing import Dict, List, Sequence, Set +from typing import Dict, List, Optional, Sequence, Set, Tuple from shapely.geometry import Polygon @@ -161,7 +161,9 @@ def step(self, sim): from smarts.core.smarts import SMARTS assert isinstance(sim, SMARTS) - captures_by_agent_id = defaultdict(list) + captures_by_agent_id: Dict[str, List[Tuple[str, Trap, Mission]]] = defaultdict( + list + ) # Do an optimization to only check if there are pending agents. if ( @@ -248,7 +250,7 @@ def largest_vehicle_plane_dimension(vehicle: Vehicle): captures = captures_by_agent_id[agent_id] - vehicle = None + vehicle: Optional[Vehicle] = None if len(captures) > 0: vehicle_id, trap, mission = rand.choice(captures) vehicle = self._take_existing_vehicle( @@ -297,7 +299,9 @@ def traps(self) -> Dict[str, Trap]: return self._traps @staticmethod - def _take_existing_vehicle(sim, vehicle_id, agent_id, mission, social=False): + def _take_existing_vehicle( + sim, vehicle_id, agent_id, mission, social=False + ) -> Optional[Vehicle]: from smarts.core.smarts import SMARTS assert isinstance(sim, SMARTS) @@ -313,7 +317,9 @@ def _take_existing_vehicle(sim, vehicle_id, agent_id, mission, social=False): return vehicle @staticmethod - def _make_new_vehicle(sim, agent_id, mission, initial_speed, social=False): + def _make_new_vehicle( + sim, agent_id, mission, initial_speed, social=False + ) -> Optional[Vehicle]: from smarts.core.smarts import SMARTS assert isinstance(sim, SMARTS) From 238fccf9fbfc1768f44cda8ae766d4407abbae29 Mon Sep 17 00:00:00 2001 From: Montgomery Alban Date: Fri, 10 Mar 2023 19:42:20 +0000 Subject: [PATCH 8/8] Update changelog. --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 498d8b42cc..e6075760ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Copy and pasting the git commit messages is __NOT__ enough. ## [Unreleased] ### Added +- Agent manager now has `add_and_emit_social_agent` to generate a new social agent that is immediately in control of a vehicle. ### Changed ### Deprecated ### Fixed @@ -483,7 +484,7 @@ the missions for all agents. ### Changed – Note any changes to the software’s existing functionality. ### Deprecated -– Note any features that were once stable but are no longer and have thus been removed. +– Note any features that were once stable but are no longer and have thus been scheduled for removal. ### Fixed – List any bugs or errors that have been fixed in a change. ### Removed