asz.view
asz.view is one conversation, rebuilt from its Session Flow and its Session Data, as one document:
everything a viewer renders, and nothing a viewer must compute. It is the final form of a
conversation, the one a page draws and the one a person reads. Every talk is a tree of runs and
steps with the text the referenced records carry, and the streams, the segments, the relations,
the rounds and the files sit beside them, each verified. The evidence is inside it once, so a
viewer never opens a .sd file; the ref on a node is a citation, not a pointer to fetch.
It is never a file that the project writes. Package pkg/sessionview defines and owns the shape,
and three things produce it from the same code:
| How | What you get |
|---|---|
asz conversation -json ID |
the document on standard output, indented |
asz conversation -yaml ID |
the same document rendered as YAML, with the same keys in the same order |
asz view, at /api/c/{id}/view |
the document as the page’s own response, built once per fold |
A server that holds the same .sd and .sf files, such as the SkyWalking OAP, builds the same
document and answers a conversation query with it. Every reader shares the shape, and a change to
it is a change to the version: a 1.x adds keys and never removes or renames one; a 2.0 may do
either.
The document is JSON. Keys are snake_case, as in the two source formats, and are written in the
order this page lists them. Times are unix milliseconds, read from the .sd record a node
references; a view is read and never digested, so it carries no RFC 3339 strings. The same head
round over the same files gives the same document, so one built by asz view and one built by
another server compare equal as documents.
A complete example. asz-view-example.yaml is the whole document for
the fixture session of the format pages, three talks across a main stream and a child agent, a
tool, a synthetic error and a context reset, exactly as asz conversation -yaml prints it. It is
generated from the scenario tests/scenarios/fixture.yaml by make asz-view-example, and a test
fails when the committed file no longer matches what the code produces.
Top level
{"format":"asz.view","version":"1.0",
"conversation":"0438c73b-…","sessions":["0438c73b-…"],
"head":{"round":4,"digest":"2e77…"},"parser":"v1","policy":"v1+idle=10m0s",
"summary":{"title":"Check otel-rules support for meter mal","state":"verified","problems":[],
"talks":357,"steps":16121,"streams":132,"segments":50,"rounds":4,"unresolved":0,
"from":1786033245531,"to":1786502191749,
"kinds":{"llm.call":4656,"tool":5606,…},"relation_types":{"in_segment":346,…},"quality":{"exact_unique":338,…}},
"rounds":[{"round":1,"digest":"6d2d…","previous":null,"from_seq":1,"through_seq":5,"input_digest":"…",
"from_time":1786033245531,"through_time":1786322168715,"verified":true}, …],
"files":[{"file":"0438c73b-…/streams/main/transcript-…-000002.sd","format":"sd","kind":"transcript",
"seq":2,"round":null,"stream":"main","run":null,"lines":1017,"bytes":2097000,"digest":"9a39…",
"from_time":1786033290556,"through_time":1786036000000}, …,
{"file":"_conversations/0438c73b-…/rounds/r000001-6d2d93b21f79.sf","format":"sf","kind":"round",
"seq":null,"round":1,"stream":null,"run":null,"lines":3575,"bytes":1026788,"digest":"f3fe…",
"from_time":1786033245531,"through_time":1786322168715}],
"streams":[{"id":"stream/main","name":"main","role":"main","label":"","parent":"","records":10728,
"steps":7310,"talk":"talk/main/95a1…","named_by":"","opened_by":[]}, …],
"segments":[{"id":"segment/at_2_7","state":"candidate","committable":true,"talks":24,"from":1786033290556,"to":1786039329174}, …],
"talks":[{"id":"talk/main/95a1…","kind":"talk","parent":"epoch/main/0","stream":"main","at":1786033290556,
"ref":{"seq":2,"row":7},"attrs":{"loops":1,"runs":1,"trigger":"external"},
"label":"for meter mal, …","reply":"## Short answer…","runs":1,"steps":132,"tools":51,
"from":1786033290556,"to":1786033717089,"segment":"segment/at_2_7",
"children":[…],"edges":[{"type":"in_segment","other":"segment/at_2_7","dir":"out","quality":"exact_unique","via":"activity window"}]}, …],
"relations":[{"id":"rel/starts/…","type":"starts","from":"tool/toolu_…","to":"stream/a1…","quality":"exact_unique",
"via":"parent tool result","evidence":[{"seq":1,"row":12}]}, …],
"unresolved":[]}
| Key | Value |
|---|---|
format, version |
asz.view, 1.0. A reader that does not know the version stops here. |
conversation, sessions |
the conversation id, and the sessions that contributed to it, from the fold’s session nodes; one session, equal to the conversation id, for the Claude Code adapter |
head |
round and digest of the newest round the document was folded to |
parser, policy |
from the head round’s header |
summary |
title; state, one of verified, incomplete when a round or a file is missing, mismatch when a digest failed; problems, one line each, empty when verified; the counts talks, steps, streams, segments, rounds, unresolved; from and to, when the session began and its last activity, from the session node; and kinds, relation_types and quality, the fold sized by node kind, by relation type and by how well each relation is known |
rounds |
one per round, in order: round, digest, previous (null on round 1), from_seq, through_seq, input_digest, from_time, through_time (the record time range of the files the round consumed, null when none carries a time), verified |
files |
one per .sd file, then one per round: file (its path on the wire), format (sd or sf), kind, seq or round, stream or run, lines, bytes, digest, from_time, through_time. Absent values are null. Together with rounds, this is exactly what a rebuild needs. |
streams |
one per execution stream: id, name, role (main or child), label, parent, records, steps, talk, named_by, and opened_by, every step the assembler could tie to the start of the stream as {step, stream, talk, quality}; several means it did not choose, and neither does a view |
segments |
one per activity window: id, state, committable, talks, from, to |
talks |
one tree per talk, in time order. See the node below |
loose |
the runs and steps no talk contains, as trees from their highest such ancestor: a child’s output the fold parented to the session because the child’s stream opened no talk, for instance. Empty for most conversations. With talks, it holds every run and step of the fold, so the document covers the whole session |
relations |
one per relation of the fold: id, type, from, to, quality, via, evidence |
unresolved |
one per reference the assembler could not resolve, open or since resolved: id, kind, ref, reason, state |
Verification is content, not an error. A gap in the chain or a failed digest is written into
summary.state and summary.problems, each round says whether it verified, and the rest of the
document holds whatever could still be folded: the fold stops before a missing or broken round,
head names the last round it reached, and the rounds after the gap are listed and not verified.
A viewer shows the problem; it never gets an error instead of a document. Only a chain with no
usable round at all is an error, because there is nothing to show.
A node in talks
{"id":"tool/toolu_01KH…","kind":"tool","parent":"call/msg_011C…","stream":"main","at":1786033295667,
"ref":{"seq":2,"row":13,"block":0},"refs":[{"seq":2,"row":13,"block":0},{"seq":2,"row":14,"block":0}],
"attrs":{"name":"Bash","result":"available","result_join":"exact_unique","timing":"unavailable"},
"text":"{\"command\":\"ls …\"}","state":"available","bytes":145,"flags":["finished"],
"name":"Bash","result":"…","result_state":"available","result_bytes":2048,
"request_to_result_ms":1312,"request_to_result_join":"exact_unique",
"edges":[{"type":"starts","other":"stream/a1…","dir":"out","quality":"exact_unique","via":"parent tool result"}],
"children":[…]}
| Key | Value |
|---|---|
id, kind, parent, stream, attrs |
the node as the fold holds it; kind is one of the node kinds of Session Flow |
at |
when its record happened, from the record; 0 when nothing observed it |
ref, refs |
the record it stands on and every record it covers, as {seq, row, block}, kept so a viewer can show the evidence |
text, state, bytes |
the part the node stands on: its readable text, clipped to the longest prefix of whole characters within 2,000 bytes, whether the content is available, and its full size. For a data part the text is the data as compact JSON. A reader wanting the whole record reads it by address. |
usage, flags, dropped |
what else the referenced record says, copied once: on an llm.call, the token counts in, out, cache_read, cache_write from the one record usage_at names, never a sum over fragments; the record’s flags; and its dropped list, so a viewer can say what was left out and why |
| a talk adds | label, reply (its last assistant message, clipped the same way), runs, steps, tools, from, to, child, segment |
| a tool or agent call adds | name, failed, result, result_state, result_bytes, request_to_result_ms and request_to_result_join, the time from the request record to the result record where the assembler joined them exactly |
a turn.duration step adds |
duration_ms, duration_measured_by |
children |
containment, in record order: a talk holds runs, a run holds steps, a call holds what it produced |
edges |
every relation touching the node, in both directions, as {type, other, dir, quality, via}, ordered by relation id and then direction, so a viewer draws cross-stream flow without searching relations and the same fold gives the same list |
Keys a node has no value for are absent, not null. Nothing in a document is inferred beyond what
the fold and the records say. Where the fold says unavailable, the document says it too.
Rendering the whole conversation
The document is complete: a viewer draws every view of a conversation from it and fetches
nothing else, however many rounds the session was parsed in. A session landed and parsed in three
stages has three rounds and its landed files cut at each stage, and the document built from the
head holds every round, every file, and every talk, run and step, exactly as one built from a
single parse would; the scenario three-rounds checks that at each stage, and every scenario
checks the property view_covers_the_session at its end. This is how each view reads it.
| View | Read |
|---|---|
| Transcript | talks, in order. Each talk’s label is the person’s input and its reply the last assistant message; its children are the runs, a run’s children the steps, and a call’s children what it produced: thinking, messages, tools. text is what to show for a step, name and result for a tool, usage on a call. loose holds whatever no talk contains, and is usually empty. |
| Flow timeline | every node of every tree by at, with kind and stream; a node with at of 0 was never observed at a time and is placed by its position. |
| Cross-stream flow | edges on a node, and relations as the whole list: starts from an agent call to the child’s stream, reports from the notification that resumed the parent, ends_with from a stream to the child’s output, follows between epochs across a reset, summarizes from a summary to its boundary, in_segment from a talk to its window. Containment never crosses a stream; a child’s work is under the child. |
| Streams and segments | streams with role, label, parent and opened_by, the step that started each; segments with the span of the talks placed in them. |
| Evidence | a node’s ref and refs, {seq, row, block}, name the record and the part it stands on. The text is already on the node, clipped to 2,000 bytes with the full size in bytes; only a reader that wants the whole of a longer part goes to the record, by that address, in files. |
| Verification | summary.state and summary.problems, every round’s verified, and every file’s digest, lines and bytes. A gap or a failed digest is content here, never an error in place of the document. |
| Counts and time | summary: the counts a list shows, the session’s from and to, and the fold sized by kinds, relation_types and quality. |
Nothing in a document is inferred beyond what the fold and the records say. Where the fold says
unavailable, the document says it too, and a viewer shows that word rather than a guess.
The YAML rendering
-yaml is a rendering of the JSON, not a second format. It is produced from the JSON, so the keys
are the same and in the same order; mappings are blocks; scalars are plain, and quoted only where
YAML would otherwise misread them, so version is "1.0" and a title with a colon is quoted; an
empty map is {}; a text with line breaks is a block scalar. Reading the YAML back gives the same
values as the JSON.
Reading it
asz view serves the document at /api/c/{id}/view and builds it once per fold, so a second reader
pays nothing until a new round arrives. asz conversation -json ID prints the same document to
standard output, and -yaml prints it as YAML, for a terminal or a diff. The page draws the same
document with Horizon’s conversation renderer, which asz embeds from a pinned Horizon commit, so a
conversation looks the same in asz view and in the SkyWalking UI.
The largest conversation measured, 357 talks and 16,121 steps, is 19 MB as one document and was
built in 0.7 s.