Skip to content

Agent message card, and an alias that actually resolves (v0.10.0) - #19

Merged
happyren merged 1 commit into
masterfrom
feat/agent-message-card
Aug 15, 2026
Merged

happyren merged 1 commit into
masterfrom
feat/agent-message-card

Conversation

@happyren

Copy link
Copy Markdown
Owner

What

An arriving peer message now renders as its own transcript card instead of a generic Context injection row.

The card

It names the sender, says what the delivery cost (interrupted this step / next turn / delivered quietly), says what this session was told it may do about it, and shows the message rather than the framing around it. Verified live in a real dsh web host with two model sessions, in light and dark.

This is the plugin's first browser half — a client bundle the harness serves to the Web UI, built separately from the host half with its own tsconfig, DOM and JSX.

Two rows, deliberately

The harness's own context row stays directly beneath the card. Every registered projection that matches an event contributes a node — a plugin cannot claim an event away from the harness — and that is the right arrangement anyway: the card is the human-facing view, and the row beneath it holds the exact bytes the model read, untrusted-content notice included. The card never claims to be that text, and says so when it cannot parse it.

The design that did not survive contact

A custom durable event would have given the card a row of its own. But a session log carrying an event type outside KNOWN_SESSION_EVENT_TYPES is refused on reload unless the envelope is marked ignorable, and Session.append exposes no way to mark one — the harness records that a registration surface for downstream plugin events is deferred until a consumer exists. Shipping it would have made a user's session unopenable. The card derives from the event the harness already writes; this plugin adds nothing to the log.

Bug found by running it live

A session that published an alias could not be addressed by it. peer_send matched session ids and folded display names only, so a peer that read a capability card and repeated the alias addressed a stranger — the failure v0.9.1 fixed for blocked_on, still present in the one place that matters most.

Aliases now reach the peer directory itself, so sending, verifying, group fan-out and wait graphs share one address vocabulary, and the special case in the composition root is deleted rather than duplicated.

Verification

  • 358 tests, 33 on the card's projection, formatting and record
  • the record the host writes is read back by the card in tests/agent-sink.test.ts, so the two halves cannot drift apart quietly
  • tests/render.test.ts pins the framing round trip: a change to either side that the other does not follow fails, rather than silently blanking a card
  • live: bundle loads and materializes, styles install, card renders, alias resolves
  • removes summarizeInbound, which nothing called

Still not visible

presentCall/presentResult on tool calls — re-checked at v0.10.0, the Web UI still renders the generic row. Declarations stay (correct against the published types, free to carry); no claim is made for them.

A peer message used to reach the transcript as a generic Context injection
row, indistinguishable from a skill catalog or a reconciled instruction
file, attributed to an opaque session id. Nothing about the transcript said
another agent had spoken.

This ships the plugin's browser half. A conversation node recognizes this
plugin's own messages in the durable log; a renderer registered against the
harness's keyed conversation.chat.node slot draws them. The card names the
sender, says how the message arrived and what this session may do about it,
and shows the message rather than the framing around it.

The harness's own context row stays directly beneath. Every projection that
matches an event contributes a node -- no plugin can claim an event away
from the harness -- and that is the right arrangement anyway: the card is
the human view, the row beneath it is the exact bytes the model read.

A custom durable event was the first design, and it does not survive
contact: a log carrying an event type outside KNOWN_SESSION_EVENT_TYPES is
refused on reload unless marked ignorable, and Session.append cannot mark
one. Shipping it would have made a user's session unopenable. Nothing is
added to the log; the card derives from the event the harness already
writes.

Also fixes address resolution, found by running this live: a session that
published an alias could not be reached by it. peer_send resolved session
ids and folded display names only, so a peer that read a capability card
and repeated the alias addressed a stranger. Aliases now reach the peer
directory itself, so sending, verifying, group fan-out and wait graphs
share one vocabulary -- and the special case in the composition root is
gone rather than duplicated.

Verified in a real dsh web host with two model sessions: the bundle loads,
the card renders in light and dark, and the alias resolves.

- 358 tests, 33 of them on the card's projection, formatting and record
- the record the host writes is read back by the card in agent-sink.test.ts,
  so the two halves cannot drift apart quietly
- removes summarizeInbound, which nothing called
@happyren
happyren force-pushed the feat/agent-message-card branch from bbaa966 to 3ca64f5 Compare August 15, 2026 10:05
@happyren
happyren merged commit 851dbbd into master Aug 15, 2026
@happyren
happyren deleted the feat/agent-message-card branch August 15, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant