workflow

package
v1.11.0 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package workflow owns the solo dynamic-workflow task board: a dependency graph of tasks the root agent plans at runtime and an append-only session log that makes the board restart-safe. The wf_task_* tools mutate it as the "root" actor; the in-process dispatch engine (internal/agent) mutates it as the "system" actor, executing structure the root already declared — the writer matrix in store.go is the load-bearing wall.

The package is deliberately free of agent imports so downstream embedders can reuse the board with their own dispatch loop. See docs/roadmap/PRD/solo-dynamic-workflow.md for the design; the execution model is the swarm DWF wave's (docs/roadmap/PRD/swarm-dynamic-workflow.md) ported to solo.

Index

Constants

View Source
const Domain = "workflow"

Domain is the observable.Change.Domain value every board change carries. Consumers switch on this string and type-assert Payload to []workflow.Task.

Variables

View Source
var (
	ErrNotFound    = errors.New("task not found")
	ErrDepNotFound = errors.New("dependency not found")
)

Sentinel errors callers branch on. Detail is wrapped with %w.

Functions

func Names

func Names() []tools.ToolName

Names lists every tool name this package contributes, for profile composition alongside the other package Names() helpers.

Types

type Actor

type Actor string

Actor identifies who is holding the pen for a transition. Root is the judgment writer (the wf_task_* tools); System is the engine performing strictly mechanical edges. There is no worker actor — solo workers have no board tools; their terminal report arrives through the engine.

const (
	ActorRoot   Actor = "root"
	ActorSystem Actor = "system"
)

type AgentTypesLookup

type AgentTypesLookup func() []string

AgentTypesLookup resolves the live subagent-type list for worker validation at create time, or nil when unknown (validation then defers to dispatch).

type Comment

type Comment struct {
	By   Actor     `json:"by"`
	Note string    `json:"note"`
	At   time.Time `json:"at"`
}

Comment is one append-only audit line: force-unblocks, verdicts, worker-lost resets, and root notes all land here.

type CreateTool

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

func NewCreate

func NewCreate(s *Store, dispatch DispatcherLookup, agentTypes AgentTypesLookup) *CreateTool

func (*CreateTool) Description

func (t *CreateTool) Description() string

func (*CreateTool) Execute

func (t *CreateTool) Execute(_ context.Context, logger *slog.Logger, input json.RawMessage) (tools.Result, error)

func (*CreateTool) Name

func (t *CreateTool) Name() string

func (*CreateTool) Schema

func (t *CreateTool) Schema() json.RawMessage

type Dispatcher

type Dispatcher interface {
	Sweep()
}

Dispatcher is the engine seam: after a readiness-changing mutation the tools poke it so ready tasks dispatch on the same tool call that made them ready. Late-bound through a lookup because the engine wires up after the toolset is built; a nil Dispatcher (tests, embedders with their own loop) leaves the board fully functional minus auto-dispatch.

type DispatcherLookup

type DispatcherLookup func() Dispatcher

DispatcherLookup resolves the current Dispatcher, or nil.

type GetTool

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

func NewGet

func NewGet(s *Store) *GetTool

func (*GetTool) Description

func (t *GetTool) Description() string

func (*GetTool) Execute

func (t *GetTool) Execute(_ context.Context, _ *slog.Logger, input json.RawMessage) (tools.Result, error)

func (*GetTool) Name

func (t *GetTool) Name() string

func (*GetTool) Schema

func (t *GetTool) Schema() json.RawMessage

type ListTool

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

func NewList

func NewList(s *Store) *ListTool

func (*ListTool) Description

func (t *ListTool) Description() string

func (*ListTool) Execute

func (t *ListTool) Execute(_ context.Context, _ *slog.Logger, input json.RawMessage) (tools.Result, error)

func (*ListTool) Name

func (t *ListTool) Name() string

func (*ListTool) Schema

func (t *ListTool) Schema() json.RawMessage

type Patch

type Patch struct {
	Subject     *string
	Description *string
	ActiveForm  *string
	Note        string
}

Patch carries the root's field edits for Update. nil pointers leave the field untouched; Note (non-empty) appends an audit comment.

type Status

type Status string

Status enumerates the board's task states. `blocked` is birth-only (a task created with incomplete dependencies) plus the force-unblock escape; `verifying` holds a worker's recorded result awaiting judgment. There is no suspended and no failed — a worker failure lands in verifying with WorkerFailed set, and abandonment is deletion or simply never verifying.

const (
	StatusBlocked   Status = "blocked"
	StatusPending   Status = "pending"
	StatusRunning   Status = "running"
	StatusVerifying Status = "verifying"
	StatusCompleted Status = "completed"
)

func (Status) IsValid

func (s Status) IsValid() bool

IsValid reports whether s is one of the canonical statuses.

type Store

type Store struct {
	observable.Observable
	// contains filtered or unexported fields
}

Store is the per-agent board: tasks, their dependency edges, and the append-only session log that rebuilds them on resume.

Mutations are guarded by mu; Notify fires after the lock is released so observers may freely call back into the store. Safe for concurrent access — the wf_task_* tools (root actor) and the dispatch engine (system actor) share one instance via ToolState.

func NewStore

func NewStore() *Store

NewStore returns an empty, in-memory board. Call SetPersistence + SetSession to attach it to a session log.

func (*Store) Close

func (s *Store) Close()

Close releases the session log handle. The board stays readable.

func (*Store) CompleteWork

func (s *Store) CompleteWork(id, result string, failed bool) (Task, error)

CompleteWork is the worker's terminal report arriving through the engine: result recorded and running → verifying in one mutation. failed marks a crash/kill instead of a natural report — the matrix then refuses to auto-complete regardless of the verify policy.

func (*Store) Counts

func (s *Store) Counts() map[Status]int

Counts tallies tasks per status — the workflow daemon's snapshot meta.

func (*Store) Create

func (s *Store) Create(t Task) (Task, error)

Create validates and inserts a task. The store assigns the id and the birth status: blocked iff any dependency is incomplete, else pending (a dependency on an already-completed task is satisfied at birth). Dependencies must reference existing tasks — with edges immutable after creation this makes the graph acyclic by construction.

func (*Store) Delete

func (s *Store) Delete(id string) error

Delete removes a task created in error. Guarded: a running task has a live worker (stop it first), and a depended-on task would orphan its dependents' edges — both are refused with the reason.

func (*Store) Dependents

func (s *Store) Dependents(id string) []string

Dependents lists ids of tasks that name id in their DependsOn — the cascade a completion may unblock, in creation order.

func (*Store) Dispatch

func (s *Store) Dispatch(id, owner string) (Task, error)

Dispatch is the engine's pending → running edge plus the owner stamp in one mutation: the task records which worker daemon carries it.

func (*Store) Dispatchable

func (s *Store) Dispatchable() []Task

Dispatchable returns, in creation order, every pending engine-managed task. Pending implies dependencies are satisfied (blocked birth + the unblock edge) or the root force-unblocked — either way the engine may dispatch. The concurrency cap is the engine's business, not the store's.

func (*Store) Domain

func (s *Store) Domain() string

Domain identifies this store on the change stream. Required by the observable.Store interface.

func (*Store) Get

func (s *Store) Get(id string) (Task, bool)

Get returns a copy of one task.

func (*Store) List

func (s *Store) List() []Task

List returns copies of every task in creation order.

func (*Store) ResetLostRunning

func (s *Store) ResetLostRunning(alive func(owner string) bool) []Task

ResetLostRunning re-queues running tasks whose worker did not survive a restart: alive reports whether an owner daemon id is still live. Each reset task returns to pending with an audit comment; the caller follows with a dispatch sweep. The restart-recovery half of the engine.

func (*Store) SetPersistence

func (s *Store) SetPersistence(dir string)

SetPersistence sets the directory session logs live under (<dir>/<session>.jsonl). Takes effect at the next SetSession.

func (*Store) SetSession

func (s *Store) SetSession(id string) error

SetSession rotates the board to the given session id: the in-memory state is reset and, when a persistence dir is configured, an existing session log is replayed (a fresh id starts an empty board). The log file itself is created lazily on the first mutation, so a session that never touches the board leaves nothing on disk. Mirrors checkpoint.Manager.SetSession — called at boot, on resume, and on /clear.

func (*Store) Transition

func (s *Store) Transition(id string, to Status, actor Actor, note string) (Task, error)

Transition moves a task along one edge of the writer matrix, appending note (when non-empty) to the audit comments. Owner and WorkerFailed are cleared on any edge back to pending so a re-dispatch starts clean.

func (*Store) UnblockDependents

func (s *Store) UnblockDependents(completedID string) []Task

UnblockDependents flips every blocked dependent of completedID whose dependencies are now all complete to pending (the system edge). Returns the flipped tasks. Idempotent — a second call finds nothing blocked. Transition runs this automatically on every edge into completed; the exported form exists for defensive resweeps.

func (*Store) UnmetDeps

func (s *Store) UnmetDeps(id string) []string

UnmetDeps returns the incomplete dependency ids of a task.

func (*Store) Update

func (s *Store) Update(id string, p Patch) (Task, error)

Update applies the root's field edits. Completed tasks are history — only a note may be appended.

type Task

type Task struct {
	ID          string      `json:"id"`
	Subject     string      `json:"subject"`
	Description string      `json:"description,omitempty"`
	ActiveForm  string      `json:"active_form,omitempty"`
	Status      Status      `json:"status"`
	Verify      Verify      `json:"verify"`
	Worker      *WorkerSpec `json:"worker,omitempty"`
	DependsOn   []string    `json:"depends_on,omitempty"`
	Owner       string      `json:"owner,omitempty"` // daemon id of the running worker
	Result      string      `json:"result,omitempty"`
	// WorkerFailed marks a verifying task whose worker crashed or was
	// killed instead of reporting; the store refuses to auto-complete it
	// regardless of the verify policy.
	WorkerFailed bool      `json:"worker_failed,omitempty"`
	Comments     []Comment `json:"comments,omitempty"`
	CreatedAt    time.Time `json:"created_at"`
	UpdatedAt    time.Time `json:"updated_at"`
}

Task is one node of the board graph.

DependsOn edges may only reference tasks that already exist and are immutable after creation, so the graph is acyclic by construction — no cycle detection exists anywhere (the DWF rule).

func (Task) EngineManaged

func (t Task) EngineManaged() bool

EngineManaged reports whether the engine dispatches this task (it carries a worker spec). Self-tasks are root-managed on every edge.

type UpdateTool

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

func NewUpdate

func NewUpdate(s *Store, dispatch DispatcherLookup) *UpdateTool

func (*UpdateTool) Description

func (t *UpdateTool) Description() string

func (*UpdateTool) Execute

func (t *UpdateTool) Execute(_ context.Context, logger *slog.Logger, input json.RawMessage) (tools.Result, error)

func (*UpdateTool) Name

func (t *UpdateTool) Name() string

func (*UpdateTool) Schema

func (t *UpdateTool) Schema() json.RawMessage

type Verify

type Verify string

Verify is the per-task verification policy, fixed at creation. "leader" (the default) folds the worker's result into the root conversation for judgment; "auto" lets the engine complete the task mechanically and cascade dependents with zero root wakes. Stored as text so a future machine-evidence policy ("checks") slots in without a schema change.

const (
	VerifyLeader Verify = "leader"
	VerifyAuto   Verify = "auto"
)

type VerifyTool

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

func NewVerify

func NewVerify(s *Store, dispatch DispatcherLookup) *VerifyTool

func (*VerifyTool) Description

func (t *VerifyTool) Description() string

func (*VerifyTool) Execute

func (t *VerifyTool) Execute(_ context.Context, logger *slog.Logger, input json.RawMessage) (tools.Result, error)

func (*VerifyTool) Name

func (t *VerifyTool) Name() string

func (*VerifyTool) Schema

func (t *VerifyTool) Schema() json.RawMessage

type WorkerSpec

type WorkerSpec struct {
	AgentType string `json:"agent_type"`
	Isolation string `json:"isolation,omitempty"`
	Level     int    `json:"level,omitempty"`
}

WorkerSpec is the spawn spec fixed at creation for an engine-managed task: which subagent kind to dispatch, whether it runs in an isolated git worktree, and the model capability tier (the AGENT tool's level). A task without a WorkerSpec is a self-task — a step the root agent does itself, tracked on the same graph.

Jump to

Keyboard shortcuts

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