Skip to main content

The Guardrails That Make Agents Useful

What makes an agent genuinely useful? Guardrails. The Guardrails That Make Agents Useful considers why clear boundaries matter when agents are asked to act, not just respond.

Code Cracking
15m read
#AI#AIAgents#AISafety
The Guardrails That Make Agents Useful - Featured blog post image
Mahmoud Zalt

1:1 Mentor

Are you a software engineer moving into AI?

Let's have a call. I'll help you modernize your skills and learn the tools, systems, and architecture behind reliable AI products. One session or ongoing.

Vibe Coding
with Confidence

The Vibecoder's Handbook, from idea to production

4.8

Everything you need to know about shipping software with AI, from the App idea to production.

What it covers

  • 0IntroductionWhat this book is & how to read it
  • 1Set UpGet your tools and a running app ready
  • 2PlanStructure your idea into a clear specification
  • 3ArchitectLay out a modular codebase for your AI
Start Reading Free

We’re examining how Microsoft AutoGen’s AssistantAgent controls an agent turn. AutoGen is an agent framework, and this class is where a chat request becomes a stateful workflow that can use memory, call tools, stream events, and hand work to another agent. That makes AssistantAgent more important than a model wrapper: it defines when the model may act, what state survives, and where a turn stops.

I’m Mahmoud Zalt, an AI solutions architect. The revealing question is not whether the model can choose useful actions; it is whether the surrounding workflow can bound, observe, and safely recover from those actions. We’ll trace the turn as a state machine, then examine the limits on tool use, the boundary between parallel work and sequential decisions, and the operational controls this class leaves to its deployment.

The Turn Is a Workflow

AssistantAgent behaves less like a chat function than a controller. It accepts new messages, updates persistent context, enriches that context from memory, asks the model for a result, executes requested tools, records receipts, and either continues or finalizes the turn.

User messages
      |
      v
Persistent context <--- Memory.update_context()
      |
      v
ChatCompletionClient.create/create_stream()
      |
      +--- text/structured output ---------> final Response
      |
      +--- FunctionCall tickets
                 |
                 v
       Workbench tools in parallel
                 |
                 v
       results added to context
                 |
        +--------+---------+
        |                  |
     handoff        infer again / reflect
        |                  |
        +--------> final Response
A turn moves through explicit state transitions: ingest, enrich, infer, execute, record, and finalize.

The ordering is part of the contract. Callers provide only new messages because previous history lives in ChatCompletionContext; memory then adds relevant context before inference. A generated message_id correlates streamed fragments with the eventual final message.

Turn orchestration, lines 658–678
await self._add_messages_to_context(
    model_context=model_context,
    messages=messages,
)

for event_msg in await self._update_model_context_with_memory(
    memory=memory,
    model_context=model_context,
    agent_name=agent_name,
):
    yield event_msg

message_id = str(uuid.uuid4())

Drawing this state machine first makes the rest of the file easier to reason about. Streaming, tool execution, and handoffs are not separate features; they are branches in one turn protocol. That perspective also reveals the primary design lesson: an agent becomes useful when model freedom lives inside enforceable workflow boundaries.

Bounded Autonomy

_process_model_result gives the model choices without giving it indefinite control. It permits at most max_tool_iterations sequential tool rounds, with a default of one. A turn may end with text, structured output, a handoff, a tool summary, or a reflection request, but each path has a defined termination condition.

Model resultAgent actionBoundary
TextReturn TextMessageEnds the loop immediately
Structured JSONValidate through output_content_typeRequires the declared output model
Function callsExecute tools and record resultsStops at max_tool_iterations
Handoff callReturn HandoffMessageSelects only the first requested handoff
Reflection after tool useMake a final model requesttool_choice="none" prevents another tool cycle

Construction-time validation supports the same policy. Tool and handoff names must be unique; explicit tools cannot coexist with a configured workbench; and a tool-capable agent requires a model that supports function calling. Rejecting ambiguous configuration before a turn begins is safer than resolving it after a model requests an action.

The class also preserves protocol continuity when a tool call contains malformed JSON. Rather than crashing the whole turn, it returns a failed FunctionExecutionResult tied to the original call ID. The model receives a receipt for its ticket, but the receipt accurately says that execution failed.

Parallel Tools, Sequential Decisions

Bounding the loop controls action depth; parallel execution controls latency within one round. The class uses asyncio.gather to execute independent function calls concurrently. Two independent tools can therefore complete near the duration of the slower call rather than the sum of both calls.

Parallel tool execution, lines 876–898
results = await asyncio.gather(
    *[
        cls._execute_tool_call(
            tool_call=call,
            workbench=workbench,
            handoff_tools=handoff_tools,
            agent_name=agent_name,
            cancellation_token=cancellation_token,
            stream=stream_queue,
        )
        for call in function_calls
    ]
)

Parallel execution does not make the workflow fully parallel. The next model request must wait for all results because its decision depends on them. The correct mental model is simple: calls inside a batch may run concurrently; model/tool rounds remain sequential.

Streaming introduces a related trade-off. A queue lets a workbench publish events while a tool is still running, which improves progressive feedback. But the queue is unbounded. Without backpressure—the mechanism that slows a producer when its consumer cannot keep up—a fast tool can keep increasing memory use if the client consumes events slowly.

Reflection adds another deliberate serial step. With reflect_on_tool_use, the agent asks the model to turn tool receipts into a final response. That can improve presentation, especially for structured output, but it adds an LLM round trip. Reflection is a product and latency decision, not merely formatting.

State and Production Boundaries

The strongest limits in a single turn do not protect concurrent turns. AssistantAgent owns mutable conversation context, and its documentation says it is neither thread-safe nor coroutine-safe. Two invocations can interleave writes to _model_context and corrupt the logical order of a conversation.

The constructor initializes self._is_running = False, but the analyzed implementation does not use it as a lifecycle guard. A fail-fast guard around the streaming workflow would make concurrent use explicit; a finally block would need to release it after cancellation or dependency failure. For concurrent conversations, callers should use separate agent instances or serialize access to each instance.

Context growth is the other hidden state boundary. When no context is supplied, the agent creates an UnboundedChatCompletionContext. Every turn can then increase memory consumption, prompt tokens, latency, and cost. A buffered or token-limited context complements max_tool_iterations: one bounds conversational history, the other bounds action depth.

The class passes a CancellationToken into model and tool calls, but it does not impose deadlines, retries, backoff, circuit breakers, or concurrency limits. Those policies belong in callers or provider and workbench adapters, where retry semantics can distinguish a read-only model request from a side-effecting tool call. Likewise, forwarding FunctionCall.id can support idempotency, but this class does not deduplicate calls or guarantee exactly-once execution.

Observability should mirror the workflow: trace each on_messages_stream invocation, then record child spans for memory updates, inference, tool calls, handoff construction, and reflection. Monitor end-to-end turn duration, LLM requests per turn, context size or estimated prompt tokens, and stream queue depth. Keep raw prompts, arguments, tool results, and model thoughts out of telemetry unless they are explicitly redacted; debug events may contain personal data, proprietary prompts, or secrets.

Where Useful Agents Stop

The primary lesson is that useful agents come from bounded autonomy, not unconstrained model freedom. AssistantAgent demonstrates the pattern through explicit turn ordering, capped tool iterations, typed outcomes, parallel work only where dependencies allow it, and seams where deployment policy can be applied.

  • Model the agent as a state machine. Specify ingestion, enrichment, inference, execution, and termination before adding capabilities.
  • Bound each growth dimension separately. Tool iterations, context size, queue depth, duration, and tool fan-out require independent limits.
  • Make lifecycle and authorization enforceable. Fail fast on concurrent access, and treat every model-generated tool request as untrusted input.

As agents gain more tools and persistent state, their reliability will depend less on how clever the next model response is and more on whether the workflow can stop, explain what happened, and fail without exceeding its operational boundaries.

Full Source Code

Direct source from the upstream repository. Preview it inline or open it on GitHub.

heads/main/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py

microsoft/autogen • refs

Read Code on GitHub

Thanks for reading! I hope this was useful. If you have questions or thoughts, feel free to reach out.

Content Creation Process: This article was generated via a semi-automated workflow using AI tools. I prepared the strategic framework, including specific prompts and data sources. From there, the automation system conducted the research, analysis, and writing. The content passed through automated verification steps before being finalized and published without manual intervention.

Mahmoud Zalt

About the Author

I’m Zalt, a technologist with 16+ years of experience, passionate about designing and building AI systems that move us closer to a world where machines handle everything and humans reclaim wonder.

Let's connect if you're working on interesting AI projects, looking for technical advice or want to discuss anything.

Support this content

Share this article

Stay in touch

An occasional note when I build or write something new. Leave anytime.

Hire AI Employees

Hire AI Employees that work 24/7. No code.