---
title: Showcase: a real turn
description: One real conversation with a live ggui-rendering agent, narrated beat by beat — every "On the wire" panel is a frame recorded off the wire, not prose about what would happen.
---

{/* Every claim this page makes about how a turn WENT — the counts AND the
    which-call-shape flags — is read off the same frames the panels show,
    never asserted in prose and never defaulted. The manifest declares the
    same four facts; they are reconciled here rather than trusted, so a
    manifest that drifts from the recording reds the build instead of putting
    a false sentence above a panel that contradicts it. */}
export const renderStats = (turn, n) => {
  const kinds = frameKinds(turn.frames);
  const results = turn.frames.filter((_, i) => kinds[i] === "message:tool.done:ggui_render");
  const refusals = results.filter((f) => f.data.isError === true);
  const succeeded = results.filter((f) => f.data.isError !== true);
  const counted = {
    renderAttempts: results.length,
    refusedRenders: refusals.length,
    hasRender: succeeded.length > 0,
    // Mirrors hasRender, and mirrors the capture rig's turnFlags(): a REFUSED
    // update is not an update. Counting any tool.done would make a recording
    // with a rejected ggui_update disagree with its own manifest.
    hasUpdate: turn.frames.some(
      (f, i) => kinds[i] === "message:tool.done:ggui_update" && f.data.isError !== true
    ),
  };
  // Under the read-plane-only posture (guuey#209 C3, armed on prod
  // 2026-08-16) the tool result carries NO _meta: identity only, on
  // structuredContent + the text block; the mount material lives behind
  // resourceUri via resources/read. A recording that DOES carry the
  // slice (a host or deployment that emits it) is narrated as such.
  const bootstrap = succeeded[0]?.data._meta?.["ai.ggui/render"];
  const carriesSlice = bootstrap !== undefined;
  const structured = succeeded[0]?.data.structuredContent;
  const okText = String(succeeded[0]?.data.content?.[0]?.text ?? "");
  // A successful render's first content block is the result JSON, but a host
  // that framed it differently must not red the build with a bare SyntaxError.
  let okResult = null;
  try {
    okResult = JSON.parse(okText);
  } catch {
    okResult = null;
  }
  // Honesty gate (read-plane-only posture): a successful render result MUST carry
  // its identity on structuredContent — under _meta withhold that is the ONLY
  // machine-readable place resourceUri lives, and beat 3's prose says so. A
  // recording where it is missing would narrate a contract the panel refutes.
  if (succeeded.length > 0 && !(structured !== null && typeof structured === "object" && typeof structured.resourceUri === "string")) {
    throw new Error(
      `showcase-trimly: turn ${n}'s successful render carries no structuredContent.resourceUri — the read-plane-only ` +
        `posture requires the identity there (beat 3 narrates it); check the recording's host framing`
    );
  }
  const carriesOverride = turn.frames.some(
    (f, i) =>
      kinds[i] === "message:tool.args.assembled:ggui_render" && f.data.input?.override !== undefined
  );
  const declared = manifest.turns.find((t) => t.n === n);
  if (!declared) {
    throw new Error(`showcase-trimly: the manifest carries no entry for turn ${n}`);
  }
  if (declared.renderAttempts === undefined || declared.refusedRenders === undefined) {
    throw new Error(
      `showcase-trimly: manifest turn ${n} carries no renderAttempts/refusedRenders — it predates ` +
        `the render-count fields; re-run the capture with --remanifest so the counts match the frames`
    );
  }
  if (
    declared.renderAttempts !== counted.renderAttempts ||
    declared.refusedRenders !== counted.refusedRenders
  ) {
    throw new Error(
      `showcase-trimly: manifest turn ${n} declares ${declared.renderAttempts} render attempt(s) / ` +
        `${declared.refusedRenders} refused, the recorded frames show ${counted.renderAttempts} / ${counted.refusedRenders} ` +
        `— the manifest and the capture disagree, so one of them is describing a turn that did not happen`
    );
  }
  if (declared.hasRender !== counted.hasRender || declared.hasUpdate !== counted.hasUpdate) {
    throw new Error(
      `showcase-trimly: manifest turn ${n} declares hasRender=${declared.hasRender} / hasUpdate=${declared.hasUpdate}, ` +
        `the recorded frames show hasRender=${counted.hasRender} / hasUpdate=${counted.hasUpdate} ` +
        `— the page would narrate a call shape the recording does not contain`
    );
  }
  const firstViolation = String(refusals[0]?.data.content?.[0]?.text ?? "")
    .split("\n")
    .find((line) => line.startsWith("- "));
  if (refusals.length > 0 && !firstViolation) {
    throw new Error(
      `showcase-trimly: turn ${n}'s refused render carries no violation text to quote — ` +
        `the page would print an empty quotation where the evidence should be`
    );
  }
  return {
    attempts: counted.renderAttempts,
    refused: counted.refusedRenders,
    succeeded: counted.renderAttempts - counted.refusedRenders,
    hasRender: counted.hasRender,
    hasUpdate: counted.hasUpdate,
    carriesSlice,
    carriesCode: typeof bootstrap?.codeB64 === "string",
    carriesOverride,
    cacheKind: okResult?.cache?.kind ?? null,
    cacheHit: okResult?.cache?.hit ?? null,
    firstViolation: firstViolation ? firstViolation.slice(2) : null,
  };
};
export const r1 = renderStats(turn1, 1);
export const r2 = renderStats(turn2, 2);

{/* What the handshake negotiated, read off the two frames beat 2's panel
    renders: what the agent's draft asked for, what the server proposed back,
    and which gaps between them it raised. Beat 2 tells that story from these
    values, so the next recording re-narrates itself. */}
export const handshakeFacts = (turn) => {
  const kinds = frameKinds(turn.frames);
  const draft = turn.frames.find(
    (_, i) => kinds[i] === "message:tool.args.assembled:ggui_handshake"
  )?.data.input?.blueprintDraft?.contract;
  const resultText = String(
    turn.frames.find((_, i) => kinds[i] === "message:tool.done:ggui_handshake")?.data.content?.[0]
      ?.text ?? ""
  );
  let result = null;
  try {
    result = JSON.parse(resultText);
  } catch {
    result = null;
  }
  const summary = result?.suggestion?.proposedContractSummary ?? null;
  const gaps = (result?.suggestion?.validationFindings ?? []).filter(
    (f) => f.code === "COVERAGE_GAP"
  );
  const advice = /Default to [^,]+/.exec(gaps[0]?.message ?? "");
  return {
    draftActions: Object.keys(draft?.actionSpec ?? {}),
    action: result?.action ?? null,
    origin: result?.suggestion?.origin ?? null,
    summary,
    gapPaths: gaps.map((f) => f.path),
    gapSeverity: gaps[0]?.severity ?? null,
    advice: advice ? advice[0] : null,
    // The proposal is what the agent accepted whenever it posted no override.
    proposalDeclaresActions: summary === null ? null : !/actions=∅/.test(summary),
  };
};
export const h1 = handshakeFacts(turn1);
export const grid1IsReadOnly = !r1.carriesOverride && h1.proposalDeclaresActions === false;

{/* Equal placeholders mean equal ids only when the capture redacted both
    turns through ONE map, which the manifest states as redaction:"single-map".
    Absent that stamp — or two session frames that disagree — the shared thread
    is something the rig did, not something these frames evidence. */}
export const redaction = manifest.redaction ?? null;

{/* Whether ONE placeholder map spans both turns is provable from the fixtures
    themselves: under one map, turn 2's newly-seen ids continue turn 1's
    numbering; under per-turn maps every kind restarts at _1, so no kind in
    turn 2 could exceed turn 1's maximum. Derived, so the page neither claims
    nor disclaims evidence it has. A manifest stamped redaction:"single-map"
    is an additional sufficient proof, not a required one. */}
export const placeholderMax = (turn) => {
  const max = new Map();
  JSON.stringify(turn.frames).replace(/"([a-z_]+)_(\d+)"/g, (whole, kind, n) => {
    const v = Number(n);
    if (!max.has(kind) || max.get(kind) < v) max.set(kind, v);
    return whole;
  });
  return max;
};
export const maxTurn1 = placeholderMax(turn1);
export const continuing = [...placeholderMax(turn2).entries()].filter(
  ([kind, n]) => n > (maxTurn1.get(kind) ?? 0)
);
export const sharedMap = maxTurn1.size > 0 && continuing.length > 0;
export const independentMaps = maxTurn1.size > 0 && continuing.length === 0;
export const continuityExamples = continuing
  .map(([kind]) => `${kind}_${(maxTurn1.get(kind) ?? 0) + 1}`)
  .sort()
  .slice(0, 3)
  .join(", ");
export const doneThreadIdOf = (turn) => {
  const kinds = frameKinds(turn.frames);
  const frame = turn.frames.find((_, i) => kinds[i] === "done");
  return frame?.data.threadId ?? null;
};
export const threadIdOf = (turn) => {
  const kinds = frameKinds(turn.frames);
  const session = turn.frames.find((_, i) => kinds[i] === "session");
  return session?.data.threadId ?? null;
};
export const sameThread = threadIdOf(turn1) !== null && threadIdOf(turn1) === threadIdOf(turn2);

{/* The session-space thread is on screen four times: both `session` frames and
    both `done` frames, which the closing panels already select. */}
export const threadOnScreenFourTimes =
  sameThread &&
  [turn1, turn2].every((t) => doneThreadIdOf(t) !== null && doneThreadIdOf(t) === threadIdOf(t));

{/* Two id spaces share the key name `threadId`: the session frame's is the
    platform's durable conversation thread, the AgJSON stream frames carry
    their own per-turn one. Only explain that while the recording actually
    shows both — a wire that later unifies them must not leave a stale note. */}
export const streamThreadIdOf = (turn) => {
  const kinds = frameKinds(turn.frames);
  const frame = turn.frames.find((_, i) => kinds[i] === "message:turn.start");
  return frame?.data.threadId ?? null;
};
export const twoThreadSpaces = [turn1, turn2].some(
  (t) => streamThreadIdOf(t) !== null && streamThreadIdOf(t) !== threadIdOf(t)
);

:::note[What you are looking at]
Recorded **{new Date(manifest.capturedAt).toISOString().slice(0, 10)}** against protocol **<code>{manifest.protocolSchemaVersion}</code>**, from guuey's release-tier demo agent _Trimly_ — a fictional booking SaaS running at <code>{manifest.endpointHost}</code> (app <code>{manifest.agentAppId}</code>). guuey is a separate hosting platform that speaks the ggui protocol; this page is about the **protocol**, and the frames are the evidence. Ids are stable placeholders; credentials were removed at capture, never recorded.
:::

Two turns, one thread, two rendered surfaces. Read the story; open any **On the wire** panel to see exactly what crossed the wire at that beat. Nothing on this page is typed by hand: each panel is read out of the committed recording at build time, and a panel that names a frame the recording does not contain fails the build rather than quietly showing you prose.

## 1 · The ask

A visitor — anonymous, no account — types _"{turn1.prompt}"_. The rig behind this recording invoked the agent the way that visitor would: over the host's guest wire, with no sign-in and no bearer token. One POST goes out, and the first frame back is the `session`, which is where that shows up on the wire: `authMode: "anonymous"`, a caller id standing in for the guest hash the real exchange carried (placeholdered here like every other id), and the `threadId` that names the durable thread every later turn replays against.

<WireFrames showcase="trimly" turn={1} select={["session"]} />

## 2 · Handshake — the contract, negotiated

Before any UI exists, the agent calls **`ggui_handshake`** with an _intent_ and a _draft contract_. The draft is the interesting half: the agent describes, in schema, the surface it wants. The server answers with a **proposed** contract, an `action` (here <code>{h1.action}</code>), and an `origin` — `cache` (a similar UI was built before and is reused), `agent` (the draft was already clean), or `synth` (the server repaired it). Open the panel to read the server's rationale: which blueprint it matched, at what confidence, and every coverage gap between the draft and the cached surface.

The contract is where the protocol's placement rule lives: `actionSpec` = discrete events that drive the agent's next turn, `contextSpec` = observable state the agent reads when it next works, and there is no third category — see [the placement test](/glossary/#contract) and [Envelopes](/protocol/envelopes/).

{h1.draftActions.length > 0 && h1.gapPaths.length > 0 ? (

  <p>
    Watch what the two frames in the panel do with that. This ask wanted an action: the draft declares{" "}
    <code>actionSpec.{h1.draftActions[0]}</code> alongside its props. The contract the server proposed —
    reused from a cached blueprint — carries no such thing: <code>{h1.summary}</code>. {h1.gapPaths.length === 1 ? "The difference comes" : "The differences come"}
    {" "}back as {h1.gapPaths.length === 1 ? "one " : `${h1.gapPaths.length} `}
    <code>COVERAGE_GAP</code> finding{h1.gapPaths.length === 1 ? "" : "s"} at <code>{h1.gapSeverity}</code>
    {h1.gapPaths.length > 1 ? <> — {h1.gapPaths.map((p, i) => <><code key={p}>{p}</code>{i < h1.gapPaths.length - 1 ? ", " : ""}</>)} — </> : " "}
    never a veto{h1.advice ? <>, with the server's own counsel attached: "{h1.advice}"</> : null}. The agent
    accepted, so the grid this recording mounted is a read-only card. Re-aiming the contract instead is a
    call the protocol gives it — beat 6 is where that lever appears.
  </p>
) : null}

<WireFrames
  showcase="trimly"
  turn={1}
  select={[
    "message:tool.start:ggui_handshake",
    "message:tool.args.assembled:ggui_handshake",
    "message:tool.done:ggui_handshake",
  ]}
/>

One detail the raw frames show and this page smooths over: hosts may namespace MCP tool names. In these recordings the wire name is `mcp__ggui__ggui_handshake`; the protocol name is `ggui_handshake`, and readers of the stream should match on the bare name.

## 3 · Render — and the contract check that refuses a bad one

**`ggui_render`** commits the contract and returns the view's **identity** — not its material. On this host's stream that arrives as a `tool.done` frame carrying the render's JSON both as text and as the result's `structuredContent`: `resourceUri`, the view's durable `ui://` identity; `action`, reporting whether the surface was created or reused; `contractHash`; and a `cache` record. What is deliberately **not** on the wire is the mount material — the bootstrap that boots the runtime and opens the live channel, the compiled component, the theme. A host resolves those by reading `resourceUri` (MCP `resources/read`), the read-plane-only posture: the tool result names the view, the read plane serves it — see [Bootstrap handshake](/protocol/bootstrap-handshake/).

Note what the agent never writes: component code. Its call is `handshakeId` plus `props` — and, when it wants to re-aim the contract it was offered, an `override` — but read the assembled-arguments frame in the panel and you will not find markup, styling, or a component anywhere in it. Authoring the UI is not the agent's job.

{!r1.carriesSlice ? (
  <p>
    So where is the code? Not in the panel — and that is the point. Under the read-plane-only posture
    the tool result carries no <code>ai.ggui/render</code> slice at all: the host takes the{" "}
    <code>resourceUri</code> above to MCP <code>resources/read</code> and receives the shell — the bootstrap
    (which runtime to load, which live channel to open), the compiled component, the theme — from the read
    plane, cached and content-addressed. The agent's stream stays small and identity-shaped; the multi-kilobyte
    material never rides the conversation. It was generated server-side, from the contract; the agent neither
    wrote it nor saw it.
  </p>
) : r1.carriesCode ? (
  <p>
    The code lives in the material coming back. This recording's <code>ai.ggui/render</code> slice hands the
    host a compiled component as <code>codeB64</code> alongside <code>propsJson</code> and <code>theme</code> —
    that is what makes the view mountable without a second fetch, and it is why the frame in the panel is
    several kilobytes of base64. It was generated server-side, from the contract; the agent neither wrote
    it nor saw it.
  </p>
) : (
  <p>
    In this recording the <code>ai.ggui/render</code> slice carries no <code>codeB64</code> — open the panel
    to see what it does hand the host, and how the host is expected to resolve the view from it.
  </p>
)}

A negotiated contract is enforced, not advised. When the props an agent commits violate the contract it just agreed to, the render is **refused** with a typed error naming the exact offending path, and the agent repairs the call and renders again — no human in the loop. That is the check working, not the agent failing.

{r1.refused > 0 ? (

  <p>
    In this turn the check fired. The panel below carries every render call and result in wire order:
    the {r1.refused === 1 ? "first result is a refusal" : `first ${r1.refused} results are refusals`}
    {r1.succeeded > 0
      ? ", and the last is the success"
      : ", and every call was refused — the surface never mounted"}. The violation is worth reading in
    full — it names a path and the vocabulary the contract declared:{" "}
    <code>{r1.firstViolation}</code>. Other violations take the same shape: a field the contract never
    declared, a string where an object was specified, each reported by path.
  </p>
) : (
  <p>
    In this turn the check had nothing to refuse: the agent's props conformed on the first call. The panel
    below is that single exchange — the call it assembled, and the result it got back.
  </p>
)}

<WireFrames
  showcase="trimly"
  turn={1}
  select={["message:tool.args.assembled:ggui_render", "message:tool.done:ggui_render"]}
/>

## 4 · The view boots

The host mounts the material; the runtime inside the frame answers the MCP-Apps `ui/initialize` handshake, posts `ggui:renderer-ready`, and opens the live WebSocket named in the bootstrap. This beat happens **inside the host page**, not on the agent's SSE stream — which is why there is no wire panel here: the recorded stream shows the agent's side only. The exact boot flow and host obligations are specified in [Bootstrap handshake → the boot flow](/protocol/bootstrap-handshake/#the-boot-flow).

## 5 · The gesture (and how a real one arrives)

A surface that declares an action routes a tap this way: the visitor taps, the runtime emits an `ActionEnvelope` on the live channel, and the agent drains it with **`ggui_consume`** and reasons about it on its next turn — see [Envelopes → ActionEnvelope](/protocol/envelopes/#actionenvelope).

{grid1IsReadOnly ? (

  <p>
    This grid declares none. Beat 2's coverage gap is why: the agent asked for an action, the cached
    contract it accepted has no <code>actionSpec</code> at all, so there is nothing here to tap. Whether the live
    demo negotiates you the same read-only card or one with an action is its own handshake's call — this
    page speaks only for the recording. The recording was also made by a script that sent no gesture, so
    no <code>ggui_consume</code> frame exists and nothing on this page claims one does. What the frames do
    show is the end of the turn: the agent hands control back with a next step, offering to take the
    conversation further.
  </p>
) : (
  <p>
    This recording was made by a script that sent no gesture, so there is no <code>ggui_consume</code> frame
    to show and nothing here claims one happened. What the frames do show is the end of the turn: the agent
    hands control back with a next step, offering to take the conversation further.
  </p>
)}

<WireFrames
  showcase="trimly"
  turn={1}
  select={["message:text.delta", "done"]}
  title="On the wire — the turn closes"
/>

## 6 · Turn 2 — a second ask on the same thread

_"{turn2.prompt}"_ The invoke carries the `threadId` learned in beat 1, so the agent has the whole conversation. The panel below opens turn 2 with its own `session` frame — {sameThread && sharedMap ? (<>naming the same <code>{threadIdOf(turn2)}</code> you read in beat 1, and the numbering is what makes that evidence rather than coincidence: one placeholder map spans the whole recording, so turn 2's ids <em>continue</em> turn 1's instead of restarting at <code>_1</code> ({continuityExamples}). Equal placeholders therefore mean equal ids{threadOnScreenFourTimes ? <>, and this thread is on screen four times across the panels on this page — both <code>session</code> frames and both <code>done</code> frames, all agreeing</> : null}</>) : sameThread && redaction === "single-map" ? (<>naming the same <code>{threadIdOf(turn2)}</code> you read in beat 1, which this capture stamps as redacted through one placeholder map (<code>redaction: "single-map"</code>), so equal placeholders mean equal ids</>) : independentMaps ? (<>the rig replayed the <code>threadId</code> it learned in turn 1; this recording's turns were redacted through independent maps, each restarting at <code>_1</code>, so equal placeholders are not evidence of it here</>) : (<>the rig replayed the <code>threadId</code> it learned in turn 1</>)}. The handshake that follows it negotiates a contract for the new ask.

<WireFrames
  showcase="trimly"
  turn={2}
  select={["session", "message:tool.start:ggui_handshake", "message:tool.done:ggui_handshake"]}
  title="On the wire — turn 2 opens"
/>

{twoThreadSpaces ? (

  <p>
    One caution if you open the raw fixtures: two different id spaces share the key name{" "}
    <code>threadId</code>. The <code>session</code> frame's is the platform's durable conversation thread —
    the one a replayed turn rejoins, and the one every claim on this page is about. The message-stream
    frames (<code>turn.start</code>, <code>message.start</code>) carry the AgJSON stream's own per-turn
    thread id under the same key: <code>{streamThreadIdOf(turn1)}</code> in turn 1 and{" "}
    <code>{streamThreadIdOf(turn2)}</code> in turn 2, neither of them{" "}
    <code>{threadIdOf(turn1)}</code>. Different spaces, same spelling; compare session frames to session
    frames.
  </p>
) : null}

{r2.hasUpdate ? (

  <>
    <p><strong>This recording mutates the mounted surface.</strong> The agent called <code>ggui_update</code>: the props were swapped and fanned out to the live view, plus a new history card and an advanced epoch. No fresh <code>ggui_render</code>, so the view is never replaced. See the callout below for how that differs from <code>ggui_amend</code>.</p>
    <WireFrames showcase="trimly" turn={2} select={["message:tool.args.assembled:ggui_update", "message:tool.done:ggui_update"]} />
  </>
) : r2.hasRender ? (
  <>
    <p><strong>This recording shows a second render.</strong> The agent negotiated again — a second <code>ggui_handshake</code> on the same thread — and rendered a fresh comparison surface. That is a legitimate choice for a <em>different</em> surface: a two-option slot comparison is not the week grid. The contract check from beat 3 applies here too.</p>
    {r2.refused > 0 ? (
      <p>The panel is again in wire order: the {r2.refused === 1 ? "first result is a refusal" : `first ${r2.refused} results are refusals`}{r2.succeeded > 0 ? ", the last is the success" : ", and every call was refused — this surface never mounted"}.</p>
    ) : (
      <p>This time nothing was refused — the agent's props conformed on the first call, so the panel holds one call and one successful result. Beat 3 is where you can see the check turn a call back.</p>
    )}
    <WireFrames showcase="trimly" turn={2} select={["message:tool.args.assembled:ggui_render", "message:tool.done:ggui_render"]} title="On the wire — turn 2 renders" />
    {r2.carriesOverride ? (
      <p>
        This render call carries a third field{!r1.carriesOverride ? " the turn-1 call did not" : ""}:{" "}
        <code>override</code>. That is
        step 3 of the handshake — the agent takes the proposed contract as a starting point and re-aims
        it, posting the contract it actually wants (open <code>override.contract</code> in the panel).
        An override re-resolves the render's effective identity{r2.cacheHit === false ? (
          <>, so the server had nothing stored under it: this render reports <code>cache: {"{"}hit: false, kind: "{r2.cacheKind}"{"}"}</code> and the component was generated fresh</>
        ) : r2.cacheHit === true ? (
          <>, and this one still landed on something stored: <code>cache: {"{"}hit: true, kind: "{r2.cacheKind}"{"}"}</code></>
        ) : null}
        {!r1.carriesOverride && r1.cacheHit === true
          ? ` — where turn 1, which posted no override and accepted the proposal as-is, reused a stored blueprint (${r1.cacheKind})`
          : ""}
        . Reuse is the reward for accepting a proposal; re-aiming it can cost a generation, and the agent
        gets to decide which it wants.
      </p>
    ) : null}
    <aside class="callout">
      <p><strong>What it would look like if the agent mutated the mounted surface instead.</strong> When the right move is to change what an already-mounted view shows — the same grid, different week — the rule is <em>never re-render to mutate</em>. The agent calls <a href="/api/mcp-protocol/"><code>ggui_amend</code></a>: no generation runs, the props are swapped and pushed down the live channel, and the card the user is looking at updates in place — no new card, the history number does not advance, and it updates <em>without losing scroll position, focus, or uncommitted input</em>. <code>ggui_update</code> is that same swap plus a new history card, and it advances the epoch. Either way the wire shape is one message — a <a href="/api/websocket-protocol/#props_update"><code>props_update</code></a> on the live WebSocket carrying <code>{"{sessionId, props, epoch}"}</code>, a full props replacement rather than a patch. The agent-side loop is written up in <a href="/how-it-works/">How it works</a>. This recording exercises neither call; nothing here claims it does.</p>
    </aside>
  </>
) : (
  <p><strong>In this recording turn 2 was answered in prose</strong> — the agent judged no new surface was needed. The frames below are the turn as recorded. Mutating an already-mounted view is a different call, <code>ggui_amend</code> (or <code>ggui_update</code>, which also mints a history card and advances the epoch); both fan out as a <a href="/api/websocket-protocol/#props_update"><code>props_update</code></a> on the live channel, and this recording does not exercise either.</p>
)}

<WireFrames
  showcase="trimly"
  turn={2}
  select={["message:text.delta", "done"]}
  title="On the wire — turn 2 closes"
/>

## 7 · What a conforming implementation had to do

At each beat above, an implementation either did the specified thing or the turn would not have completed: answered the handshake with a conforming contract (beat 2), validated the committed props against that contract before mounting anything (beat 3), returned a durable identity the host resolves to mount material through the read plane (beat 3), booted and negotiated inside the frame (beat 4), and — for a surface that declares an action — would route a gesture back as an `ActionEnvelope` (beat 5).

{r1.refused + r2.refused > 0 ? (

  <p>
    Validation is the one of those you can watch happen rather than take on faith:{" "}
    {r1.refused + r2.refused === 1 ? "one call in this recording was" : `${r1.refused + r2.refused} calls in this recording were`}{" "}
    refused for violating the contract
    {(() => {
      const repaired = [
        r1.refused > 0 && r1.succeeded > 0 ? "beat 3's panel" : null,
        r2.refused > 0 && r2.succeeded > 0 ? "beat 6's panel" : null,
      ].filter(Boolean);
      if (repaired.length === 0) return ", and no repaired call followed — the refused surface never mounted";
      if (repaired.length === 1) return `, and the repair that followed is in ${repaired[0]}, one frame later`;
      return `, and the repairs that followed are in ${repaired.join(" and ")}, each one frame later`;
    })()}.
  </p>
) : (
  <p>
    No call was refused anywhere in this recording — every render conformed on its first attempt, so
    validation is the one obligation above that you have to take on faith here. Beat 3 describes the shape
    a refusal takes when it happens.
  </p>
)}

The arbiter of "conforming" is not this page — it is the [conformance kit](/protocol/conformance/); the fixtures behind these panels are the kind of input it runs.

## 8 · Run it yourself, check yours

- **See it live:** [guuey.com/demos/scheduler](https://guuey.com/demos/scheduler) — run the same ask and watch a negotiation of your own.
- **Check your implementation:** take the recording itself — [turn 1](/showcase/trimly/turn-1.frames.json), [turn 2](/showcase/trimly/turn-2.frames.json), and its [manifest](/showcase/trimly/manifest.json) — and feed the frames to your host or server as fixtures. They are the same files these panels are built from, ids placeholdered, nothing else altered.
- **Build your own agent on the protocol:** [Build an agent on hosted ggui](/quickstart/hosted-agent/) or [self-host](/quickstart/self-hosted/).

<style>{`
  .callout { border-left: 3px solid var(--sl-color-accent); padding: 0.25rem 1rem; margin: 1rem 0; background: var(--sl-color-gray-6); border-radius: 0 0.5rem 0.5rem 0; }
`}</style>