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), withselectedon<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