You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/09/27 00:04:17 UTC

(skywalking-ai-sessionizer) 01/01: State the rules a reader follows on the format pages

This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch format-reading-rules
in repository https://gitbox.apache.org/repos/asf/skywalking-ai-sessionizer.git

commit 2aab690e8fb25e90b49cff172d8fee3fb77fc838
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Sun Sep 27 08:04:06 2026 +0800

    State the rules a reader follows on the format pages
    
    The formats are JSON lines and JSON, and a reader in another language,
    such as the SkyWalking OAP, builds the same asz.view document from the
    same files. Where a page did not state a rule, such a reader had to find
    it in asz's Go code, and ended up copying what Go's libraries do. The
    pages now state these rules, so a reader follows the pages.
    
    - Session Data: the type of every record, part and dropped field, and of
      the usage values; a null is a field left out; a reader ignores a field
      the page does not list; asz's reader stops at a line it cannot decode.
      Times are RFC 3339 with a four-digit year, compared as instants.
    - Session Flow: attrs keys and ids sort in code point order, which the
      page defines, and the provider_bodies rule asz's reader already applies.
    - asz.view: times round down to the millisecond; header times are null
      or 0; attrs and data are as written; a data part's text; white space is
      Unicode's White_Space property; which records give a call its result;
      how request_to_result_ms is cut.
    
    Documentation only. The formats stay sd/1, sf/1 and asz.view 1.0.
---
 docs/en/changes/changes.md      |  8 ++++++++
 docs/en/formats/asz-view.md     | 31 +++++++++++++++++++++++--------
 docs/en/formats/session-data.md | 32 +++++++++++++++++++++++++++++++-
 docs/en/formats/session-flow.md |  7 ++++++-
 4 files changed, 68 insertions(+), 10 deletions(-)

diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 0caaf72..a78a107 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -134,6 +134,14 @@
 
 ## Documentation
 
+- The format pages state every rule a reader needs to build the same document, so a reader in any
+  language follows the pages and never asz's code. [Session Data](../formats/session-data.md#reading-a-record)
+  gives the type of every record and part field, what a reader does with a line of another type, and
+  how times are written and compared. [Session Flow](../formats/session-flow.md) says that ids and
+  `attrs` keys sort in code point order, and when a call's `provider_bodies` makes a round
+  unreadable. [asz.view](../formats/asz-view.md) says that times round down to the millisecond,
+  that `attrs` and data are as written, what white space is, and which record gives a call its
+  result.
 - The Claude Code adapter page said a `claude_code.tool.execution` span from Claude Code's
   OpenTelemetry export supplies a tool's execution time. asz keeps nothing of that export, and no
   such span was measured, so the page now says the export may carry it and that this is unmeasured.
diff --git a/docs/en/formats/asz-view.md b/docs/en/formats/asz-view.md
index 8279736..48ddad0 100644
--- a/docs/en/formats/asz-view.md
+++ b/docs/en/formats/asz-view.md
@@ -24,8 +24,10 @@ it is a change to the version: a 1.x adds keys and never removes or renames one;
 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
+order this page lists them. Times are unix milliseconds, rounded down, read from the `.sd` record a
+node references; a view is read and never digested, so it carries no RFC 3339 strings. A time a
+round's header carries is null when the header has none, and 0 when its value is not a time as
+[Reading a record](session-data.md#reading-a-record) defines one. 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.
 
@@ -97,11 +99,13 @@ A stream's `opened_by`, the `relations` list and a node's `edges` are in the ord
 never in the order of an id. Inside one stream or one workflow run, a record's position orders it:
 its file's `seq`, then its `row`, then its `block`. A position means nothing across streams, since a
 child's file can land before its parent's, so streams and runs are merged by time: each next item is
-the earliest of the next items of every stream, a timed item before an untimed one. An item several
+the earliest of the next items of every stream, a timed item before an untimed one. Times compare as
+the instants they name, as [Reading a record](session-data.md#reading-a-record) says. An item several
 records support, such as a relation, happened at the earliest of them by the same rule. Each list is
 ordered on its own. An id decides only between items that one record supports, such as two relations
-with the same evidence. Talks, workspace changes and tool executions are in time order, as their own
-sections say.
+with the same evidence, and ids compare in code point order, as [Session Flow](session-flow.md#header)
+defines it. Talks, workspace changes and tool executions are in time order, as their own sections
+say.
 
 ## A node in `talks`
 
@@ -118,10 +122,10 @@ sections say.
 
 | Key | Value |
 | --- | --- |
-| `id`, `kind`, `parent`, `stream`, `attrs` | the node as the fold holds it; `kind` is one of the node kinds of Session Flow |
+| `id`, `kind`, `parent`, `stream`, `attrs` | the node as the fold holds it; `kind` is one of the node kinds of Session Flow; `attrs` is as the round wrote it, numbers included, without `provider_bodies`, which a call gives under a key of its own |
 | `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. Earlier writers put `\u003c`, `\u003e` and `\u0026` there for `<`, `>` and `&`, so their step text can differ (see [What data holds](session-data.md#what-data-holds)). A reader wanting the whole record reads it by address. |
+| `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 record's readable text when it has one: its `text` parts, or the prompt inside a `queued_command` envelope, the form a message typed while the agent works arrives in. Otherwise it is the data exactly as the record holds it, which is compact JSON. Earlier w [...]
 | `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` and `reply`, clipped the same way and described below, then `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; `changes` and `executions`, the records joined to it |
@@ -133,6 +137,16 @@ sections say.
 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.
 
+Where the document trims white space from a text, or turns a run of it into one space, white space
+is what Unicode's `White_Space` property lists. A no-break space is white space.
+
+**A call's result.** A tool or agent call's first reference is its request, and its other
+references name what came back. They are read in order until one gives the call a `result`. From
+each, the part read is the one its `block` names, or else the record's first `result` part; a
+record with neither gives nothing. The `result` is that part's `text`, or its `data` when it has no
+text. Each part read sets `result_state` and `result_bytes`, even when it has neither. `failed`
+comes from the first part that has it, the request's included.
+
 **A talk's label and reply.** A talk's `label` is the text of the first `message.external` step in
 it. A talk can have none. A talk opened by a command typed locally is one example, because the
 runtime records no origin for such a command. The label is then the first text that is not empty
@@ -145,7 +159,8 @@ earlier messages are what the agent said between tool calls, as the
 explains. Either key is absent when no step gives it a text.
 
 **Request to result is not tool time.** `request_to_result_ms` is the time between two records the
-runtime wrote, the request and its result, tied together by the tool-use id. It is not how long the
+runtime wrote, the request and its result, tied together by the tool-use id: the difference of
+their times, in whole milliseconds, with the rest dropped. It is not how long the
 tool ran. It can include waiting and other work between the two records. So `timing` in `attrs`
 stays `unavailable` beside it, for the reason the
 [Claude Code adapter](../adapters/claude-code.md#step-mapping) gives. Both keys are absent when the
diff --git a/docs/en/formats/session-data.md b/docs/en/formats/session-data.md
index 9791380..ce71172 100644
--- a/docs/en/formats/session-data.md
+++ b/docs/en/formats/session-data.md
@@ -142,6 +142,32 @@ a snapshot cursor, so a lost one is found only by the round chain (see
 [Retention](storage-root.md#retention)). A repeated record is not a gap (see
 [Landed files](storage-root.md#landed-files)).
 
+### Reading a record
+
+The format is JSON, and a reader in any language reads it with its own JSON library. The rules
+below and the rest of this page are the contract. A reader does not have to copy what asz's code
+does with input that asz never writes.
+
+Every field of a record, as the table in [Record](#record) lists them, has one type. `ord`, `off`
+and `bytes` are integers, and `ord` and `off` are never negative. `flags` is a list of strings.
+`parts` and `dropped` are lists of objects, typed in [Parts](#parts) and [Dropped](#dropped).
+`usage` is an object whose `in`, `out`, `cache_read` and `cache_write` are integers. Every other
+field is a string. An integer is a whole number, written with no fraction and no exponent, that
+fits in 64 bits with its sign. A field written as null is the same as a field left out. A reader
+ignores a field this page does not list, so a later version can add one.
+
+A line with a field of another type is not a record. asz's reader stops at a line it cannot
+decode, such as one whose `off` is a string, as it stops at a line that is not JSON, and reads no
+record after it in the file.
+
+A `time` is written as RFC 3339: a four-digit year, the letter `T`, the time of day to the
+second with a fraction of up to nine digits or none, and `Z` or an offset such as `+08:00`. Times
+compare as the instants they name, never as text. As text, `2026-01-01T12:00:00.11Z` sorts before
+`2026-01-01T12:00:00.1Z`, and `2026-01-01T12:00:00.5Z` before `2026-01-01T12:00:00Z`: in both
+pairs the first is the later time. `2026-01-01T20:00:00+08:00` sorts after `2026-01-01T12:00:01Z`
+as text, though it is a second earlier. A value that is not such a time means the record has no
+time.
+
 ### Addressing a record
 
 A round points at a record with `{seq, row}`, and at one of its parts with `{seq, row, block}`
@@ -240,6 +266,9 @@ reads every landed file this way.
 | `data` | structure that is not prose: a record the runtime keeps for itself, a manifest | `data` |
 | `unknown` | content the dialect could not describe | the bytes in `data` as one JSON string, the bytes themselves or their base64 when `encoding` is `base64`, and the reason in `text` |
 
+In a part, `k`, `text`, `id`, `name`, `of`, `server`, `server_tool`, `media`, `encoding` and
+`state` are strings, `bytes` is an integer, `failed` is a boolean, and `data` is any JSON value.
+
 `server` and `server_tool` say which MCP server a call is addressed to and which of its tools. An
 adapter sets them only where its runtime's name for the call splits exactly. Claude Code names such a
 call `mcp__<server>__<tool>`, and the split is made only when exactly one `__` follows `mcp__`: a
@@ -381,7 +410,8 @@ runs after every call to an MCP server. Measured on Claude Code 2.1.282:
 
 `dropped` exists so a loss is stated rather than silent. It lists only what the dialect understood
 and chose not to carry. Everything it did not understand travels as an `unknown` part, so a later
-version of the dialect can interpret it without collecting again.
+version of the dialect can interpret it without collecting again. In an entry, `what` and `why`
+are strings and `bytes` is an integer.
 
 A source record is not always kept whole, so a `.sd` file is the only landed copy of what its parts
 hold, and everything above the adapter treats it as the authority. Content that was converted to
diff --git a/docs/en/formats/session-flow.md b/docs/en/formats/session-flow.md
index e818501..91e71c2 100644
--- a/docs/en/formats/session-flow.md
+++ b/docs/en/formats/session-flow.md
@@ -73,7 +73,9 @@ fields above reproduce with the round.
 The order of the frames is part of the bytes. asz writes the header, then every node, then every
 relation, then every unresolved entry, each group sorted by id, and then the commit. Without the
 sort, two runs over the same input could order the frames differently and produce different
-digests.
+digests. For the same reason, a node's `attrs` is one JSON object whose keys asz writes in sorted
+order. Ids and keys sort in code point order: two strings compare by their Unicode code points,
+one by one, which is also the order of their UTF-8 bytes.
 
 ### Entities
 
@@ -228,6 +230,9 @@ another reader about what the round said.
 - A `revision` differs from the header's `round`.
 - A reference is sequence 0 row 0, or names a sequence past `through_seq`, which the round's input
   digest does not cover.
+- A node's `provider_bodies` attribute is neither null nor a list of `{role, ref}`, a `role` is
+  neither `request` nor `response`, or a `ref` has a `seq` or a `row` of 0. Each `ref` is a
+  reference, and the rule above applies to it as well.
 - A relation that is not a tombstone lacks `type`, `from` or `to`.
 - An unresolved entry that is not a tombstone has a state other than `open`, `resolved` or
   `terminal`.