klaude-code 1.2.4__py3-none-any.whl → 1.2.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,8 +10,9 @@ from rich.text import Text
10
10
  from klaude_code import ui
11
11
  from klaude_code.command import has_interactive_command
12
12
  from klaude_code.config import Config, load_config
13
- from klaude_code.core.agent import DefaultModelProfileProvider, VanillaModelProfileProvider
14
- from klaude_code.core.executor import Executor, LLMClients
13
+ from klaude_code.core.agent import Agent, DefaultModelProfileProvider, VanillaModelProfileProvider
14
+ from klaude_code.core.executor import Executor
15
+ from klaude_code.core.manager import build_llm_clients
15
16
  from klaude_code.core.tool import SkillLoader, SkillTool
16
17
  from klaude_code.protocol import events, op
17
18
  from klaude_code.protocol.model import UserInputPayload
@@ -103,7 +104,7 @@ async def initialize_app_components(init_config: AppInitConfig) -> AppComponents
103
104
  # Initialize LLM clients
104
105
  try:
105
106
  enabled_sub_agents = [p.name for p in iter_sub_agent_profiles()]
106
- llm_clients = LLMClients.from_config(
107
+ llm_clients = build_llm_clients(
107
108
  config,
108
109
  model_override=init_config.model,
109
110
  enabled_sub_agents=enabled_sub_agents,
@@ -239,7 +240,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
239
240
 
240
241
  # Create status provider for bottom toolbar
241
242
  def _status_provider() -> REPLStatusSnapshot:
242
- agent = None
243
+ agent: Agent | None = None
243
244
  if session_id and session_id in components.executor.context.active_agents:
244
245
  agent = components.executor.context.active_agents[session_id]
245
246
 
@@ -9,88 +9,67 @@ from __future__ import annotations
9
9
 
10
10
  import asyncio
11
11
  from dataclasses import dataclass
12
- from dataclasses import field as dataclass_field
13
12
 
14
13
  from klaude_code.command import InputAction, InputActionType, dispatch_command
15
- from klaude_code.config import Config, load_config
16
14
  from klaude_code.core.agent import Agent, DefaultModelProfileProvider, ModelProfileProvider
15
+ from klaude_code.core.manager import AgentManager, LLMClients, SubAgentManager
17
16
  from klaude_code.core.tool import current_run_subtask_callback
18
- from klaude_code.llm.client import LLMClientABC
19
- from klaude_code.llm.registry import create_llm_client
20
- from klaude_code.protocol import commands, events, model, op
17
+ from klaude_code.protocol import events, model, op
21
18
  from klaude_code.protocol.op_handler import OperationHandler
22
- from klaude_code.protocol.sub_agent import SubAgentResult, get_sub_agent_profile
23
- from klaude_code.protocol.tools import SubAgentType
24
- from klaude_code.session.session import Session
19
+ from klaude_code.protocol.sub_agent import SubAgentResult
25
20
  from klaude_code.trace import DebugType, log_debug
26
21
 
27
22
 
28
23
  @dataclass
29
- class LLMClients:
30
- """Container for LLM clients used by main agent and sub-agents."""
31
-
32
- main: LLMClientABC
33
- sub_clients: dict[SubAgentType, LLMClientABC] = dataclass_field(default_factory=lambda: {})
34
-
35
- def get_client(self, sub_agent_type: SubAgentType | None = None) -> LLMClientABC:
36
- """Get client for given sub-agent type, or main client if None."""
37
- if sub_agent_type is None:
38
- return self.main
39
- return self.sub_clients.get(sub_agent_type) or self.main
40
-
41
- @classmethod
42
- def from_config(
43
- cls,
44
- config: Config,
45
- model_override: str | None = None,
46
- enabled_sub_agents: list[SubAgentType] | None = None,
47
- ) -> LLMClients:
48
- """Create LLMClients from application config.
24
+ class ActiveTask:
25
+ """Track an in-flight task and its owning session."""
49
26
 
50
- Args:
51
- config: Application configuration
52
- model_override: Optional model name to override the main model
53
- enabled_sub_agents: List of sub-agent types to initialize clients for
27
+ task: asyncio.Task[None]
28
+ session_id: str
54
29
 
55
- Returns:
56
- LLMClients instance
57
- """
58
- # Resolve main agent LLM config
59
- if model_override:
60
- llm_config = config.get_model_config(model_override)
61
- else:
62
- llm_config = config.get_main_model_config()
63
30
 
64
- log_debug(
65
- "Main LLM config",
66
- llm_config.model_dump_json(exclude_none=True),
67
- style="yellow",
68
- debug_type=DebugType.LLM_CONFIG,
69
- )
31
+ class TaskManager:
32
+ """Manager that tracks active tasks keyed by submission id."""
70
33
 
71
- main_client = create_llm_client(llm_config)
72
- sub_clients: dict[SubAgentType, LLMClientABC] = {}
34
+ def __init__(self) -> None:
35
+ self._tasks: dict[str, ActiveTask] = {}
73
36
 
74
- # Initialize sub-agent clients
75
- for sub_agent_type in enabled_sub_agents or []:
76
- model_name = config.subagent_models.get(sub_agent_type)
77
- if not model_name:
78
- continue
79
- profile = get_sub_agent_profile(sub_agent_type)
80
- if not profile.enabled_for_model(main_client.model_name):
81
- continue
82
- sub_llm_config = config.get_model_config(model_name)
83
- sub_clients[sub_agent_type] = create_llm_client(sub_llm_config)
37
+ def register(self, submission_id: str, task: asyncio.Task[None], session_id: str) -> None:
38
+ """Register a new active task for a submission id."""
84
39
 
85
- return cls(main=main_client, sub_clients=sub_clients)
40
+ self._tasks[submission_id] = ActiveTask(task=task, session_id=session_id)
86
41
 
42
+ def get(self, submission_id: str) -> ActiveTask | None:
43
+ """Return the active task for a submission id if present."""
87
44
 
88
- @dataclass
89
- class ActiveTask:
90
- """Track an in-flight task and its owning session."""
45
+ return self._tasks.get(submission_id)
91
46
 
92
- task: asyncio.Task[None]
93
- session_id: str
47
+ def remove(self, submission_id: str) -> None:
48
+ """Remove the active task associated with a submission id if present."""
49
+
50
+ self._tasks.pop(submission_id, None)
51
+
52
+ def values(self) -> list[ActiveTask]:
53
+ """Return a snapshot list of all active tasks."""
54
+
55
+ return list(self._tasks.values())
56
+
57
+ def cancel_tasks_for_sessions(self, session_ids: set[str] | None = None) -> list[tuple[str, asyncio.Task[None]]]:
58
+ """Collect tasks that should be cancelled for given sessions."""
59
+
60
+ tasks_to_cancel: list[tuple[str, asyncio.Task[None]]] = []
61
+ for task_id, active in list(self._tasks.items()):
62
+ task = active.task
63
+ if task.done():
64
+ continue
65
+ if session_ids is None or active.session_id in session_ids:
66
+ tasks_to_cancel.append((task_id, task))
67
+ return tasks_to_cancel
68
+
69
+ def clear(self) -> None:
70
+ """Remove all tracked tasks from the manager."""
71
+
72
+ self._tasks.clear()
94
73
 
95
74
 
96
75
  class ExecutorContext:
@@ -110,56 +89,35 @@ class ExecutorContext:
110
89
  model_profile_provider: ModelProfileProvider | None = None,
111
90
  ):
112
91
  self.event_queue: asyncio.Queue[events.Event] = event_queue
113
- self.llm_clients: LLMClients = llm_clients
114
- self.model_profile_provider: ModelProfileProvider = model_profile_provider or DefaultModelProfileProvider()
115
92
 
116
- # Track active agents by session ID
117
- self.active_agents: dict[str, Agent] = {}
118
- # Track active tasks by submission ID, retaining owning session for filtering/cancellation
119
- self.active_tasks: dict[str, ActiveTask] = {}
93
+ resolved_profile_provider = model_profile_provider or DefaultModelProfileProvider()
94
+ self.model_profile_provider: ModelProfileProvider = resolved_profile_provider
95
+
96
+ # Delegate responsibilities to helper components
97
+ self.agent_manager = AgentManager(event_queue, llm_clients, resolved_profile_provider)
98
+ self.task_manager = TaskManager()
99
+ self.subagent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
120
100
 
121
101
  async def emit_event(self, event: events.Event) -> None:
122
102
  """Emit an event to the UI display system."""
123
103
  await self.event_queue.put(event)
124
104
 
125
- async def _ensure_agent(self, session_id: str) -> Agent:
126
- """Return an existing agent for the session or create a new one."""
127
-
128
- agent = self.active_agents.get(session_id)
129
- if agent is not None:
130
- return agent
105
+ @property
106
+ def active_agents(self) -> dict[str, Agent]:
107
+ """Expose currently active agents keyed by session id.
131
108
 
132
- session = Session.load(session_id)
133
- profile = self.model_profile_provider.build_profile(self.llm_clients.main)
134
- agent = Agent(
135
- session=session,
136
- profile=profile,
137
- )
138
-
139
- async for evt in agent.replay_history():
140
- await self.emit_event(evt)
141
-
142
- await self.emit_event(
143
- events.WelcomeEvent(
144
- work_dir=str(session.work_dir),
145
- llm_config=self.llm_clients.main.get_llm_config(),
146
- )
147
- )
109
+ This property preserves the previous public attribute used by the
110
+ CLI status provider while delegating storage to :class:`AgentManager`.
111
+ """
148
112
 
149
- self.active_agents[session_id] = agent
150
- log_debug(
151
- f"Initialized agent for session: {session_id}",
152
- style="cyan",
153
- debug_type=DebugType.EXECUTION,
154
- )
155
- return agent
113
+ return self.agent_manager.all_active_agents()
156
114
 
157
115
  async def handle_init_agent(self, operation: op.InitAgentOperation) -> None:
158
116
  """Initialize an agent for a session and replay history to UI."""
159
117
  if operation.session_id is None:
160
118
  raise ValueError("session_id cannot be None")
161
119
 
162
- await self._ensure_agent(operation.session_id)
120
+ await self.agent_manager.ensure_agent(operation.session_id)
163
121
 
164
122
  async def handle_user_input(self, operation: op.UserInputOperation) -> None:
165
123
  """Handle a user input operation by running it through an agent."""
@@ -168,7 +126,7 @@ class ExecutorContext:
168
126
  raise ValueError("session_id cannot be None")
169
127
 
170
128
  session_id = operation.session_id
171
- agent = await self._ensure_agent(session_id)
129
+ agent = await self.agent_manager.ensure_agent(session_id)
172
130
  user_input = operation.input
173
131
 
174
132
  # emit user input event
@@ -204,69 +162,29 @@ class ExecutorContext:
204
162
  if action.type == InputActionType.RUN_AGENT:
205
163
  task_input = model.UserInputPayload(text=action.text, images=operation.input.images)
206
164
 
207
- existing_active = self.active_tasks.get(operation.id)
165
+ existing_active = self.task_manager.get(operation.id)
208
166
  if existing_active is not None and not existing_active.task.done():
209
167
  raise RuntimeError(f"Active task already registered for operation {operation.id}")
210
168
 
211
169
  task: asyncio.Task[None] = asyncio.create_task(
212
170
  self._run_agent_task(agent, task_input, operation.id, session_id)
213
171
  )
214
- self.active_tasks[operation.id] = ActiveTask(task=task, session_id=session_id)
172
+ self.task_manager.register(operation.id, task, session_id)
215
173
  return
216
174
 
217
175
  if action.type == InputActionType.CHANGE_MODEL:
218
176
  if not action.model_name:
219
177
  raise ValueError("ChangeModel action requires model_name")
220
178
 
221
- await self._apply_model_change(agent, action.model_name)
179
+ await self.agent_manager.apply_model_change(agent, action.model_name)
222
180
  return
223
181
 
224
182
  if action.type == InputActionType.CLEAR:
225
- await self._apply_clear(agent)
183
+ await self.agent_manager.apply_clear(agent)
226
184
  return
227
185
 
228
186
  raise ValueError(f"Unsupported input action type: {action.type}")
229
187
 
230
- async def _apply_model_change(self, agent: Agent, model_name: str) -> None:
231
- config = load_config()
232
- if config is None:
233
- raise ValueError("Configuration must be initialized before changing model")
234
-
235
- llm_config = config.get_model_config(model_name)
236
- llm_client = create_llm_client(llm_config)
237
- agent.set_model_profile(self.model_profile_provider.build_profile(llm_client))
238
-
239
- developer_item = model.DeveloperMessageItem(
240
- content=f"switched to model: {model_name}",
241
- command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
242
- )
243
- agent.session.append_history([developer_item])
244
-
245
- await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
246
- await self.emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
247
-
248
- async def _apply_clear(self, agent: Agent) -> None:
249
- old_session_id = agent.session.id
250
-
251
- # Create a new session instance to replace the current one
252
- new_session = Session(work_dir=agent.session.work_dir)
253
- new_session.model_name = agent.session.model_name
254
-
255
- # Replace the agent's session with the new one
256
- agent.session = new_session
257
- agent.session.save()
258
-
259
- # Update the active_agents mapping
260
- self.active_agents.pop(old_session_id, None)
261
- self.active_agents[new_session.id] = agent
262
-
263
- developer_item = model.DeveloperMessageItem(
264
- content="started new conversation",
265
- command_output=model.CommandOutput(command_name=commands.CommandName.CLEAR),
266
- )
267
-
268
- await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
269
-
270
188
  async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
271
189
  """Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
272
190
 
@@ -274,11 +192,11 @@ class ExecutorContext:
274
192
  if operation.target_session_id is not None:
275
193
  session_ids: list[str] = [operation.target_session_id]
276
194
  else:
277
- session_ids = list(self.active_agents.keys())
195
+ session_ids = self.agent_manager.active_session_ids()
278
196
 
279
197
  # Call cancel() on each affected agent to persist an interrupt marker
280
198
  for sid in session_ids:
281
- agent = self.active_agents.get(sid)
199
+ agent = self.agent_manager.get_active_agent(sid)
282
200
  if agent is not None:
283
201
  for evt in agent.cancel():
284
202
  await self.emit_event(evt)
@@ -287,16 +205,12 @@ class ExecutorContext:
287
205
  await self.emit_event(events.InterruptEvent(session_id=operation.target_session_id or "all"))
288
206
 
289
207
  # Find tasks to cancel (filter by target sessions if provided)
290
- tasks_to_cancel: list[tuple[str, asyncio.Task[None]]] = []
291
- for task_id, active in list(self.active_tasks.items()):
292
- task = active.task
293
- if task.done():
294
- continue
295
- if operation.target_session_id is None:
296
- tasks_to_cancel.append((task_id, task))
297
- else:
298
- if active.session_id == operation.target_session_id:
299
- tasks_to_cancel.append((task_id, task))
208
+ if operation.target_session_id is None:
209
+ session_filter: set[str] | None = None
210
+ else:
211
+ session_filter = {operation.target_session_id}
212
+
213
+ tasks_to_cancel = self.task_manager.cancel_tasks_for_sessions(session_filter)
300
214
 
301
215
  scope = operation.target_session_id or "all"
302
216
  log_debug(
@@ -309,7 +223,7 @@ class ExecutorContext:
309
223
  for task_id, task in tasks_to_cancel:
310
224
  task.cancel()
311
225
  # Remove from active tasks immediately
312
- self.active_tasks.pop(task_id, None)
226
+ self.task_manager.remove(task_id)
313
227
 
314
228
  async def _run_agent_task(
315
229
  self, agent: Agent, user_input: model.UserInputPayload, task_id: str, session_id: str
@@ -329,7 +243,7 @@ class ExecutorContext:
329
243
 
330
244
  # Inject subtask runner into tool context for nested Task tool usage
331
245
  async def _runner(state: model.SubAgentState) -> SubAgentResult:
332
- return await self._run_subagent_task(agent, state)
246
+ return await self.subagent_manager.run_subagent(agent, state)
333
247
 
334
248
  token = current_run_subtask_callback.set(_runner)
335
249
  try:
@@ -367,69 +281,25 @@ class ExecutorContext:
367
281
 
368
282
  finally:
369
283
  # Clean up the task from active tasks
370
- self.active_tasks.pop(task_id, None)
284
+ self.task_manager.remove(task_id)
371
285
  log_debug(
372
286
  f"Cleaned up agent task {task_id}",
373
287
  style="cyan",
374
288
  debug_type=DebugType.EXECUTION,
375
289
  )
376
290
 
377
- async def _run_subagent_task(self, parent_agent: Agent, state: model.SubAgentState) -> SubAgentResult:
378
- """Run a nested sub-agent task and return the final task_result text.
291
+ def get_active_task(self, submission_id: str) -> asyncio.Task[None] | None:
292
+ """Return the asyncio.Task for a submission id if one is registered."""
379
293
 
380
- - Creates a child session linked to the parent session
381
- - Streams the child agent's events to the same event queue
382
- - Returns the last assistant message content as the result
383
- """
384
- # Create a child session under the same workdir
385
- parent_session = parent_agent.session
386
- child_session = Session(work_dir=parent_session.work_dir)
387
- child_session.sub_agent_state = state
388
-
389
- child_profile = self.model_profile_provider.build_profile(
390
- self.llm_clients.get_client(state.sub_agent_type),
391
- state.sub_agent_type,
392
- )
393
- child_agent = Agent(
394
- session=child_session,
395
- profile=child_profile,
396
- )
294
+ active = self.task_manager.get(submission_id)
295
+ if active is None:
296
+ return None
297
+ return active.task
397
298
 
398
- log_debug(
399
- f"Running sub-agent {state.sub_agent_type} in session {child_session.id}",
400
- style="cyan",
401
- debug_type=DebugType.EXECUTION,
402
- )
299
+ def has_active_task(self, submission_id: str) -> bool:
300
+ """Return True if a task is registered for the submission id."""
403
301
 
404
- try:
405
- # Not emit the subtask's user input since task tool call is already rendered
406
- result: str = ""
407
- sub_agent_input = model.UserInputPayload(text=state.sub_agent_prompt, images=None)
408
- async for event in child_agent.run_task(sub_agent_input):
409
- # Capture TaskFinishEvent content for return
410
- if isinstance(event, events.TaskFinishEvent):
411
- result = event.task_result
412
- await self.emit_event(event)
413
- return SubAgentResult(task_result=result, session_id=child_session.id)
414
- except asyncio.CancelledError:
415
- # Propagate cancellation so tooling can treat it as user interrupt
416
- log_debug(
417
- f"Subagent task for {state.sub_agent_type} was cancelled",
418
- style="yellow",
419
- debug_type=DebugType.EXECUTION,
420
- )
421
- raise
422
- except Exception as e:
423
- log_debug(
424
- f"Subagent task failed: [{e.__class__.__name__}] {str(e)}",
425
- style="red",
426
- debug_type=DebugType.EXECUTION,
427
- )
428
- return SubAgentResult(
429
- task_result=f"Subagent task failed: [{e.__class__.__name__}] {str(e)}",
430
- session_id="",
431
- error=True,
432
- )
302
+ return self.task_manager.get(submission_id) is not None
433
303
 
434
304
 
435
305
  class Executor:
@@ -533,7 +403,7 @@ class Executor:
533
403
  """Stop the executor and clean up resources."""
534
404
  # Cancel all active tasks and collect them for awaiting
535
405
  tasks_to_await: list[asyncio.Task[None]] = []
536
- for active in self.context.active_tasks.values():
406
+ for active in self.context.task_manager.values():
537
407
  task = active.task
538
408
  if not task.done():
539
409
  task.cancel()
@@ -543,8 +413,8 @@ class Executor:
543
413
  if tasks_to_await:
544
414
  await asyncio.gather(*tasks_to_await, return_exceptions=True)
545
415
 
546
- # Clear the active_tasks dictionary
547
- self.context.active_tasks.clear()
416
+ # Clear the active task manager
417
+ self.context.task_manager.clear()
548
418
 
549
419
  # Send EndOperation to wake up the start() loop
550
420
  try:
@@ -577,25 +447,23 @@ class Executor:
577
447
  # Execute to spawn the agent task in context
578
448
  await submission.operation.execute(handler=self.context)
579
449
 
580
- async def _await_agent_and_complete() -> None:
581
- # Wait for the agent task tied to this submission id
582
- active = self.context.active_tasks.get(submission.id)
583
- if active is not None:
584
- try:
585
- await active.task
586
- finally:
587
- event = self._completion_events.get(submission.id)
588
- if event is not None:
589
- event.set()
590
-
591
- # Run in background so the submission loop can continue (e.g., to handle interrupts)
592
- asyncio.create_task(_await_agent_and_complete())
593
-
594
- # For operations without ActiveTask (e.g., InitAgentOperation), signal completion immediately
595
- if submission.id not in self.context.active_tasks:
450
+ task = self.context.get_active_task(submission.id)
451
+
452
+ async def _await_agent_and_complete(captured_task: asyncio.Task[None]) -> None:
453
+ try:
454
+ await captured_task
455
+ finally:
456
+ event = self._completion_events.get(submission.id)
457
+ if event is not None:
458
+ event.set()
459
+
460
+ if task is None:
596
461
  event = self._completion_events.get(submission.id)
597
462
  if event is not None:
598
463
  event.set()
464
+ else:
465
+ # Run in background so the submission loop can continue (e.g., to handle interrupts)
466
+ asyncio.create_task(_await_agent_and_complete(task))
599
467
 
600
468
  except Exception as e:
601
469
  log_debug(
@@ -0,0 +1,19 @@
1
+ """Core runtime and state management components.
2
+
3
+ Expose the manager layer via package imports to reduce module churn in
4
+ callers. This keeps long-lived runtime state helpers (agents, tasks,
5
+ LLM clients, sub-agents) distinct from per-session execution logic in
6
+ ``klaude_code.core``.
7
+ """
8
+
9
+ from klaude_code.core.manager.agent_manager import AgentManager
10
+ from klaude_code.core.manager.llm_clients import LLMClients
11
+ from klaude_code.core.manager.llm_clients_builder import build_llm_clients
12
+ from klaude_code.core.manager.sub_agent_manager import SubAgentManager
13
+
14
+ __all__ = [
15
+ "AgentManager",
16
+ "LLMClients",
17
+ "SubAgentManager",
18
+ "build_llm_clients",
19
+ ]
@@ -0,0 +1,127 @@
1
+ """Agent and session manager.
2
+
3
+ This module contains :class:`AgentManager`, a helper responsible for
4
+ creating and tracking agents per session, applying model changes, and
5
+ clearing conversations. It is used by the executor context to keep
6
+ agent-related responsibilities separate from operation dispatch.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+
13
+ from klaude_code.config import load_config
14
+ from klaude_code.core.agent import Agent, DefaultModelProfileProvider, ModelProfileProvider
15
+ from klaude_code.core.manager.llm_clients import LLMClients
16
+ from klaude_code.llm.registry import create_llm_client
17
+ from klaude_code.protocol import commands, events, model
18
+ from klaude_code.session.session import Session
19
+ from klaude_code.trace import DebugType, log_debug
20
+
21
+
22
+ class AgentManager:
23
+ """Manager component that tracks agents and their sessions."""
24
+
25
+ def __init__(
26
+ self,
27
+ event_queue: asyncio.Queue[events.Event],
28
+ llm_clients: LLMClients,
29
+ model_profile_provider: ModelProfileProvider | None = None,
30
+ ) -> None:
31
+ self._event_queue: asyncio.Queue[events.Event] = event_queue
32
+ self._llm_clients: LLMClients = llm_clients
33
+ self._model_profile_provider: ModelProfileProvider = model_profile_provider or DefaultModelProfileProvider()
34
+ self._active_agents: dict[str, Agent] = {}
35
+
36
+ async def emit_event(self, event: events.Event) -> None:
37
+ """Emit an event to the shared event queue."""
38
+
39
+ await self._event_queue.put(event)
40
+
41
+ async def ensure_agent(self, session_id: str) -> Agent:
42
+ """Return an existing agent for the session or create a new one."""
43
+
44
+ agent = self._active_agents.get(session_id)
45
+ if agent is not None:
46
+ return agent
47
+
48
+ session = Session.load(session_id)
49
+ profile = self._model_profile_provider.build_profile(self._llm_clients.main)
50
+ agent = Agent(session=session, profile=profile)
51
+
52
+ async for evt in agent.replay_history():
53
+ await self.emit_event(evt)
54
+
55
+ await self.emit_event(
56
+ events.WelcomeEvent(
57
+ work_dir=str(session.work_dir),
58
+ llm_config=self._llm_clients.main.get_llm_config(),
59
+ )
60
+ )
61
+
62
+ self._active_agents[session_id] = agent
63
+ log_debug(
64
+ f"Initialized agent for session: {session_id}",
65
+ style="cyan",
66
+ debug_type=DebugType.EXECUTION,
67
+ )
68
+ return agent
69
+
70
+ async def apply_model_change(self, agent: Agent, model_name: str) -> None:
71
+ """Change the model used by an agent and notify the UI."""
72
+
73
+ config = load_config()
74
+ if config is None:
75
+ raise ValueError("Configuration must be initialized before changing model")
76
+
77
+ llm_config = config.get_model_config(model_name)
78
+ llm_client = create_llm_client(llm_config)
79
+ agent.set_model_profile(self._model_profile_provider.build_profile(llm_client))
80
+
81
+ developer_item = model.DeveloperMessageItem(
82
+ content=f"switched to model: {model_name}",
83
+ command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
84
+ )
85
+ agent.session.append_history([developer_item])
86
+
87
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
88
+ await self.emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
89
+
90
+ async def apply_clear(self, agent: Agent) -> None:
91
+ """Start a new conversation for an agent and notify the UI."""
92
+
93
+ old_session_id = agent.session.id
94
+
95
+ # Create a new session instance to replace the current one
96
+ new_session = Session(work_dir=agent.session.work_dir)
97
+ new_session.model_name = agent.session.model_name
98
+
99
+ # Replace the agent's session with the new one
100
+ agent.session = new_session
101
+ agent.session.save()
102
+
103
+ # Update the active_agents mapping
104
+ self._active_agents.pop(old_session_id, None)
105
+ self._active_agents[new_session.id] = agent
106
+
107
+ developer_item = model.DeveloperMessageItem(
108
+ content="started new conversation",
109
+ command_output=model.CommandOutput(command_name=commands.CommandName.CLEAR),
110
+ )
111
+
112
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
113
+
114
+ def get_active_agent(self, session_id: str) -> Agent | None:
115
+ """Return the active agent for a session id if present."""
116
+
117
+ return self._active_agents.get(session_id)
118
+
119
+ def active_session_ids(self) -> list[str]:
120
+ """Return a snapshot list of session ids that currently have agents."""
121
+
122
+ return list(self._active_agents.keys())
123
+
124
+ def all_active_agents(self) -> dict[str, Agent]:
125
+ """Return a snapshot of all active agents keyed by session id."""
126
+
127
+ return dict(self._active_agents)