minion-code 0.1.0__py3-none-any.whl → 0.1.2__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.
Files changed (115) hide show
  1. examples/cli_entrypoint.py +60 -0
  2. examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
  3. examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
  4. examples/components/messages_component.py +199 -0
  5. examples/file_freshness_example.py +22 -22
  6. examples/file_watching_example.py +32 -26
  7. examples/interruptible_tui.py +921 -3
  8. examples/repl_tui.py +129 -0
  9. examples/skills/example_usage.py +57 -0
  10. examples/start.py +173 -0
  11. minion_code/__init__.py +1 -1
  12. minion_code/acp_server/__init__.py +34 -0
  13. minion_code/acp_server/agent.py +539 -0
  14. minion_code/acp_server/hooks.py +354 -0
  15. minion_code/acp_server/main.py +194 -0
  16. minion_code/acp_server/permissions.py +142 -0
  17. minion_code/acp_server/test_client.py +104 -0
  18. minion_code/adapters/__init__.py +22 -0
  19. minion_code/adapters/output_adapter.py +207 -0
  20. minion_code/adapters/rich_adapter.py +169 -0
  21. minion_code/adapters/textual_adapter.py +254 -0
  22. minion_code/agents/__init__.py +2 -2
  23. minion_code/agents/code_agent.py +517 -104
  24. minion_code/agents/hooks.py +378 -0
  25. minion_code/cli.py +538 -429
  26. minion_code/cli_simple.py +665 -0
  27. minion_code/commands/__init__.py +136 -29
  28. minion_code/commands/clear_command.py +19 -46
  29. minion_code/commands/help_command.py +33 -49
  30. minion_code/commands/history_command.py +37 -55
  31. minion_code/commands/model_command.py +194 -0
  32. minion_code/commands/quit_command.py +9 -12
  33. minion_code/commands/resume_command.py +181 -0
  34. minion_code/commands/skill_command.py +89 -0
  35. minion_code/commands/status_command.py +48 -73
  36. minion_code/commands/tools_command.py +54 -52
  37. minion_code/commands/version_command.py +34 -69
  38. minion_code/components/ConfirmDialog.py +430 -0
  39. minion_code/components/Message.py +318 -97
  40. minion_code/components/MessageResponse.py +30 -29
  41. minion_code/components/Messages.py +351 -0
  42. minion_code/components/PromptInput.py +499 -245
  43. minion_code/components/__init__.py +24 -17
  44. minion_code/const.py +7 -0
  45. minion_code/screens/REPL.py +1453 -469
  46. minion_code/screens/__init__.py +1 -1
  47. minion_code/services/__init__.py +20 -20
  48. minion_code/services/event_system.py +19 -14
  49. minion_code/services/file_freshness_service.py +223 -170
  50. minion_code/skills/__init__.py +25 -0
  51. minion_code/skills/skill.py +128 -0
  52. minion_code/skills/skill_loader.py +198 -0
  53. minion_code/skills/skill_registry.py +177 -0
  54. minion_code/subagents/__init__.py +31 -0
  55. minion_code/subagents/builtin/__init__.py +30 -0
  56. minion_code/subagents/builtin/claude_code_guide.py +32 -0
  57. minion_code/subagents/builtin/explore.py +36 -0
  58. minion_code/subagents/builtin/general_purpose.py +19 -0
  59. minion_code/subagents/builtin/plan.py +61 -0
  60. minion_code/subagents/subagent.py +116 -0
  61. minion_code/subagents/subagent_loader.py +147 -0
  62. minion_code/subagents/subagent_registry.py +151 -0
  63. minion_code/tools/__init__.py +8 -2
  64. minion_code/tools/bash_tool.py +16 -3
  65. minion_code/tools/file_edit_tool.py +201 -104
  66. minion_code/tools/file_read_tool.py +183 -26
  67. minion_code/tools/file_write_tool.py +17 -3
  68. minion_code/tools/glob_tool.py +23 -2
  69. minion_code/tools/grep_tool.py +229 -21
  70. minion_code/tools/ls_tool.py +28 -3
  71. minion_code/tools/multi_edit_tool.py +89 -84
  72. minion_code/tools/python_interpreter_tool.py +9 -1
  73. minion_code/tools/skill_tool.py +210 -0
  74. minion_code/tools/task_tool.py +287 -0
  75. minion_code/tools/todo_read_tool.py +28 -24
  76. minion_code/tools/todo_write_tool.py +82 -65
  77. minion_code/{types.py → type_defs.py} +15 -2
  78. minion_code/utils/__init__.py +45 -17
  79. minion_code/utils/config.py +610 -0
  80. minion_code/utils/history.py +114 -0
  81. minion_code/utils/logs.py +53 -0
  82. minion_code/utils/mcp_loader.py +153 -55
  83. minion_code/utils/output_truncator.py +233 -0
  84. minion_code/utils/session_storage.py +369 -0
  85. minion_code/utils/todo_file_utils.py +26 -22
  86. minion_code/utils/todo_storage.py +43 -33
  87. minion_code/web/__init__.py +9 -0
  88. minion_code/web/adapters/__init__.py +5 -0
  89. minion_code/web/adapters/web_adapter.py +524 -0
  90. minion_code/web/api/__init__.py +7 -0
  91. minion_code/web/api/chat.py +277 -0
  92. minion_code/web/api/interactions.py +136 -0
  93. minion_code/web/api/sessions.py +135 -0
  94. minion_code/web/server.py +149 -0
  95. minion_code/web/services/__init__.py +5 -0
  96. minion_code/web/services/session_manager.py +420 -0
  97. minion_code-0.1.2.dist-info/METADATA +476 -0
  98. minion_code-0.1.2.dist-info/RECORD +111 -0
  99. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
  100. minion_code-0.1.2.dist-info/entry_points.txt +6 -0
  101. tests/test_adapter.py +67 -0
  102. tests/test_adapter_simple.py +79 -0
  103. tests/test_file_read_tool.py +144 -0
  104. tests/test_readonly_tools.py +0 -2
  105. tests/test_skills.py +441 -0
  106. examples/advance_tui.py +0 -508
  107. examples/rich_example.py +0 -4
  108. examples/simple_file_watching.py +0 -57
  109. examples/simple_tui.py +0 -267
  110. examples/simple_usage.py +0 -69
  111. minion_code-0.1.0.dist-info/METADATA +0 -350
  112. minion_code-0.1.0.dist-info/RECORD +0 -59
  113. minion_code-0.1.0.dist-info/entry_points.txt +0 -4
  114. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/licenses/LICENSE +0 -0
  115. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,539 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ ACP Agent implementation for minion-code.
5
+
6
+ Implements the ACP Agent protocol to allow minion-code to be used
7
+ with ACP-compatible clients.
8
+ """
9
+
10
+ import asyncio
11
+ import logging
12
+ import os
13
+ import sys
14
+ import uuid
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from acp import Client, text_block
18
+ from acp.helpers import (
19
+ update_agent_message_text,
20
+ update_agent_thought_text,
21
+ )
22
+ from acp.schema import (
23
+ AgentCapabilities,
24
+ AgentMessageChunk,
25
+ AgentThoughtChunk,
26
+ AudioContentBlock,
27
+ AuthenticateResponse,
28
+ ClientCapabilities,
29
+ ContentToolCallContent,
30
+ EmbeddedResourceContentBlock,
31
+ ForkSessionResponse,
32
+ HttpMcpServer,
33
+ ImageContentBlock,
34
+ Implementation,
35
+ InitializeResponse,
36
+ ListSessionsResponse,
37
+ LoadSessionResponse,
38
+ McpServerStdio,
39
+ NewSessionResponse,
40
+ PromptResponse,
41
+ ResourceContentBlock,
42
+ ResumeSessionResponse,
43
+ SetSessionModelResponse,
44
+ SetSessionModeResponse,
45
+ SseMcpServer,
46
+ TextContentBlock,
47
+ ToolCallStart,
48
+ ToolCallProgress,
49
+ )
50
+
51
+ from ..agents.code_agent import MinionCodeAgent
52
+ from ..agents.hooks import HookConfig, wrap_tools_with_hooks
53
+ from .hooks import create_acp_hooks
54
+ from .permissions import PermissionStore
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+ # Protocol version
59
+ PROTOCOL_VERSION = 1
60
+
61
+
62
+ class MinionACPAgent:
63
+ """
64
+ ACP Agent implementation wrapping MinionCodeAgent.
65
+
66
+ This class implements the ACP Agent protocol, allowing minion-code
67
+ to communicate with ACP clients over stdio.
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ skip_permissions: bool = False,
73
+ config: Optional[Dict] = None,
74
+ cwd: Optional[str] = None,
75
+ model: Optional[str] = None,
76
+ ):
77
+ self.client: Optional[Client] = None
78
+ self.sessions: Dict[str, "ACPSession"] = {}
79
+ self._cancel_events: Dict[str, asyncio.Event] = {}
80
+ self.skip_permissions = skip_permissions
81
+ self.config = config or {}
82
+ self.cwd = cwd or os.getcwd()
83
+ self.model = model # LLM model to use
84
+
85
+ def on_connect(self, conn: Client) -> None:
86
+ """Called when connected to an ACP client."""
87
+ self.client = conn
88
+ logger.info("Connected to ACP client")
89
+
90
+ async def initialize(
91
+ self,
92
+ protocol_version: int,
93
+ client_capabilities: Optional[ClientCapabilities] = None,
94
+ client_info: Optional[Implementation] = None,
95
+ **kwargs: Any,
96
+ ) -> InitializeResponse:
97
+ """Initialize the agent and negotiate capabilities."""
98
+ logger.info(f"Initializing with protocol version {protocol_version}")
99
+
100
+ return InitializeResponse(
101
+ protocol_version=min(protocol_version, PROTOCOL_VERSION),
102
+ agent_info=Implementation(
103
+ name="minion-code",
104
+ version="0.1.0",
105
+ ),
106
+ agent_capabilities=AgentCapabilities(
107
+ streaming=True,
108
+ ),
109
+ )
110
+
111
+ async def new_session(
112
+ self,
113
+ cwd: str,
114
+ mcp_servers: List[HttpMcpServer | SseMcpServer | McpServerStdio],
115
+ **kwargs: Any,
116
+ ) -> NewSessionResponse:
117
+ """Create a new session."""
118
+ session_id = str(uuid.uuid4())
119
+ pid = os.getpid()
120
+ session_count = len(self.sessions) + 1
121
+
122
+ # Use CLI-provided cwd as fallback if client doesn't provide one
123
+ if not cwd:
124
+ cwd = self.cwd
125
+ logger.info(
126
+ f"[PID={pid}] Creating session #{session_count}: {session_id} in {cwd}"
127
+ )
128
+
129
+ # Create session
130
+ session = ACPSession(
131
+ session_id=session_id,
132
+ cwd=cwd,
133
+ client=self.client,
134
+ mcp_servers=mcp_servers,
135
+ skip_permissions=self.skip_permissions,
136
+ model=self.model,
137
+ )
138
+ await session.initialize()
139
+
140
+ self.sessions[session_id] = session
141
+ self._cancel_events[session_id] = asyncio.Event()
142
+
143
+ return NewSessionResponse(session_id=session_id)
144
+
145
+ async def load_session(
146
+ self,
147
+ cwd: str,
148
+ mcp_servers: List[HttpMcpServer | SseMcpServer | McpServerStdio],
149
+ session_id: str,
150
+ **kwargs: Any,
151
+ ) -> Optional[LoadSessionResponse]:
152
+ """Load an existing session."""
153
+ if session_id in self.sessions:
154
+ return LoadSessionResponse()
155
+ return None
156
+
157
+ async def list_sessions(
158
+ self,
159
+ cursor: Optional[str] = None,
160
+ cwd: Optional[str] = None,
161
+ **kwargs: Any,
162
+ ) -> ListSessionsResponse:
163
+ """List available sessions."""
164
+ # Simple implementation - just return session IDs
165
+ sessions = []
166
+ for sid in self.sessions.keys():
167
+ sessions.append({"session_id": sid})
168
+
169
+ return ListSessionsResponse(sessions=sessions)
170
+
171
+ async def set_session_mode(
172
+ self,
173
+ mode_id: str,
174
+ session_id: str,
175
+ **kwargs: Any,
176
+ ) -> Optional[SetSessionModeResponse]:
177
+ """Set the session mode (not implemented)."""
178
+ return None
179
+
180
+ async def set_session_model(
181
+ self,
182
+ model_id: str,
183
+ session_id: str,
184
+ **kwargs: Any,
185
+ ) -> Optional[SetSessionModelResponse]:
186
+ """Set the session model (not implemented)."""
187
+ return None
188
+
189
+ async def authenticate(
190
+ self,
191
+ method_id: str,
192
+ **kwargs: Any,
193
+ ) -> Optional[AuthenticateResponse]:
194
+ """Authenticate (not implemented)."""
195
+ return None
196
+
197
+ async def prompt(
198
+ self,
199
+ prompt: List[
200
+ TextContentBlock
201
+ | ImageContentBlock
202
+ | AudioContentBlock
203
+ | ResourceContentBlock
204
+ | EmbeddedResourceContentBlock
205
+ ],
206
+ session_id: str,
207
+ **kwargs: Any,
208
+ ) -> PromptResponse:
209
+ """Process a user prompt."""
210
+ pid = os.getpid()
211
+ logger.info(f"[PID={pid}] Processing prompt for session {session_id}")
212
+ logger.info(f"[PID={pid}] Prompt content: {prompt}")
213
+
214
+ session = self.sessions.get(session_id)
215
+ if not session:
216
+ logger.error(f"Session {session_id} not found")
217
+ return PromptResponse(stop_reason="refusal")
218
+
219
+ # Clear cancel event
220
+ cancel_event = self._cancel_events.get(session_id)
221
+ if cancel_event:
222
+ cancel_event.clear()
223
+
224
+ # Extract text from prompt (handle both Pydantic models and dicts)
225
+ text_parts = []
226
+ for block in prompt:
227
+ if isinstance(block, dict):
228
+ # Dict format
229
+ if block.get("type") == "text":
230
+ text_parts.append(block.get("text", ""))
231
+ elif isinstance(block, TextContentBlock):
232
+ text_parts.append(block.text)
233
+ elif hasattr(block, "text"):
234
+ text_parts.append(block.text)
235
+
236
+ user_message = "\n".join(text_parts)
237
+
238
+ try:
239
+ # Run the agent
240
+ stop_reason = await session.run_prompt(
241
+ message=user_message,
242
+ cancel_event=cancel_event,
243
+ )
244
+ return PromptResponse(stop_reason=stop_reason)
245
+ except Exception as e:
246
+ logger.error(f"Error processing prompt: {e}")
247
+ # Send error message
248
+ if self.client:
249
+ await self.client.session_update(
250
+ session_id=session_id,
251
+ update=update_agent_message_text(f"Error: {str(e)}"),
252
+ )
253
+ return PromptResponse(stop_reason="refusal")
254
+
255
+ async def fork_session(
256
+ self,
257
+ cwd: str,
258
+ session_id: str,
259
+ mcp_servers: Optional[
260
+ List[HttpMcpServer | SseMcpServer | McpServerStdio]
261
+ ] = None,
262
+ **kwargs: Any,
263
+ ) -> ForkSessionResponse:
264
+ """Fork an existing session."""
265
+ new_session_id = str(uuid.uuid4())
266
+
267
+ # Copy session state (simplified)
268
+ if session_id in self.sessions:
269
+ old_session = self.sessions[session_id]
270
+ new_session = ACPSession(
271
+ session_id=new_session_id,
272
+ cwd=cwd,
273
+ client=self.client,
274
+ mcp_servers=mcp_servers or old_session.mcp_servers,
275
+ skip_permissions=self.skip_permissions,
276
+ )
277
+ await new_session.initialize()
278
+ self.sessions[new_session_id] = new_session
279
+ self._cancel_events[new_session_id] = asyncio.Event()
280
+
281
+ return ForkSessionResponse(session_id=new_session_id)
282
+
283
+ async def resume_session(
284
+ self,
285
+ cwd: str,
286
+ session_id: str,
287
+ mcp_servers: Optional[
288
+ List[HttpMcpServer | SseMcpServer | McpServerStdio]
289
+ ] = None,
290
+ **kwargs: Any,
291
+ ) -> ResumeSessionResponse:
292
+ """Resume an existing session."""
293
+ if session_id in self.sessions:
294
+ return ResumeSessionResponse()
295
+
296
+ # Create new session with the given ID
297
+ session = ACPSession(
298
+ session_id=session_id,
299
+ cwd=cwd,
300
+ client=self.client,
301
+ mcp_servers=mcp_servers or [],
302
+ skip_permissions=self.skip_permissions,
303
+ )
304
+ await session.initialize()
305
+ self.sessions[session_id] = session
306
+ self._cancel_events[session_id] = asyncio.Event()
307
+
308
+ return ResumeSessionResponse()
309
+
310
+ async def cancel(self, session_id: str, **kwargs: Any) -> None:
311
+ """Cancel the current operation in a session."""
312
+ logger.info(f"Cancelling session {session_id}")
313
+ cancel_event = self._cancel_events.get(session_id)
314
+ if cancel_event:
315
+ cancel_event.set()
316
+
317
+ async def ext_method(self, method: str, params: Dict[str, Any]) -> Dict[str, Any]:
318
+ """Handle extension method (not implemented)."""
319
+ return {}
320
+
321
+ async def ext_notification(self, method: str, params: Dict[str, Any]) -> None:
322
+ """Handle extension notification (not implemented)."""
323
+ pass
324
+
325
+
326
+ class ACPSession:
327
+ """
328
+ Represents an ACP session with an underlying MinionCodeAgent.
329
+ """
330
+
331
+ def __init__(
332
+ self,
333
+ session_id: str,
334
+ cwd: str,
335
+ client: Optional[Client],
336
+ mcp_servers: List[Any],
337
+ skip_permissions: bool = False,
338
+ model: Optional[str] = None,
339
+ ):
340
+ self.session_id = session_id
341
+ self.cwd = cwd
342
+ self.client = client
343
+ self.mcp_servers = mcp_servers
344
+ self.skip_permissions = skip_permissions
345
+ self.model = model
346
+ self.agent: Optional[MinionCodeAgent] = None
347
+ self.hooks: Optional[HookConfig] = None
348
+ self.permission_store: Optional[PermissionStore] = None
349
+ self._message_history: List[Dict[str, Any]] = []
350
+ # Track current code execution tool call ID for pairing start/result
351
+ self._current_code_call_id: Optional[str] = None
352
+
353
+ async def initialize(self) -> None:
354
+ """Initialize the session and create the agent."""
355
+ # Create permission store for this project
356
+ self.permission_store = PermissionStore(cwd=self.cwd)
357
+
358
+ # Create ACP hooks
359
+ if self.client:
360
+ self.hooks = create_acp_hooks(
361
+ client=self.client,
362
+ session_id=self.session_id,
363
+ request_permission=not self.skip_permissions, # Ask user permission unless skipped
364
+ include_dangerous_check=True,
365
+ permission_store=self.permission_store,
366
+ )
367
+
368
+ # Create the agent with optional model override
369
+ create_kwargs = {
370
+ "hooks": self.hooks,
371
+ "workdir": self.cwd,
372
+ # History decay: save large outputs to file after N steps
373
+ "decay_enabled": True,
374
+ "decay_ttl_steps": 3,
375
+ "decay_min_size": 100_000, # 100KB
376
+ }
377
+ if self.model:
378
+ create_kwargs["llm"] = self.model
379
+ logger.info(f"Creating agent with model: {self.model}")
380
+
381
+ self.agent = await MinionCodeAgent.create(**create_kwargs)
382
+
383
+ async def run_prompt(
384
+ self,
385
+ message: str,
386
+ cancel_event: Optional[asyncio.Event] = None,
387
+ ) -> str:
388
+ """
389
+ Run a prompt through the agent and stream results.
390
+
391
+ Returns the stop reason for the prompt response.
392
+ """
393
+ if not self.agent or not self.client:
394
+ return "refusal"
395
+
396
+ try:
397
+ # Run agent with streaming - await to get async generator, then iterate
398
+ stream = await self.agent.run_async(message, stream=True)
399
+ async for chunk in stream:
400
+ # Check for cancellation
401
+ if cancel_event and cancel_event.is_set():
402
+ return "cancelled"
403
+
404
+ # Handle different chunk types
405
+ await self._handle_stream_chunk(chunk)
406
+
407
+ return "end_turn"
408
+
409
+ except asyncio.CancelledError:
410
+ return "cancelled"
411
+ except Exception as e:
412
+ logger.error(f"Error in run_prompt: {e}")
413
+ return "refusal"
414
+
415
+ async def _handle_stream_chunk(self, chunk: Any) -> None:
416
+ """Handle a stream chunk from the agent and convert to ACP events."""
417
+ if not self.client:
418
+ return
419
+
420
+ # Import StreamChunk type
421
+ from minion.types import StreamChunk, AgentResponse
422
+
423
+ if isinstance(chunk, StreamChunk):
424
+ chunk_type = chunk.chunk_type
425
+ content = chunk.content
426
+ metadata = getattr(chunk, "metadata", {}) or {}
427
+
428
+ if chunk_type == "thinking":
429
+ # LLM reasoning/thinking - send as thought chunk
430
+ if content:
431
+ await self.client.session_update(
432
+ session_id=self.session_id,
433
+ update=update_agent_thought_text(content),
434
+ )
435
+
436
+ elif chunk_type in ("text", "content"):
437
+ # Regular assistant message content
438
+ if content:
439
+ await self.client.session_update(
440
+ session_id=self.session_id,
441
+ update=update_agent_message_text(content),
442
+ )
443
+
444
+ elif chunk_type == "code_start":
445
+ # Code execution starting - send ToolCallStart
446
+ self._current_code_call_id = str(uuid.uuid4())
447
+ await self.client.session_update(
448
+ session_id=self.session_id,
449
+ update=ToolCallStart(
450
+ session_update="tool_call",
451
+ tool_call_id=self._current_code_call_id,
452
+ title="Executing Python code",
453
+ kind="execute",
454
+ status="in_progress",
455
+ raw_input=f"```python\n{content}\n```",
456
+ content=[
457
+ ContentToolCallContent(
458
+ type="content",
459
+ content=TextContentBlock(
460
+ type="text", text=f"```python\n{content}\n```"
461
+ ),
462
+ )
463
+ ],
464
+ ),
465
+ )
466
+
467
+ elif chunk_type == "code_result":
468
+ # Code execution result - send ToolCallProgress
469
+ if self._current_code_call_id:
470
+ success = metadata.get("success", True)
471
+ await self.client.session_update(
472
+ session_id=self.session_id,
473
+ update=ToolCallProgress(
474
+ session_update="tool_call_update",
475
+ tool_call_id=self._current_code_call_id,
476
+ status="completed" if success else "failed",
477
+ content=[
478
+ ContentToolCallContent(
479
+ type="content",
480
+ content=TextContentBlock(
481
+ type="text",
482
+ text=(
483
+ content
484
+ if content
485
+ else "Executed successfully"
486
+ ),
487
+ ),
488
+ )
489
+ ],
490
+ ),
491
+ )
492
+ self._current_code_call_id = None
493
+
494
+ elif chunk_type == "step_start":
495
+ # Step start notification - can be logged or sent as info
496
+ logger.debug(f"Step started: {metadata.get('iteration', '?')}")
497
+
498
+ elif chunk_type == "tool_call":
499
+ # Direct tool call (non-code execution) - handled by pre_tool_use hook
500
+ # But we can also send notification here if needed
501
+ pass
502
+
503
+ elif chunk_type == "tool_response":
504
+ # Tool response - handled by post_tool_use hook
505
+ pass
506
+
507
+ elif chunk_type == "error":
508
+ # Error message
509
+ if content:
510
+ await self.client.session_update(
511
+ session_id=self.session_id,
512
+ update=update_agent_message_text(f"Error: {content}"),
513
+ )
514
+
515
+ elif chunk_type == "final_answer":
516
+ # Final answer reached
517
+ if content:
518
+ await self.client.session_update(
519
+ session_id=self.session_id,
520
+ update=update_agent_message_text(content),
521
+ )
522
+
523
+ elif isinstance(chunk, AgentResponse):
524
+ # Final AgentResponse with answer
525
+ if chunk.answer:
526
+ await self.client.session_update(
527
+ session_id=self.session_id,
528
+ update=update_agent_message_text(chunk.answer),
529
+ )
530
+
531
+ elif hasattr(chunk, "answer") and chunk.answer:
532
+ # Fallback for objects with answer attribute
533
+ await self.client.session_update(
534
+ session_id=self.session_id,
535
+ update=update_agent_message_text(chunk.answer),
536
+ )
537
+
538
+
539
+ __all__ = ["MinionACPAgent", "ACPSession"]