skilleffect

package
v0.44.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package skilleffect makes skill mutations revertible.

It is a thin layer *above* the skill write helpers in internal/configmcp, which are unchanged: the same os.Root-confined, atomic writes do the work. What this package adds is a journal entry, written before each mutation, that captures the on-disk state the mutation is about to replace — and an inverse that replays that state at most once.

Layering: agent ← configmcp ← skilleffect. This package may import both; neither may import it (configmcp receives an implementation of its own SkillWriter interface instead).

Loop guard for a future auto-revert curator: a revision whose actor is SkillActorRevert records an undo, not a mistake. Nothing enforces this today, but an automated reverter that undoes such a revision would oscillate — it must skip them.

Inspired by "A Programming Paradigm for Spatiotemporal Composability" (Shi, Zhang & Cui): https://github.com/cordiverse/paper/blob/main/paper.pdf

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrJournalDisabled means no revision store is wired, so there is no
	// history to revert. Mutations still work — they are simply untracked.
	ErrJournalDisabled = errors.New("skill revision journal is not enabled")
	// ErrNoRevision means nothing armed was found to revert.
	ErrNoRevision = errors.New("no revertible skill revision found")
	// ErrAlreadyReverted means another caller (or an earlier call) claimed the
	// revision first. The claim is persisted, so this survives a restart.
	ErrAlreadyReverted = errors.New("skill revision has already been reverted")
)

Sentinel errors callers classify on.

Functions

This section is empty.

Types

type Binding

type Binding struct {
	// contains filtered or unexported fields
}

Binding is an Effector with the engine, actor and size cap already fixed, so a call site reads like the plain Apply* call it replaces. It mints a fresh transition id per call.

Threading a per-turn transition id down from the engine's tool loop — so that several skill edits in one turn share a transition and revert together — is a follow-up; today every mutation is its own single-entry transition.

*Binding satisfies configmcp.SkillWriter.

func (*Binding) Create

func (b *Binding) Create(ctx context.Context, payload string) error

func (*Binding) Delete

func (b *Binding) Delete(ctx context.Context, name string) error

func (*Binding) Rename

func (b *Binding) Rename(ctx context.Context, oldName, payload string) error

func (*Binding) Revert

func (b *Binding) Revert(ctx context.Context, name string) (*agent.SkillRevision, error)

Revert undoes the newest armed revision for a skill, or for the whole agent when name is empty. The inverse is journaled with actor "revert" regardless of the binding's own actor.

func (*Binding) RevertTransition

func (b *Binding) RevertTransition(ctx context.Context, tid string) ([]agent.SkillRevision, error)

RevertTransition undoes a whole transition, newest revision first.

func (*Binding) Update

func (b *Binding) Update(ctx context.Context, name, payload string) error

type Effector

type Effector struct {
	// contains filtered or unexported fields
}

Effector performs tracked skill mutations for one agent.

A nil store is a supported configuration: every mutation then degrades to the plain configmcp.Apply* call it wraps, so a deployment without a journal-capable store behaves exactly as it did before. Only Revert and RevertTransition require the journal.

func New

func New(store Store, agentName string, logger *slog.Logger) *Effector

New returns an Effector for one agent. store may be nil (journaling off).

func (*Effector) Bind

func (e *Effector) Bind(sa SkillAccess, actor string, maxBytes int) *Binding

Bind fixes the per-surface arguments. actor is one of the agent.SkillActor* constants: which surface asked for the change.

func (*Effector) Create

func (e *Effector) Create(ctx context.Context, sa SkillAccess, tid, payload, actor string, maxBytes int) error

Create writes a new skill and journals that it did not exist before.

Tracked-write ordering, shared by all four mutators: read the current state, append the revision, *then* perform the mutation. Journal-before-write means a crash between the two leaves a revision describing a mutation that never happened — reverting it restores the payload already on disk, an idempotent no-op. Write-before-journal would leave an untracked mutation, which is the one failure mode this design exists to prevent. A failed append therefore aborts the mutation: fail closed.

func (*Effector) Delete

func (e *Effector) Delete(ctx context.Context, sa SkillAccess, tid, name, actor string, _ int) error

Delete removes a skill, journaling the full bytes it held so the file can be restored verbatim.

maxBytes is accepted for symmetry with the other mutators (and so a caller can bind one writer for all four); a removal writes nothing, so it is unused.

func (*Effector) Rename

func (e *Effector) Rename(ctx context.Context, sa SkillAccess, tid, oldName, payload, actor string, maxBytes int) error

Rename writes payload under the name its frontmatter declares and removes oldName, journaling both the old name and the bytes it held.

func (*Effector) Revert

func (e *Effector) Revert(ctx context.Context, sa SkillAccess, name string, maxBytes int) (*agent.SkillRevision, error)

Revert undoes the newest armed revision for a skill, or for the whole agent when name is empty, and returns the revision it undid.

func (*Effector) RevertByID

func (e *Effector) RevertByID(ctx context.Context, sa SkillAccess, id int64, maxBytes int) (*agent.SkillRevision, error)

RevertByID undoes one specific revision and returns it.

func (*Effector) RevertTransition

func (e *Effector) RevertTransition(ctx context.Context, sa SkillAccess, tid string, maxBytes int) ([]agent.SkillRevision, error)

RevertTransition undoes every armed revision of one transition, newest first. LIFO is required, not cosmetic: a rename followed by an update must be undone update-then-rename, or the update targets a name that no longer exists.

It stops at the first failure and reports how far it got; the revisions it did undo stay undone (each is claimed and applied individually).

func (*Effector) Update

func (e *Effector) Update(ctx context.Context, sa SkillAccess, tid, name, payload, actor string, maxBytes int) error

Update replaces a skill's file, journaling the bytes it replaces.

type SkillAccess

type SkillAccess interface {
	SkillsDir() string
	GetSkill(name string) (skill.Skill, bool)
	AppendSkill(s skill.Skill)
	UpdateSkill(name string, s skill.Skill) bool
	RemoveSkill(name string) bool
}

SkillAccess is the slice of engine surface the effector needs. It is satisfied as-is by *agent.Engine.

type Store

type Store interface {
	AppendSkillRevision(ctx context.Context, r agent.SkillRevision) (int64, error)
	LatestSkillRevision(ctx context.Context, agentName, skillName string) (*agent.SkillRevision, error)
	GetSkillRevision(ctx context.Context, id int64) (*agent.SkillRevision, error)
	MarkSkillRevisionReverted(ctx context.Context, id int64) (bool, error)
	TransitionSkillRevisions(ctx context.Context, agentName, transitionID string) ([]agent.SkillRevision, error)
}

Store is the slice of the memory store the journal needs. It is satisfied by *agent.SQLiteMemoryStore.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL