File size: 27,800 Bytes
6e38ce1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
import asyncio
from pathlib import Path
import shutil
import tempfile
from typing import AsyncGenerator, List, Sequence, Optional
import re
from typing import Any, Mapping
import uuid
from loguru import logger
from datetime import datetime
from pydantic import Field
from autogen_core import CancellationToken, ComponentModel, Component
from autogen_core.models import (
    ChatCompletionClient,
    UserMessage,
    SystemMessage,
)
from pydantic import BaseModel
from typing_extensions import Self

from autogen_agentchat.agents import BaseChatAgent
from autogen_core.code_executor import CodeBlock, CodeExecutor
from autogen_core.model_context import (
    ChatCompletionContext,
    TokenLimitedChatCompletionContext,
)
from autogen_agentchat.base import Response
from autogen_agentchat.state import BaseState
from autogen_agentchat.messages import (
    BaseAgentEvent,
    BaseChatMessage,
    TextMessage,
    MessageFactory,
)
from autogen_core.code_executor import CodeResult
from autogen_ext.code_executors.local import LocalCommandLineCodeExecutor
from autogen_ext.code_executors.docker import DockerCommandLineCodeExecutor

from ..utils import thread_to_context
from ._utils import exec_command_umask_patched

from ..approval_guard import BaseApprovalGuard
from ..guarded_action import ApprovalDeniedError, TrivialGuardedAction

DockerCommandLineCodeExecutor._execute_command = exec_command_umask_patched  # type: ignore


def _extract_markdown_code_blocks(markdown_text: str) -> List[CodeBlock]:
    pattern = re.compile(r"```(?:\s*([\w\+\-]+))?\n([\s\S]*?)```")
    matches = pattern.findall(markdown_text)
    code_blocks: List[CodeBlock] = []
    for match in matches:
        language = match[0].strip() if match[0] else ""
        code_content = match[1]
        code_blocks.append(CodeBlock(code=code_content, language=language))
    return code_blocks


async def _invoke_action_guard(
    thread: Sequence[BaseChatMessage | BaseAgentEvent],
    delta: Sequence[BaseChatMessage | BaseAgentEvent],
    code_message: TextMessage,
    agent_name: str,
    model_client: ChatCompletionClient,
    approval_guard: BaseApprovalGuard | None,
) -> None:
    # Get approval for the coding request. We could conceivably do extra work to enable interactive approval here,
    # but the value for many users is likely to be low, as it may not be appropriate to assume knowledge of coding,
    # and thus the user will not have the context necessary to approve/deny the incremental execution of code blocks.
    guarded_action = TrivialGuardedAction("coding", baseline_override="maybe")

    # Note that delta already contains the code message.
    assert delta[-1] == code_message

    thread = list(thread) + list(delta)

    context = thread_to_context(
        thread,
        agent_name,
        is_multimodal=model_client.model_info["vision"],
    )
    action_description_for_user = TextMessage(
        content="Do you want to execute the code above?",
        source=agent_name,
    )

    await guarded_action.invoke_with_approval(
        {}, code_message, context, approval_guard, action_description_for_user
    )


async def _coding_and_debug(
    system_prompt: str,
    thread: Sequence[BaseChatMessage],
    agent_name: str,
    model_client: ChatCompletionClient,
    code_executor: CodeExecutor,
    max_debug_rounds: int,
    cancellation_token: CancellationToken,
    model_context: ChatCompletionContext,
    approval_guard: BaseApprovalGuard | None,
) -> AsyncGenerator[TextMessage | bool, None]:
    """Write and debug code using the model and executor.

    It generates code based on the system prompt and the thread of messages,
    executes it, and returns the output. It continues to generate and execute
    code until the maximum number of debug rounds is reached or the code
    executes successfully.

    When the cancellation token is set, the execution will stop.
    Args:
        system_prompt (str): The system prompt to guide the model.
        thread (Sequence[BaseChatMessage]): The thread of messages to use as context.
        agent_name (str): The name of the agent.
        model_client (ChatCompletionClient): The model client to use for code generation.
        code_executor (CodeExecutor): The code executor to use for executing the code.
        max_debug_rounds (int): The maximum number of debug rounds to perform.
        cancellation_token (CancellationToken): The cancellation token to stop execution.
        model_context (ChatCompletionContext): The context for the model.
        approval_guard (ApprovalGuard | None): The approval guard to use for code execution.

    Yields:
        TextMessage: The intermediate messages generated by the model and executor.
        bool: A flag indicating whether any code execution was performed.

    Raises:
        ApprovalDeniedError: If the user denies the approval for the coding request.
    """
    # The list of new messages to be added to the thread.
    delta: Sequence[BaseChatMessage | BaseAgentEvent] = []
    executed_code = False

    for i in range(max_debug_rounds):
        # Add system prompt as the last message before generation
        current_thread = (
            list(thread)
            + list(delta)
            + [TextMessage(source="user", content=system_prompt)]
        )

        # create an LLM context from system message, global chat history, and inner messages
        context = [SystemMessage(content=system_prompt)] + thread_to_context(
            current_thread,
            agent_name,
            is_multimodal=model_client.model_info["vision"],
        )

        # Re-initialize model context to meet token limit quota
        try:
            await model_context.clear()
            for msg in context:
                await model_context.add_message(msg)
            token_limited_context = await model_context.get_messages()
        except Exception:
            token_limited_context = context
        # Generate code using the model.
        create_result = await model_client.create(
            messages=token_limited_context, cancellation_token=cancellation_token
        )
        assert isinstance(create_result.content, str)
        code_msg = TextMessage(
            source=agent_name + "-llm",
            metadata={"internal": "no", "type": "potential_code"},
            content=create_result.content,
        )
        # add LLM's response to the current thread.
        delta.append(code_msg)
        yield code_msg

        # extract code blocks from the LLM's response
        code_block_list = _extract_markdown_code_blocks(create_result.content)
        # if no code to execute, return
        if len(code_block_list) == 0:
            break

        # Now that we know we have code to execute, make sure we have permission to do so.
        # Note that we will be getting this permission per round of code/execute/debug.
        if approval_guard is not None:
            await _invoke_action_guard(
                thread=thread,
                delta=delta,
                code_message=code_msg,  # note that this `code_msg is delta[-1]`
                agent_name=agent_name,
                model_client=model_client,
                approval_guard=approval_guard,
            )

        code_output_list: List[str] = []
        exit_code_list: List[int] = []
        executed_code = True
        try:
            for cb in code_block_list:
                # execute the code block
                exit_code: int = 1
                encountered_exception: bool = False
                code_output: str = ""
                result: CodeResult | None = None
                try:
                    result = await code_executor.execute_code_blocks(
                        [cb], cancellation_token
                    )
                    exit_code = result.exit_code or 0
                    code_output = result.output
                except Exception as e:
                    code_output = str(e)
                    encountered_exception = True
                if encountered_exception or result is None:
                    code_output = f"An exception occurred while executing the code block: {code_output}"
                elif code_output.strip() == "":
                    # No output
                    code_output = f"The script ran but produced no output to console. The POSIX exit code was: {result.exit_code}. If you were expecting output, consider revising the script to ensure content is printed to stdout."
                elif exit_code != 0:
                    # Error
                    code_output = f"The script ran, then exited with an error (POSIX exit code: {result.exit_code})\nIts output was:\n{result.output}"
                code_output_list.append(code_output)
                code_output_msg = TextMessage(
                    source=agent_name + "-executor",
                    metadata={"internal": "no", "type": "code_execution"},
                    content=f"Execution result of code block {i + 1}:\n```console\n{code_output}\n```",
                )
                exit_code_list.append(exit_code)
                yield code_output_msg

            final_code_output = ""
            for i, code_output in enumerate(code_output_list):
                final_code_output += f"\n\nExecution Result of Code Block {i + 1}:\n```console\n{code_output}\n```"

            # add executors response to thread.
            executor_msg = TextMessage(
                source=agent_name + "-executor",
                metadata={"internal": "yes"},
                content=final_code_output,
            )
            delta.append(executor_msg)
            yield executor_msg

            # break if the code execution was successful
            if all([code_output == 0 for code_output in exit_code_list]):
                break
        except asyncio.TimeoutError:
            # If the task times out, we treat it as an error.
            executor_msg = TextMessage(
                source=agent_name + "-executor",
                metadata={"internal": "yes"},
                content="Code execution timed out.",
            )
            delta.append(executor_msg)
            yield executor_msg

    yield executed_code


async def _summarize_coding(
    agent_name: str,
    model_client: ChatCompletionClient,
    thread: Sequence[BaseChatMessage | BaseAgentEvent],
    cancellation_token: CancellationToken,
    model_context: ChatCompletionContext,
) -> TextMessage:
    # Create a summary from the inner messages using an extra LLM call.
    input_messages = (
        [SystemMessage(content="You are an agent that can write and debug code")]
        + thread_to_context(
            list(thread), agent_name, is_multimodal=model_client.model_info["vision"]
        )
        + [
            UserMessage(
                content="""
                The above is a transcript of your previous messages and a request that was given to you in the begining.
                You need to summarize them to answer the request given to you. Generate a summary of everything that happened.
                If there was code that was executed, please copy the final code that was executed without errors.
                Don't mention that this is a summary, just give the summary.""",
                source="user",
            )
        ]
    )

    # Re-initialize model context to meet token limit quota
    try:
        await model_context.clear()
        for msg in input_messages:
            await model_context.add_message(msg)
        token_limited_input_messages = await model_context.get_messages()
    except Exception:
        token_limited_input_messages = input_messages

    summary_result = await model_client.create(
        messages=token_limited_input_messages, cancellation_token=cancellation_token
    )
    assert isinstance(summary_result.content, str)
    return TextMessage(
        source=agent_name,
        metadata={"internal": "yes"},
        content=summary_result.content,
    )


class CoderAgentConfig(BaseModel):
    name: str
    model_client: ComponentModel
    description: str = """
    An agent that can write and execute code to solve tasks or use its language skills to summarize, write, solve math and logic problems.
    It understands images and can use them to help it complete the task.
    It can access files if given the path and manipulate them using python code. Use the coder if you want to manipulate a file or read a csv or excel files.
    """
    max_debug_rounds: int = 3
    summarize_output: bool = False
    # Optionally add code_executor config if needed


class CoderAgentState(BaseState):
    chat_history: List[BaseChatMessage] = Field(default_factory=list[BaseChatMessage])
    type: str = Field(default="CoderAgentState")


class CoderAgent(BaseChatAgent, Component[CoderAgentConfig]):
    """An agent capable of writing, executing, and debugging code.

    The agent uses either a local or Docker-based code executor to run the generated code
    in a controlled environment. It maintains a chat history and can be paused/resumed
    during execution.
    """

    component_type = "agent"
    component_config_schema = CoderAgentConfig
    component_provider_override = "magentic_ui.agents.CoderAgent"

    DEFAULT_DESCRIPTION = """
    An agent that can write and execute code to solve tasks or use its language skills to summarize, write, solve math and logic problems.
    It understands images and can use them to help it complete the task.
    It can access files if given the path and manipulate them using python code. Use the coder if you want to manipulate a file or read a csv or excel files.
    In a single step when you ask the agent to do something: it can write code, and then immediately execute the code. If there are errors it can debug the code and try again.
    """

    system_prompt_coder_template = """
    You are helpful assistant.
    In addition to responding with text you can write code and execute code that you generate.
    The date today is: {date_today}

    Rules to follow for Code:
    - Generate py or sh code blocks in the order you'd like your code to be executed.
    - Code block must indicate language type. Do not try to predict the answer of execution. Code blocks will be automatically executed for you.
    - If you want to stop executing code, make sure to not write any code in your message and your turn will be over.
    - Do not generate code that relies on API keys that you don't have access to. Try different approaches.

    Tips:
    - You don't have to generate code if the task is not related to code, for instance writing a poem, paraphrasing a text, etc.
    - If you are asked to solve math or logical problems, first try to answer them without code and then if needed try to use python to solve them.
    - You have access to the standard Python libraries in addition to numpy, pandas, scikit-learn, matplotlib, pillow, requests, beautifulsoup4.
    - If you need to use an external library, write first a shell script that installs the library first using pip install, then add code blocks to use the library.
    - Always use print statements to output your work and partial results.
    - For showing plots or other visualizations that are not just text, make sure to save them to file with the right extension for them to be displayed.

   VERY IMPORTANT: If you intend to write code to be executed, do not end your response without a code block. If you want to write code you must provide a code block in the current generation.
    """

    def __init__(
        self,
        name: str,
        model_client: ChatCompletionClient,
        model_context_token_limit: int = 128000,
        description: str = DEFAULT_DESCRIPTION,
        max_debug_rounds: int = 3,
        summarize_output: bool = False,
        code_executor: Optional[CodeExecutor] = None,
        work_dir: Path | str | None = None,
        bind_dir: Path | str | None = None,
        use_local_executor: bool = False,
        approval_guard: BaseApprovalGuard | None = None,
    ) -> None:
        """Initialize the CoderAgent.

        Args:
            name (str): The name of the agent
            model_client (ChatCompletionClient): The language model client to use
            description (str, optional): Description of the agent's capabilities. Default: DEFAULT_DESCRIPTION.
            max_debug_rounds (int, optional): Maximum number of code debugging iterations. Default: 3.
            summarize_output (bool, optional): Whether to summarize code execution results. Default: False.
            code_executor (Optional[CodeExecutor], optional): Custom code executor to use. Default: None.
            work_dir (Path | str | None, optional): Working directory for code execution. Default: None.
            bind_dir (Path | str | None, optional): Directory to bind for Docker executor. Default: None.
            use_local_executor (bool, optional): Whether to use local instead of Docker executor. Default: False.
        """
        super().__init__(name, description)
        self._model_client = model_client
        self._model_context = TokenLimitedChatCompletionContext(
            model_client, token_limit=model_context_token_limit
        )
        self._chat_history: List[BaseChatMessage] = []
        self._max_debug_rounds = max_debug_rounds
        self._summarize_output = summarize_output
        self.is_paused = False
        self._paused = asyncio.Event()
        self._approval_guard = approval_guard
        self._did_lazy_init = False
        if work_dir is None:
            self._work_dir = Path(tempfile.mkdtemp())
            self._cleanup_work_dir = True
        else:
            self._work_dir = Path(work_dir)
            self._cleanup_work_dir = False
        if code_executor:
            self._code_executor = code_executor
        elif use_local_executor:
            self._code_executor = LocalCommandLineCodeExecutor(work_dir=self._work_dir)
        else:
            from .._docker import PYTHON_IMAGE

            name = f"{name}-{uuid.uuid4()}"
            self._code_executor = DockerCommandLineCodeExecutor(
                container_name=name,
                image=PYTHON_IMAGE,
                work_dir=self._work_dir,
                bind_dir=bind_dir,
                delete_tmp_files=True,
            )

    async def lazy_init(self) -> None:
        """Initialize the code executor if it has a start method.

        This method is called after initialization to set up any async resources
        needed by the code executor.
        """
        if self._did_lazy_init:
            return
        if self._code_executor:
            # check if the code executor has a start method
            if hasattr(self._code_executor, "start"):
                # TODO: we should add a no-op start() method to the base class.
                await self._code_executor.start()  # type: ignore
        self._did_lazy_init = True

    async def close(self) -> None:
        """Clean up resources used by the agent.

        This method:
        - Stops the code executor
        - Removes the work directory if it was created
        - Closes the model client
        """
        logger.info("Closing Coder...")
        await self._code_executor.stop()
        # Remove the work directory if it was created.
        if self._cleanup_work_dir and self._work_dir.exists():
            await asyncio.to_thread(shutil.rmtree, self._work_dir)
        # Close the model client.
        await self._model_client.close()

    async def pause(self) -> None:
        """Pause the agent by setting the paused state."""
        self.is_paused = True
        self._paused.set()

    async def resume(self) -> None:
        """Resume the agent by clearing the paused state."""
        self.is_paused = False
        self._paused.clear()

    @property
    def produced_message_types(self) -> Sequence[type[BaseChatMessage]]:
        """Get the types of messages produced by the agent."""
        return (TextMessage,)

    async def on_messages(
        self, messages: Sequence[BaseChatMessage], cancellation_token: CancellationToken
    ) -> Response:
        """Handle incoming messages and return a single response. Calls the on_messages_stream."""
        response: Response | None = None
        async for message in self.on_messages_stream(messages, cancellation_token):
            if isinstance(message, Response):
                response = message
        assert response is not None
        return response

    async def on_messages_stream(
        self, messages: Sequence[BaseChatMessage], cancellation_token: CancellationToken
    ) -> AsyncGenerator[BaseAgentEvent | BaseChatMessage | Response, None]:
        """Handle incoming messages and yield responses as a stream. Append the request to agents chat history."""
        await self.lazy_init()

        if self.is_paused:
            yield Response(
                chat_message=TextMessage(
                    content="The Coder is paused.",
                    source=self.name,
                    metadata={"internal": "yes"},
                )
            )
            return
        self._chat_history.extend(messages)
        last_message_received: BaseChatMessage = messages[-1]
        inner_messages: List[BaseChatMessage] = []

        # Set up the cancellation token for the code execution.
        code_execution_token = CancellationToken()

        # Cancel the code execution if the handler's cancellation token is set.
        cancellation_token.add_callback(lambda: code_execution_token.cancel())

        # Set up background task to monitor the pause event and cancel the code execution if paused.
        async def monitor_pause() -> None:
            await self._paused.wait()
            code_execution_token.cancel()

        monitor_pause_task = asyncio.create_task(monitor_pause())

        system_prompt_coder = self.system_prompt_coder_template.format(
            date_today=datetime.now().strftime("%Y-%m-%d")
        )

        try:
            executed_code = False
            # Run the code execution and debugging process.
            async for msg in _coding_and_debug(
                system_prompt=system_prompt_coder,
                thread=self._chat_history,
                agent_name=self.name,
                model_client=self._model_client,
                code_executor=self._code_executor,
                max_debug_rounds=self._max_debug_rounds,
                cancellation_token=code_execution_token,
                model_context=self._model_context,
                approval_guard=self._approval_guard,
            ):
                if isinstance(msg, bool):
                    executed_code = msg
                    break
                inner_messages.append(msg)
                self._chat_history.append(msg)
                yield msg

            # New conditional block based on the configuration flag.
            if self._summarize_output and executed_code:
                summary_msg = await _summarize_coding(
                    agent_name=self.name,
                    model_client=self._model_client,
                    thread=[last_message_received] + inner_messages,
                    cancellation_token=code_execution_token,
                    model_context=self._model_context,
                )
                self._chat_history.append(summary_msg)
                yield Response(chat_message=summary_msg, inner_messages=inner_messages)
            else:
                # Instead of only executor output, return a transcript of all code and execution steps.
                combined_output = ""
                for txt_msg in inner_messages:
                    assert isinstance(txt_msg, TextMessage)
                    combined_output += f"{txt_msg.content}\n"
                final_response_msg = TextMessage(
                    source=self.name,
                    metadata={"internal": "yes"},
                    content=combined_output or "No output.",
                )
                # TODO: do we not want to add this to the chat history?
                yield Response(
                    chat_message=final_response_msg, inner_messages=inner_messages
                )
        except ApprovalDeniedError:
            # If the user denies the approval, we respond with a message.
            yield Response(
                chat_message=TextMessage(
                    content="The user did not approve the code execution.",
                    source=self.name,
                    metadata={"internal": "no"},
                ),
                inner_messages=inner_messages,
            )
        except asyncio.CancelledError:
            # If the task is cancelled, we respond with a message.
            yield Response(
                chat_message=TextMessage(
                    content="The task was cancelled by the user.",
                    source=self.name,
                    metadata={"internal": "yes"},
                ),
                inner_messages=inner_messages,
            )
        except Exception as e:
            logger.error(f"Error in CoderAgent: {e}")
            # add to chat history
            self._chat_history.append(
                TextMessage(
                    content=f"An error occurred while executing the code: {e}",
                    source=self.name,
                )
            )
            yield Response(
                chat_message=TextMessage(
                    content=f"An error occurred in the coder agent: {e}",
                    source=self.name,
                    metadata={"internal": "no"},
                ),
                inner_messages=inner_messages,
            )
        finally:
            # Cancel the monitor task.
            try:
                monitor_pause_task.cancel()
                await monitor_pause_task
            except asyncio.CancelledError:
                pass

    async def on_reset(self, cancellation_token: CancellationToken) -> None:
        """Clear the chat history."""
        self._chat_history.clear()

    def _to_config(self) -> CoderAgentConfig:
        """Convert the agent's state to a configuration object."""
        return CoderAgentConfig(
            name=self.name,
            model_client=self._model_client.dump_component(),
            description=self.description,
            max_debug_rounds=self._max_debug_rounds,
            summarize_output=self._summarize_output,
            # TODO: Optionally add code_executor configuration if supported
        )

    @classmethod
    def _from_config(cls, config: CoderAgentConfig) -> Self:
        """Create an agent instance from a configuration object."""
        return cls(
            name=config.name,
            model_client=ChatCompletionClient.load_component(config.model_client),
            description=config.description,
            max_debug_rounds=config.max_debug_rounds,
            summarize_output=config.summarize_output,
            # TODO: Optionally load code_executor from config if provided
        )

    async def save_state(self) -> Mapping[str, Any]:
        """
        Save the state of the agent.
        """
        return {
            "chat_history": [msg.dump() for msg in self._chat_history],
        }

    async def load_state(self, state: Mapping[str, Any]) -> None:
        """
        Load the state of the agent.
        """
        # Create message factory for deserialization.
        message_factory = MessageFactory()
        self._chat_history = []
        for msg_data in state["chat_history"]:
            msg = message_factory.create(msg_data)
            assert isinstance(msg, BaseChatMessage)
            self._chat_history.append(msg)