Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

gnomem.gno

10.28 Kb · 325 lines
  1// Package gnomem is a shared, contestable memory for multiple agents.
  2//
  3// Most "agent memory" products store text and retrieve similar chunks.
  4// That works for one agent talking to itself. It breaks the moment several
  5// independent agents — possibly run by different people, on different
  6// models — must maintain a *shared* understanding of the same world and
  7// sometimes disagree about it.
  8//
  9// gnomem models memory not as text but as a graph of structured claims.
 10// Each claim is a (subject, predicate, object) triple with an author,
 11// evidence and a confidence. Other agents can support it, contest it, add
 12// evidence, supersede it with a better claim, or trigger adjudication. No
 13// agent can silently erase an inconvenient finding: claims can be
 14// superseded or retracted, but the history stays visible and ordered.
 15//
 16// The point of the demo is the structure, not access control: anyone may
 17// write. Gating *who* may write to which claim is exactly what the
 18// capability-wallet demo adds on top.
 19package gnomem
 20
 21import (
 22	"chain"
 23	"chain/runtime"
 24	"strings"
 25
 26	"gno.land/p/nt/ufmt/v0"
 27)
 28
 29// Status is a claim's position in the argument lifecycle.
 30type Status string
 31
 32const (
 33	StatusProposed   Status = "proposed"   // asserted, no endorsements yet
 34	StatusSupported  Status = "supported"  // has support, no live contest
 35	StatusContested  Status = "contested"  // at least one live contest
 36	StatusAccepted   Status = "accepted"   // adjudicated true (terminal)
 37	StatusRetracted  Status = "retracted"  // adjudicated false / withdrawn (terminal)
 38	StatusSuperseded Status = "superseded" // replaced by a better claim (terminal)
 39)
 40
 41// Endorsement is a support or contest signal from one agent.
 42type Endorsement struct {
 43	Agent      address
 44	Confidence uint8 // 0..100
 45	Note       string
 46	Height     int64
 47}
 48
 49// Evidence points at off-chain material backing (or refuting) a claim.
 50type Evidence struct {
 51	By     address
 52	Kind   string // "report", "exploit", "dataset", "citation", ...
 53	Hash   string // commitment to the off-chain artifact
 54	Height int64
 55}
 56
 57// Claim is one node in the shared memory graph.
 58type Claim struct {
 59	ID           uint64
 60	Subject      string
 61	Predicate    string
 62	Object       string
 63	Author       address
 64	Confidence   uint8
 65	Status       Status
 66	CreatedAt    int64
 67	Supports     []Endorsement
 68	Contests     []Endorsement
 69	Evidence     []Evidence
 70	SupersededBy uint64 // 0 if none
 71	Supersedes   uint64 // 0 if none
 72	Resolver     address
 73	ResolvedAt   int64
 74}
 75
 76var claims []*Claim // index 0 => ID 1
 77
 78// ProposeClaim asserts a new (subject, predicate, object) triple.
 79func ProposeClaim(cur realm, subject, predicate, object, evidenceHash string, confidence uint8) uint64 {
 80	assert(subject != "" && predicate != "" && object != "", "empty triple field")
 81	assertConfidence(confidence)
 82
 83	id := uint64(len(claims)) + 1
 84	c := &Claim{
 85		ID:         id,
 86		Subject:    subject,
 87		Predicate:  predicate,
 88		Object:     object,
 89		Author:     cur.Previous().Address(),
 90		Confidence: confidence,
 91		Status:     StatusProposed,
 92		CreatedAt:  runtime.ChainHeight(),
 93	}
 94	if evidenceHash != "" {
 95		c.Evidence = append(c.Evidence, Evidence{
 96			By: c.Author, Kind: "initial", Hash: evidenceHash, Height: c.CreatedAt,
 97		})
 98	}
 99	claims = append(claims, c)
100	chain.Emit("ClaimProposed", "id", ufmt.Sprintf("%d", id), "subject", subject, "predicate", predicate)
101	return id
102}
103
104// SupportClaim endorses a claim. Support never overrides a live contest —
105// a contested claim stays contested until adjudicated.
106func SupportClaim(cur realm, id uint64, confidence uint8, note string) {
107	c := mustOpen(id)
108	assertConfidence(confidence)
109	c.Supports = append(c.Supports, Endorsement{
110		Agent: cur.Previous().Address(), Confidence: confidence, Note: note, Height: runtime.ChainHeight(),
111	})
112	if c.Status == StatusProposed {
113		c.Status = StatusSupported
114	}
115	chain.Emit("ClaimSupported", "id", ufmt.Sprintf("%d", id))
116}
117
118// ContestClaim challenges a claim, optionally attaching refuting evidence.
119func ContestClaim(cur realm, id uint64, note, evidenceHash string) {
120	c := mustOpen(id)
121	agent := cur.Previous().Address()
122	h := runtime.ChainHeight()
123	c.Contests = append(c.Contests, Endorsement{
124		Agent: agent, Confidence: 0, Note: note, Height: h,
125	})
126	if evidenceHash != "" {
127		c.Evidence = append(c.Evidence, Evidence{By: agent, Kind: "refutation", Hash: evidenceHash, Height: h})
128	}
129	c.Status = StatusContested
130	chain.Emit("ClaimContested", "id", ufmt.Sprintf("%d", id))
131}
132
133// AddEvidence attaches supporting or contextual off-chain material.
134func AddEvidence(cur realm, id uint64, kind, hash string) {
135	c := mustOpen(id)
136	assert(hash != "", "empty evidence hash")
137	c.Evidence = append(c.Evidence, Evidence{
138		By: cur.Previous().Address(), Kind: kind, Hash: hash, Height: runtime.ChainHeight(),
139	})
140}
141
142// SupersedeClaim replaces an open claim with a new one. The old claim is
143// marked superseded (terminal) and linked to its replacement, so the
144// correction is visible rather than a silent overwrite.
145func SupersedeClaim(cur realm, oldID uint64, subject, predicate, object, evidenceHash string, confidence uint8) uint64 {
146	old := mustOpen(oldID)
147	newID := ProposeClaim(cur, subject, predicate, object, evidenceHash, confidence)
148	newC := claims[newID-1]
149	newC.Supersedes = oldID
150	old.Status = StatusSuperseded
151	old.SupersededBy = newID
152	chain.Emit("ClaimSuperseded", "old", ufmt.Sprintf("%d", oldID), "new", ufmt.Sprintf("%d", newID))
153	return newID
154}
155
156// ResolveClaim adjudicates an open claim as accepted (true) or retracted
157// (false/withdrawn). The resolver's address is recorded — resolution is
158// itself a provenance-bearing act, not an anonymous verdict.
159func ResolveClaim(cur realm, id uint64, accepted bool) {
160	c := mustOpen(id)
161	if accepted {
162		c.Status = StatusAccepted
163	} else {
164		c.Status = StatusRetracted
165	}
166	c.Resolver = cur.Previous().Address()
167	c.ResolvedAt = runtime.ChainHeight()
168	chain.Emit("ClaimResolved", "id", ufmt.Sprintf("%d", id), "status", string(c.Status))
169}
170
171// ---- read-only API ----
172
173// Get returns a copy of a claim by id.
174func Get(id uint64) Claim { return *mustGet(id) }
175
176// Count returns the number of claims in the graph.
177func Count() int { return len(claims) }
178
179// IsOpen reports whether a claim can still be supported/contested/resolved.
180func IsOpen(id uint64) bool { return isOpen(mustGet(id).Status) }
181
182// ---- internal ----
183
184func isOpen(s Status) bool {
185	return s == StatusProposed || s == StatusSupported || s == StatusContested
186}
187
188func mustGet(id uint64) *Claim {
189	assert(id >= 1 && id <= uint64(len(claims)), "unknown claim")
190	return claims[id-1]
191}
192
193func mustOpen(id uint64) *Claim {
194	c := mustGet(id)
195	assert(isOpen(c.Status), "claim is resolved (terminal): "+string(c.Status))
196	return c
197}
198
199func assertConfidence(c uint8) { assert(c <= 100, "confidence must be 0..100") }
200
201func assert(cond bool, msg string) {
202	if !cond {
203		panic(msg)
204	}
205}
206
207// Render shows the full graph, or one claim's argument tree at :<id>.
208func Render(path string) string {
209	if path == "" {
210		return renderIndex()
211	}
212	return renderClaim(parseID(path))
213}
214
215func renderIndex() string {
216	var sb strings.Builder
217	sb.WriteString("# GnoMem — Contested Shared Memory\n\n")
218	sb.WriteString(ufmt.Sprintf("A graph of %d structured claim(s) maintained by multiple agents.\n\n", len(claims)))
219	if len(claims) == 0 {
220		sb.WriteString("_No claims yet._\n")
221		return sb.String()
222	}
223	sb.WriteString("| # | Claim | Status | ✋ support | ⚔ contest |\n")
224	sb.WriteString("|---|---|---|---|---|\n")
225	for _, c := range claims {
226		sb.WriteString(ufmt.Sprintf("| [%d](/r/moul/agents/gnomem/v0:%d) | %s | %s | %d | %d |\n",
227			c.ID, c.ID, triple(c), statusBadge(c.Status), len(c.Supports), len(c.Contests)))
228	}
229	return sb.String()
230}
231
232func renderClaim(id uint64) string {
233	c := mustGet(id)
234	var sb strings.Builder
235	sb.WriteString(ufmt.Sprintf("# Claim #%d\n\n", c.ID))
236	sb.WriteString(ufmt.Sprintf("> **%s**\n\n", triple(c)))
237	sb.WriteString(ufmt.Sprintf("- **Status:** %s\n", statusBadge(c.Status)))
238	sb.WriteString(ufmt.Sprintf("- **Author:** %s\n", short(c.Author)))
239	sb.WriteString(ufmt.Sprintf("- **Author confidence:** %d/100\n", c.Confidence))
240	sb.WriteString(ufmt.Sprintf("- **Created at height:** %d\n", c.CreatedAt))
241	if c.Supersedes != 0 {
242		sb.WriteString(ufmt.Sprintf("- **Supersedes:** [#%d](/r/moul/agents/gnomem/v0:%d)\n", c.Supersedes, c.Supersedes))
243	}
244	if c.SupersededBy != 0 {
245		sb.WriteString(ufmt.Sprintf("- **Superseded by:** [#%d](/r/moul/agents/gnomem/v0:%d)\n", c.SupersededBy, c.SupersededBy))
246	}
247	if c.ResolvedAt != 0 {
248		sb.WriteString(ufmt.Sprintf("- **Resolved by:** %s at height %d\n", short(c.Resolver), c.ResolvedAt))
249	}
250	sb.WriteString("\n")
251
252	sb.WriteString(ufmt.Sprintf("## ✋ Support (%d)\n\n", len(c.Supports)))
253	renderEndorsements(&sb, c.Supports)
254	sb.WriteString(ufmt.Sprintf("## ⚔ Contest (%d)\n\n", len(c.Contests)))
255	renderEndorsements(&sb, c.Contests)
256
257	sb.WriteString(ufmt.Sprintf("## 📎 Evidence (%d)\n\n", len(c.Evidence)))
258	if len(c.Evidence) == 0 {
259		sb.WriteString("_None._\n")
260	} else {
261		sb.WriteString("| By | Kind | Commitment | Height |\n|---|---|---|---|\n")
262		for _, e := range c.Evidence {
263			sb.WriteString(ufmt.Sprintf("| %s | %s | `%s` | %d |\n", short(e.By), e.Kind, shortHash(e.Hash), e.Height))
264		}
265	}
266	return sb.String()
267}
268
269func renderEndorsements(sb *strings.Builder, es []Endorsement) {
270	if len(es) == 0 {
271		sb.WriteString("_None._\n\n")
272		return
273	}
274	sb.WriteString("| Agent | Confidence | Note | Height |\n|---|---|---|---|\n")
275	for _, e := range es {
276		sb.WriteString(ufmt.Sprintf("| %s | %d | %s | %d |\n", short(e.Agent), e.Confidence, e.Note, e.Height))
277	}
278	sb.WriteString("\n")
279}
280
281func triple(c *Claim) string {
282	return ufmt.Sprintf("%s — %s — %s", c.Subject, c.Predicate, c.Object)
283}
284
285func statusBadge(s Status) string {
286	switch s {
287	case StatusAccepted:
288		return "✅ accepted"
289	case StatusRetracted:
290		return "🚫 retracted"
291	case StatusSuperseded:
292		return "♻️ superseded"
293	case StatusContested:
294		return "⚔ contested"
295	case StatusSupported:
296		return "✋ supported"
297	default:
298		return "· proposed"
299	}
300}
301
302func parseID(s string) uint64 {
303	var n uint64
304	for i := 0; i < len(s); i++ {
305		c := s[i]
306		assert(c >= '0' && c <= '9', "invalid claim path: "+s)
307		n = n*10 + uint64(c-'0')
308	}
309	return n
310}
311
312func short(a address) string {
313	s := a.String()
314	if len(s) <= 12 {
315		return s
316	}
317	return s[:8] + "…" + s[len(s)-4:]
318}
319
320func shortHash(s string) string {
321	if len(s) <= 12 {
322		return s
323	}
324	return s[:6] + "…" + s[len(s)-4:]
325}