plan

package
v1.119.0 Latest Latest
Warning

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

Go to latest
Published: Jul 29, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package plan provides a toolset that lets two or more agents collaborate on plans addressed by name. Plans persist through a pluggable Storage backend; the default FilesystemStorage keeps them as JSON files in a global shared folder under the docker-agent data directory, so any agent that loads this toolset can write, read, list, and delete the same shared plans, and they persist across sessions. Embedders can inject an alternative backend (e.g. a database or remote store) with WithStorage.

Concurrency: agents that share one ToolSet instance also share its Storage, which serializes their operations. The default FilesystemStorage guards its read-modify-write revision bump with a mutex within the process and with an exclusive advisory lock on a sentinel file in the plans directory across processes, and writes atomically (write-to-temp + rename). A reader — including a separate docker-agent process — never observes a partially written plan, and two processes writing the *same* plan at the very same instant cannot both pass an optimistic-lock check: exactly one wins and the other gets a deterministic *VersionConflictError. Other backends own the same guarantees through their Upsert/Delete implementations.

Index

Constants

View Source
const (
	ToolNameWritePlan          = "write_plan"
	ToolNameReadPlan           = "read_plan"
	ToolNameListPlans          = "list_plans"
	ToolNameDeletePlan         = "delete_plan"
	ToolNameUpdatePlanFromFile = "update_plan_from_file"
	ToolNameExportPlanToFile   = "export_plan_to_file"
	ToolNameSetPlanStatus      = "set_plan_status"
	ToolNameGetPlanStatus      = "get_plan_status"
)
View Source
const (
	ChangeActionWrite  = "write"
	ChangeActionStatus = "status"
	ChangeActionDelete = "delete"
)

Change action labels reported through ChangeNotifier subscriptions.

View Source
const MaxPlanContentSize = 10 << 20

MaxPlanContentSize caps a plan's content (its markdown body): it bounds how much update_plan_from_file will read from disk, so a pathological or wrong path cannot make the agent pull an arbitrarily large file into a plan (and into the model's context), and FilesystemStorage refuses to persist content beyond it. 10 MiB is far above any realistic plan while still bounding memory, and content of exactly this size is accepted. It is exported so host-side callers (e.g. the docker agent plans CLI) bound plan content with the same limit instead of duplicating the number.

View Source
const MaxPlanFileSize = MaxPlanContentSize

MaxPlanFileSize is the name MaxPlanContentSize was first exported under.

Deprecated: use MaxPlanContentSize. Kept as an alias so external callers built against the original export keep compiling and share the same bound.

Variables

View Source
var ErrPlanNotFound = errors.New("plan not found")

ErrPlanNotFound is returned by write operations that require an existing plan (set_plan_status) when the named plan does not exist, so callers can tell a missing plan apart from a real backend failure.

Functions

func CreateToolSet

func CreateToolSet() (tools.ToolSet, error)

CreateToolSet is used by the tools registry. It returns a process-wide singleton so that all agents collaborating in the same process share one storage over the global plans folder.

func DefaultDir

func DefaultDir() string

DefaultDir is the global shared folder where plans are stored, under the docker-agent data directory.

func OpenContentFile added in v1.119.0

func OpenContentFile(path string) (*os.File, error)

OpenContentFile opens a plan-content source file read-only so the caller can validate what it actually opened — with File.Stat on the returned descriptor — before reading, instead of trusting a pre-open stat of the path that a concurrent swap could invalidate. The open itself must not hang: a plain open(2) of a FIFO with no writer blocks forever, and some devices (e.g. serial lines) block on open too, so the file is opened with O_NONBLOCK, which makes those opens return immediately. Callers reject anything non-regular after inspecting the descriptor; for the regular files that remain, O_NONBLOCK has no effect on reads, so the flag is left set. It is exported so host-side callers (e.g. the docker agent plans CLI) open user-supplied content paths with the same hang-safety instead of duplicating the platform logic.

func ValidateName added in v1.119.0

func ValidateName(name string) error

ValidateName rejects names that do not match namePattern. It is the canonical plan-name rule, exported so host-side callers (e.g. pkg/plans) validate exactly like the storage instead of duplicating the pattern.

Types

type Change added in v1.119.0

type Change struct {
	Name     string
	Action   string
	Revision int
}

Change describes one successful mutation of a shared plan. It carries identity and version only — never content — so observers refresh through their own storage view instead of trusting an event payload. Revision is the version after a write, or the guard version of a guarded delete (0 when the delete was unguarded). It deliberately carries no mutator identity: the shared singleton toolset executes tool calls for whichever session's agent is running, so a subscriber must not attribute a change to its own agent. The plan's Author field is the collaborative attribution.

type ChangeNotifier added in v1.119.0

type ChangeNotifier interface {
	// SubscribeChanges registers cb and returns an idempotent unsubscribe
	// function that removes exactly this subscription.
	SubscribeChanges(cb func(Change)) (unsubscribe func())
}

ChangeNotifier is the optional host-notification capability of the plan toolset: hosts (e.g. one runtime stream per TUI tab) subscribe callbacks that fire after every successful mutation and never for failed ones. Discover it through toolset wrappers with tools.As. Any number of subscribers may be active at once — every one receives every change — because the shared singleton toolset serves all sessions in the process. Callbacks must be safe for concurrent use and must not block; they do not alter tool schemas, outputs, or storage behavior in any way.

type CorruptPlanError added in v1.119.0

type CorruptPlanError struct {
	// File is the base name of the plan file that failed to decode.
	File string
	Err  error
}

CorruptPlanError is returned when a plan file exists but cannot be decoded, so callers can tell a damaged plan apart from a missing one or a plain I/O failure without matching on error text.

func (*CorruptPlanError) Error added in v1.119.0

func (e *CorruptPlanError) Error() string

func (*CorruptPlanError) Unwrap added in v1.119.0

func (e *CorruptPlanError) Unwrap() error

type DeletePlanArgs

type DeletePlanArgs struct {
	Name              string `json:"name" jsonschema:"The name of the plan to delete."`
	LastKnownRevision *int   `` /* 183-byte string literal not displayed */
}

type ExportPlanToFileArgs added in v1.89.0

type ExportPlanToFileArgs struct {
	Name string `json:"name" jsonschema:"The name of the plan to export."`
	Path string `` /* 185-byte string literal not displayed */
}

type ExportResult added in v1.89.0

type ExportResult struct {
	Name         string `json:"name"`
	Path         string `json:"path"`
	Title        string `json:"title,omitempty"`
	Status       string `json:"status,omitempty"`
	Revision     int    `json:"revision"`
	BytesWritten int    `json:"bytesWritten"`
}

ExportResult is the result of export_plan_to_file. It deliberately omits the plan content: the content is written to disk, not returned, so materialising a plan on disk before an edit costs no output tokens.

type FilesystemStorage added in v1.88.0

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

FilesystemStorage is the default Storage. It persists each plan as a JSON file named <name>.json in a directory, with atomic temp+rename writes, plan name validation, and unreadable-file warnings on List. Upsert and Delete hold an exclusive advisory lock on a persistent sentinel file (lockFileName) in the same directory, so their check-and-mutate windows are serialized against other processes sharing the directory and a stale ExpectedRevision always fails deterministically instead of clobbering a concurrent write. A mutex serializes the local check-and-mutate windows within the process; mutations take it only after the sentinel lock is held, so a writer waiting for a contended sentinel never blocks another local mutation's chance to fail fast on the sentinel. Get and List take neither the file lock nor the mutex: writes are atomic (temp + rename), so a lock-free reader always sees a complete plan — either the previous or the new revision, never a partial file — and a mutation blocked on wedged storage can never stall readers.

func NewFilesystemStorage added in v1.88.0

func NewFilesystemStorage(dir string) *FilesystemStorage

NewFilesystemStorage returns a filesystem-backed Storage rooted at dir. The directory is created lazily on the first write, not here, so a parent that is momentarily unavailable at startup is recovered from automatically.

func (*FilesystemStorage) Delete added in v1.88.0

func (s *FilesystemStorage) Delete(ctx context.Context, name string, expectedRevision *int) (bool, error)

func (*FilesystemStorage) Get added in v1.88.0

func (s *FilesystemStorage) Get(ctx context.Context, name string) (Plan, bool, error)

func (*FilesystemStorage) List added in v1.88.0

func (s *FilesystemStorage) List(ctx context.Context) ([]Summary, []string, error)

func (*FilesystemStorage) String added in v1.88.0

func (s *FilesystemStorage) String() string

String renders the backend for ToolSet.Describe, e.g. "dir=/path/to/plans".

func (*FilesystemStorage) Upsert added in v1.88.0

func (s *FilesystemStorage) Upsert(ctx context.Context, req UpsertRequest) (Plan, error)

type GetPlanStatusArgs added in v1.89.0

type GetPlanStatusArgs struct {
	Name string `json:"name" jsonschema:"The name of the plan whose status to read."`
}

type ListResult

type ListResult struct {
	Plans    []Summary `json:"plans"`
	Warnings []string  `json:"warnings,omitempty"`
}

ListResult is the output of list_plans. Warnings lists plan files that could not be read or decoded, so a caller can tell "no plans exist" apart from "some plans failed to load" — important because an agent that mistakes a temporarily unreadable plan for a missing one could recreate and clobber it.

type Option added in v1.88.0

type Option func(*ToolSet)

Option configures a ToolSet.

func WithStorage added in v1.88.0

func WithStorage(storage Storage) Option

WithStorage injects a custom Storage backend in place of the default FilesystemStorage, letting embedders supply their own store and get per-instance isolation. The provided storage must not be nil.

type Plan

type Plan struct {
	Name    string `json:"name"`
	Title   string `json:"title,omitempty"`
	Content string `json:"content"`
	// Author is a free-form label identifying who last wrote the plan
	// (typically the agent name). It helps collaborators see who made the
	// most recent change.
	Author string `json:"author,omitempty"`
	// Status is a free-form lifecycle label (e.g. "idle", "in-progress",
	// "blocked", "done"). There is no fixed vocabulary: agents and users define
	// their own in the system prompt.
	Status string `json:"status,omitempty"`
	// Revision is the plan's monotonically increasing version number, bumped on
	// every write. Callers capture it on a read and pass it back as
	// last_known_revision to detect concurrent modifications (optimistic
	// locking).
	Revision  int    `json:"revision"`
	UpdatedAt string `json:"updatedAt"`
}

Plan is a shared document collaborated on by the agents.

type ReadPlanArgs

type ReadPlanArgs struct {
	Name string `json:"name" jsonschema:"The name of the plan to read."`
}

type SetPlanStatusArgs added in v1.89.0

type SetPlanStatusArgs struct {
	Name              string `json:"name" jsonschema:"The name of the plan whose status to set."`
	Status            string `` /* 131-byte string literal not displayed */
	LastKnownRevision *int   `` /* 182-byte string literal not displayed */
}

type StatusView added in v1.89.0

type StatusView struct {
	Name     string `json:"name"`
	Status   string `json:"status"`
	Revision int    `json:"revision"`
}

StatusView is the lightweight result of set_plan_status and get_plan_status: just the status and the revision, without the plan body, so reading or writing the status never costs the tokens of the full content.

type Storage added in v1.88.0

type Storage interface {
	// Get returns the named plan. The bool is false with a nil error when no
	// such plan exists, so callers can tell a missing plan apart from a real
	// read failure (returned as a non-nil error).
	Get(ctx context.Context, name string) (Plan, bool, error)
	// Upsert creates or updates a plan as described by req: nil fields are
	// preserved from the previous revision, the optimistic-lock check and the
	// existence guards are honoured atomically with the write, the revision is
	// bumped and UpdatedAt stamped. It returns a *VersionConflictError on a
	// revision mismatch or when req.MustNotExist is set but the plan exists
	// (Expected 0, Current the plan's revision — even revision 0), and
	// ErrPlanNotFound when req.MustExist is set but the plan is absent.
	Upsert(ctx context.Context, req UpsertRequest) (Plan, error)
	// List returns a summary of every stored plan. Warnings carries entries
	// that could not be read, so a caller can tell "no plans" apart from "some
	// plans failed to load".
	List(ctx context.Context) (plans []Summary, warnings []string, err error)
	// Delete removes the named plan. The bool is false with a nil error when
	// there was no such plan to delete. When expectedRevision is non-nil the
	// delete is rejected with a *VersionConflictError unless it matches the
	// plan's current revision.
	Delete(ctx context.Context, name string, expectedRevision *int) (deleted bool, err error)
}

Storage persists the plans a ToolSet operates on. Implementations decide how a plan is stored (files, memory, a database, a remote service) and own the revision bump in Upsert, so a backend can make it atomic. The default FilesystemStorage is used when WithStorage injects nothing else.

func SharedStorage added in v1.119.0

func SharedStorage() Storage

SharedStorage returns the Storage behind the process-wide shared ToolSet (CreateToolSet). Host code that manages plans outside agent tool calls (e.g. a TUI) should use it so both sides serialize on the same instance and mutex. ToolSets built with New keep their own independent storage.

type Summary

type Summary struct {
	Name      string `json:"name"`
	Title     string `json:"title,omitempty"`
	Author    string `json:"author,omitempty"`
	Status    string `json:"status,omitempty"`
	Revision  int    `json:"revision"`
	UpdatedAt string `json:"updatedAt"`
}

Summary is a lightweight view of a plan returned by list_plans.

type ToolSet

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

func New

func New(opts ...Option) *ToolSet

New builds a per-instance plan toolset. With no options it uses the default FilesystemStorage rooted at DefaultDir(); pass WithStorage to inject another backend. Each call returns an independent instance — use CreateToolSet for the process-wide singleton shared by collaborating agents.

func (*ToolSet) Describe

func (t *ToolSet) Describe() string

func (*ToolSet) Instructions

func (t *ToolSet) Instructions() string

func (*ToolSet) SubscribeChanges added in v1.119.0

func (t *ToolSet) SubscribeChanges(cb func(Change)) func()

SubscribeChanges registers cb to run after every successful plan mutation and returns an unsubscribe function. Every active subscriber receives every change exactly once; unsubscribing removes only that subscription and is idempotent. A nil cb registers nothing and returns a no-op unsubscribe. Implements ChangeNotifier.

func (*ToolSet) Tools

func (t *ToolSet) Tools(context.Context) ([]tools.Tool, error)

type UpdatePlanFromFileArgs added in v1.89.0

type UpdatePlanFromFileArgs struct {
	Name              string `json:"name" jsonschema:"The plan name. Lowercase letters, digits, '-' and '_' only."`
	Path              string `` /* 144-byte string literal not displayed */
	Title             string `json:"title,omitempty" jsonschema:"Optional human-readable title. Preserved from the previous revision when omitted."`
	Author            string `` /* 139-byte string literal not displayed */
	Status            string `json:"status,omitempty" jsonschema:"Optional free-form lifecycle status. Preserved from the previous revision when omitted."`
	LastKnownRevision *int   `` /* 182-byte string literal not displayed */
}

type UpsertRequest added in v1.89.0

type UpsertRequest struct {
	Name             string
	Content          *string
	Title            *string
	Author           *string
	Status           *string
	ExpectedRevision *int
	MustExist        bool
	MustNotExist     bool
}

UpsertRequest describes a create-or-update operation on a plan. Content, Title, Author and Status are pointers so an omitted field (nil) preserves the previous value while an explicit value (including "") overwrites it; this lets a caller change just the status without rewriting the body. ExpectedRevision enables optimistic locking: when non-nil, the write is rejected with a *VersionConflictError unless it equals the plan's current revision. MustExist makes the write fail with ErrPlanNotFound when the plan does not already exist (used by set_plan_status, which must not create a plan). MustNotExist makes the write create-only by existence, not by revision: any existing plan is rejected with a *VersionConflictError carrying Expected 0 and the plan's current revision — including revision 0, which a stored plan can legitimately carry (a hand-written or foreign file that omits the field), so ExpectedRevision 0 alone is not a create-only guard.

type VersionConflictError added in v1.89.0

type VersionConflictError struct {
	Name     string
	Expected int
	Current  int
}

VersionConflictError is returned by a write when the caller's last_known_revision does not match the plan's current revision, signalling that another writer changed the plan in the meantime. The caller should re-read the plan and retry against the new revision.

func (*VersionConflictError) Error added in v1.89.0

func (e *VersionConflictError) Error() string

type WritePlanArgs

type WritePlanArgs struct {
	Name              string `json:"name" jsonschema:"The plan name. Lowercase letters, digits, '-' and '_' only (e.g. 'release', 'db-migration')."`
	Content           string `json:"content" jsonschema:"The full plan content (markdown). Replaces the existing plan."`
	Title             string `json:"title,omitempty" jsonschema:"Optional human-readable title. Preserved from the previous revision when omitted."`
	Author            string `` /* 166-byte string literal not displayed */
	Status            string `` /* 164-byte string literal not displayed */
	LastKnownRevision *int   `` /* 219-byte string literal not displayed */
}

Jump to

Keyboard shortcuts

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