ACP Lesson 8: several issues

I’m trying to run the Lesson 8 chaining code (I’ve successfully done 1-7). I get this error:

File "/Users/donohoe/work/python/.venv/lib/python3.11/site-packages/smolagents/models.py", line 291, in get_clean_message_list
    role = message.role
           ^^^^^^^^^^^^
AttributeError: 'dict' object has no attribute 'role'

The issue turns out to be that the fastacp.py code is passing in a dict where the agent code is expecting ChatMessage data class. I had to add some verbose debug logging to figure this out:

            except Exception as e:
                self.logger.log(f"Error in step {step_num + 1}: {str(e)}", level=LogLevel.ERROR)

                # JDD-debugging
                import traceback
                # Print the full traceback to see the original error
                print("=== FULL EXCEPTION CHAIN ===")
                traceback.print_exc()
                # Also check for chained exceptions
                if hasattr(e, '__cause__') and e.__cause__:
                    print("\n=== ORIGINAL CAUSE ===")
                    traceback.print_exception(type(e.__cause__), e.__cause__, e.__cause__.__traceback__)
                if hasattr(e, '__context__') and e.__context__:
                    print("\n=== EXCEPTION CONTEXT ===")
                    traceback.print_exception(type(e.__context__), e.__context__, e.__context__.__traceback__)
                # JDD-debugging-end

So to get around this I added this converter:

from smolagents.models import ChatMessage as SmolChatMessage

def convert_to_chat_message(msg_dict):
    if isinstance(msg_dict, dict):
        return SmolChatMessage(
            role=msg_dict['role'],
            content=msg_dict.get('content'),
            tool_calls=msg_dict.get('tool_calls'),
            raw=msg_dict.get('raw'),
            token_usage=msg_dict.get('token_usage')
        )
    return msg_dict  # Already a ChatMessage

And used it here to convert memory_messages from a list of dict to smolagent’s ChatMessage.

        try:
            # JDD: convert to actual type API expects (sigh)
            chat_messages = [convert_to_chat_message(msg) for msg in memory_messages]

            model_message: ChatMessage = self.model(
                chat_messages,
                tools_to_call_from=list(self.tools.values())[:-1],
                stop_sequences=["Observation:", "Calling agents:"],
            )
            memory_step.model_output_message = model_message

This moves things along, and I see the agents getting called and answers getting returned, but it just runs through the 10 loops, ending with:

Final result: I wasn't able to complete this task within the maximum number of steps.

I’ve run out of patience trying to understand the code in fastacp.py. This class is frustrating because there is no real recourse for when things go wrong. It all assumes a happy path. The fastacp.py is 600+ lines of code that is hard to decipher.

This feels like the end state for any vibe coding experimentation. When things go wrong, I, the programmer, don’t really understand everything that is going on because I didn’t build it up piece by piece. Sure, I have now been introduced to some libraries and concepts but I haven’t internalized it at all, so debugging is a slog.

Anyhow, I’ve found this course a bit lacking, and sorry to say, the video explainers add very little value.

This is the output of Step #10. It seems like it is getting good results, but the “i’m actually done” state isn’t triggering. I don’t really know how it arrives at that conclusion (don’t understand the code yet).

[INFO] Step 10/10
[DEBUG] Output message of the LLM:
[DEBUG] ModelResponse(id='chatcmpl-BoEX7wb5NtCOtuHL11BheVCjWMARg', created=1751311965, model='gpt-4.1-2025-04-14', object='chat.completion', system_fingerprint='fp_799e4ca3f1', choices=[Choices(finish_reason='tool_calls', index=0, message=Message(content=None, role='assistant', tool_calls=[ChatCompletionMessageToolCall(function=Function(arguments='{"input":"Please confirm the maximum waiting period for rehabilitation hospital treatment after shoulder reconstruction surgery in Australia under the Private Health Insurance Act 2007, including if it is required due to a pre-existing condition."}', name='policy_agent'), id='call_UneGiAx565yeYyfH0URu9VX6', type='function')], function_call=None, provider_specific_fields={'refusal': None}, annotations=[]))], usage=Usage(completion_tokens=51, prompt_tokens=7384, total_tokens=7435, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=0, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=0, text_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=0, cached_tokens=4864, text_tokens=None, image_tokens=None)), service_tier='default')
[INFO] Calling agent: 'policy_agent' with arguments: {"input":"Please confirm the maximum waiting period for rehabilitation hospital treatment after shoulder reconstruction surgery in Australia under the Private Health Insurance Act 2007, including if it is required due to a pre-existing condition."}
Tool being called with args: ('{"input":"Please confirm the maximum waiting period for rehabilitation hospital treatment after shoulder reconstruction surgery in Australia under the Private Health Insurance Act 2007, including if it is required due to a pre-existing condition."}',) and kwargs: {'sanitize_inputs_outputs': True}
{"input":"Please confirm the maximum waiting period for rehabilitation hospital treatment after shoulder reconstruction surgery in Australia under the Private Health Insurance Act 2007, including if it is required due to a pre-existing condition."}
run_id=UUID('e8608ea8-1761-4128-9ea2-6e527a715022') agent_name='policy_agent' session_id=UUID('8ac67c2d-df66-4138-8c09-72418375ff1c') status=<RunStatus.COMPLETED: 'completed'> await_request=None output=[Message(role='agent/policy_agent', parts=[MessagePart(name=None, content_type='text/plain', content='Under the Private Health Insurance Act 2007 in Australia, the maximum waiting period for rehabilitation hospital treatment (such as that required after shoulder reconstruction surgery) is generally 2 months. However, if the rehabilitation hospital treatment is required due to a pre-existing condition, a waiting period of up to 12 months may apply.\n\nA pre-existing condition is defined as any ailment, illness, or condition that showed signs or symptoms during the 6 months before joining or upgrading to a higher level of hospital cover, as determined by a medical practitioner appointed by the health insurer. It does not require prior diagnosis or treatment by your doctor for it to be classed as pre-existing.\n\nTherefore, for rehabilitation hospital treatment following shoulder reconstruction surgery, if the need for treatment is determined to be related to a pre-existing condition, you may be subject to a waiting period of up to 12 months. If it is not related to a pre-existing condition, the standard waiting period is 2 months.', content_encoding='plain', content_url=None, metadata=None)], created_at=datetime.datetime(2025, 6, 30, 19, 32, 51, 381599, tzinfo=TzInfo(UTC)), completed_at=datetime.datetime(2025, 6, 30, 19, 32, 51, 381604, tzinfo=TzInfo(UTC)))] error=None created_at=datetime.datetime(2025, 6, 30, 19, 32, 47, 85690, tzinfo=TzInfo(UTC)) finished_at=datetime.datetime(2025, 6, 30, 19, 32, 51, 382985, tzinfo=TzInfo(UTC))
[DEBUG] Saved to memory: policy_agent_response=Under the Private Health Insurance Act 2007 in Australia, the maximum waiting period for rehabilitation hospital treatment (such as that required after shoulder reconstruction surgery) is generally 2 months. However, if the rehabilitation hospital treatment is required due to a pre-existing condition, a waiting period of up to 12 months may apply.

A pre-existing condition is defined as any ailment, illness, or condition that showed signs or symptoms during the 6 months before joining or upgrading to a higher level of hospital cover, as determined by a medical practitioner appointed by the health insurer. It does not require prior diagnosis or treatment by your doctor for it to be classed as pre-existing.

Therefore, for rehabilitation hospital treatment following shoulder reconstruction surgery, if the need for treatment is determined to be related to a pre-existing condition, you may be subject to a waiting period of up to 12 months. If it is not related to a pre-existing condition, the standard waiting period is 2 months.
[INFO] Observations: Under the Private Health Insurance Act 2007 in Australia, the maximum waiting period for rehabilitation hospital treatment (such as that required after shoulder reconstruction surgery) is generally 2 months. However, if the rehabilitation hospital treatment is required due to a pre-existing condition, a waiting period of up to 12 months may apply.

A pre-existing condition is defined as any ailment, illness, or condition that showed signs or symptoms during the 6 months before joining or upgrading to a higher level of hospital cover, as determined by a medical practitioner appointed by the health insurer. It does not require prior diagnosis or treatment by your doctor for it to be classed as pre-existing.

Therefore, for rehabilitation hospital treatment following shoulder reconstruction surgery, if the need for treatment is determined to be related to a pre-existing condition, you may be subject to a waiting period of up to 12 months. If it is not related to a pre-existing condition, the standard waiting period is 2 months.
Final result: I wasn't able to complete this task within the maximum number of steps.

So, I finally figured out why my local code wasn’t getting to a final answer. I had at some point changed the ChatGPT model from 4 to 4.1:

model = LiteLLMModel(
    model_id="openai/gpt-4"
)

to

model = LiteLLMModel(
    model_id="openai/gpt-4.1"
)

I believe I did this because I was having trouble calling ChatGPT at the beginning of this course due to what ended up being either dependency version issues or API key issues. Nevertheless, changing it back to 4 solves my problem.

However, it begs the question why upping the version from 4 to 4.1 causes things to break? I’ve created a lesson8.py script that I can run directly and step through the debugger. It seems that v4.1 keeps sending back requests to query the policy_agent. I’m not sure why, but will keep digging.

It does make me wonder about the fragility of these agent chaining systems. Seems like very easy to break and very hard to debug. The complexity of the async def step() method should give any solid engineer pause.

As to why it breaks in 4.1, ChatGPT says:

Summary of the design

Your ACPCallingAgent does the following in __init__():

  • Sets up a prompt template that instructs the model:

Always call the appropriate agents, and when complete, call the final_answer tool with your response.

  • Wraps each ACP agent (health_agent, policy_agent, etc.) in a Tool.
  • Dynamically wires those tools so that each can be called by the model.
  • Adds a final_answer tool as the only way to return a result.

So far so good.

Why this breaks in GPT-4.1

Because GPT-4.1 batch-calls tools in parallel, your agent now receives a single response like:

tool_calls = [
  {"function": {"name": "health_agent", "arguments": ...}},
  {"function": {"name": "policy_agent", "arguments": ...}},
]

But unless both of those return values are passed back to the model in the next turn, it never reaches the point of calling final_answer. It’s waiting for the full set of tool responses.

:white_check_mark: What you need to fix

Update the loop that handles tool calls so that it:

  1. Detects and dispatches all tools in tool_calls, not just one.
  2. Collects results for each tool_call.id.
  3. Returns all results in a single update, e.g.:
[
  {
    "tool_call_id": "...",
    "output": "Here's info from health agent"
  },
  {
    "tool_call_id": "...",
    "output": "Here's policy info"
  }
]

This unlocks the final_answer step.

My Next Steps

I may try this on my own for fun.

Last comment for course authors (if they read this stuff). It seems like the course would have been better to build up this code from scratch rather than give fastacp.py to us in one fell swoop. Learning the ChatGPT API around function calling is critical.

Thanks @dougdonohoe for trying out multiple steps and posting here the updates. I have pretty much the same troubles, and end up using a wrapper to make it compatible with ACPCallingAgent.
However, when I do another similar project, still unable to make it work due to various errors caused by ACPCallingAgent.