Documentation
¶
Overview ¶
Package thread gives weft sessions: a conversation as an append-only tree of entries, durable through a Storage backend, with branching, compaction (ADR 0020) and approvals (ADR 0021) all built on the same tree. ADR 0011 is the design record; this package owns the format and the Storage contract, and the backends live beside it (jsonl on disk, Memory in process, sqlite later).
A session is a file you can read with jq and back up with cp: one header line ({"weft":1,"type":"session",…} — the envelope integer every weft wire document carries, ADR 0005), then one line per entry in append order. Nothing is ever rewritten or deleted in place; the leaf is the entry the next one attaches to, and the model's context is built by walking leaf → root (Session.Context, v0.1).
The entry kinds are sealed, like weft.Event: message, turn, compaction, branch_summary, leaf, label, info, custom, custom_message — one wire discriminator each, restored by UnmarshalEntry; a message entry embeds the core's message wire (ADR 0001) verbatim. The format grows without breaking readers: an entry kind added after format 1 carries "v":N, the minimum reader version, and a reader that cannot decode an entry fails with ErrNewerFormat instead of skipping it. Golden files in testdata pin every format version, and every release reads them all back.
Storage is the backend interface — Create, Append, Load, List, Delete — and threadtest is the conformance table every backend runs before it may call itself one. Entries and headers never alias storage internals: what Load returns is the caller's to keep.
v0.1 completes the layer above this format (Session, Send/Turn, branching, compaction); the current surface is the format, the Storage contract, and the Session tree they stand on — Create/Open, the context walk, entries, leaf and path, labels, info, custom state, branching. Send and compaction follow in the next steps of v0.1.
Index ¶
- Constants
- Variables
- func Delete(ctx context.Context, st Storage, id string) error
- func NewEntryID() string
- func NewSessionID() string
- func ValidID(id string) bool
- type ApprovalAuditEntry
- type ApprovalDecisionEntry
- type ApprovalRequestEntry
- type Approver
- type Arg
- type BranchOption
- type BranchSummaryEntry
- type CompactOption
- type Compaction
- type CompactionEntry
- type Compactor
- type CorruptError
- type CustomEntry
- type CustomMessageEntry
- type Decision
- type Entry
- type Estimator
- type Flusher
- type Grant
- type GrantEntry
- type GrantRevokedEntry
- type GrantStore
- type Header
- type InfoEntry
- type Key
- type Keyring
- type LabelEntry
- type LeafEntry
- type Lineage
- type LoadReport
- type MessageEntry
- type NativeCompactor
- type OpenConfig
- type OpenOption
- type Outcome
- type Page
- type ParentRef
- type Policy
- type PoolReceiptEntry
- type Preparation
- type Query
- type QueuedSteer
- type Reason
- type ReceiptEntry
- type Request
- type SendOption
- type Session
- func (s *Session) AppendApprovalRequests(ctx context.Context, reqs ...ApprovalRequestEntry) ([]ApprovalRequestEntry, error)
- func (s *Session) AppendPoolReceipt(ctx context.Context, e PoolReceiptEntry) (PoolReceiptEntry, error)
- func (s *Session) ApplyCompaction(ctx context.Context, c *Compaction) error
- func (s *Session) Audit() []Entry
- func (s *Session) Branch(ctx context.Context, entryID string, opts ...BranchOption) error
- func (s *Session) ClearQueue(ctx context.Context) (int, error)
- func (s *Session) Compact(ctx context.Context, opts ...CompactOption) error
- func (s *Session) Context() []weft.Message
- func (s *Session) Custom(ctx context.Context, kind string, data json.RawMessage) error
- func (s *Session) CustomMessage(ctx context.Context, kind string, msg weft.Message) error
- func (s *Session) Decide(ctx context.Context, ds ...Decision) (*Turn, error)
- func (s *Session) DecideSigned(ctx context.Context, sd SignedDecision) (*Turn, error)
- func (s *Session) Entries() []Entry
- func (s *Session) Fork(ctx context.Context, entryID string, opts ...SessionOption) (*Session, error)
- func (s *Session) Grant(ctx context.Context, g Grant) error
- func (s *Session) ID() string
- func (s *Session) Label(ctx context.Context, entryID, name string) error
- func (s *Session) Leaf() string
- func (s *Session) Lineage() Lineage
- func (s *Session) Meta() map[string]string
- func (s *Session) Path(entryID string) ([]Entry, error)
- func (s *Session) Pending() []Request
- func (s *Session) Pin(ctx context.Context, entryID string) error
- func (s *Session) PreviewCompaction(ctx context.Context, opts ...CompactOption) (*Compaction, error)
- func (s *Session) Queue() []QueuedSteer
- func (s *Session) Request(callID string) (Request, error)
- func (s *Session) Resume(ctx context.Context) (*Turn, error)
- func (s *Session) Revoke(ctx context.Context, grantID string) error
- func (s *Session) Send(ctx context.Context, msg weft.Message, opts ...SendOption) (*Turn, error)
- func (s *Session) SetInfo(ctx context.Context, title string, meta map[string]string) error
- func (s *Session) Storage() Storage
- func (s *Session) Title() string
- func (s *Session) Uncompact(ctx context.Context) error
- func (s *Session) Usage() Usage
- type SessionOption
- func AfterCompact(fn func(ctx context.Context, e CompactionEntry)) SessionOption
- func ApproverTimeout(d time.Duration) SessionOption
- func AutoResume(on bool) SessionOption
- func BeforeCompact(fn func(ctx context.Context, p *Preparation) (Verdict, error)) SessionOption
- func BusyPolicy(p Policy) SessionOption
- func CheckSummary(fn func(Summary) error) SessionOption
- func ClearOldToolResults(keepLast int) SessionOption
- func CompactFailed(fn func(ctx context.Context, r Reason, err error)) SessionOption
- func ContextWindow(n int64) SessionOption
- func Disabled() SessionOption
- func IDs(id func() string) SessionOption
- func KeepRecent(n int64) SessionOption
- func MaxPerSession(n int) SessionOption
- func MinTurnsBetween(n int) SessionOption
- func ModelReserves(reserves map[weft.ModelInfo]int64) SessionOption
- func ModelWindows(windows map[weft.ModelInfo]int64) SessionOption
- func OnRequest(fn func(Request)) SessionOption
- func PreferNative() SessionOption
- func Quorum(n int) SessionOption
- func ReRunOnOverflow(on bool) SessionOption
- func RequestExpiry(d time.Duration) SessionOption
- func RequireSigned() SessionOption
- func Reserve(n int64) SessionOption
- func SummaryFocus(focus string) SessionOption
- func SummaryMaxTokens(n int) SessionOption
- func SummaryModel(m weft.Model) SessionOption
- func SummaryPrompt(tmpl string) SessionOption
- func TriggerFunc(fn func(TriggerInput) bool) SessionOption
- func WithApprover(a Approver) SessionOption
- func WithCompactor(c Compactor) SessionOption
- func WithEstimator(est Estimator) SessionOption
- func WithGrantStore(gs GrantStore) SessionOption
- func WithKeyring(r *Keyring) SessionOption
- func WithLineage(parentSession, call string) SessionOption
- func WithMeta(meta map[string]string) SessionOption
- func WithSummarizer(s Summarizer) SessionOption
- func WithTrimmer(t Trimmer) SessionOption
- type SharedGrant
- type SignedDecision
- type Storage
- type Summarizer
- type Summary
- type SummaryInput
- type TriggerInput
- type TrimReport
- type Trimmer
- type Turn
- type TurnEntry
- type Usage
- type Verdict
- type Watcher
Examples ¶
Constants ¶
const ( StepGrant = "grant" // step 2.2: a matching live grant StepApprover = "approver" // the live chain step, consulted and bounded StepPark = "park" // the request persisted, the turn ended pending StepExpiry = "expiry" // an expired request denied on resume StepResume = "resume" // the parked boundary resumed under its decisions )
The audit entry's steps (ADR 0021 §2): every step the decision chain takes leaves an approval_audit entry naming its step. Grants join in step 2.2.
const ( // ReceiptQueued marks acceptance: the message is held for the // running turn's steering drain. ReceiptQueued = "queued" // ReceiptDelivered marks a message the running run drained: it is // an ordinary transcript message of that run, named by RunID. ReceiptDelivered = "delivered" // ReceiptDeferred marks a message that became a follow-up turn // (Turn names its receipt): a StopWhen end, an open approval // boundary, or the run ended before the drain. ReceiptDeferred = "deferred" // ReceiptDropped marks a message removed by ClearQueue before // delivery: it never reaches the model. ReceiptDropped = "dropped" )
Receipt statuses — the wire values, pinned by the format-3 goldens.
const ( // PoolAccepted marks the delegation recorded and queued for a slot. PoolAccepted = "accepted" // PoolRunning marks the slot acquired and the child session's turn // started — the wait between acceptance and running is the pool's // queue, visible. PoolRunning = "running" // PoolDone marks a child that ran to its intended end; Stop is its // answer, Usage its total cost. PoolDone = "done" // PoolFailed marks a child whose run failed; Stop is the cause. PoolFailed = "failed" // PoolCanceled marks a child canceled by an explicit Cancel (or the // pool's Close) — never by the submitting turn's own end, which an // async child survives by design (ADR 0022 D4). PoolCanceled = "canceled" // PoolCapped marks a child that died on a budget — ErrMaxSteps or // ErrUsageLimit (DeerFlow's token-capped/turn-capped collapsed; the // stop reason distinguishes them). PoolCapped = "capped" )
Pool receipt statuses — the wire values, pinned by the format-4 goldens. The machine is accepted → running → exactly one of done, failed, canceled, capped.
const FormatVersion = 1
FormatVersion is the wire version of every document the thread module writes (ADR 0011 §6) — the integer every weft wire document carries ("weft": 1, ADR 0005). It moves only for a layout change a reader cannot handle additively: a new entry kind, a new optional key, a new entry version never move it. Readers read every version forever; writers write the latest; a layout change ships its reader plus an explicit, atomic Migrate — never a silent rewrite on open.
Variables ¶
var ( // ErrNotImplemented named the pieces of the v0.1 design whose step // had not landed yet — SummarizeLeft until step 1.8 wrote branch // summaries (ADR 0020 §6). Every such step has since shipped and // nothing returns it today; it stays exported so code written // against the v0.1 shape — callers that matched it to degrade // gracefully — still compiles and matches. ErrNotImplemented = errors.New("thread: feature not implemented in this build") // ErrBusy is returned by Send under the Reject busy policy when the // session is already running a turn: the follow-up was not // accepted and nothing was written (ADR 0011 §4). ErrBusy = errors.New("thread: session is busy with another turn") // ErrNotFound is returned by Load, Append, and Delete for a session // id the storage does not hold. ErrNotFound = errors.New("thread: session not found") // ErrExists is returned by Create for a session id the storage // already holds — in this process, or in any other sharing the // backend. A session is never silently replaced (ADR 0011 §5). ErrExists = errors.New("thread: session already exists") // ErrLocked is returned by a backend that enforces the one-writer // rule (ADR 0011 §5) when a session is already held by another // writer — another process, or another Storage in this one. // Readers never lock: Load and List always work. ErrLocked = errors.New("thread: session is locked by another writer") // ErrCorrupt wraps the failures a backend reports for stored data // it cannot decode: a malformed line that is not a torn tail (a // torn final line is a crash, dropped and reported through Load's // LoadReport; data from a newer weft is ErrNewerFormat, never // skipped). Salvage, the jsonl open option, downgrades this to a // skip reported in the LoadReport. ErrCorrupt = errors.New("thread: stored session data is corrupt") // ErrNewerFormat wraps the decode failures UnmarshalEntry and // Header decoding report for data written by a newer weft than // this build: an entry kind this build does not know, an entry // carrying a "v" above the version this build reads of its kind, // or a session header whose envelope is ahead of FormatVersion // (ADR 0011 §5–§6). Loud over silent: a session must decode to // exactly what was written, and an older weft says so instead of // guessing. List, which reads headers only, still works — an older // reader sees a session whose entries are newer and Load names why // it cannot open it. The one thing List cannot show is a session // whose header itself is newer: the envelope gates the whole file, // so such a session is invisible to an older List — visible again // as soon as its directory is read by a weft that knows the format. ErrNewerFormat = errors.New("thread: session format newer than this build") // ErrNotPending is returned by Decide for a decision addressing a // call that is not pending — decided already, resumed already, or // never parked — and by Resume with no open boundary. The core's // rule (ADR 0007: a decision names a pending call) made strict and // raised before any entry lands or any run starts: a session never // records a decision it cannot apply. ErrNotPending = errors.New("thread: call is not pending") // The signed-decision failures (ADR 0021 §3), each its own error // because each is its own operational answer: the key the ring // does not hold, the signature that does not verify, the challenge // that lapsed, the nonce that was answered already, and the // arguments that changed under the request. All are fail-closed: // nothing is recorded until every check passes. ErrUnknownKey = errors.New("thread: signing key not in the keyring") ErrBadSignature = errors.New("thread: decision signature does not verify") ErrExpired = errors.New("thread: signing challenge expired") ErrReplay = errors.New("thread: decision signature replayed") ErrArgsChanged = errors.New("thread: request arguments changed under the signature") // ErrSignatureRequired is returned by Decide on a session opened // with RequireSigned: the unsigned door is closed, and only // DecideSigned records caller-held decisions (ADR 0021 §3). ErrSignatureRequired = errors.New("thread: this session requires signed decisions") )
var Cancel = Verdict{/* contains filtered or unexported fields */}
Cancel stops it: nothing is written.
var Proceed = Verdict{}
Proceed runs the compaction as computed.
Functions ¶
func Delete ¶
Delete removes a session and its entries from st. A Session value already loaded keeps its in-memory tree, but its next append fails with ErrNotFound: history is removed with the session, never rewritten (ADR 0011 §5).
func NewEntryID ¶
func NewEntryID() string
NewEntryID returns a new entry id: "e_" plus 26 time-sortable random characters (see NewSessionID).
func NewSessionID ¶
func NewSessionID() string
NewSessionID returns a new session id: "s_" plus 26 characters — a millisecond-precision timestamp then random bits, Crockford base32, the ULID shape — so ids sort with the sessions they name. Sorting is to the millisecond only: ids born in the same millisecond order randomly among themselves, and a session's order of record is its entry order, never id order.
Example ¶
NewSessionID and NewEntryID mint time-sortable ids: "s_" and "e_" plus 26 characters, so a directory of sessions lists in creation order and a session's entries debug readably.
package main
import (
"fmt"
"github.com/weftgo/weft/thread"
)
func main() {
fmt.Println(len(thread.NewSessionID()), len(thread.NewEntryID()))
}
Output: 28 28
func ValidID ¶
ValidID reports whether id is safe as a session or entry id: 1 to 128 characters of letters, digits, '_' or '-', so an id is always exactly one path component — never a separator, a traversal, or a dotfile (ADR 0011 §5: backends vet the ids they are given). weft's own ids satisfy it by construction; the ids a caller supplies must stay inside it too.
Types ¶
type ApprovalAuditEntry ¶
type ApprovalAuditEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
CallID string `json:"call_id,omitempty"`
Step string `json:"step"`
Outcome string `json:"outcome,omitempty"`
Detail string `json:"detail,omitempty"`
RunID string `json:"run_id,omitempty"`
}
ApprovalAuditEntry is the chain's own trail (ADR 0021 §2): every step the decision chain takes over a call — the Approver consulted (decided, declined, timed out), the park, an expiry denial, a resume — leaves one of these, including automatic approvals, so s.Audit() can tell the whole story from the file alone. Step and Outcome name the step and how it ended; Detail carries anything beyond that. It never enters the model's context.
func (ApprovalAuditEntry) MarshalJSON ¶
func (e ApprovalAuditEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator and "v":2.
type ApprovalDecisionEntry ¶
type ApprovalDecisionEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
CallID string `json:"call_id"`
Outcome Outcome `json:"outcome"`
Reason string `json:"reason,omitempty"`
Content string `json:"content,omitempty"`
Who string `json:"who,omitempty"`
Via string `json:"via,omitempty"`
RunID string `json:"run_id,omitempty"`
// Nonce and KeyID are the signed-decision replay guard's record
// (ADR 0021 §3): a decision that arrived signed carries the nonce
// it answered and the key that vouched for it, so a replayed
// signature is detectable from the file alone — across restarts.
// Empty on the in-process paths, which mint no challenge.
Nonce string `json:"nonce,omitempty"`
KeyID string `json:"key_id,omitempty"`
}
ApprovalDecisionEntry is one decision over a parked call (ADR 0021 §1): approve, deny with a reason, or resolve with content computed outside the process (resolve_error marks it an error). It records Who decided, When (the entry's Created), Via which channel — "user" for a plain Decide, "approver" for the live chain step, "expiry" for an expired request's automatic denial — and the run the decided request belonged to. It never enters the model's context; the model sees the decision only through the result the resumed run produces.
func (ApprovalDecisionEntry) MarshalJSON ¶
func (e ApprovalDecisionEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator and "v":2.
type ApprovalRequestEntry ¶
type ApprovalRequestEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
CallID string `json:"call_id"`
Tool string `json:"tool"`
Args json.RawMessage `json:"args,omitempty"`
ArgsSHA256 string `json:"args_sha256"`
RunID string `json:"run_id"`
Reason string `json:"reason,omitempty"`
Expiry time.Time `json:"expiry,omitzero"`
// Child names the delegated child session this request mirrors,
// and Wrapper the parent-side delegating call it parks under (ADR
// 0022 §7): a pool child's parked call is requested in its own
// session and mirrored here so the parent's Pending surfaces it
// with its lineage. Both empty on an ordinary request.
Child string `json:"child,omitempty"`
Wrapper string `json:"wrapper,omitempty"`
}
ApprovalRequestEntry is a parked call made durable (ADR 0021 §1): a call the core's approval boundary left unexecuted, recorded with the arguments and their SHA-256 so a decision can name exactly what it decided, the run that parked it, why it parked, and an optional expiry — a request past its expiry is denied with the stated reason on the next resume (ADR 0021 §5). It is written in the same Append as the turn that parked it, so no window exists where the turn is durable and the request is not. It never enters the model's context; the pending call itself stays unresolved in the transcript until a decision resolves it.
func (ApprovalRequestEntry) MarshalJSON ¶
func (e ApprovalRequestEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator and "v":2.
type Approver ¶
Approver is the decision chain's live step (ADR 0021 §2): the "ask now" path for a terminal or an already-connected UI. It returns the Decision and true when it decided, or false to decline (a pipe, no TTY). It is never given unbounded time: the session consults it under ApproverTimeout, and a timeout reads as a decline. It must not call back into the same Session — the chain consults it outside the session lock, but the turn is still running.
type Arg ¶
type Arg struct {
// Pointer is the RFC 6901 JSON pointer to the argument: "/order_id",
// "/file/path", "/command".
Pointer string `json:"pointer"`
// Equals is the raw JSON the value must equal (ArgEquals).
Equals json.RawMessage `json:"equals,omitempty"`
// Prefix is the string prefix the value must have (ArgPrefix).
Prefix string `json:"prefix,omitempty"`
// Glob is the glob the string value must match (ArgGlob) — *
// spans separators, a command is not a path.
Glob string `json:"glob,omitempty"`
}
Arg is one argument predicate (ADR 0021 §4): a JSON pointer into the call's arguments and the test the value there must pass — equality with ArgEquals, a string prefix with ArgPrefix, a glob with ArgGlob. Build one with its constructor; a hand-made Arg with several tests set uses Equals first, then Prefix, then Glob.
func ArgEquals ¶
func ArgEquals(pointer string, value json.RawMessage) Arg
ArgEquals returns the predicate requiring args[pointer] to equal value as JSON: numbers compare numerically, strings and booleans exactly, arrays and objects deeply.
func ArgGlob ¶
ArgGlob returns the predicate requiring args[pointer] to be a string matching glob — the "go test …" command shape. A command is not a path: * spans separators, so "go test*" matches "go test ./..." (and anything after it on the line — anchor the glob's tail when that matters).
func ArgPrefix ¶
ArgPrefix returns the predicate requiring args[pointer] to be a string with prefix — the "path stays under the workspace" shape. A prefix is a plain string prefix, nothing more: end it with the separator or it matches siblings too ("/ws" matches "/ws-evil") — the prefix's one sharp edge, documented rather than hidden. An empty prefix matches nothing (an Arg with no test never matches, the same rule a hand-made Arg follows): the tool-wide grant is no Args at all, not an empty prefix.
type BranchOption ¶
type BranchOption interface {
// contains filtered or unexported methods
}
BranchOption configures one Branch call — the per-call layer over the session's own SessionOptions. v0.1 step 1.6 carries one: SummarizeLeft, wired in step 1.8.
func SummarizeLeft ¶
func SummarizeLeft() BranchOption
SummarizeLeft returns the BranchOption that summarizes the branch being left — back to the common ancestor of the branch and the new branch point — into a branch_summary entry the new branch's context carries in the abandoned branch's place (ADR 0020 §6). The summarizer runs under the session's compaction chain (SummaryModel or the session's own model, the skeleton prompt) with the lock released, then the navigation and the summary land as one atomic batch.
type BranchSummaryEntry ¶
type BranchSummaryEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
Summary string `json:"summary"`
FromEntry string `json:"from_entry"`
}
BranchSummaryEntry summarizes the branch a Session.Branch leaves behind (ADR 0020 §6): Summary is the text and FromEntry is the entry the abandoned branch grew from, so the context shows the summary in place of that branch's messages. Branch summaries share no cache prefix with the main line; that cost is documented, not hidden.
func (BranchSummaryEntry) MarshalJSON ¶
func (e BranchSummaryEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator.
type CompactOption ¶
type CompactOption interface {
// contains filtered or unexported methods
}
CompactOption configures one compaction call.
func Instructions ¶
func Instructions(text string) CompactOption
Instructions returns the CompactOption appending per-call instructions to this compaction's summary prompt: s.Compact(ctx, thread.Instructions("focus on the API design")).
type Compaction ¶
type Compaction struct {
// Summary is the summarizer's text, shown in the context behind
// the fixed marker from the entry's first kept entry onward.
Summary string
// FirstKept is the id of the first entry the context still shows
// raw after this compaction.
FirstKept string
// TokensBefore is the estimated size of the whole context at
// compaction time — the number the compaction was judged against.
TokensBefore int64
// Reason is why it ran: manual here, threshold for the trigger,
// from_hook and trim in step 1.9, overflow in v0.3.
Reason Reason
// SummarizerUsage and SummarizerModel record what the summary
// cost and which model made it — the cost ledger's inputs.
SummarizerUsage weft.Usage
SummarizerModel weft.ModelInfo
// FilesRead lists the file URLs the summarized range carried,
// sorted and deduplicated; FilesModified fills in when the sandbox
// write log lands (v0.6, ADR 0023) — the lists are the summary's
// memory across iterative compactions.
FilesRead []string
FilesModified []string
// Pinned lists the pinned entry ids this compaction kept raw; the
// Pin call arrives in step 1.9 and the list is its record.
Pinned []string
// RangeHash is the SHA-256 of the serialized range the summarizer
// was fed — the audit that a summary summarizes exactly this.
RangeHash string
}
Compaction is one computed compaction: the plan PreviewCompaction returns, ApplyCompaction writes as a compaction entry, and Compact does both. It carries everything the entry records (ADR 0020 §1) — nothing else: applying writes exactly these fields, nothing is recomputed.
type CompactionEntry ¶
type CompactionEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
Summary string `json:"summary,omitempty"`
FirstKept string `json:"first_kept"`
TokensBefore int64 `json:"tokens_before"`
Reason Reason `json:"reason,omitempty"`
// SummarizerUsage and SummarizerModel name what the summary cost
// and which model made it (the cost ledger, ADR 0020 §4) — absent
// on a trim, which summarizes nothing.
SummarizerUsage weft.Usage `json:"summarizer_usage,omitzero"`
SummarizerModel weft.ModelInfo `json:"summarizer_model,omitzero"`
FilesRead []string `json:"files_read,omitempty"`
FilesModified []string `json:"files_modified,omitempty"`
Pinned []string `json:"pinned,omitempty"`
RangeHash string `json:"range_hash,omitempty"`
}
CompactionEntry records one compaction (ADR 0020 §1): the summary text, the id of the first entry kept raw after it, the token count before, the reason it ran, the summarizer's cost and identity, the details (files read, files modified, pinned entries kept through), and a hash of the summarized range. Nothing is deleted — the summarized entries stay in the file, and the context at a leaf is the latest compaction's summary on the path plus the entries from its first kept id onward. A trim that needed no summary is the same entry with an empty Summary and Reason "trim".
func (CompactionEntry) MarshalJSON ¶
func (e CompactionEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator.
type Compactor ¶
type Compactor interface {
Compact(ctx context.Context, p Preparation) (*Compaction, error)
}
Compactor is the whole compaction algorithm, swapped in whole: it receives the Preparation and returns the Compaction to write. The default cuts, serializes, summarizes and hashes; a replacement may ignore the suggested cut and keep any FirstKept it can name.
type CorruptError ¶
type CorruptError struct {
Session string
Line int // 1-based; 0 when the failure is not one line's
Err error
}
CorruptError is the typed shape ErrCorrupt takes when the failure belongs to one line of a stored session (ADR 0011 §5: "ErrCorrupt naming the line"): it carries the session and the 1-based line number — the header is line 1 — so a caller, a log, or a UI can point at the place. Match the class with errors.Is(err, ErrCorrupt) and take the place with errors.As; backends construct it directly through their own decode-failure paths.
func (*CorruptError) Error ¶
func (e *CorruptError) Error() string
func (*CorruptError) Unwrap ¶
func (e *CorruptError) Unwrap() error
Unwrap makes errors.Is(err, ErrCorrupt) true — the class — while Err keeps the cause for a caller who wants it.
type CustomEntry ¶
type CustomEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
Kind string `json:"kind"`
Data json.RawMessage `json:"data,omitempty"`
}
CustomEntry carries application state: a caller-chosen Kind and opaque JSON Data. It survives every compaction (ADR 0020 §4) and never enters the model's context. A nil Data writes no "data" key — omitempty keeps nil and absent the same value both ways, so a round trip through the wire never turns "no data" into "null".
func (CustomEntry) MarshalJSON ¶
func (e CustomEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator.
type CustomMessageEntry ¶
type CustomMessageEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
Kind string `json:"kind"`
Message weft.Message `json:"message"`
}
CustomMessageEntry carries an application message: a caller-chosen Kind and a weft.Message that is always in the model's context — how an application injects a note the model must see without attributing it to the user.
func (CustomMessageEntry) MarshalJSON ¶
func (e CustomMessageEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator.
type Decision ¶
type Decision struct {
// CallID is the pending call the decision addresses.
CallID string
// Kind is the outcome: approve, deny, resolve or resolve_error.
Kind Outcome
// Reason is the deny reason — the text the model sees after
// "DENIED: ".
Reason string
// Content is the resolve payload, verbatim; resolve_error marks it
// an error.
Content string
// Who decided, for the audit trail — and the quorum's identity:
// distinct Who values are distinct approvers (ADR 0021 §5).
Who string
// Via which channel the decision arrived.
Via string
// Always approves and grants the same thing for the future: the
// tool plus the call's exact arguments become a session grant
// ("approve and always allow this", ADR 0021 §4), recorded with
// the decision in one append. Build it with ApproveAlways.
Always bool
}
Decision is one call's outcome, the value Decide records (ADR 0021 §1). Build one with Approve, Deny, Resolve or ResolveError; Who and Via are the caller's to fill — Who names the deciding identity for the audit trail, Via the channel ("cli", "webhook", …); Via is forced to "approver" and "expiry" on the paths that own it.
func Approve ¶
Approve returns a Decision that resumes the call: the run executes it through the ordinary tool chain with Call.Approved set (ADR 0007).
func ApproveAlways ¶
ApproveAlways returns a Decision that approves the call and grants the same thing for the future (ADR 0021 §4's "approve and always allow this command"): the tool plus the call's exact arguments become a session grant, recorded with the decision in one append — so the next such call never parks. A richer grant (a command glob, a path prefix) is s.Grant's to make.
func Deny ¶
Deny returns a Decision that resolves the call without running it: the model sees "DENIED: <reason>" and the loop continues.
func Resolve ¶
Resolve returns a Decision that resumes the call with a result computed outside the process — the human-as-tool-executor shape. The handler never runs; the content becomes the call's result verbatim.
func ResolveError ¶
ResolveError is Resolve with the result marked as an error.
type Entry ¶
type Entry interface {
// contains filtered or unexported methods
}
Entry is the sealed set of session entry kinds (ADR 0011 §2): a session is an append-only tree of these, and the leaf is the entry the next one attaches to. External types cannot join, so switches over entries stay exhaustively lintable; weft adds kinds additively under the "v" rule below. On the wire every entry carries a "type" discriminator and UnmarshalEntry restores it — the same rule and the same compatibility contract as the core's events and message parts (ADR 0001, ADR 0004).
Every entry, of every kind, carries an ID, a ParentID (empty for the session's root entry) and a Created time. Whether a kind's content enters the model's context is part of the kind's contract: message and custom_message do; the bookkeeping kinds never do; compaction and branch_summary contribute their summary text.
func UnmarshalEntry ¶
UnmarshalEntry decodes one entry line, dispatching on its "type" discriminator. The loud rules of the format (ADR 0011 §5–§6): an entry kind this build does not know fails with ErrNewerFormat — it was written by a newer weft and is never silently skipped; a known kind carrying a "v" above the version this build reads of it fails the same way; every other unknown key on the line is additive and ignored.
type Estimator ¶
Estimator estimates the token weight of messages — the trigger's delta and the cut's walk. The default is a quarter of the wire bytes; a provider-aware implementation can do better.
type Flusher ¶
Flusher is the optional Storage capability that completes buffered durability work — the small-interface rule (ADR 0011 §5: capabilities are discovered by type assertion, so Storage never grows). With FsyncOnFlush, an append writes without fsyncing and Flush makes the session durable; with the default policy Flush is a no-op that still returns any error the storage holds for the session. Flushing a session the storage does not hold fails with ErrNotFound; a session another writer holds but this one never buffered flushes nothing.
type Grant ¶
type Grant struct {
// Tool is the tool name the grant matches, exactly.
Tool string `json:"tool"`
// Args are the argument predicates; every one must match. Empty
// matches any arguments, which is the tool-wide grant Crush
// taught the field to want more than (ADR 0021 §4's rejected).
Args []Arg `json:"args,omitempty"`
// Deny makes the grant a standing refusal: the chain denies the
// call at once, with Reason the model sees.
Deny bool `json:"deny,omitempty"`
// Reason is the refusal's text on a deny-grant — the model-visible
// bytes, "denied by grant" when empty — or the record's note on an
// approval grant.
Reason string `json:"reason,omitempty"`
// Expiry is when the grant lapses; zero means never. Expired
// strictly after, the requests' rule.
Expiry time.Time `json:"expiry,omitzero"`
// MaxUses bounds how many times a session-scoped grant may match
// (its audit entries count the uses); 0 means unlimited. A shared
// grant's uses are the store's own business — the session cannot
// see other sessions' matches.
MaxUses int `json:"max_uses,omitempty"`
}
A Grant approves — or, with Deny, refuses — future calls without asking (ADR 0021 §4): one tool, and argument predicates every one of which the call's arguments must satisfy, so "allow run_command for go test …" is expressible, not just "allow run_command". Scope, expiry, uses and revocation are the grant's lifetime: a session-scoped grant is an entry (s.Grant), a shared grant lives behind the GrantStore interface; an expiry passes; MaxUses bounds the times a session-scoped grant may match (counted from its audit trail); s.Revoke ends one with an entry.
type GrantEntry ¶
type GrantEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
Grant
}
GrantEntry is a session-scoped grant made durable (ADR 0021 §4): a standing approval — or, with Deny, a standing refusal — for future calls of one tool whose arguments match every predicate. It is the decision chain's first step: a matching live grant decides at once, audited, including the automatic approval. Liveness is derived, never stored: a GrantRevokedEntry naming the grant ends it, an Expiry passes, or its MaxUses is reached — uses counted from the audit entries the chain writes when it matches. It never enters the model's context; the model sees a grant only through the result of the call it allowed or refused.
func (GrantEntry) MarshalJSON ¶
func (e GrantEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator and "v":2.
type GrantRevokedEntry ¶
type GrantRevokedEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
GrantID string `json:"grant_id"`
}
GrantRevokedEntry ends a grant (ADR 0021 §4): revocation is an append, never a rewrite — the grant entry stays, the walk reads the revocation after it, and the audit trail keeps both.
func (GrantRevokedEntry) MarshalJSON ¶
func (e GrantRevokedEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator and "v":2.
type GrantStore ¶
type GrantStore interface {
// Grants returns the store's live grants. The chain consults it
// once per parked call, before parking; an error reads as no
// match, with a warning through the agent's logger — a broken
// store never parks nothing silently, and never decides either.
Grants(ctx context.Context) ([]SharedGrant, error)
}
A GrantStore is the application-wide grant scope (ADR 0021 §4): grants that outlive any one session, consulted by every session handed the store. The store owns their lifetime — expiry, revocation, use counts — because only it can see every session's matches; the session's entry-scoped grants keep their own rules.
type Header ¶
type Header struct {
// Weft is the envelope integer. The zero value writes and means
// FormatVersion; Create rejects anything else, so a caller never
// has to know the number to construct a valid header.
Weft int `json:"weft"`
ID string `json:"id"`
Created time.Time `json:"created"`
Parent *ParentRef `json:"parent,omitempty"`
Lineage *Lineage `json:"lineage,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
Header is a session's first line: its identity, its creation time, its fork origin and caller metadata. A Fork names the session and entry it grew from in Parent (ADR 0011 §3), so the new session is self-contained but traceable; beyond that the header is immutable — title and metadata edits are info entries appended to the file, never rewrites of it.
Example ¶
A session's header is its first line: the weft envelope integer, the session's id and creation time, and — for a fork — where it came from (ADR 0011 §3, §6).
package main
import (
"encoding/json"
"fmt"
"time"
"github.com/weftgo/weft/thread"
)
func main() {
h := thread.Header{
ID: "s_01J8X9M2K7QW4R5N8T6V2B3C4Q",
Created: time.Date(2026, 9, 28, 12, 0, 0, 123456789, time.UTC),
Parent: &thread.ParentRef{Session: "s_01J8X9M2K7QW4R5N8T6V2B3C4D", Entry: "e_01J8X9M2K7QW4R5N8T6V2B3C4G"},
}
b, err := json.Marshal(h)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(b))
}
Output: {"type":"session","weft":1,"id":"s_01J8X9M2K7QW4R5N8T6V2B3C4Q","created":"2026-09-28T12:00:00.123456789Z","parent":{"session":"s_01J8X9M2K7QW4R5N8T6V2B3C4D","entry":"e_01J8X9M2K7QW4R5N8T6V2B3C4G"}}
func (Header) MarshalJSON ¶
MarshalJSON encodes the header with its "type":"session" discriminator. A zero Weft writes FormatVersion — the zero Header is a valid, current-format header.
func (*Header) UnmarshalJSON ¶
UnmarshalJSON decodes a session header and enforces the envelope rule (ADR 0011 §6): a header from a newer format fails with ErrNewerFormat instead of being read with this build's tags, and a line without the session shape was never a thread session's first line. Unknown keys are additive and ignored.
type InfoEntry ¶
type InfoEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
Title string `json:"title,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
InfoEntry edits the session's title and metadata as an append, never a rewrite: the current title is the last info entry's Title, and the current metadata is every info entry's Meta merged in order.
func (InfoEntry) MarshalJSON ¶
MarshalJSON encodes the entry with its "type" discriminator.
type Key ¶
Key is one key in a Keyring: an id, the secret bytes (any non-empty length; HMAC-SHA256 derives its key from them), and whether it is the ring's one active signing key. At most one Key in a ring may be active; a ring with none signs nothing (verify-only).
type Keyring ¶
type Keyring struct {
// contains filtered or unexported fields
}
A Keyring holds several HMAC keys by id; exactly one is active for signing and all of them verify (ADR 0021 §3): rotation without invalidating requests in flight — the new active key signs new challenges while the old keys vouch for decisions still arriving. Keys never touch the session file; only key ids do, so a leaked file cannot forge a decision. Construct one with NewKeyring and hand it to the sessions that verify signed decisions.
func NewKeyring ¶
NewKeyring builds a ring from keys: ids must be unique and secrets non-empty, and at most one key may be active. The ring is immutable from here — rotation is a new ring handed to new sessions, while the old ring keeps verifying the requests it minted.
type LabelEntry ¶
type LabelEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
Entry string `json:"entry"`
Name string `json:"name"`
}
LabelEntry names an entry — bookmarks, checkpoints, a UI's anchors. The label never enters the model's context.
func (LabelEntry) MarshalJSON ¶
func (e LabelEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator.
type LeafEntry ¶
type LeafEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
Entry string `json:"entry"`
}
LeafEntry moves the session's leaf to an existing entry — branch navigation without a rewrite (ADR 0011 §3). The next entry attaches to Entry, and the model's context is rebuilt from there.
func (LeafEntry) MarshalJSON ¶
MarshalJSON encodes the entry with its "type" discriminator.
type Lineage ¶ added in v0.5.0
type Lineage struct {
Session string `json:"parent_session"`
Call string `json:"parent_call_id,omitempty"`
}
Lineage names a pool child's origin (ADR 0022 §3): the parent session and, for a wrapped delegation, the call that delegated. It is a reference, not a copy — unlike Parent, a fork's self-contained path, the child's file holds only its own entries, and the link is how a nested approval finds the session that must resume the child.
type LoadReport ¶
type LoadReport struct {
// Torn is the 1-based line number of a torn final line dropped
// from the session file — a writer cut mid-write, so the bytes
// after the last complete newline are not an entry — or 0 when
// the file ended cleanly.
Torn int
// Skipped lists the 1-based line numbers of malformed lines the
// load skipped under Salvage (the jsonl open option), in file
// order. Without Salvage, a malformed line fails the load with
// ErrCorrupt instead.
Skipped []int
}
LoadReport names what a load had to drop or skip to return a session — a repair is never silent (ADR 0011 §5). A clean load returns a nil report.
type MessageEntry ¶
type MessageEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
Message weft.Message `json:"message"`
}
MessageEntry is one weft.Message in the transcript, embedded with the core's message wire (ADR 0001) verbatim. It is how conversation content is stored, and it is always in the model's context.
Example ¶
A session entry marshals with its "type" discriminator — one JSON line per entry is the on-disk format (ADR 0011 §2) — and UnmarshalEntry restores the sealed type.
package main
import (
"encoding/json"
"fmt"
"time"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
)
func main() {
e := thread.MessageEntry{
ID: "e_01J8X9M2K7QW4R5N8T6V2B3C4E",
Created: time.Date(2026, 9, 28, 12, 0, 0, 123456789, time.UTC),
Message: weft.User("Where is order 1234?"),
}
b, err := json.Marshal(e)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(b))
restored, err := thread.UnmarshalEntry(b)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(restored.(thread.MessageEntry).Message.Role)
}
Output: {"type":"message","id":"e_01J8X9M2K7QW4R5N8T6V2B3C4E","created":"2026-09-28T12:00:00.123456789Z","message":{"role":"user","content":[{"type":"text","text":"Where is order 1234?"}]}} user
func (MessageEntry) MarshalJSON ¶
func (e MessageEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator.
type NativeCompactor ¶
type NativeCompactor interface {
CompactNative(ctx context.Context, req weft.ModelRequest, instructions string) (weft.Message, weft.Usage, error)
}
NativeCompactor is implemented by adapters whose provider compacts server-side. Root types only, so an adapter implements it without importing thread (ADR 0020 §7).
type OpenConfig ¶
type OpenConfig struct {
// Salvage downgrades a malformed line from a load failure
// (ErrCorrupt) to a skip reported in the LoadReport. The unknown
// and the newer stay loud: ErrNewerFormat is never salvaged.
Salvage bool
// SyncEveryAppend is the durability cadence: true (the default)
// fsyncs every Append before it returns; false defers the fsync to
// the Flusher capability — the turn-end cadence a Session drives.
SyncEveryAppend bool
}
OpenConfig is the resolved configuration of an open: the defaults with every option applied. Backends resolve it through ResolveOpen; callers never construct it.
func ResolveOpen ¶
func ResolveOpen(opts ...OpenOption) OpenConfig
ResolveOpen folds opts over the defaults. Exported because backends outside this package resolve the same option vocabulary into the same configuration.
type OpenOption ¶
type OpenOption interface {
// contains filtered or unexported methods
}
OpenOption configures a Storage backend at open, one value per concern, applied over the defaults. The options live here — in the package that owns the Storage contract — so every backend accepts the same vocabulary and a caller never learns a backend to say Salvage (ADR 0011 §5 names it thread.Salvage).
func FsyncEveryAppend ¶
func FsyncEveryAppend() OpenOption
FsyncEveryAppend returns the open option that restores the default durability: every Append fsyncs before returning, so an accepted entry is durable before anything replies on it — the rule behind "the prompt is durable before the run starts" (ADR 0011 §4).
func FsyncOnFlush ¶
func FsyncOnFlush() OpenOption
FsyncOnFlush returns the open option that defers the fsync to the Flusher capability — the turn-end cadence, cheaper than an fsync per append. A crash between appends and the flush can lose the tail of a turn, never a synced one; the file stays readable (a torn final line is dropped and reported). The caller who flushes at turn ends — Session, from v0.1 commit 7 — owns the durability window; a plain Flush after the prompt keeps ADR 0011 §4's promise.
func Salvage ¶
func Salvage() OpenOption
Salvage returns the open option that skips malformed lines instead of failing the load: each skip is reported in Load's LoadReport (Skipped, in file order). A torn final line is always dropped and reported — that is a crash, not damage — and data from a newer weft still fails with ErrNewerFormat: salvage repairs what a crash wrote, never what it cannot read (ADR 0011 §5).
type Outcome ¶
type Outcome string
Outcome is a decision's kind (ADR 0021 §1): approve (run the call through the ordinary chain), deny with a reason, or resolve with content computed outside the process, resolve_error marking it an error. The wire values are the decision entry's "outcome" field; they never change without a format version.
type Page ¶
type Page struct {
// Sessions is the page, newest first by Created (ties by id,
// descending), headers only — Load returns the entries.
Sessions []Header
// Total is the number of sessions matching the query, ignoring
// Before and Limit — the "how many pages are there" number.
Total int
}
Page is one List result.
type ParentRef ¶
ParentRef names a fork's origin: the session the new session was forked from, and the entry its copied path ends at (ADR 0011 §3).
type Policy ¶
type Policy int
Policy is what a Send does when the session is already running a turn — the busy policy (ADR 0011 §4). Queue is the default.
const ( // Queue holds the follow-up and runs it when the current turn // ends, in acceptance order: the send is accepted, its Turn is // returned at once, and its prompt becomes durable when its turn // starts. The default. Queue Policy = iota // Reject fails the send with ErrBusy: one run per session at a // time, and a busy session says so instead of holding work. Reject // Steer delivers the send into the running turn instead of // waiting for it (ADR 0019): the message is accepted at once — // its queued receipt entry durable, flushed — and handed to the // run's next steering drain point: after the step's tool batch, // every call paired with its result, or at what would have been // the final step, which the delivery redirects into one more // step. A steer that meets an intended end (StopWhen) or an open // approval boundary is never drained: it becomes a deferred // follow-up that runs as the next turn, the receipt recording // the fate. The Send's Turn is the receipt: it finishes — nil // result, nil error — when the steer reaches its final state, // and its Next is the follow-up turn a deferred steer became. Steer // Interrupt cancels the running turn and runs the message next // (plan §6): the in-flight run's context is canceled, the calls // its partial transcript left without a result carry the golden // interruption text, an approval boundary that holds the session // is denied with the interrupted reason, and the message runs as // the next turn. The interrupted turn's entries stay on the // tree — evidence, never deleted. Interrupt // Rollback is an Interrupt that also branches the leaf back to // before the interrupted turn's receipt entry: the follow-up runs // as though the interrupted turn never happened, while its // entries keep their own line of the tree (nothing lost). Rollback )
type PoolReceiptEntry ¶ added in v0.5.0
type PoolReceiptEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
Receipt string `json:"receipt,omitempty"`
Status string `json:"status"`
Child string `json:"child,omitempty"`
Call string `json:"call,omitempty"`
Prompt string `json:"prompt,omitempty"`
Stop string `json:"stop,omitempty"`
Usage weft.Usage `json:"usage,omitzero"`
}
PoolReceiptEntry is the pool receipt (ADR 0022 §4): the journey of one child run a thread/pool started for this session. One entry records acceptance — Status "accepted", the child session on Child, the task on Prompt — a second records the slot acquisition and start ("running"), and a third, linked by Receipt, records the settlement: "done" (Stop carries the child's answer, Usage its total), "failed" (Stop the cause), "canceled" (an explicit Cancel), or "capped" (the child died on a budget — MaxSteps or a usage limit). Call names the delegating tool call for wrapped delegations. Pool receipt entries never enter the model's context: the answer reaches the model as the delegating call's result (a sync delegation) or however the application delivers it (an async one); the entry is the ledger, not the channel.
func (PoolReceiptEntry) MarshalJSON ¶ added in v0.5.0
func (e PoolReceiptEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator and "v":4.
type Preparation ¶
type Preparation struct {
Reason Reason
Context []weft.Message
Messages []weft.Message
SplitPrefix []weft.Message
PrevSummary string
FirstKept string
TokensBefore int64
Instructions string
Pinned []string
}
Preparation is a computed compaction on its way to becoming one: everything the algorithm and the BeforeCompact hook see. Reason is why it runs; Context is the leaf's whole context; Messages the default cut's range to summarize; SplitPrefix the split turn's prefix when one turn alone overflows (nil in the one-call default, which folds it into Messages with the previous summary); PrevSummary the iterative chain's last link; FirstKept, TokensBefore and Pinned the entry's facts; Instructions the per-call text.
type Query ¶
type Query struct {
// Before is the paging cursor: only sessions created strictly
// before it are returned; zero means start at the newest. Offsets
// are deliberately absent — they drift under concurrent inserts
// (the store's rule, ADR 0010 §0.1, inherited here). Sessions
// sharing a Created time are ordered by id, so pages are
// deterministic; a cursor that lands inside such a group needs the
// last page's ids as well — callers paging through distinct times,
// the normal shape, never see the seam.
Before time.Time
// Limit caps the page: 0 means 50, values above 500 clamp.
Limit int
// Meta filters by session metadata: every key must match its value
// exactly. A session matches when its header's Meta (the merged
// view Load returns) holds every pair. Backends may answer this
// from the header alone — the cheap path; the title filter below is
// the one that can cost more.
Meta map[string]string
// TitleSearch filters by the session's current title — the last
// info entry's Title — matching case-insensitively as a substring.
// A title is entry state, not header state, so this filter is the
// one shape of List that may read beyond headers (a backend scans
// the session's info entries): opt-in by the query, priced
// accordingly, and never paid by a query without it.
TitleSearch string
}
Query selects sessions for List. The zero value lists every session, newest first, 50 at a time.
Example ¶
Query filters the session list: metadata pairs match the header exactly, and the title search matches the session's current title — the last info entry's — case-insensitively as a substring. Total counts the matches; the cursor and limit page them.
package main
import (
"context"
"fmt"
"time"
"github.com/weftgo/weft/thread"
)
func main() {
ctx := context.Background()
st := thread.Memory()
created := time.Date(2026, 9, 29, 12, 0, 0, 0, time.UTC)
titled := func(id string, meta map[string]string, title string) {
h := thread.Header{ID: id, Created: created, Meta: meta}
if err := st.Create(ctx, h); err != nil {
fmt.Println(err)
return
}
if title != "" {
if err := st.Append(ctx, id, thread.InfoEntry{
ID: "e_" + id, Created: created, Title: title,
}); err != nil {
fmt.Println(err)
}
}
}
titled("s_prod", map[string]string{"env": "prod"}, "Checkout bug")
titled("s_dev", map[string]string{"env": "dev"}, "login flow")
titled("s_prod_2", map[string]string{"env": "prod"}, "checkout again")
p, err := st.List(ctx, thread.Query{Meta: map[string]string{"env": "prod"}})
if err != nil {
fmt.Println(err)
return
}
fmt.Println("env=prod:", p.Total)
p, err = st.List(ctx, thread.Query{TitleSearch: "CHECKOUT"})
if err != nil {
fmt.Println(err)
return
}
for _, h := range p.Sessions {
fmt.Println("title match:", h.ID)
}
}
Output: env=prod: 2 title match: s_prod_2 title match: s_prod
type QueuedSteer ¶ added in v0.3.0
type QueuedSteer struct {
// Receipt is the queued receipt entry's id — the handle the
// steer's Turn reports as its own ID.
Receipt string
// Msg is the message held for the running turn's drain.
Msg weft.Message
}
QueuedSteer is one entry of the session's live steer queue, as Queue reports it: the receipt entry's id and the message held for delivery.
type Reason ¶
type Reason string
Reason is why a compaction ran (ADR 0020 §1): manual (the caller asked), threshold (the configured trigger), trim (a trimmer pre-pass brought the context under the line, so no summary was made), from_hook (BeforeCompact replaced the plan), and overflow — an ErrContextOverflow turn compacted and re-run, which arrives with thread v0.3.
type ReceiptEntry ¶ added in v0.3.0
type ReceiptEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
Receipt string `json:"receipt,omitempty"`
Status string `json:"status"`
Msg *weft.Message `json:"msg,omitempty"`
RunID string `json:"run_id,omitempty"`
Turn string `json:"turn,omitempty"`
}
ReceiptEntry is the steering receipt (ADR 0019, plan §6): the journey of one message accepted while the session was busy under the Steer policy. One entry records acceptance — Status "queued", the message on Msg — and a second, linked by Receipt, records the fate: "delivered" (the running run's steering drain took it; RunID names the run, and the message landed in that run's transcript), "deferred" (it runs as the next turn instead — a StopWhen end, an open approval boundary, or the run ended before the drain; Turn names the follow-up turn's receipt), or "dropped" (ClearQueue). Receipt entries never enter the model's context: the message reaches the model through the run that delivered it or the follow-up turn that ran it, exactly once.
func (ReceiptEntry) MarshalJSON ¶ added in v0.3.0
func (e ReceiptEntry) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entry with its "type" discriminator and "v":3.
type Request ¶
type Request struct {
// Session is the session the request lives in.
Session string
// CallID is the pending call's id — the key decisions address and
// the core's Approve/Deny/Resolve key on.
CallID string
// Tool is the call's tool name.
Tool string
// Args is the call's arguments, verbatim as the model wrote them.
Args json.RawMessage
// ArgsSHA256 is the hex SHA-256 of Args — a decision names what it
// decided by this hash, and step 2.2's signed decisions carry it in
// the challenge.
ArgsSHA256 string
// RunID is the run that parked the call.
RunID string
// Reason is why the call parked: "tool requires approval" for a
// RequireApproval tool, "middleware required approval" when the
// tool chain parked it.
Reason string
// Expiry is when the request lapses — zero means never. A request
// is expired strictly after this time, never at it; the next resume
// denies it with the stated reason (ADR 0021 §5).
Expiry time.Time
// Created is when the request was persisted.
Created time.Time
// Nonce and KeyID are the challenge a signed decision answers (ADR
// 0021 §3): set by Session.Request — the signing side's handle on
// exactly what it vouched for — and empty on the Pending view,
// which mints no challenge.
Nonce string
// Child names the delegated session this call parks in, when the
// call is a pool child's mirrored onto this parent (ADR 0022 §7):
// the lineage a decision routes by — decide it through the pool,
// which resumes the child and then completes the delegation. Empty
// on an ordinary request.
Child string
KeyID string
}
Request is a parked call awaiting a decision (ADR 0021 §1): what the model asked for, hashed and named so a decision can state exactly what it decided. Session.Pending returns the undecided ones; they survive restarts because they are entries.
type SendOption ¶
type SendOption interface {
// contains filtered or unexported methods
}
SendOption configures one Send. Step 1.7 carries one: RunOptions, the extra run options for this turn's run.
func As ¶ added in v0.3.0
func As(p Policy) SendOption
As returns the SendOption overriding the session's busy policy for this one Send (ADR 0019): As(Steer) steers a message into the running turn on a Queue session; As(Queue) holds a message for the next turn on a Steer session. The policy in force is the one Send ran under, captured with the turn's other settings.
Example ¶
As steers one Send into a running turn on a Queue session: the message is accepted at once — its queued receipt durable — and the run's next drain point delivers it after the tool batch, the receipt recording the fate (ADR 0019).
package main
import (
"context"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/wefttest"
)
func main() {
ctx := context.Background()
release := make(chan struct{})
wait := weft.Tool("wait", "blocks until released", func(ctx context.Context, _ struct{}) (string, error) {
select {
case <-release:
return "ok", nil
case <-ctx.Done():
return "", ctx.Err()
}
})
model := wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "wait"}),
wefttest.Say("done, in metric units"),
)
var s *thread.Session
agent := weft.New(model, wait, weft.Tap(func(_ context.Context, ev weft.Event) {
if _, ok := ev.(weft.ToolStart); ok {
if _, err := s.Send(ctx, weft.User("use metric units"), thread.As(thread.Steer)); err != nil {
fmt.Println(err)
}
}
}))
s, _ = thread.Create(ctx, thread.Memory(), agent)
t, _ := s.Send(ctx, weft.User("convert this"))
close(release)
res, err := t.Wait()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(res.Text())
for _, e := range s.Entries() {
r, ok := e.(thread.ReceiptEntry)
if !ok {
continue
}
if r.Receipt == "" {
fmt.Println("receipt queued:", r.Msg.Text()) // acceptance, durable
continue
}
fmt.Println("receipt", r.Status) // the fate, linked by Receipt
}
}
Output: done, in metric units receipt queued: use metric units receipt delivered
func RunOptions ¶
func RunOptions(opts ...weft.RunOption) SendOption
RunOptions returns the SendOption carrying extra weft.RunOptions into this turn's run — budgets, taps, thinking, an approval resume (weft.Approve, weft.Deny). The transcript is Send's to build and the run id the session's to mint: an option that would set the run's messages (weft.Messages, weft.Prompt) or its id (weft.RunID) is rejected when the Send runs, because either would quietly detach the run from the session's tree (ADR 0011 §4).
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
A Session is a loaded conversation: the append-only entry tree (ADR 0011 §2) held in memory, every write going through the storage's Append, the leaf tracked as the entry the next one attaches to. Create or Open return one; it is safe for concurrent use by any number of goroutines. The Session is the session's one writer — entries appended to the storage behind its back are invisible to it until the next Open.
func Create ¶
func Create(ctx context.Context, st Storage, agent *weft.Agent, opts ...SessionOption) (*Session, error)
Create starts a new session in st: a fresh header under a new time-sortable id (or the IDs option's), the tree empty. agent is the session's own — Send runs it (step 1.7) and compaction summarizes with its model (step 1.8) — and must not be nil, like the core's New. The header is all that is written: nothing else lands in the storage until the first append.
Example ¶
A session carries the conversation: application state survives outside the model's context, application messages ride inside it, and the title and metadata are editable appends. The IDs option pins deterministic ids so the output is stable.
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/wefttest"
)
func main() {
ctx := context.Background()
agent := weft.New(wefttest.Script()) // no run happens here; Send arrives in step 1.7
st := thread.Memory()
next := 0
ids := []string{"s_demo1", "e_demo2", "e_demo3", "e_demo4", "e_demo5"}
deterministic := thread.IDs(func() string { id := ids[next]; next++; return id })
s, err := thread.Create(ctx, st, agent, deterministic)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("session", s.ID())
if err := s.SetInfo(ctx, "Order support", map[string]string{"team": "ops"}); err != nil {
fmt.Println(err)
return
}
if err := s.Custom(ctx, "cart", json.RawMessage(`{"items":2}`)); err != nil {
fmt.Println(err)
return
}
if err := s.CustomMessage(ctx, "note", weft.User("The customer's quote covers two items.")); err != nil {
fmt.Println(err)
return
}
again, err := thread.Open(ctx, st, s.ID(), agent, thread.IDs(func() string { return "e_demo6" }))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(again.Title(), again.Meta()["team"])
for _, m := range again.Context() {
fmt.Println(m.Role, ":", m.Text())
}
}
Output: session s_demo1 Order support ops user : The customer's quote covers two items.
func Open ¶
func Open(ctx context.Context, st Storage, id string, agent *weft.Agent, opts ...SessionOption) (*Session, error)
Open loads an existing session from st, positioned at its leaf: the entry the next append attaches to, recovered by replaying the entries in append order (a trailing leaf entry redirects; any other entry leaves the leaf at itself). A load that had to drop a torn tail or skip a salvaged line reports it once, as a warning through the agent's logger — a repair is never silent (ADR 0011 §5) — and the session still opens: the entries that survived are the session.
func SessionFromContext ¶ added in v0.5.0
SessionFromContext returns the session whose run ctx carries it, or nil outside a session's run — a wrapped tool invoked through a bare Generate has no parent session, and the pool falls back to the ordinary subagent path (ADR 0022 §2).
func (*Session) AppendApprovalRequests ¶ added in v0.5.0
func (s *Session) AppendApprovalRequests(ctx context.Context, reqs ...ApprovalRequestEntry) ([]ApprovalRequestEntry, error)
AppendApprovalRequests appends mirrored approval requests in one atomic batch (ADR 0022 §7): the pool writes a child session's parked calls onto the parent's tree — Child naming the session they park in, Wrapper the delegating call they park under — so the parent's Pending surfaces them and a decision records like any other. The entries' tree fields are minted here; the stored entries return. Mirrors are ledger until decided: they never join the model's context, and their resolution is the pool's to route.
func (*Session) AppendPoolReceipt ¶ added in v0.5.0
func (s *Session) AppendPoolReceipt(ctx context.Context, e PoolReceiptEntry) (PoolReceiptEntry, error)
AppendPoolReceipt appends one pool receipt entry (ADR 0022 §4) and returns it as stored, its minted ID the receipt handle a later entry links back to with Receipt. The pool calls this for every state its delegations pass through — acceptance, the start, the settlement — under the rule the entry kind's contract states: pool receipts are ledger, never model context, and a child's answer reaches the model only through its delegating call's result or the application. A hand caller owns the same rules.
func (*Session) ApplyCompaction ¶
func (s *Session) ApplyCompaction(ctx context.Context, c *Compaction) error
ApplyCompaction writes a computed Compaction as the session's next compaction entry, appended at the leaf. c must name a FirstKept on the leaf's path — a value the walk could never reach is an error, not a fallback — and one at or after the previous compaction's kept boundary: an iterative compaction never summarizes what a summary already replaced (ADR 0020 §1). A summary-less compaction must be a trim. Anything else is an error, and nothing is written. The entry lands whatever else happened between its computation and this call — the tree only grew, so the kept range stays correct.
func (*Session) Audit ¶
Audit returns the session's approval trail (ADR 0021 §5): every request, chain step, decision, grant and revocation entry, in append order, each carrying its entry id and time — the whole file, not only the leaf's path, because the trail is what happened, not what the leaf remembers. An expiry denial reads as its audit entry plus the decision with Via "expiry"; a grant match as the audit entry naming the grant.
func (*Session) Branch ¶
Branch navigates the session to entryID: it appends a leaf entry (never a rewrite, ADR 0011 §3), and the next write attaches there — the model's context is rebuilt from that path alone, and nothing appended on the abandoned branch reaches it again (pi's invariant). entryID may name any entry the session holds, on any branch, including the current leaf (a recorded no-op navigation); "" is the root, restarting the conversation from nothing while the file keeps everything. An id the session does not hold is an error.
Branching is a between-turns operation: while a turn runs the session holds the line the run's transcript must land on, and a navigation underneath it would strand the run's messages on a branch whose context the model never saw — so a Branch while a turn is in flight fails with ErrBusy, like a Send under the Reject policy. A parked approval boundary is not a running turn: branching away from it is the documented way out of an unwanted boundary (the armed resume reads ErrNotPending).
Example ¶
Branch navigates the tree and Fork copies a path into a new session: the branch's context drops the abandoned entries, the fork carries the whole copied path and names its origin in its header.
package main
import (
"context"
"fmt"
"time"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/wefttest"
)
func main() {
ctx := context.Background()
agent := weft.New(wefttest.Script())
st := thread.Memory()
next := 0
ids := []string{"s_nav", "e_nav"}
s, err := thread.Create(ctx, st, agent, thread.IDs(func() string { id := ids[next]; next++; return id }))
if err != nil {
fmt.Println(err)
return
}
// Two messages of history, appended the way Send will from step 1.7.
if err := st.Append(ctx, s.ID(),
thread.MessageEntry{ID: "e_1", Created: time.Now().UTC(), Message: weft.User("draft the intro")},
thread.MessageEntry{ID: "e_2", ParentID: "e_1", Created: time.Now().UTC(), Message: weft.Assistant("done")},
); err != nil {
fmt.Println(err)
return
}
s, err = thread.Open(ctx, st, s.ID(), agent, thread.IDs(func() string { id := ids[next]; next++; return id }))
if err != nil {
fmt.Println(err)
return
}
// Navigate back to the first entry: the abandoned reply drops out
// of the context, and the file keeps it.
if err := s.Branch(ctx, "e_1"); err != nil {
fmt.Println(err)
return
}
fmt.Println("branch context:", len(s.Context()))
// Fork the full path into a new session: self-contained, its
// header naming where it grew from.
f, err := s.Fork(ctx, "e_2", thread.IDs(func() string { return "s_fork" }))
if err != nil {
fmt.Println(err)
return
}
page, _ := thread.List(ctx, st, thread.Query{})
for _, h := range page.Sessions {
if h.ID == "s_fork" && h.Parent != nil {
fmt.Println("fork of", h.Parent.Session, "at", h.Parent.Entry, "carries", len(f.Context()), "messages")
}
}
}
Output: branch context: 1 fork of s_nav at e_2 carries 2 messages
func (*Session) ClearQueue ¶ added in v0.3.0
ClearQueue drops every queued steer that has not been delivered, marking each receipt dropped: the messages never reach the model. Steers already delivered or deferred are untouched — their runs own them. It returns how many steers were dropped.
func (*Session) Compact ¶
func (s *Session) Compact(ctx context.Context, opts ...CompactOption) error
Compact computes and applies the session's next compaction in one call: PreviewCompaction, then ApplyCompaction. Nothing is deleted — the summarized entries stay in the file, and the context at the leaf becomes the summary, then the entries from the compaction's first kept entry onward.
Example ¶
Compaction summarizes the old part of a session and keeps the recent part raw — nothing is deleted, and Uncompact branches right back.
ctx := context.Background()
// The same model summarizes; a script makes it deterministic.
rec := &recordingModel{reply: "Goal: ship the order service."}
agent := weft.New(rec)
st := thread.Memory()
s, _ := thread.Create(ctx, st, agent)
// A long history, appended the way Send does.
msgs := []string{strings.Repeat("order ", 12_000), strings.Repeat("invoice ", 12_000), strings.Repeat("refund ", 12_000)}
parent := ""
var batch []thread.Entry
for i, text := range msgs {
id := fmt.Sprintf("e_%d", i)
batch = append(batch, thread.MessageEntry{ID: id, ParentID: parent, Created: time.Now().UTC(), Message: weft.User(text)})
parent = id
}
if err := st.Append(ctx, s.ID(), batch...); err != nil {
fmt.Println(err)
return
}
s, err := thread.Open(ctx, st, s.ID(), agent)
if err != nil {
fmt.Println(err)
return
}
if err := s.Compact(ctx); err != nil {
fmt.Println(err)
return
}
fmt.Println("after Compact:", len(s.Context()), "messages")
fmt.Println("summary rides first:", strings.HasPrefix(s.Context()[0].Text(), "<weft-summary>"))
fmt.Println("kept raw:", len(s.Entries()) > 3)
if err := s.Uncompact(ctx); err != nil {
fmt.Println(err)
return
}
fmt.Println("after Uncompact:", len(s.Context()), "messages")
Output: after Compact: 2 messages summary rides first: true kept raw: true after Uncompact: 3 messages
func (*Session) Context ¶
Context returns the messages the model sees at the session's leaf: the message and custom_message entries on the leaf's path, in conversation order, with weft.Repair applied last — every call the transcript shows has a result (ADR 0011 §2, ADR 0001). Entries of the bookkeeping kinds never reach it; a custom entry's whole point is to survive outside it. A call left pending by its turn is shown repaired here — the caller's view; the run Send starts repairs pending calls itself, so the decision options can resolve them.
This walk is where compaction lands: from step 1.8, the latest compaction entry on the path contributes its summary ahead of the entries from its first kept id onward, and a branch_summary entry contributes the abandoned branch's summary in that branch's place (ADR 0020 §1, §6). Nothing before step 1.8 writes either kind, so the walk below reads the whole path raw.
func (*Session) Custom ¶
Custom appends application state: a caller-chosen Kind and opaque JSON Data, surviving every compaction and never entering the model's context (ADR 0011 §2, ADR 0020 §4). data is stored verbatim — nil stores no data key at all; bytes that are not JSON are rejected, the same failure the storage's Append would raise, caught before anything is written. An empty kind is rejected: state nobody can name is not state.
func (*Session) CustomMessage ¶
CustomMessage appends an application message: a caller-chosen Kind and a weft.Message that is always in the model's context — how an application puts a note the model must see without attributing it to the user (ADR 0011 §2). An empty kind is rejected.
func (*Session) Decide ¶
Decide records decisions over the session's pending calls, durably, and resumes the boundary when they complete it (ADR 0021 §1).
Every decision must address a currently pending call: one that does not fails with ErrNotPending and nothing is recorded — the error is raised before any entry lands and before any run starts, the core's rule made strict. When, after recording, every pending call of the boundary has a decision and AutoResume is on (the default), the resume run starts and Decide returns its Turn; otherwise the return is (nil, nil) and the caller drives Resume. The undecided calls a Resume-driven run carries are denied with the core's "no decision" text; expired ones were denied on the spot with the expiry reason.
Example ¶
Approvals make a parked call durable: the turn ends pending, the request survives a restart as an entry, and Decide records the decision and resumes the conversation on its own (ADR 0021).
package main
import (
"context"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/wefttest"
)
func main() {
ctx := context.Background()
agent := weft.New(
wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "refund", ID: "call_1", Args: `{"order_id":"1234"}`}),
wefttest.Say("Refund issued."),
),
weft.Tool("refund", "Refund an order.",
func(ctx context.Context, in struct {
OrderID string `json:"order_id"`
}) (string, error) {
return "refunded " + in.OrderID, nil
},
weft.RequireApproval()),
)
next := 0
ids := []string{"s_appr", "e_1", "e_2", "e_3", "e_4", "e_5", "e_6", "e_7", "e_8", "e_9", "e_10", "e_11"}
s, _ := thread.Create(ctx, thread.Memory(), agent,
thread.IDs(func() string { id := ids[next]; next++; return id }))
t1, err := s.Send(ctx, weft.User("Refund order 1234."))
if err != nil {
fmt.Println(err)
return
}
res, err := t1.Wait()
if err != nil {
fmt.Println(err) // a pending turn is a success
return
}
fmt.Println("parked:", res.Pending[0].Name)
for _, r := range s.Pending() {
fmt.Println("pending request:", r.CallID, "on", r.Tool)
}
rt, err := s.Decide(ctx, thread.Approve("call_1"))
if err != nil {
fmt.Println(err)
return
}
if _, err := rt.Wait(); err != nil {
fmt.Println(err)
return
}
fmt.Println("pending after decide:", len(s.Pending()))
for _, m := range s.Context() {
if r, ok := lastResult(m); ok {
fmt.Println("result:", r)
}
}
}
// lastResult reports the message's last tool result text, if any.
func lastResult(m weft.Message) (string, bool) {
for i := len(m.Content) - 1; i >= 0; i-- {
if r, ok := m.Content[i].(weft.ToolResultPart); ok {
return r.Content, true
}
}
return "", false
}
Output: parked: refund pending request: call_1 on refund pending after decide: 0 result: refunded 1234
func (*Session) DecideSigned ¶
DecideSigned verifies sd fail-closed, then records it as the request's decision — with the nonce and key id, so a replay of the same signature is detectable from the file — and resumes when the boundary completes (AutoResume, like Decide).
The verification order is the error catalogue (ADR 0021 §3): a key the ring does not hold is ErrUnknownKey; a MAC that does not match the canonical encoding of the claimed fields — including the session, the tool, and the outcome — is ErrBadSignature, compared constant-time; a challenge past its expiry is ErrExpired; a nonce a recorded decision already answered is ErrReplay; and a signature whose arguments hash names different arguments than the pending request holds is ErrArgsChanged — the call re-parked with new arguments, and the decision was about the old ones. Nothing is recorded until every check passes; ErrNotPending keeps Decide's rule, raised before any run starts.
func (*Session) Entries ¶
Entries returns the session's whole tree in append order — every entry ever written, abandoned branches included. The slice is fresh and the entries' bookkeeping fields are copies: mutating what comes back never reaches the session or the storage.
func (*Session) Fork ¶
func (s *Session) Fork(ctx context.Context, entryID string, opts ...SessionOption) (*Session, error)
Fork copies the session's path root → entryID into a new session in the same storage, and returns it positioned at entryID: the new session is self-contained — its file holds the whole copied path with the entries' own ids and parent links — and traceable, its header naming the session and entry it grew from (ADR 0011 §3). entryID follows Branch's rule: any held entry, or "" for the root (a fork of an empty session is an empty session with a parent). opts configure the new session, not this one: its ids option mints the fork's session id and its later entries; the copied entries keep the ids they were born with. The fork starts with no title or metadata — those belong to the original's info entries.
func (*Session) Grant ¶
Grant appends a session-scoped grant (ADR 0021 §4): the decision chain consults it — newest grant first — for every call about to park, from the next boundary on. The tool name must not be empty: a grant that matches every tool is a policy this shape does not have.
func (*Session) Label ¶
Label names an entry — bookmarks, checkpoints, the anchors a UI lists. The label is an appended entry, never a rewrite, and never enters the model's context. entryID must name an entry the session holds; the name must not be empty.
func (*Session) Leaf ¶
Leaf returns the entry the next appended entry attaches to: the id of the last entry, the target of a trailing leaf entry, or "" while the session holds no entries (the next entry is the root).
func (*Session) Lineage ¶ added in v0.5.0
Lineage returns the session's pool lineage (ADR 0022 §3): the parent session and delegating call it was started from, as recorded in its header at Create. The zero value means the session is nobody's child.
func (*Session) Meta ¶
Meta returns the session's current metadata as a fresh map the caller owns: the header's Meta as the base layer, overlaid by every info entry's Meta in append order. nil before anything set any.
func (*Session) Path ¶
Path returns the entries from the root to entryID, inclusive, in conversation order — the chain of parent links. entryID "" is the root and returns no entries; an id the session does not hold is an error. Path names tree structure: a leaf entry's own parent is where it was appended, not the entry it navigated to.
func (*Session) Pending ¶
Pending returns the session's undecided requests, in call order: the calls the leaf's path leaves dangling that have an approval request and no decision yet. It works after a restart — requests and decisions are entries — and it never shows a call whose boundary a resume already resolved.
func (*Session) Pin ¶
Pin marks an entry to survive every compaction raw — a requirement, a key decision — by appending a pin record (a custom entry, so it survives compaction itself, the way everything custom does). The cut moves forward past pinned entries: no compaction summarizes one.
Example ¶
Pin keeps an entry in the context through every compaction — the requirement, the key decision — recorded as a reserved custom entry that survives compaction the way all custom state does.
package main
import (
"context"
"fmt"
"time"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/wefttest"
)
func main() {
ctx := context.Background()
agent := weft.New(wefttest.Script())
st := thread.Memory()
s, _ := thread.Create(ctx, st, agent)
if err := st.Append(ctx, s.ID(), thread.MessageEntry{
ID: "e_req", Created: time.Now().UTC(),
Message: weft.User("THE REQUIREMENT: ship by Friday"),
}); err != nil {
fmt.Println(err)
return
}
open, _ := thread.Open(ctx, st, s.ID(), agent)
if err := open.Pin(ctx, "e_req"); err != nil {
fmt.Println(err)
return
}
fmt.Println("pinned")
}
Output: pinned
func (*Session) PreviewCompaction ¶
func (s *Session) PreviewCompaction(ctx context.Context, opts ...CompactOption) (*Compaction, error)
PreviewCompaction computes the session's next compaction without writing anything: the cut, the summarized range, the summary text, and the entry fields a Compact would record. It runs the summarizer — the session's own model (ADR 0020 §2) — with the skeleton prompt, the previous summary (iterative compaction) as the range's first message, prompt-cache writes left off (the request carries no cache hints) and the output capped at 0.8 × Reserve. A session whose tail fits inside KeepRecent has nothing to compact and gets an error, not a no-op entry.
func (*Session) Queue ¶ added in v0.3.0
func (s *Session) Queue() []QueuedSteer
Queue returns the session's live steer queue: the messages accepted under the Steer busy policy that are waiting for the running turn's drain, in acceptance order. Delivered and deferred steers are not listed — their stories live on the receipt entries (Entries).
func (*Session) Request ¶
Request returns the pending request for callID with a fresh challenge minted under the keyring's active key: a nonce this session has never issued, the key's id, and everything a signer needs to vouch for a decision over exactly this request (ADR 0021 §3). The nonce makes the challenge single-use — DecideSigned records it, and a replay of the same signature fails with ErrReplay, across restarts, because nonces are entries.
func (*Session) Resume ¶
Resume starts the boundary's resume run now (ADR 0021 §1): the decided calls resolve under their decisions — approved calls run with Call.Approved set, denied ones show their reason, resolved ones their content — and every undecided call is denied with the core's "no decision" text. Requests strictly past their expiry were denied on the spot, with the stated reason, before the run starts. The parked boundary must still be open: Resume with nothing to resume fails with ErrNotPending. The returned Turn is the resume run's receipt and handle, like any Send's.
func (*Session) Revoke ¶
Revoke ends a session-scoped grant with an entry (ADR 0021 §4): revocation is an append — the grant entry stays, the walk reads the revocation after it. grantID must name a grant entry the session holds; a shared grant's revocation is its store's own operation.
func (*Session) Send ¶
Send appends the prompt and runs the session's agent on the leaf's context, returning the turn's receipt at once. The prompt entry is appended and flushed before the run starts (ADR 0011 §4: accepted input is durable input), the run carries the session's context plus msg and the run id <session>-t<n>, and at the end — success, failure or cancellation — the run's new messages and a turn entry are appended in one atomic batch under a context.WithoutCancel window, so a caller who walks away still leaves a complete session behind.
Settings are captured at turn start: the agent, the extra run options, and the busy policy in force when Send ran. On a busy session the policy decides — Queue (the default) holds the follow-up, in order, and Reject fails with ErrBusy.
A Send while approval requests are pending is queued, not run (ADR 0021 §5): the parked boundary must resolve first — Decide (which resumes on its own when AutoResume is on) or Resume — and the queued follow-up then runs with the boundary's transcript completed. Under Reject a pending boundary reads as busy.
Example ¶
Send runs a turn: the prompt is durable before the run starts, the reply and the turn's ledger land after it, and a reopen sees the whole conversation.
package main
import (
"context"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/wefttest"
)
func main() {
ctx := context.Background()
// A scripted model makes the example deterministic and offline.
agent := weft.New(wefttest.Script(wefttest.Say("It shipped Tuesday."), wefttest.Say("Order 1234, two items.")))
st := thread.Memory()
next := 0
ids := []string{"s_demo", "e_1", "e_2", "e_3", "e_4", "e_5", "e_6"}
s, _ := thread.Create(ctx, st, agent, thread.IDs(func() string { id := ids[next]; next++; return id }))
t1, err := s.Send(ctx, weft.User("Where is order 1234?"))
if err != nil {
fmt.Println(err)
return
}
res, err := t1.Wait()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("run", t1.RunID(), "replied:", res.Text())
t2, err := s.Send(ctx, weft.User("And what was in it?"))
if err != nil {
fmt.Println(err)
return
}
if _, err := t2.Wait(); err != nil {
fmt.Println(err)
return
}
again, err := thread.Open(ctx, st, s.ID(), agent)
if err != nil {
fmt.Println(err)
return
}
for _, m := range again.Context() {
if m.Text() != "" {
fmt.Println(m.Role, ":", m.Text())
}
}
}
Output: run s_demo-t1 replied: It shipped Tuesday. user : Where is order 1234? assistant : It shipped Tuesday. user : And what was in it? assistant : Order 1234, two items.
func (*Session) SetInfo ¶
SetInfo edits the session's title and metadata as an appended info entry, never a rewrite of the header (ADR 0011 §2): the current title is the last info entry's Title, the current metadata every info entry's Meta merged in append order — Title and Meta resolve them (an empty title keeps the current one; a nil meta map changes nothing; a later entry overwrites the keys it names and leaves the rest).
func (*Session) Storage ¶ added in v0.5.0
Storage returns the storage the session writes through — the handle thread/pool holds to create child sessions in the same place (ADR 0022 §3). It is the app's own storage returned, read-only by convention: every write goes through a Session, never behind one.
func (*Session) Title ¶
Title returns the session's current title — the last info entry's (SetInfo's rule), "" before any SetInfo named one.
func (*Session) Uncompact ¶
Uncompact undoes the latest compaction on the leaf's path the only way the format allows: a branch back to the entry before the compaction entry (ADR 0020 §1) — a leaf entry, no rewrite, nothing deleted. The context is exactly what it was before the compaction ran; the compaction and everything after it stay in the file, off the path.
func (*Session) Usage ¶
Usage returns the session's cost ledger over every entry in the file, not only the leaf's path: turns from the per-turn ledger, summarizer costs in their own bucket (ADR 0011 §2, ADR 0020 §4).
Example ¶
The tree in memory and the session in storage agree, and the leaf is where the next entry attaches: Entries walks the whole tree, Path one root-to-entry chain, and Usage reads the cost ledger.
package main
import (
"context"
"fmt"
"time"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/wefttest"
)
func main() {
ctx := context.Background()
agent := weft.New(wefttest.Script())
st := thread.Memory()
s, err := thread.Create(ctx, st, agent, thread.IDs(func() string { return "s_ledger" }))
if err != nil {
fmt.Println(err)
return
}
// A finished turn, recorded the way Send will from step 1.7.
if err := st.Append(ctx, s.ID(),
thread.MessageEntry{ID: "e_q", Created: time.Now().UTC(), Message: weft.User("Summarize the plan.")},
thread.TurnEntry{
ID: "e_t", ParentID: "e_q", Created: time.Now().UTC(),
RunID: "s_ledger-t1", StopReason: weft.StopEndTurn,
Usage: weft.Usage{InputTokens: 120, OutputTokens: 30},
},
); err != nil {
fmt.Println(err)
return
}
open, err := thread.Open(ctx, st, "s_ledger", agent)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("entries:", len(open.Entries()), "leaf:", open.Leaf())
fmt.Println("path:", len(mustPath(open, "e_t")))
u := open.Usage()
fmt.Println("turn input tokens:", u.Turns.InputTokens, "summary tokens:", u.Summaries.InputTokens)
}
func mustPath(s *thread.Session, id string) []thread.Entry {
path, err := s.Path(id)
if err != nil {
panic(err)
}
return path
}
Output: entries: 2 leaf: e_t path: 2 turn input tokens: 120 summary tokens: 0
type SessionOption ¶
type SessionOption interface {
// contains filtered or unexported methods
}
SessionOption configures a session at Create or Open — one value per concern, folded over the defaults, the constructor convention the core set (ADR 0011 §6 borrows it). Options later steps add (busy policy and run options in step 1.7, the compaction layers in steps 1.8–1.9) join the same interface.
func AfterCompact ¶
func AfterCompact(fn func(ctx context.Context, e CompactionEntry)) SessionOption
AfterCompact returns the SessionOption setting the hook that runs after a compaction entry lands, with the entry — the durable record, not the plan.
func ApproverTimeout ¶
func ApproverTimeout(d time.Duration) SessionOption
ApproverTimeout bounds the chain's live consultation: the Approver runs under a context with this deadline, and a timeout or panic reads as a decline, audited, before the call parks. Values <= 0 leave the Approver unconsulted — set one to arm WithApprover.
func AutoResume ¶
func AutoResume(on bool) SessionOption
AutoResume sets whether the session resumes on its own once every pending call of the parked boundary has a decision (ADR 0021 §1). The default is on; AutoResume(false) parks the decided boundary until the caller calls Resume. Either way a plain Send never runs while a boundary is open — only the resume run resolves it.
func BeforeCompact ¶
func BeforeCompact(fn func(ctx context.Context, p *Preparation) (Verdict, error)) SessionOption
BeforeCompact returns the SessionOption setting the hook that sees every computed compaction before it is written, told the reason, and decides: Proceed, Cancel, or Replace with a hook-made summary. A hook error fails the compaction with that error.
func BusyPolicy ¶
func BusyPolicy(p Policy) SessionOption
BusyPolicy returns the SessionOption setting what Send does when the session is busy: Queue (the default), Reject, Steer, Interrupt, or Rollback. A single Send overrides it with As.
Example ¶
A session under the Reject busy policy says ErrBusy instead of holding a follow-up while a turn runs.
package main
import (
"context"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/wefttest"
)
func main() {
ctx := context.Background()
agent := weft.New(wefttest.Script())
s, _ := thread.Create(ctx, thread.Memory(), agent, thread.BusyPolicy(thread.Reject))
_ = s
// With a turn in flight (a blocking tool, say):
// if _, err := s.Send(ctx, weft.User("one more thing")); errors.Is(err, thread.ErrBusy) { … }
fmt.Println("Queue is the default; Reject is one option away")
}
Output: Queue is the default; Reject is one option away
func CheckSummary ¶
func CheckSummary(fn func(Summary) error) SessionOption
CheckSummary returns the SessionOption validating each summary. A failure retries the same model once, then falls back down the chain (SummaryModel → session model → no compaction, reported through CompactFailed) — the headings-present, length-floor checks a careful caller writes.
func ClearOldToolResults ¶
func ClearOldToolResults(keepLast int) SessionOption
ClearOldToolResults returns the SessionOption turning on the built-in trimmer: every tool result in the context except the last keepLast is replaced, at compaction time, with the stub naming the call. If the trimmed context fits the window, no summary is made and a trim record lands in the compaction entry instead.
func CompactFailed ¶
func CompactFailed(fn func(ctx context.Context, r Reason, err error)) SessionOption
CompactFailed returns the SessionOption setting the hook that runs when a compaction fails after every fallback — the session is unchanged, and the caller is told why, with the reason it was attempted for.
func ContextWindow ¶
func ContextWindow(n int64) SessionOption
ContextWindow returns the SessionOption setting the context window the trigger budgets against, in tokens. Zero or negative is ignored (the window stays unknown: no automatic compaction, one warning). ModelWindows overrides it per model.
func Disabled ¶
func Disabled() SessionOption
Disabled returns the SessionOption turning automatic compaction off entirely — no trigger, no warning: the session that compacts only by hand. Manual Compact, PreviewCompaction and Uncompact are unaffected (ADR 0020 §3).
func IDs ¶
func IDs(id func() string) SessionOption
func KeepRecent ¶
func KeepRecent(n int64) SessionOption
KeepRecent returns the SessionOption setting roughly how many estimated tokens of the tail stay raw on either side of the cut. Zero or negative keeps the default 20,000.
func MaxPerSession ¶
func MaxPerSession(n int) SessionOption
MaxPerSession returns the SessionOption capping how many automatic compactions a session may run — zero (the default) means no cap; after the cap the trigger stops firing and manual Compact keeps working.
func MinTurnsBetween ¶
func MinTurnsBetween(n int) SessionOption
MinTurnsBetween returns the SessionOption rate-limiting automatic compaction: no automatic compaction within n turns of the last one (manual Compact always works). Zero, the default, means no limit. The stop against a context that re-crosses the line every turn.
func ModelReserves ¶
func ModelReserves(reserves map[weft.ModelInfo]int64) SessionOption
ModelReserves returns the SessionOption setting per-model reserves, keyed by ModelInfo, overriding Reserve for this session's model — the small-window model that needs more headroom than the default.
func ModelWindows ¶
func ModelWindows(windows map[weft.ModelInfo]int64) SessionOption
ModelWindows returns the SessionOption setting per-model context windows, keyed by ModelInfo (provider + name): the session's own model is looked up at Create/Open, and its entry overrides ContextWindow for this session — pi's per-model settings, for the agent that switches models mid-project.
func OnRequest ¶
func OnRequest(fn func(Request)) SessionOption
OnRequest sets the notification fired when a request parks (ADR 0021 §5): called after the request entry is durable, once per parked call, so an application can push it anywhere. The session does no I/O of its own for this; a panic in fn is contained and logged, never failing the turn. A nil fn is ignored.
func PreferNative ¶
func PreferNative() SessionOption
PreferNative returns the SessionOption asking thread to use the provider's own compaction when the summarizer model (or the one it wraps) implements NativeCompactor — found by following Unwrap() through middleware. The returned message's text is stored as the summary and the entry records the model that made it. On any other model, a switch, or an error, the text summary runs: the seam, with the fallback.
func Quorum ¶
func Quorum(n int) SessionOption
Quorum sets how many decisions from distinct approver identities a call needs before it resolves (ADR 0021 §5): n approvals with different Who values. Values below 2 read as the default — one decision resolves. Conflicting decisions resolve to deny, with the pinned reason.
func ReRunOnOverflow ¶ added in v0.3.0
func ReRunOnOverflow(on bool) SessionOption
ReRunOnOverflow sets whether a turn that fails with weft.ErrContextOverflow compacts (reason overflow) and re-runs once over the shrunken path (ADR 0020 §5) — on by default. A second overflow fails the turn with both errors joined; with the re-run off, the first overflow fails it directly.
Example ¶
ReRunOnOverflow arms the overflow recovery (ADR 0020 §5): a turn failing with weft.ErrContextOverflow compacts — reason overflow — and runs once more over the shrunken path, so the caller sees one turn with the re-run's answer.
package main
import (
"context"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/wefttest"
)
func main() {
ctx := context.Background()
model := wefttest.Script(
wefttest.Say("a first answer"), // a prior turn, so the compaction has a cut
wefttest.Fail(weft.ErrContextOverflow), // the overflowing request
wefttest.Say("the summary of what came before"), // the compaction's summarizer call
wefttest.Say("recovered after compaction"), // the re-run
)
s, _ := thread.Create(ctx, thread.Memory(), weft.New(model), thread.KeepRecent(1))
t0, _ := s.Send(ctx, weft.User("a first question"))
if _, err := t0.Wait(); err != nil {
fmt.Println(err)
return
}
t1, _ := s.Send(ctx, weft.User("a prompt that overflows"))
res, err := t1.Wait()
fmt.Println(err)
fmt.Println(res.Text())
}
Output: <nil> recovered after compaction
func RequestExpiry ¶
func RequestExpiry(d time.Duration) SessionOption
RequestExpiry gives every request the session parks a lifetime: a request strictly past Created plus d is denied with the stated reason on the next resume (ADR 0021 §5). Values <= 0 (the default) mean requests never expire.
func RequireSigned ¶
func RequireSigned() SessionOption
RequireSigned returns the SessionOption that closes the in-process door: Decide — the unsigned path — is rejected with ErrSignatureRequired, and only DecideSigned records decisions (the session's own machinery — the Approver, grants, expiry — still records, with its own audit trail; it is the caller-held unsigned Decide that closes).
func Reserve ¶
func Reserve(n int64) SessionOption
Reserve returns the SessionOption setting the trigger's headroom: compaction fires before the context is within Reserve tokens of the window. Zero or negative keeps the default 16,384.
func SummaryFocus ¶
func SummaryFocus(focus string) SessionOption
SummaryFocus returns the SessionOption appending instructions to the summary prompt (a replacement or the skeleton): "keep every file path" — the caller's standing concern, appended after the skeleton's own rules.
func SummaryMaxTokens ¶
func SummaryMaxTokens(n int) SessionOption
SummaryMaxTokens returns the SessionOption overriding the summarizer output cap (the default 0.8 × Reserve).
func SummaryModel ¶
func SummaryModel(m weft.Model) SessionOption
SummaryModel returns the SessionOption setting a different model for summaries — a cheap one. On failure it falls back to the session's own model (the chain: SummaryModel → session model → no compaction, reported through CompactFailed), because a weak summary costs more in rework than a cheap model saves only when it works at all.
func SummaryPrompt ¶
func SummaryPrompt(tmpl string) SessionOption
SummaryPrompt returns the SessionOption replacing the skeleton system prompt whole. The replacement's bytes are the caller's; the default skeleton stays pinned by its golden.
func TriggerFunc ¶
func TriggerFunc(fn func(TriggerInput) bool) SessionOption
TriggerFunc returns the SessionOption replacing the trigger's condition — fire when in says so. It is consulted only when a window is known and a provider-reported input exists: the no-estimated- signal rule stands under a custom trigger too.
func WithApprover ¶
func WithApprover(a Approver) SessionOption
WithApprover sets the decision chain's live step (ADR 0021 §2). The Approver is consulted for a call about to park, after grants and before parking; it is armed only when ApproverTimeout is positive — the default of no timeout means no waiting, so a headless session never blocks on it (the ADR's "default 0 = no waiting when not interactive"). A nil approver is ignored.
func WithCompactor ¶
func WithCompactor(c Compactor) SessionOption
WithCompactor returns the SessionOption replacing the whole algorithm.
func WithEstimator ¶
func WithEstimator(est Estimator) SessionOption
WithEstimator returns the SessionOption replacing the token estimator. (The ADR sketches thread.Estimator(est); the interface already owns that name, so the option takes the With* shape its layer-3 neighbours use.)
func WithGrantStore ¶
func WithGrantStore(gs GrantStore) SessionOption
WithGrantStore returns the SessionOption adding the shared grant scope (ADR 0021 §4): grants many sessions consult, application-wide, behind the interface the application owns. The chain consults it after the session's own grants. A nil store is ignored.
func WithKeyring ¶
func WithKeyring(r *Keyring) SessionOption
WithKeyring returns the SessionOption making the session verify signed decisions under r — and mint signed challenges from r's active key (s.Request). A nil ring is ignored.
func WithLineage ¶ added in v0.5.0
func WithLineage(parentSession, call string) SessionOption
WithLineage sets a created session's pool lineage (ADR 0022 §3): the parent session and the delegating call it grew from. A Create-time option — the header is immutable once written; Open reads the lineage from the file and ignores it.
func WithMeta ¶ added in v0.5.0
func WithMeta(meta map[string]string) SessionOption
WithMeta adds keys to a created session's header metadata — the caller metadata List surfaces (ADR 0011 §1). A Create-time option, merged over earlier ones; SetInfo is the later-life edit.
func WithSummarizer ¶
func WithSummarizer(s Summarizer) SessionOption
WithSummarizer returns the SessionOption replacing text production — just the text; the cut and the entry stay the session's.
func WithTrimmer ¶
func WithTrimmer(t Trimmer) SessionOption
WithTrimmer returns the SessionOption setting a custom trimmer. The default trimmer is off; ClearOldToolResults turns a built-in one on.
type SharedGrant ¶
type SharedGrant struct {
}
A SharedGrant is one grant of a GrantStore, with the store's id for the audit trail.
type SignedDecision ¶
type SignedDecision struct {
Session string
CallID string
Tool string
ArgsSHA256 string
Expiry time.Time
Kind Outcome
Reason string
Content string
Who string
// Always approves and grants the same thing for the future, the
// signed ApproveAlways (Decision.Always carries it through
// SignDecision).
Always bool
Nonce string
KeyID string
MAC []byte
}
SignedDecision is a decision that crossed a process boundary (ADR 0021 §3): what the client decided over the challenge it was handed, vouched by the MAC the challenge's key computed. Build one with SignDecision on the signing side; hand it to Session.DecideSigned on the session side. The fields are the challenge's claims — the verifier checks them against the pending request, so a stale or retargeted signature fails loudly, each way its own error.
func SignDecision ¶
func SignDecision(key []byte, r Request, d Decision) SignedDecision
SignDecision signs d over r's challenge under key — the client-side half of the exchange. r is the Request the session minted (its Nonce and KeyID name the challenge); d is what the human decided; the MAC covers the challenge fields and the decision together, so neither can be altered in flight without failing verification.
type Storage ¶
type Storage interface {
// Create writes a session's header as its first line. The header's
// ID must satisfy ValidID — an id is a path component in some
// backend, so every backend vets it — a zero Weft means the current
// FormatVersion, any other envelope is rejected, and creating a
// session that already exists fails with ErrExists: a session is
// never silently replaced.
Create(ctx context.Context, h Header) error
// Append adds entries to a session in arrival order, after the
// entries it already holds. Appending several entries is atomic —
// all or none become visible: they are validated (an entry must
// encode) before anything is written, and a backend that cannot
// write the whole batch writes none of it. Appending to a session
// the storage does not hold fails with ErrNotFound.
Append(ctx context.Context, session string, entries ...Entry) error
// Load returns a session's header and every entry in append order,
// and a LoadReport naming anything the load had to drop or skip —
// nil when the load was clean. The returned values never alias the
// storage: they are the caller's to keep and to mutate. An unknown
// session fails with ErrNotFound; data the storage cannot decode
// fails loudly — ErrNewerFormat for the unknown kind and the newer
// version, ErrCorrupt for the malformed line (ADR 0011 §5).
Load(ctx context.Context, session string) (Header, []Entry, *LoadReport, error)
// List returns a page of session headers — headers only, never
// entries: a list body that read whole sessions would be the
// storage bloat every surveyed store had to walk back (ADR 0010's
// listTracesLight lesson, the same shape here). See Query for the
// cursor and the limit.
List(ctx context.Context, q Query) (Page, error)
// Delete removes a session and its entries; an unknown session
// fails with ErrNotFound. History is otherwise forever — nothing
// else in the interface removes data.
Delete(ctx context.Context, session string) error
}
Storage is a session backend: Memory in process, jsonl on disk, sqlite later — all running the threadtest conformance table. Implementations must be safe for concurrent use: one writer per session is the policy (ADR 0011 §5), and readers may read at any time. context.Context comes first in every signature; a canceled context fails the call before anything is written.
func Memory ¶
func Memory() Storage
Memory returns a ready-to-use in-process session storage: sessions in a map behind a mutex, gone when the process is. For tests, examples, and as the reference behaviour the durable backends are compared against — every backend runs the same threadtest table, corruption rows included (Memory implements the table's threadtest.RawInjector hook by holding the raw bytes).
Example ¶
Memory is the in-process Storage: create a session, append entries, load them back — the shape every backend shares (threadtest pins the rest).
package main
import (
"context"
"fmt"
"time"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
)
func main() {
ctx := context.Background()
st := thread.Memory()
h := thread.Header{
ID: "s_demo",
Created: time.Date(2026, 9, 28, 12, 0, 0, 0, time.UTC),
Meta: map[string]string{"room": "table-1"},
}
if err := st.Create(ctx, h); err != nil {
fmt.Println(err)
return
}
if err := st.Append(ctx, h.ID, thread.MessageEntry{
ID: "e_demo1",
Created: h.Created.Add(time.Second),
Message: weft.User("Where is order 1234?"),
}); err != nil {
fmt.Println(err)
return
}
header, entries, report, err := st.Load(ctx, h.ID)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(header.ID, header.Meta["room"], len(entries), report)
for _, e := range entries {
switch e := e.(type) {
case thread.MessageEntry:
fmt.Println(e.Message.Role, "asks:", e.Message.Text())
}
}
}
Output: s_demo table-1 1 <nil> user asks: Where is order 1234?
type Summarizer ¶
type Summarizer interface {
Summarize(ctx context.Context, in SummaryInput) (Summary, error)
}
Summarizer produces summary text — just the text: the cut, the serialization and the entry are the session's. Implemented over a model by default (the session's own, or SummaryModel).
type Summary ¶
type Summary struct {
Text string
Reason Reason
// Usage and Model are what the summary cost and which model made
// it — the cost ledger's inputs; a custom Summarizer that runs no
// model leaves them zero.
Usage weft.Usage
Model weft.ModelInfo
}
Summary is a summarizer's output: the text, and the reason it ran.
type SummaryInput ¶
type SummaryInput struct {
Messages []weft.Message
PrevSummary string
Instructions string
MaxTokens int
Reason Reason
SystemPrompt string
SummaryModel weft.Model
}
SummaryInput is everything a Summarizer needs: the range already serialized for summarizing (tool results capped, signed reasoning dropped, files as names), the previous summary when one exists, the per-call instructions, the output cap, and the reason it runs.
type TriggerInput ¶
TriggerInput is what a TriggerFunc decides on: the provider-reported input of the last model step, the estimated tokens added since, and the window and reserve in force. The reported number is the signal; only the delta is estimated (ADR 0020 §2).
type TrimReport ¶
TrimReport says what a trimmer did: how many tool results it replaced with the stub, and roughly how many estimated tokens that saved.
type Trimmer ¶
type Trimmer interface {
Trim(ctx context.Context, msgs []weft.Message) ([]weft.Message, TrimReport)
}
Trimmer is the cheap pre-pass: before summarizing, replace old tool results with a stub naming the call, and the context may fit again (Anthropic clear_tool_uses, Vercel pruneMessages). It runs on the automatic path only — a manual Compact summarizes.
type Turn ¶
type Turn struct {
// contains filtered or unexported fields
}
A Turn is the receipt and the handle of one accepted Send — or of a resume run over a parked approval boundary, which has no prompt of its own: its receipt names its turn entry.
func (*Turn) Events ¶
Events yields the turn's run events in emission order, forwarded by the session as it observes the run. Unlike the core's Run, a Turn's Events may be ranged more than once — the session, not the consumer, drives the run, so abandoning the stream mid-way (breaking out of the range) never cancels it; the turn's end wakes a suspended range and ends it. A failed run delivers its error exactly once as the final element, the core's rule.
func (*Turn) ID ¶
ID returns the turn's receipt: the prompt entry's id, minted when the Send was accepted and durable before the run started — or, for a resume turn, its turn entry's id, reused from the mint at arming (a resume writes no prompt of its own). Looking a turn up in the tree starts here.
func (*Turn) Next ¶
Next returns the turn the session resumed this turn's parked approval boundary with, or nil while there is none — this turn did not park, its boundary resumed under a turn the caller already holds (Decide and Resume return it), or the boundary is still undecided. Following Next is how a caller watches an approval flow through: Send's turn parks; the auto-resume the decision chain or a completed decision set starts links here, and its Wait is the conversation's continuation. One boundary resumes at most once, so the link is set at most once.
func (*Turn) RunID ¶
RunID returns the run's id, <session>-t<n> — the key the run store holds the run's records under, unique across a reopen. A turn that re-ran after an overflow reports the re-run's id (the run that produced its outcome).
func (*Turn) Wait ¶
Wait blocks until the turn ends and returns the run's result and error — *weft.RunError on failure, its Result carrying the partial transcript, exactly as the core's Wait reports it. The session drains the run itself, so Wait alone observes every event; it is safe to call after or while ranging over Events.
type TurnEntry ¶
type TurnEntry struct {
ID string `json:"id"`
ParentID string `json:"parent,omitempty"`
Created time.Time `json:"created"`
RunID string `json:"run_id"`
StopReason weft.StopReason `json:"stop_reason,omitempty"`
Usage weft.Usage `json:"usage"`
Steps int `json:"steps,omitempty"`
Err string `json:"err,omitempty"`
Pending []weft.ToolCallPart `json:"pending,omitempty"`
Canceled bool `json:"canceled,omitempty"`
// LastInput is the compaction trigger's baseline for the turn:
// the provider-reported input of the run's final model step, plus
// the estimated tokens of the tail that report cannot cover (the
// final step's own messages) — one number, recovered identically
// by a live session and a reopen (ADR 0020 §2: reported tokens
// are the signal; only what the report cannot cover is estimated).
// Absent on turns that ran no step.
LastInput int64 `json:"last_input,omitempty"`
}
TurnEntry is the per-turn ledger: the run's id (<session>-t<n>), its stop reason, usage and step count, the error text on failure, the calls left pending on the approval boundary, and whether the turn was canceled. It records the turn's outcome, never its content — the messages are MessageEntries — and it does not enter the model's context.
func (TurnEntry) MarshalJSON ¶
MarshalJSON encodes the entry with its "type" discriminator.
type Usage ¶
type Usage struct {
// Turns sums every turn entry's usage — every run the session ever
// made, abandoned branches included: the tokens were spent.
Turns weft.Usage
// Summaries sums the summarizer usage of every compaction entry —
// the cost of keeping the context small, in its own bucket.
Summaries weft.Usage
// Delegated sums the usage every settled pool receipt carries —
// the cost of work handed to thread/pool children (ADR 0022 D3),
// in its own bucket: a delegated child's tokens are not this
// session's turns, and the ledger never mixes kinds.
Delegated weft.Usage
}
Usage is a session's cost ledger (ADR 0020 §4): what its turns cost and what its summaries cost, never mixed.
type Verdict ¶
type Verdict struct {
// contains filtered or unexported fields
}
Verdict is what a BeforeCompact hook returns: proceed with the computed compaction, cancel it, or replace it with a hook-made one (recorded from_hook).
func Replace ¶
func Replace(c *Compaction) Verdict
Replace writes c instead — the hook's summary, recorded from_hook.
type Watcher ¶
type Watcher interface {
Watch(ctx context.Context, session string, after string) (iter.Seq2[Entry, error], error)
}
Watcher is the optional Storage capability that tails a session as it is appended to — the same small-interface rule. It arrives in v0.4 (plan §7, the live tail); it is declared now, unimplemented, so the capability pattern and its vocabulary ship with the first release instead of being retrofitted. Watch yields the session's entries in arrival order, starting after the entry named by after (empty — from the beginning), and ends when ctx is done; a session the storage does not hold fails with ErrNotFound before the first yield. Readers never lock: a watcher is a reader that waits.
Example ¶
A watcher tails a session as it is appended to — the live tail a second process reads while the first writes. The stream yields the backlog in arrival order, then each new entry exactly once, and ends when its context is canceled.
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/thread/jsonl"
)
func main() {
ctx := context.Background()
dir, err := os.MkdirTemp("", "weft-watch")
if err != nil {
fmt.Println(err)
return
}
defer func() { _ = os.RemoveAll(dir) }()
st, err := jsonl.Open(dir)
if err != nil {
fmt.Println(err)
return
}
if err := st.Create(ctx, thread.Header{ID: "s_demo", Created: time.Now().UTC()}); err != nil {
fmt.Println(err)
return
}
if err := st.Append(ctx, "s_demo",
thread.MessageEntry{ID: "e_1", Created: time.Now().UTC(), Message: weft.User("one")}); err != nil {
fmt.Println(err)
return
}
watch := st.(thread.Watcher)
wctx, cancel := context.WithCancel(ctx)
defer cancel()
seq, err := watch.Watch(wctx, "s_demo", "")
if err != nil {
fmt.Println(err)
return
}
go func() {
for e, err := range seq {
if err != nil {
return
}
fmt.Println("watched:", e.(thread.MessageEntry).Message.Text())
}
}()
if err := st.Append(ctx, "s_demo",
thread.MessageEntry{ID: "e_2", Created: time.Now().UTC(), Message: weft.User("two")}); err != nil {
fmt.Println(err)
return
}
time.Sleep(600 * time.Millisecond) // let the tail catch both
cancel()
time.Sleep(50 * time.Millisecond)
}
Output: watched: one watched: two
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
approvals
command
Command approvals walks one weft/thread session through the approval flow v0.2 exists for (ADR 0021): a gated call parks, the process "restarts" — the session is reopened from the JSONL file — a decision arrives signed over the challenge the session minted, and the conversation resumes under it.
|
Command approvals walks one weft/thread session through the approval flow v0.2 exists for (ADR 0021): a gated call parks, the process "restarts" — the session is reopened from the JSONL file — a decision arrives signed over the challenge the session minted, and the conversation resumes under it. |
|
session
command
Command session walks one weft/thread session through its whole v0.1 life: two turns, a label, a branch off the first answer, a fork of the branch, a manual compaction with preview, and a reopen from disk — everything on a JSONL backend, everything offline through a scripted model, every id deterministic.
|
Command session walks one weft/thread session through its whole v0.1 life: two turns, a label, a branch off the first answer, a fork of the branch, a manual compaction with preview, and a reopen from disk — everything on a JSONL backend, everything offline through a scripted model, every id deterministic. |
|
Package jsonl is the default durable thread.Storage: one directory, one <id>.jsonl file per session — the header line first, then one entry per line in append order (ADR 0011).
|
Package jsonl is the default durable thread.Storage: one directory, one <id>.jsonl file per session — the header line first, then one entry per line in append order (ADR 0011). |
|
Package pool provides bounded concurrent child runs for thread sessions (ADR 0022): one process-wide semaphore with FIFO fairness, subagent tools whose children hold a slot, child sessions linked to the parent session and call, and receipts recording every delegation's journey.
|
Package pool provides bounded concurrent child runs for thread sessions (ADR 0022): one process-wide semaphore with FIFO fairness, subagent tools whose children hold a slot, child sessions linked to the parent session and call, and receipts recording every delegation's journey. |
|
sqlite
module
|
|
|
Package threadtest is the shared conformance table for thread.Storage backends — the executable form of ADR 0011 §5's promises.
|
Package threadtest is the shared conformance table for thread.Storage backends — the executable form of ADR 0011 §5's promises. |