Documentation
¶
Overview ¶
Package note is the note-passing medium for multi-agent driftlessaf runs.
Different agents don't share memory, so they pass work to each other by persisting it: a step Puts a note body, and a downstream step Gets it back by the same coordinates. A note's identity is four coordinates — (Key, Run, Name, Author) — and its content-addressed primary key is Ref, a SHA-256 any agent can compute from the coordinates alone, so a note is addressable without a lookup or an opaque handle.
The store ¶
Store is the persistent read/write medium and the durable audit copy at once. This package ships the in-memory backend (NewMem) alongside the contract — it pulls in no cloud SDK, so it stays here; only a durable backend that needs a cloud SDK (GCS) lives in a sibling sub-package. Following "accept interfaces, return structs", a backend returns a concrete type and a consumer accepts Store — or a narrower interface of just the methods it uses.
Listing ¶
Store.List filters by any subset of coordinates and returns a bounded Page of coordinates; a caller pages by passing Page.Cursor back in Filter.Cursor until it is empty. Bodies are streamed on Store.Get, so a fan-in lists coordinates and reads only the bodies it needs.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNotExist = errors.New("note: not found")
ErrNotExist is returned by Get when no note exists for the given coordinates.
Functions ¶
func Ref ¶
Ref is the deterministic primary key for a note: the hex SHA-256 of its four coordinates in canonical (Key, Run, Name, Author) order. Any agent can compute it from the coordinates alone, so a note is addressable without a lookup or an opaque handle. There is deliberately no Ref field on Note — the key is always derived, never stored, so it can't disagree with the coordinates.
Each string coordinate is length-prefixed (an 8-byte big-endian length, then the bytes) rather than joined by a separator. Length-prefixing is injective for ALL inputs: field boundaries come from the lengths, not the content, so no coordinate value — even one containing the byte a separator would use — can be shifted across a boundary to forge another note's Ref. A bare separator (e.g. NUL) only disambiguates coordinates that never contain that byte; a coordinate free to hold it can collide, e.g. ("a","\x00b") vs ("a\x00","b").
Example ¶
ExampleRef shows the content-addressed key: any agent computes the same Ref from a note's coordinates, so it can address another agent's note without a lookup — and changing any coordinate yields a different key.
package main
import (
"fmt"
"chainguard.dev/driftlessaf/agents/note"
)
func main() {
ref := note.Ref("harden:widget", 1, "fix_plan", "claude")
same := note.Ref("harden:widget", 1, "fix_plan", "claude")
otherAuthor := note.Ref("harden:widget", 1, "fix_plan", "gemini")
fmt.Println("stable:", ref == same)
fmt.Println("per-author:", ref != otherAuthor)
}
Output: stable: true per-author: true
Types ¶
type Filter ¶
type Filter struct {
Key string // "" matches any key
Run *int // nil matches any run
Name string // "" matches any name
Author *string // nil matches any author; &"" selects the shared/synthesizer scope exactly
Limit int // 0 = the store's default cap; List never returns an unbounded slice
Cursor string // opaque continuation from a prior Page for the SAME filter; "" = the first page
}
Filter selects notes by any subset of coordinates; empty/nil fields are wildcards. Limit and Cursor bound and page a List.
type Mem ¶
type Mem struct {
// contains filtered or unexported fields
}
Mem is an in-memory Store for tests and single-process use. It is safe for concurrent use. The zero value is not usable; call NewMem.
Example ¶
ExampleMem passes notes through the in-memory store: two authors each Put their fix_plan for one key+run, a consumer Gets one back by its coordinates, and List gathers every author's fix_plan for that run.
package main
import (
"context"
"fmt"
"io"
"strings"
"chainguard.dev/driftlessaf/agents/note"
)
func main() {
ctx := context.Background()
store := note.NewMem()
run := 1
_ = store.Put(ctx, note.Note{Key: "harden:widget", Run: run, Name: "fix_plan", Author: "claude"}, strings.NewReader("plan A"))
_ = store.Put(ctx, note.Note{Key: "harden:widget", Run: run, Name: "fix_plan", Author: "gemini"}, strings.NewReader("plan B"))
_, rc, err := store.Get(ctx, "harden:widget", run, "fix_plan", "claude")
if err != nil {
panic(err)
}
defer rc.Close()
body, err := io.ReadAll(rc)
if err != nil {
panic(err)
}
fmt.Printf("claude fix_plan: %s\n", body)
// Gather every author's fix_plan for this key+run — the adversarial-review fan-in.
page, err := store.List(ctx, note.Filter{Key: "harden:widget", Run: &run, Name: "fix_plan"})
if err != nil {
panic(err)
}
fmt.Println("fix_plans in run:", len(page.Notes))
}
Output: claude fix_plan: plan A fix_plans in run: 2
func (*Mem) Get ¶
func (m *Mem) Get(_ context.Context, key string, run int, name, author string) (Note, io.ReadCloser, error)
Get returns the note's coordinates and a reader over a copy of its body, or ErrNotExist when absent. The reader needs no cleanup, but is a ReadCloser to satisfy Store so callers Close it uniformly across backends.
func (*Mem) List ¶
List returns a bounded, Ref-ordered Page of the coordinates matching f. It caps at Filter.Limit (or defaultListLimit) and sets Page.Cursor when more matches remain; pass that cursor back in Filter.Cursor for the next page.
type Note ¶
type Note struct {
Key string // workqueue key — the item the run is about (e.g. "harden:<skill>")
Run int // run number — the pipeline attempt for this key, 1-based
Name string // step/note name: "fix_plan", "critique", …
Author string // who produced it: a model, a synthesizer, or a human ("" for shared notes)
}
Note is the identity of one note passed between agents: the four coordinates that address it. Because agents don't share memory, a note is passed by being persisted — a step Puts its body under these coordinates and a downstream step Gets it back by the same ones. The body is streamed separately (see Store), not carried on the struct, so a note of any size moves without buffering it in full.
func (Note) Validate ¶
Validate reports whether n has the minimum coordinates a store will accept. It is the single gate every backend's Put calls, so "what is a storable note" is defined once rather than drifting per backend. Author is optional ("" is the shared/synthesizer scope); the body may be empty.
type Store ¶
type Store interface {
// Put stores body under Ref(n.Key, n.Run, n.Name, n.Author), overwriting any
// existing note with those coordinates. It reads body to completion (a nil
// body is empty) and returns the error from n.Validate when n is not a
// storable note.
Put(ctx context.Context, n Note, body io.Reader) error
// Get returns the note's coordinates and an open reader over its body, or
// ErrNotExist when no note is stored under the coordinates. The caller must
// Close the returned reader.
Get(ctx context.Context, key string, run int, name, author string) (Note, io.ReadCloser, error)
// List returns a bounded Page of the coordinates matching f, in a stable
// order; bodies are fetched separately with Get. When Page.Cursor is non-empty
// there are more matches: pass it back in Filter.Cursor for the next page. A
// cursor is only valid replayed against the same Filter (its other fields
// unchanged); pairing it with a different filter is undefined.
List(ctx context.Context, f Filter) (Page, error)
}
Store is the note-passing medium: a persistent read/write store of note bodies keyed by (Key, Run, Name, Author). It is the passing medium and the durable copy at once. A backend returns a concrete type implementing Store (see NewMem); a consumer accepts Store, or a narrower interface of just the methods it uses.