chat.xml

Save your chat sessions in XML.

Let's design an XML spec for logging human-agent chat sessions.

Version 0: Messages

<chat>
  <message from="omar">What's the weather in SF?</message>
  <message from="assist">It's foggy.</message>
</chat>

Version 1: Actors, whitespace, and attribute vs. element rules

Problems with Version 0:

  • from="assist" does not distinguish a human from an agent.
  • Indenting bare text inside <message> alters leading/trailing whitespace in prompts and code.
  • Without a placement rule, writers mix <node id="1"/> and <node><id>1</id></node>.

Rules:

  • Attributes hold scalar metadata, enums, and references (id, from, kind).
  • Elements hold content payloads (<text>) or structural containers (<participants>, <timeline>, <message>).
  • xml:space="preserve" applies to <chat>. Indent structural tags only; payload text starts immediately after the opening tag and ends immediately before the closing tag.
<chat xml:space="preserve">
  <participants>
    <actor id="omar" kind="human" name="Omar"/>
    <actor id="assist" kind="agent" name="Assist"/>
  </participants>
  <timeline>
    <message id="m1" from="omar">
      <text>What's the weather in SF?</text>
    </message>
    <message id="m2" from="assist">
      <text>It's foggy.</text>
    </message>
  </timeline>
</chat>

Version 2: Reasoning, tool calls, and attachments

An agent turn can include reasoning, multiple tool calls and results, text, and file attachments. Humans and agents share <message>; internal structure comes from ordered child parts.

<message id="m2" from="assist">
  <thinking>Need live weather before answering.</thinking>
  <tool-call id="c1" name="get_weather">
    <input mime="application/json">{"city":"San Francisco"}</input>
  </tool-call>
  <tool-result call="c1" status="ok">
    <output mime="application/json">{"tempF":64,"condition":"foggy"}</output>
  </tool-result>
  <text>It's 64°F and foggy in San Francisco.</text>
  <attachment href="media/radar.png" mime="image/png"/>
</message>
  • <tool-result call="c1"> references <tool-call id="c1"> explicitly rather than by position, supporting out-of-order parallel tool returns.
  • <input> and <output> keep their native serialization (mime="application/json").
  • Multi-step tool loops append additional <thinking>, <tool-call>, <tool-result>, and <text> elements in execution order within the same <message>.
  • <thinking> is omitted when the model does not expose reasoning tokens.

Version 3: Retries and per-run models

An agent can fail, retry, or switch models between runs, so model belongs on the run rather than <actor>.

<message id="m2" from="assist" selected="2">
  <attempt n="1" status="error" model="claude-sonnet-4-6">
    <error kind="timeout">Upstream timed out after 30s.</error>
  </attempt>
  <attempt n="2" retry-of="1" status="ok" model="gpt-5">
    <text>It's foggy.</text>
  </attempt>
</message>
  • Single-try <message>: parts live directly under <message>.
  • Multi-try <message>: retries nest under <attempt> (minOccurs="2" in XSD), with selected on <message> pointing to the chosen @n. Wrapping a single try in <attempt n="1"> is invalid under the schema.

Version 4: Environment snapshots and usage (chat.xml v1.0)

Reproducing or evaluating a run requires the system prompt and tool schemas active when that run executed, plus token and latency counts. <environment> stores immutable <env> snapshots referenced via env="...".

<?xml version="1.0" encoding="UTF-8"?>
<chat version="1.0" xml:space="preserve">
  <participants>
    <actor id="omar" kind="human" name="Omar"/>
    <actor id="assist" kind="agent" name="Assist"/>
  </participants>

  <environment>
    <env id="e1">
      <system>Use get_weather for live conditions. Be concise.</system>
      <tool name="get_weather">
        <description>Get current weather for a city.</description>
        <schema mime="application/schema+json">{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}</schema>
      </tool>
    </env>
  </environment>

  <timeline>
    <message id="m1" from="omar">
      <text>What's the weather in SF?</text>
    </message>

    <message id="m2" from="assist" selected="2">
      <attempt n="1" status="error" model="claude-sonnet-4-6" env="e1">
        <error kind="timeout">Upstream timed out after 30s.</error>
        <usage input-tokens="91" output-tokens="0" duration-ms="30000"/>
      </attempt>
      <attempt n="2" retry-of="1" status="ok" model="gpt-5" env="e1">
        <thinking>Need live weather before answering.</thinking>
        <tool-call id="c1" name="get_weather">
          <input mime="application/json">{"city":"San Francisco"}</input>
        </tool-call>
        <tool-result call="c1" status="ok">
          <output mime="application/json">{"tempF":64,"condition":"foggy"}</output>
        </tool-result>
        <text>It's 64°F and foggy in San Francisco.</text>
        <usage input-tokens="240" output-tokens="44" duration-ms="820"/>
      </attempt>
    </message>

    <message id="m3" from="omar">
      <text>And Oakland?</text>
    </message>

    <message id="m4" from="assist" status="ok" model="gpt-5" env="e1">
      <tool-call id="c2" name="get_weather">
        <input mime="application/json">{"city":"Oakland"}</input>
      </tool-call>
      <tool-result call="c2" status="ok">
        <output mime="application/json">{"tempF":67,"condition":"sunny"}</output>
      </tool-result>
      <text>Oakland is 67°F and sunny.</text>
      <usage input-tokens="318" output-tokens="39" duration-ms="690"/>
    </message>
  </timeline>
</chat>

If instructions or tools change mid-session, append <env id="e2"> and reference e2 on subsequent runs.

Schema (chat.xsd)

XSD 1.0 schema enforcing element structure, status enums, <attempt minOccurs="2">, and typed xs:key / xs:keyref constraints (@from -> actor/@id, @call -> tool-call/@id, @env -> env/@id, @selected and @retry-of -> attempt/@n):

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:simpleType name="status"><xs:restriction base="xs:string">
    <xs:enumeration value="ok"/><xs:enumeration value="error"/>
    <xs:enumeration value="aborted"/><xs:enumeration value="interrupted"/>
  </xs:restriction></xs:simpleType>
  <xs:complexType name="payload"><xs:simpleContent><xs:extension base="xs:string">
    <xs:anyAttribute namespace="http://www.w3.org/XML/1998/namespace" processContents="lax"/>
  </xs:extension></xs:simpleContent></xs:complexType>
  <xs:complexType name="data"><xs:simpleContent><xs:extension base="payload">
    <xs:attribute name="mime" type="xs:string" use="required"/>
  </xs:extension></xs:simpleContent></xs:complexType>
  <xs:complexType name="error"><xs:simpleContent><xs:extension base="payload">
    <xs:attribute name="kind" type="xs:string" use="required"/>
  </xs:extension></xs:simpleContent></xs:complexType>
  <xs:element name="usage"><xs:complexType>
    <xs:attribute name="input-tokens" type="xs:nonNegativeInteger"/>
    <xs:attribute name="output-tokens" type="xs:nonNegativeInteger"/>
    <xs:attribute name="duration-ms" type="xs:nonNegativeInteger"/>
  </xs:complexType></xs:element>
  <xs:group name="parts"><xs:choice>
    <xs:element name="text" type="payload"/><xs:element name="thinking" type="payload"/>
    <xs:element name="error" type="error"/>
    <xs:element name="attachment"><xs:complexType>
      <xs:attribute name="href" type="xs:anyURI" use="required"/>
      <xs:attribute name="mime" type="xs:string" use="required"/>
    </xs:complexType></xs:element>
    <xs:element name="tool-call"><xs:complexType><xs:sequence>
      <xs:element name="input" type="data"/>
    </xs:sequence><xs:attribute name="id" type="xs:ID" use="required"/>
      <xs:attribute name="name" type="xs:string" use="required"/>
    </xs:complexType></xs:element>
    <xs:element name="tool-result"><xs:complexType><xs:choice>
      <xs:element name="output" type="data"/><xs:element name="error" type="error"/>
    </xs:choice><xs:attribute name="call" type="xs:NCName" use="required"/>
      <xs:attribute name="status" type="status" use="required"/>
    </xs:complexType></xs:element>
  </xs:choice></xs:group>
  <xs:attributeGroup name="run">
    <xs:attribute name="model" type="xs:string"/><xs:attribute name="env" type="xs:NCName"/>
    <xs:attribute name="status" type="status"/>
  </xs:attributeGroup>
  <xs:complexType name="attempt"><xs:sequence>
    <xs:group ref="parts" maxOccurs="unbounded"/><xs:element ref="usage" minOccurs="0"/>
  </xs:sequence><xs:attribute name="n" type="xs:positiveInteger" use="required"/>
    <xs:attribute name="retry-of" type="xs:positiveInteger"/><xs:attributeGroup ref="run"/>
  </xs:complexType>
  <xs:element name="message"><xs:complexType><xs:choice>
    <xs:sequence><xs:group ref="parts" maxOccurs="unbounded"/>
      <xs:element ref="usage" minOccurs="0"/></xs:sequence>
    <xs:element name="attempt" type="attempt" minOccurs="2" maxOccurs="unbounded"/>
  </xs:choice><xs:attribute name="id" type="xs:ID" use="required"/>
    <xs:attribute name="from" type="xs:NCName" use="required"/>
    <xs:attribute name="selected" type="xs:positiveInteger"/><xs:attributeGroup ref="run"/>
  </xs:complexType>
    <xs:key name="tries"><xs:selector xpath="attempt"/><xs:field xpath="@n"/></xs:key>
    <xs:keyref name="selection" refer="tries"><xs:selector xpath="."/><xs:field xpath="@selected"/></xs:keyref>
    <xs:keyref name="retry" refer="tries"><xs:selector xpath="attempt"/><xs:field xpath="@retry-of"/></xs:keyref>
  </xs:element>
  <xs:element name="chat"><xs:complexType><xs:sequence>
    <xs:element name="participants"><xs:complexType><xs:sequence>
      <xs:element name="actor" maxOccurs="unbounded"><xs:complexType>
        <xs:attribute name="id" type="xs:ID" use="required"/>
        <xs:attribute name="kind" use="required"><xs:simpleType><xs:restriction base="xs:string">
          <xs:enumeration value="human"/><xs:enumeration value="agent"/>
        </xs:restriction></xs:simpleType></xs:attribute><xs:attribute name="name" type="xs:string"/>
      </xs:complexType></xs:element>
    </xs:sequence></xs:complexType></xs:element>
    <xs:element name="environment"><xs:complexType><xs:sequence>
      <xs:element name="env" maxOccurs="unbounded"><xs:complexType><xs:sequence>
        <xs:element name="system" type="payload"/>
        <xs:element name="tool" minOccurs="0" maxOccurs="unbounded"><xs:complexType><xs:sequence>
          <xs:element name="description" type="payload"/><xs:element name="schema" type="data"/>
        </xs:sequence><xs:attribute name="name" type="xs:string" use="required"/></xs:complexType></xs:element>
      </xs:sequence><xs:attribute name="id" type="xs:ID" use="required"/></xs:complexType></xs:element>
    </xs:sequence></xs:complexType></xs:element>
    <xs:element name="timeline"><xs:complexType><xs:sequence>
      <xs:element ref="message" maxOccurs="unbounded"/>
    </xs:sequence></xs:complexType></xs:element>
  </xs:sequence><xs:attribute name="version" type="xs:string" use="required" fixed="1.0"/>
    <xs:anyAttribute namespace="http://www.w3.org/XML/1998/namespace" processContents="lax"/>
  </xs:complexType>
    <xs:key name="actors"><xs:selector xpath="participants/actor"/><xs:field xpath="@id"/></xs:key>
    <xs:keyref name="authors" refer="actors"><xs:selector xpath="timeline/message"/><xs:field xpath="@from"/></xs:keyref>
    <xs:key name="calls"><xs:selector xpath=".//tool-call"/><xs:field xpath="@id"/></xs:key>
    <xs:keyref name="results" refer="calls"><xs:selector xpath=".//tool-result"/><xs:field xpath="@call"/></xs:keyref>
    <xs:key name="envs"><xs:selector xpath="environment/env"/><xs:field xpath="@id"/></xs:key>
    <xs:keyref name="environments" refer="envs"><xs:selector xpath=".//message | .//attempt"/><xs:field xpath="@env"/></xs:keyref>
  </xs:element>
</xs:schema>

Validate:

xmllint --noout --nonet --schema chat.xsd session.chat.xml

XPath queries

# Total output tokens across all runs (including failed retries)
xmllint --xpath 'sum(//usage/@output-tokens)' session.chat.xml

# Failed attempts and failed tool results
xmllint --xpath '//attempt[@status="error"] | //tool-result[@status="error"]' session.chat.xml

# Runs where <thinking> preceded a failed tool result
xmllint --xpath '//tool-result[@status="error"][preceding-sibling::thinking]/..' session.chat.xml

# Chosen text from retried messages
xmllint --xpath '//message/attempt[@n=../@selected]/text' session.chat.xml

# All prompts from omar
xmllint --xpath '/chat/timeline/message[@from="omar"]/text' session.chat.xml

Get new posts by email

Get an email whenever I publish a new post.

© 2026 Omar Bohsali. RSS | @omarish