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 ¶
- Variables
- type Binding
- func (b *Binding) Create(ctx context.Context, payload string) error
- func (b *Binding) Delete(ctx context.Context, name string) error
- func (b *Binding) Rename(ctx context.Context, oldName, payload string) error
- func (b *Binding) Revert(ctx context.Context, name string) (*agent.SkillRevision, error)
- func (b *Binding) RevertTransition(ctx context.Context, tid string) ([]agent.SkillRevision, error)
- func (b *Binding) Update(ctx context.Context, name, payload string) error
- type Effector
- func (e *Effector) Bind(sa SkillAccess, actor string, maxBytes int) *Binding
- func (e *Effector) Create(ctx context.Context, sa SkillAccess, tid, payload, actor string, maxBytes int) error
- func (e *Effector) Delete(ctx context.Context, sa SkillAccess, tid, name, actor string, _ int) error
- func (e *Effector) Rename(ctx context.Context, sa SkillAccess, tid, oldName, payload, actor string, ...) error
- func (e *Effector) Revert(ctx context.Context, sa SkillAccess, name string, maxBytes int) (*agent.SkillRevision, error)
- func (e *Effector) RevertByID(ctx context.Context, sa SkillAccess, id int64, maxBytes int) (*agent.SkillRevision, error)
- func (e *Effector) RevertTransition(ctx context.Context, sa SkillAccess, tid string, maxBytes int) ([]agent.SkillRevision, error)
- func (e *Effector) Update(ctx context.Context, sa SkillAccess, tid, name, payload, actor string, ...) error
- type SkillAccess
- type Store
Constants ¶
This section is empty.
Variables ¶
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) Revert ¶
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 ¶
RevertTransition undoes a whole transition, newest revision first.
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 (*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).
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.