liveBook · Manning

ContentItem

As explained in Build an AI Agent (From Scratch)

A ContentItem is a typed structured unit representing one piece of conversational, language-model, or tool-interaction content.

ContentItem is the common typed unit for conversational and tool-interaction data. It represents Messages, ToolCalls, and ToolResults in both outbound requests and inbound responses, keeping the agent’s data model consistent. section 4.5.2 section 4.5.3

The communication layer uses ContentItem variants for:

  • Messages containing user or assistant text. section 4.5.2
  • ToolCalls recording requested tool names, identifiers, and arguments. section 4.5.4
  • ToolResults recording executed tool output and its originating call linkage. section 4.5.4

ContentItems form the boundary between agent state and provider-specific API messages. LlmRequest selects and orders them, LlmClient translates them into API dictionaries, and LlmResponse returns provider output in the same standard form. section 4.5.1 section 4.5.4

A minimal request and response

An LlmRequest can contain a user Message asking “What is 2 + 2?”. After generation, LlmResponse.content contains an assistant Message with “4”. A tool-using response can instead contain an assistant Message and one or more ToolCalls, which are later paired with ToolResults. section 4.5.5 section 4.5.4

Listing 4.12 Building messages for the LLM API
def _build_messages(self, request: LlmRequest) -> List[dict]:
    """Convert LlmRequest to API message format."""
    messages = []

    for instruction in request.instructions:
        messages.append({"role": "system", "content": instruction})

    for item in request.contents:
        if isinstance(item, Message):
            messages.append({"role": item.role, "content": item.content})

        elif isinstance(item, ToolCall):
            tool_call_dict = {
                "id": item.tool_call_id,
                "type": "function",
                "function": {
                    "name": item.name,
                    "arguments": json.dumps(item.arguments)
                }
            }
            # Append to previous assistant message if exists
            if messages and messages[-1]["role"] == "assistant":
                messages[-1].setdefault("tool_calls", []).append(tool_call_dict)
            else:
                messages.append({
                    "role": "assistant",
                    "content": None,
                    "tool_calls": [tool_call_dict]
                })

        elif isinstance(item, ToolResult):
            messages.append({
                "role": "tool",
                "tool_call_id": item.tool_call_id,
                "content": str(item.content[0]) if item.content else ""
            })

    return messages

Inspect the conversion logic that preserves ContentItem semantics while combining assistant tool calls into provider-required message objects. section 4.5.4

How Build an AI Agent (From Scratch) covers it

How deep it goes
Defines typed content variants and traces their provider translation, stopping short of detailed class definitions.
Examples
Uses user and assistant Messages, ToolCalls, ToolResults, and a two-plus-two LLM interaction.
What it assumes
Assumes familiarity with agent Events and ExecutionContext while leaving provider-specific message translation to LlmClient.
What you can do after
Build provider-neutral request and response handling with typed messages, tool calls, tool results, and error normalization.
How it connects
Connects LlmRequest, LlmResponse, LlmClient, ExecutionContext, Events, Message, ToolCall, ToolResult, and LiteLLM.

This explainer was generated by Manning's knowledge graph from the text of Build an AI Agent (From Scratch). The section links point to the parts of the book it draws on.