gnomem.gno
10.38 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/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/gnomem:%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/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/gnomem:%d)\n", c.Supersedes, c.Supersedes))
243 }
244 if c.SupersededBy != 0 {
245 sb.WriteString(ufmt.Sprintf("- **Superseded by:** [#%d](/r/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/gnomem:%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}