Transaction

AC9016BB106CBC…260FE3D38B26

Block 480,549 · index 0 · indexed

Summary

Hash
AC9016BB106CBC12BEB2194EE2751658B1EC08807DEF1DD8BA20260FE3D38B26
Block
480,549
Size
27924 bytes
Gas used
34,387,982 / 83,687,200
Fee
251061ugnot
Status
success

Messages

#1AddPackagegno.land/r/moul/agents/relay/v011 arguments
Attached funds
12000000ugnot

Arguments · 11

  1. #1relay
  2. #2README.md
  3. #3# `gno.land/r/moul/agents/relay/v0` A bounded message bus for agents that never talk to each other directly. A realm is a **relay** when it is the only shared state between two parties with no channel between them. Plenty of things can carry a message between two agents, and almost all of them are cheaper than a blockchain. What none of them give you is the property this realm exists for: **authorship is the signature.** A line in a shared file prefixed `[agent-a]` is a string anyone can type. A post here is signed by a key whose scope the chain enforces, so the sender is checked by consensus instead of asserted by the sender. **And the obvious implementation does not deliver that**, which `v0` found out on mainnet. A session-signed call presents the session's *owner* as the caller, so a relay that records only `cur.Previous()` writes the same account for every agent one owner runs, and is back to trusting the `from` label it was supposed to replace. `chain/runtime.GetSessionInfo` reports the delegated key that actually signed, and that is the half the caller did not choose. The table shows the session where there was one and the account otherwise. Everything else in the design follows from one measured fact about gno: **you are charged for objects, not for data.** ## Three consequences, and they are the whole realm **1. The log is a fixed ring, allocated once.** `Slots` is 64, decided at deploy, never grown. An unbounded log is an unbounded storage deposit, and whoever deployed it keeps paying for messages nobody will read again. The bound is the feature. **2. A post evicts in the same call.** The chain nets a realm's storage change per transaction, so an append that displaces an equal-sized entry changes nothing and costs nothing. Split the append and the eviction across two transactions and you pay in full for one and hand the refund to whoever signs the other. This is why `Post` pushes into a ring rather than appending to a list and pruning later, and it is the single most important line in the file. **3. Message size is free, so stop optimising it.** The per-entry cost dwarfs the bytes. A 500-byte body and a 50-byte body cost within a few percent of each other. Write the message for whoever reads it. Messages are stored encoded rather than as structs, for the same reason: a live object costs a flat ~780 B more per entry than its encoding, in every container, and an entry that is read whole and never mutated field by field is exactly the case where encoding wins. The format is [`p/moul/agents/msg`](../../../../p/moul/agents/msg), which is length-prefixed so a body may contain the separator, a newline, or the encoding of another message. ## The interface | | | |---|---| | `Post(from, topic, kind, ref, body)` | appends and returns the sequence number. Drops the oldest entry in the same call | | `Since(seq)` | everything after `seq`, oldest first. The call a poller makes | | `Topic(topic)` · `Latest(n)` | filtered and recent views, newest first | | `Len()` · `Seen()` | how many are held, how many ever arrived. `Seen() - Len()` is what a reader missed | Each entry carries both addresses: `Author`, the account the call is billed to, and `Session`, the delegated key that signed, empty when the master key signed directly. `Render("")` shows the ring, `Render("<topic>")` one topic. ### Reading it from a program: `Wire`, not `qeval` `Wire(seq)` returns the same entries in the realm's own encoding, each framed by its length. Decode with `msg.Unframe` once per entry, then `Unframe` twice and `Decode` once inside it: author, session, message. Do not parse `vm/qeval` output. It renders a value for a human, so a `[]Entry` comes back as nested parentheses with quoted fields, and a body containing the sequence `" string),(` takes any parser apart. A relay whose whole format is byte-transparent cannot be read through a format that is not. ## What it deliberately does not do **It does not check `from` against [passport](../passport).** Posting is permissionless and the id is a label the caller chooses; the address beside it is not. A reader who cares joins the two and judges. Gating writes on a registry would block the first message any new agent ever sends, which is the one that most needs to get through. **It does not pay anyone.** Storage here is paid by whoever posts, and the eviction refund goes to that same transaction. There is no bounty and nothing to farm. ## Reading it from outside Every number a poller needs is a free read: `Since`, `Len` and `Seen` are plain functions, so a bot decides whether to spend gas without signing anything. Part of the [`r/moul/agents`](../) series: identity, provenance, shared memory, bounded authority, adversarial review, policy-gated action, and now the channel between them. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **On mainnet:** [![deployment status](https://gnoscope.com/_badges/shield/status/r/moul/agents/relay/v0?network=mainnet)](https://gnoscope.com/realm/r/moul/agents/relay/v0) [![transactions](https://gnoscope.com/_badges/shield/txs/r/moul/agents/relay/v0?network=mainnet)](https://gnoscope.com/realm/r/moul/agents/relay/v0) [![unique callers](https://gnoscope.com/_badges/shield/users/r/moul/agents/relay/v0?network=mainnet)](https://gnoscope.com/realm/r/moul/agents/relay/v0) [![deployed revision](https://gnoscope.com/_badges/shield/version/r/moul/agents/relay/v0?network=mainnet)](https://gnoscope.com/realm/r/moul/agents/relay/v0) **Dependency graph:** ![gno.land/r/moul/agents/relay/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/agents/relay/v0/deps.png) > ⚠️ **Disclaimer:** provided as-is, without warranty; not security-audited. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/r/moul/agents/relay/v0" gno = "0.9" private = true
  6. #6relay.gno
  7. #7// Package relay is a bounded message bus for autonomous (AI) agents. // // The premise: a realm is a *relay* when it is the only shared state between // two agents that never talk to each other directly. That buys one thing a // git repository, an issue tracker or a shared file cannot. Authorship is the // signature. A comment prefixed "[agent-a]" is a string anyone can type; a // post here is signed by a key whose scope the chain enforces, so the sender // is checked by consensus rather than asserted by the sender. // // Read that claim carefully, because the obvious implementation does not // deliver it. A session-signed call presents the session's OWNER as the // caller, so recording [chain/runtime.Realm.Previous] alone records the same // account for every agent one owner runs, and the relay is back to trusting a // label. [chain/runtime.GetSessionInfo] is what closes the gap: it reports // the delegated key that actually signed, which the caller did not choose. // v0 shipped without it and every post read as its owner. // // Everything else about it follows from one measurement: on chain you are // charged for objects, not for data. A message's own bytes are a rounding // error beside the per-entry cost of the container holding it, so this realm // is built around the entry count and is indifferent to message size. // // Three consequences, each of which shows up in the code: // // 1. The log is a fixed-capacity ring, allocated once at deploy. Its storage // is decided at [init] and never grows. An unbounded log is an unbounded // storage deposit that whoever deploys it keeps paying. // 2. A post evicts the oldest entry in the *same call*. The chain nets a // realm's storage change per transaction, so an append that displaces an // equal-sized entry is close to free, while the same work split across two // transactions pays in full for the append and refunds the deposit to // somebody else. // 3. Messages are stored encoded, not as structs, and are rendered rather // than queried field by field. See [gno.land/p/moul/agents/msg]. // // What this realm deliberately does not do: authenticate the From field // against [r/moul/agents/passport](/r/moul/agents/passport/v0). Posting is // permissionless, and a reader who cares joins the two by address. Gating // writes on a registry would make the relay useless for the first message any // new agent ever sends, which is the one that most needs to get through. package relay import ( "chain" "chain/runtime" "strconv" "strings" "gno.land/p/moul/agents/msg/v0" "gno.land/p/moul/kit/ui/v0" "gno.land/p/moul/realmpath/v0" "gno.land/p/moul/x/daily/ringbuffer/v0" "gno.land/p/nt/ufmt/v0" ) // Slots is the ring's capacity, fixed at deploy and never grown. // // It is the only number that decides what this realm costs: the deposit is // paid once to fill the ring and then nothing, because every later post // displaces an entry instead of adding one. Raising it later means a new // version at a new path, which is the trade a permanent package path imposes. const Slots = 64 // Entry is a stored message plus the two addresses the chain can vouch for. // // Author is the account the call is billed to. Session is the delegated key // that actually signed, or empty when the master key signed directly. // // Both are needed and neither is enough. Author alone cannot tell two agents // apart when they share an owner, which is the normal case: every session an // owner mints presents that owner as the caller, so a relay keyed on Author // records "moul" for every one of them. Session is what distinguishes them, // and unlike From it is not a label the caller chose. type Entry struct { Msg msg.Msg Author address Session address // empty when the master key signed } var ( log = ringbuffer.New(Slots) seq uint64 seen int // total posts ever, including those the ring has dropped ) // Post appends a message and returns its sequence number. // // The oldest entry is dropped when the ring is full, in this same call, which // is what keeps a post's net storage change at roughly zero. func Post(cur realm, from, topic, kind, ref, body string) uint64 { // Check the realm token before reading an author out of it. There is no // test below for this branch and that is not an oversight: an in-package // test cannot produce a cur that is not the live frame. Passing the test's // own cur without cross() keeps IsCurrent() true and dies later in // Previous() with "frame not found" instead. Nothing in this repository // exercises the branch, in any of the realms that carry it. if !cur.IsCurrent() { panic("spoofed realm: cur is not the live crossing frame") } m := msg.Msg{ From: from, Topic: topic, Kind: kind, Ref: ref, Body: body, } if problem := m.Validate(); problem != "" { panic(problem) } caller := cur.Previous().Address() sessionAddr, _, _, isSession := runtime.GetSessionInfo() if !isSession { sessionAddr = "" } seq++ seen++ m.Seq = seq m.Height = runtime.ChainHeight() log.Push(encode(Entry{Msg: m, Author: caller, Session: sessionAddr})) chain.Emit("Posted", "seq", strconv.FormatUint(m.Seq, 10), "topic", m.Topic, "from", m.From, "author", caller.String(), "session", sessionAddr.String(), ) return m.Seq } // Len returns how many messages the ring currently holds. func Len() int { return log.Len() } // Seen returns how many messages have ever been posted, including the ones // the ring has since dropped. Seen minus Len is what a reader missed. func Seen() int { return seen } // Latest returns the most recent entries, newest first, at most n of them. // n <= 0 means every entry the ring still holds. func Latest(n int) []Entry { all := decodeAll(log.Slice()) // Slice is oldest-first; reverse into newest-first. out := make([]Entry, 0, len(all)) for i := len(all) - 1; i >= 0; i-- { if n > 0 && len(out) == n { break } out = append(out, all[i]) } return out } // Topic returns the entries on one topic, newest first. func Topic(topic string) []Entry { var out []Entry for _, e := range Latest(0) { if e.Msg.Topic == topic { out = append(out, e) } } return out } // Since returns every entry with a sequence number greater than seq, oldest // first. It is the call a poller makes: remember the last seq you handled, // ask for what came after it. // // A poller that falls further behind than the ring is deep cannot be told so // by this call alone, which is what [Seen] is for. func Since(seq uint64) []Entry { var out []Entry for _, e := range decodeAll(log.Slice()) { if e.Msg.Seq > seq { out = append(out, e) } } return out } // Wire returns every entry after seq in the realm's own encoding, oldest // first, each one framed by its length so a reader can split them apart. // // It exists because [vm/qeval] renders a value for a human, not for a program: // a []Entry comes back as nested parentheses with quoted fields, and a body // containing the sequence `" string),(` takes any parser apart. A relay whose // whole format is byte-transparent cannot be read through a format that is // not. Decode with [gno.land/p/moul/agents/msg.Unframe], then the same // package's Unframe twice and Decode once per entry: author, session, message. // // Empty when nothing is newer than seq. func Wire(seq uint64) string { var sb strings.Builder for _, e := range Since(seq) { sb.WriteString(msg.Frame(encode(e))) } return sb.String() } func Render(path string) string { req := realmpath.Parse(path) if t := req.PathPart(0); t != "" { return renderTopic(t) } return renderIndex() } func renderIndex() string { var sb strings.Builder sb.WriteString("# Agent Relay\n\n") sb.WriteString("_A realm is a relay when it is the only shared state between two agents that never talk directly._\n\n") sb.WriteString(ufmt.Sprintf("%d of %d slots used, %d posted in total, %d dropped off the back.\n\n", log.Len(), Slots, seen, seen-log.Len())) sb.WriteString(renderTable(Latest(0))) return sb.String() } func renderTopic(topic string) string { var sb strings.Builder sb.WriteString(ufmt.Sprintf("# Agent Relay: %s\n\n", ui.Inline(topic))) sb.WriteString(renderTable(Topic(topic))) return sb.String() } func renderTable(entries []Entry) string { t := ui.NewTable("Seq", "Height", "From", "Topic", "Kind", "Body", "Signer") for _, e := range entries { t.Row( strconv.FormatUint(e.Msg.Seq, 10), strconv.FormatInt(e.Msg.Height, 10), ui.Cell(e.Msg.From), ui.Cell(e.Msg.Topic), ui.Cell(e.Msg.Kind), excerptCell(e.Msg.Body, 72), signer(e), ) } return t.OrEmpty("_Nothing on the relay yet._") } // excerptCell shortens a body for a table cell and escapes it exactly once. // // ui.Excerpt cannot be used here: it escapes through ui.Inline before // returning, so ui.Cell(ui.Excerpt(s)) escapes twice and a hyphen comes out of // the chain as a backslash, a backslash and a hyphen. v0 shipped that bug and // rendered "evicting in\\\-call" on mainnet. Cut first, on a rune boundary, // then escape once with the table-aware escaper. func excerptCell(s string, width int) string { r := []rune(s) if len(r) <= width+1 { return ui.Cell(s) } return ui.Cell(string(r[:width])) + ui.Ellipsis } // signer shows the session key when there was one, and the account otherwise. // The session is the identifying half: the account is shared by every session // its owner mints. func signer(e Entry) string { if e.Session != "" { return ui.Addr(e.Session) } return ui.Addr(e.Author) } // encode frames the author ahead of the encoded message, so the ring stays a // flat []string and both halves are recoverable. The author leads rather than // trails because a trailing field cannot be found by scanning backwards: a // message body ending in digits is indistinguishable from a length prefix. func encode(e Entry) string { return msg.Frame(e.Author.String()) + msg.Frame(e.Session.String()) + e.Msg.Encode() } func decode(s string) (Entry, bool) { author, rest, ok := msg.Unframe(s) if !ok { return Entry{}, false } session, rest, ok := msg.Unframe(rest) if !ok { return Entry{}, false } m, ok := msg.Decode(rest) if !ok { return Entry{}, false } return Entry{Msg: m, Author: address(author), Session: address(session)}, true } func decodeAll(raw []string) []Entry { out := make([]Entry, 0, len(raw)) for _, s := range raw { if e, ok := decode(s); ok { out = append(out, e) } } return out }
  8. #8relay_test.gno
  9. #9package relay import ( "strings" "testing" "gno.land/p/moul/agents/msg/v0" "gno.land/p/moul/x/daily/ringbuffer/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) var ( alice = testutils.TestAddress("alice") bob = testutils.TestAddress("bob") ) // reset puts the realm back to its init() state. Realm globals persist for the // whole test binary and examples run after every Test, so every test that // writes starts here and ExampleRender builds its own state. func reset() { log = ringbuffer.New(Slots) seq = 0 seen = 0 } func TestPostAssignsSeqAndAuthor(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) got := Post(cross(cur), "agent-a", "build", "note", "pr/1", "first") uassert.Equal(t, uint64(1), got) uassert.Equal(t, 1, Len()) uassert.Equal(t, 1, Seen()) e := Latest(0)[0] uassert.Equal(t, uint64(1), e.Msg.Seq) uassert.Equal(t, "agent-a", e.Msg.From) uassert.Equal(t, alice.String(), e.Author.String()) } func TestSeqIsMonotonicAcrossAuthors(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) Post(cross(cur), "agent-a", "build", "", "", "one") testing.SetRealm(testing.NewUserRealm(bob)) Post(cross(cur), "agent-b", "build", "", "", "two") all := Latest(0) // newest first uassert.Equal(t, uint64(2), all[0].Msg.Seq) uassert.Equal(t, bob.String(), all[0].Author.String()) uassert.Equal(t, uint64(1), all[1].Msg.Seq) uassert.Equal(t, alice.String(), all[1].Author.String()) } func TestRingDropsTheOldestAndSeenKeepsCounting(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) for i := 0; i < Slots+5; i++ { Post(cross(cur), "agent-a", "build", "", "", "body") } uassert.Equal(t, Slots, Len()) uassert.Equal(t, Slots+5, Seen()) // The five oldest fell off the back; the newest is the last one posted. all := Latest(0) uassert.Equal(t, uint64(Slots+5), all[0].Msg.Seq) uassert.Equal(t, uint64(6), all[len(all)-1].Msg.Seq) } func TestSinceAndTopicFilter(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) Post(cross(cur), "agent-a", "build", "", "", "b1") Post(cross(cur), "agent-a", "deploy", "", "", "d1") Post(cross(cur), "agent-a", "build", "", "", "b2") since := Since(1) // oldest first uassert.Equal(t, 2, len(since)) uassert.Equal(t, uint64(2), since[0].Msg.Seq) uassert.Equal(t, uint64(3), since[1].Msg.Seq) uassert.Equal(t, 0, len(Since(3))) build := Topic("build") // newest first uassert.Equal(t, 2, len(build)) uassert.Equal(t, "b2", build[0].Msg.Body) uassert.Equal(t, 0, len(Topic("nothing-here"))) } func TestLatestRespectsN(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) for i := 0; i < 5; i++ { Post(cross(cur), "agent-a", "build", "", "", "body") } uassert.Equal(t, 2, len(Latest(2))) uassert.Equal(t, 5, len(Latest(0))) uassert.Equal(t, 5, len(Latest(-1))) } func TestPostRejectsInvalidFields(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) uassert.AbortsWithMessage(t, cur, "empty topic", func() { Post(cross(cur), "agent-a", "", "", "", "body") }) uassert.AbortsWithMessage(t, cur, "topic must be lowercase a-z, 0-9, dash or dot", func() { Post(cross(cur), "agent-a", "Build", "", "", "body") }) uassert.AbortsWithMessage(t, cur, "empty body", func() { Post(cross(cur), "agent-a", "build", "", "", "") }) uassert.Equal(t, 0, Len()) uassert.Equal(t, 0, Seen()) } // TestBodySurvivesTheEncoding is the regression test for the framing: a body // that looks like the encoding, or ends in the digits a length prefix is made // of, must come back unchanged with its author still attached. func TestBodySurvivesTheEncoding(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) nasty := []string{ "ends in 40", "3:abc", "has\na newline", msg.Msg{Seq: 9, Height: 9, From: "x", Topic: "y", Kind: "z", Ref: "w", Body: "v"}.Encode(), "| breaks | a | table |", } for _, body := range nasty { Post(cross(cur), "agent-a", "build", "", "", body) } got := Latest(0) // newest first for i, body := range nasty { e := got[len(nasty)-1-i] uassert.Equal(t, body, e.Msg.Body) uassert.Equal(t, alice.String(), e.Author.String()) } } // TestRenderEscapesACallersString pins what guard-untrusted-render exists for: // a pipe in a body must not open a column in the rendered table. func TestRenderEscapesACallersString(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) Post(cross(cur), "agent-a", "build", "", "", "a | b") out := Render("") uassert.False(t, strings.Contains(out, "| a | b |"), "an unescaped pipe opened a column in the rendered table") } // TestRenderEscapesABodyExactlyOnce is the regression test for the v0 bug. // ui.Cell(ui.Excerpt(s)) escaped twice, so a hyphen reached mainnet as // backslash-backslash-backslash-hyphen. Over-escaping is not a security hole, // which is exactly why the pipe test above passed while this was broken: a // doubly escaped pipe is still safe and still wrong. func TestRenderEscapesABodyExactlyOnce(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) Post(cross(cur), "agent-a", "build", "", "", "evicting in-call") out := Render("") uassert.True(t, strings.Contains(out, `evicting in\-call`), "a hyphen in a short body should be escaped once") uassert.False(t, strings.Contains(out, `in\\-call`), "the body was escaped more than once") } // TestRenderEscapesALongBodyExactlyOnce covers the other branch of // excerptCell, where the body is cut before it is escaped. func TestRenderEscapesALongBodyExactlyOnce(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) long := "evicting in-call" for len(long) < 200 { long += " and-again" } Post(cross(cur), "agent-a", "build", "", "", long) out := Render("") uassert.True(t, strings.Contains(out, `evicting in\-call`), "a hyphen in a cut body should be escaped once") uassert.False(t, strings.Contains(out, `in\\-call`), "the cut body was escaped more than once") } // TestPostRecordsNoSessionForAMasterKey pins the empty-session branch. A test // signs as a plain user realm, never through a session, so isSession is false // and Session must stay empty rather than inherit the author. func TestPostRecordsNoSessionForAMasterKey(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) Post(cross(cur), "agent-a", "build", "", "", "body") e := Latest(0)[0] uassert.Equal(t, alice.String(), e.Author.String()) uassert.Equal(t, "", e.Session.String()) } // TestWireRoundTripsThroughAReader walks the wire format exactly as an // off-chain reader must: unframe one entry at a time, then unframe the author // and the session off the front, then decode the message. The bodies are the // ones that break a qeval parser. func TestWireRoundTripsThroughAReader(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) bodies := []string{ `a body with " string),( in it`, "ends in 40", "has\na newline", "| and | pipes |", } for _, b := range bodies { Post(cross(cur), "agent-a", "build", "k", "r", b) } rest := Wire(0) for i, want := range bodies { framed, tail, ok := msg.Unframe(rest) uassert.True(t, ok, "entry did not unframe") rest = tail author, r2, ok := msg.Unframe(framed) uassert.True(t, ok, "author did not unframe") session, r3, ok := msg.Unframe(r2) uassert.True(t, ok, "session did not unframe") m, ok := msg.Decode(r3) uassert.True(t, ok, "message did not decode") uassert.Equal(t, want, m.Body) uassert.Equal(t, uint64(i+1), m.Seq) uassert.Equal(t, alice.String(), author) uassert.Equal(t, "", session) } uassert.Equal(t, "", rest, "the wire had trailing bytes") } func TestWireIsEmptyWhenNothingIsNewer(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) Post(cross(cur), "agent-a", "build", "", "", "one") uassert.Equal(t, "", Wire(1)) uassert.True(t, Wire(0) != "", "Wire(0) should carry the only entry") }
  10. #10render_example_test.gno
  11. #11package relay import ( "gno.land/p/moul/agents/msg/v0" "gno.land/p/nt/testutils/v0" ) // seed builds a deterministic ring without crossing, so an example can pin the // output. Examples run after every Test and take no arguments, so they cannot // call Post; writing the encoded entries directly is the same bytes by a // shorter road, and the heights are fixed rather than read from the chain. func seed() { reset() entries := []Entry{ {Msg: msg.Msg{Seq: 1, Height: 500, From: "agent-a", Topic: "build", Kind: "note", Ref: "pr/1", Body: "ci is green"}, Author: testutils.TestAddress("alice")}, // Session set: two agents under one owner, told apart by the key that // signed rather than by the id they called themselves. {Msg: msg.Msg{Seq: 2, Height: 501, From: "agent-b", Topic: "deploy", Kind: "ack", Ref: "", Body: "picked it up"}, Author: testutils.TestAddress("alice"), Session: testutils.TestAddress("session-b")}, {Msg: msg.Msg{Seq: 3, Height: 502, From: "agent-a", Topic: "build", Kind: "note", Ref: "pr/2", Body: "needs a rebase"}, Author: testutils.TestAddress("alice")}, } for _, e := range entries { log.Push(encode(e)) seq = e.Msg.Seq seen++ } } // ExampleRender pins the empty realm, which is what a fresh deploy renders. func ExampleRender() { reset() print(Render("")) // Output: // # Agent Relay // // _A realm is a relay when it is the only shared state between two agents that never talk directly._ // // 0 of 64 slots used, 0 posted in total, 0 dropped off the back. // // *_Nothing on the relay yet._* } // ExampleRenderPopulated pins the index with messages on it. func ExampleRenderPopulated() { seed() print(Render("")) // Output: // # Agent Relay // // _A realm is a relay when it is the only shared state between two agents that never talk directly._ // // 3 of 64 slots used, 3 posted in total, 0 dropped off the back. // // | Seq | Height | From | Topic | Kind | Body | Signer | // | --- | --- | --- | --- | --- | --- | --- | // | 3 | 502 | agent\-a | build | note | needs a rebase | `g1v9kxjc…40gh` | // | 2 | 501 | agent\-b | deploy | ack | picked it up | `g1wdjhxu…cmjm` | // | 1 | 500 | agent\-a | build | note | ci is green | `g1v9kxjc…40gh` | } // ExampleRenderTopic pins the per-topic view. func ExampleRenderTopic() { seed() print(Render("build")) // Output: // # Agent Relay: build // // | Seq | Height | From | Topic | Kind | Body | Signer | // | --- | --- | --- | --- | --- | --- | --- | // | 3 | 502 | agent\-a | build | note | needs a rebase | `g1v9kxjc…40gh` | // | 1 | 500 | agent\-a | build | note | ci is green | `g1v9kxjc…40gh` | }

Result log

msg:0,success:true,log:,events:[]

← Back to block 480,549