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
- Variables
- func CreateToolSet() (tools.ToolSet, error)
- func DefaultDir() string
- func OpenContentFile(path string) (*os.File, error)
- func ValidateName(name string) error
- type Change
- type ChangeNotifier
- type CorruptPlanError
- type DeletePlanArgs
- type ExportPlanToFileArgs
- type ExportResult
- type FilesystemStorage
- func (s *FilesystemStorage) Delete(ctx context.Context, name string, expectedRevision *int) (bool, error)
- func (s *FilesystemStorage) Get(ctx context.Context, name string) (Plan, bool, error)
- func (s *FilesystemStorage) List(ctx context.Context) ([]Summary, []string, error)
- func (s *FilesystemStorage) String() string
- func (s *FilesystemStorage) Upsert(ctx context.Context, req UpsertRequest) (Plan, error)
- type GetPlanStatusArgs
- type ListResult
- type Option
- type Plan
- type ReadPlanArgs
- type SetPlanStatusArgs
- type StatusView
- type Storage
- type Summary
- type ToolSet
- type UpdatePlanFromFileArgs
- type UpsertRequest
- type VersionConflictError
- type WritePlanArgs
Constants ¶
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" )
const ( ChangeActionWrite = "write" ChangeActionStatus = "status" ChangeActionDelete = "delete" )
Change action labels reported through ChangeNotifier subscriptions.
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.
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 ¶
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 ¶
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
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
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
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 ExportPlanToFileArgs ¶ added in v1.89.0
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) 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
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 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 ¶
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) Instructions ¶
func (*ToolSet) SubscribeChanges ¶ added in v1.119.0
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.
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
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 */
}