Scenarios

A scenario is a short YAML description of a conversation, in the model’s own words: a person’s input, a call with its fragments, a tool and its result, a child agent, a context reset. asz scenario build turns it into the input a real session leaves behind, so the ordinary commands collect and parse it. asz scenario check runs it as a test.

The .sf side is never mocked. A scenario produces evidence; the ordinary parser produces the rounds. That is what makes a scenario both a fixture generator and a conformance test: built as the runtime’s own files and collected through its adapter, or landed directly as Session Data, it must fold to the same conversation.

Generate, then load, parse and export

build only generates. It writes the input and DIR/asz.yaml, whose storage root is DIR and whose adapter source is DIR/_source, then stops; the ordinary commands do the rest, each one inspectable on disk before the next.

asz scenario build FILE --format claude-code --out DIR      # generate the runtime's files, under DIR/_source
asz collect -once -config DIR/asz.yaml                      # load: the real adapter lands them as .sd
asz parse -config DIR/asz.yaml                              # parse: the real parser writes the .sf rounds
asz verify -config DIR/asz.yaml                             # every digest and every chain
asz view -config DIR/asz.yaml                               # the page, on 127.0.0.1:8787
asz push -once -config DIR/asz.yaml                         # export: every file to an OTLP receiver

With --format sd the Session Data is landed by the build itself, so the collect step is skipped and the rest is the same. To push, name the receiver in DIR/asz.yaml; build leaves the block there, commented:

export:
  otlp:
    endpoint: http://127.0.0.1:12800

The pushed records say where they came from. A claude-code build is landed by the Claude Code adapter and is attributed to Claude Code; an sd build carries the mock/1 dialect and is attributed to Mock Agent, so a receiver never lists an invented conversation as a real one.

A demo corpus is one scenario repeated: --repeat 20 builds twenty sessions end to end on the clock, the first with the scenario’s own id and the rest counted up from it, and --at 2026-06-01T09:00:00Z --scale 60 puts them on a day rather than in a burst. build refuses a DIR/asz.yaml it did not write, so a directory holds one configuration; what a person appends to it, such as the export block, is kept across builds.

Flag Meaning
--format claude-code or sd
--out the directory; it ends as a storage root
--at the base time, RFC 3339, or now (default)
--scale multiplies every delta; 60 turns a scenario typed in seconds into minutes
--interval overrides the scenario’s interval
--repeat N N sessions end to end on the clock, each with its own id
--through NAME only the steps up to the checkpoint NAME

With a fixed --at, every file is identical on every run.

The scenario

session: mock-build-and-check       # optional; default derived from the steps
title: build and check              # optional
interval: 1s                        # the gap between steps, in every stream
steps:
  - input: run the build            # a person's message: opens a run and a talk
  - inject: {type: skill_listing, text: "skills: 1"}
    after: 100ms                    # this step's own delta since the previous one
  - call:                           # one provider call, in fragments
      thinking: unavailable         # a reasoning part with no text; any other value is the text
      text: Building now.
      tool: {id: t1, name: Bash, input: {command: make build}, result: {text: build succeeded, after: 800ms}}
      usage: {in: 2, out: 50, cache_read: 900, cache_write: 100}
    checkpoint: built               # a point a test can stop at
  - call:
      agent:                        # a child agent: the call, its acknowledgement, the child's own stream
        name: checker
        prompt: check the tests
        after: 1s                   # the child's first record, relative to the call
        steps: [{call: {text: Tests pass.}}]
        notify: true                # the runtime reports the child finished: a second run
  - call: {text: Build passed and tests are green.}
  - error: "API Error: Connection lost mid-response."   # an assistant-role message the client made
  - reset: {summary: "Summary: the build was run and checked."}

Every step is exactly one of these, plus an optional after and checkpoint:

Step What it writes
input a person’s message; trigger: external; opens a run and a talk
queued: {text, mode} input that exists only as a queued attachment; mode is prompt or task-notification
inject: {type, text} material the harness put into context, of any type
call fragments in this order: thinking, text, then one of tool, agent, skill, workflow; usage on every fragment; the last carries the stop reason
result: {of, text, after, failed, string} a tool result arriving on its own, for a tool whose call gave none
error a synthetic message
reset: {summary} a context reset and its summary, the summary timestamped before the boundary as the runtime writes it
replay: N the last N main-stream records re-emitted with their run rewritten
system: {subtype, fields} a system record of any subtype

A tool without a result is an unfinished tool. A skill: {name, agent, steps} is a fork whose child is announced only in the parent’s result. A workflow: {name, children: [{name, prompt, steps}]} starts children as one batch, with a journal, a manifest and a script. Ids are stable and the same in every format: the step’s position names its records, a tool is tool/<id>, a run is <step>-cycle, a child stream’s id is derived from its name.

Check

asz scenario check FILE [--format claude-code|sd|all] [--out DIR] [--at TIME] [--scale FACTOR]

For each format, and at each checkpoint in order, check builds through the checkpoint, collects when the format needs it, parses, and compares the fold with the expectation file beside the scenario, NAME.expect.yaml. At the end it runs the properties every chain must have, and with all, the default, it compares the formats’ folds with each other. It exits non-zero on any failure and keeps its directory when one is given.

checkpoints:
  built:                                  # named in the scenario
    rounds: 1
    kinds: {tool: 1, llm.call: 1}
    nodes:
      tool/t1: {refs: 2, attrs: {result: available, timing: unavailable}}
  final:                                  # the end of the scenario
    rounds: 2
    delta: true                           # the round written here is a delta
    talks_on: {main: 2, checker: 1}       # a stream may be named by its scenario name
    runs_in: {talk/main/s1-cycle: 2}
    relations: {starts: 1, reports: 1}
    unresolved: {open: 0, resolved: 0}
    unresolved_kinds: {tool_result: none}
    session: {from: +0s, to: +11.1s}      # the session node's range, as deltas from --at
    view: {state: verified, talks: 3, files: 6, first_talk: {label: run the build, runs: 2}}
properties:                               # all on unless set false
  reproducible: true
  fold_equals_parse: true
  immutable_rounds: true
  bundle: true
  header_matches_fold: true
  records_well_formed: true
  repack_keeps_structure: true
  recollect_idempotent: true              # runtime formats only
  every_line_a_record: true               # runtime formats only
  discovery_ignores_noise: true           # runtime formats only
  cross_format: true
  records_match: true
  push_follows_the_wire: true
  view_covers_the_session: true
parse:
  max_round_bytes: 0                      # a parse setting, when the scenario needs one
push:
  kinds: [transcript, agent_meta, journal, workflow_manifest, workflow_script, round]   # kinds the push must carry

Only what is written is checked. The properties are: two parses of the same landed files write the same rounds; folding every round equals one full parse; rounds verify, link and are not writable; the landed files and rounds are self-sufficient without index and state; the head round’s header says what the fold holds; a parse with no new evidence writes nothing; every landed record carries only the fields the format states a purpose for; a repack under the smallest budget keeps every record and the whole structure; and, for a runtime format, a second collect lands nothing, every source line becomes one landed record, and discovery passes over the noise the writer plants beside the session. Across formats, the folds must agree, and so must the landed records themselves, field by field: the runtime’s adapter and the sd writer must land the same evidence from the same scenario, which is what makes a scenario a conformance test for an adapter.

The document is checked too: at the end of every scenario, view_covers_the_session holds the asz.view document to the whole session: every round, verified; every landed file with its digest as on disk; every talk, run and step of the fold in a tree; the session’s own range; and a verified state. A scenario with checkpoints is the multi-round case: three-rounds lands and parses in three stages, from the start to the first checkpoint, from there to the second, and from there to the end, so three rounds sit over landed files cut at each stage, and the final document must cover the session as one parse would.

The push is checked too. Every scenario, in both formats, is pushed to a receiver in the test, one file per request, and every request is compared with the tables of Export over OpenTelemetry: the resource and the scope, one record per file with the file’s bytes and digest, the attributes a landed file carries and the ones only a round carries, the record time range and the list attributes, and the stamp a receiver bounds a read on. A refused request must leave every file for the next pass, a second pass must send nothing, and writing every body back to its path must give a root that verifies and folds the same. push.kinds names the file kinds a scenario’s push must carry; all-kinds.yaml names all six.

The project’s own tests are scenarios under tests/scenarios/, one property of assembly each, run in both formats by go test ./tests/.