Documentation
¶
Overview ¶
Package exec is the zenflow execution core. It contains the entire historical zenflow root implementation - Orchestrator, Executor, AgentRunner, RunFlow / RunGoal / RunAgent, Storage backends, the agent / coord factory plumbing, parsers, validators, schedulers, and prompt assembly. Carved out of package zenflow root so the public SDK surface (the facade files in zenflow/) sits cleanly above the implementation. Public types and constructors are re-exported via type alias from package zenflow's *_facade.go files; external SDK consumers' `import "github.com/zendev-sh/zenflow"` keeps working unchanged.
Package exec - drop_fanout.go contains the dropFanout dispatcher for DropEvent callbacks (Plan §12.1 / F3 - WithDropCallback / WithDropCallbackBufferSize). It supports both synchronous dispatch (bufSize <= 0) and an async buffered worker goroutine path. The "zero silent drops" invariant is enforced via a synchronous fallback when the buffered channel is full.
Package exec - executor_coord.go contains the executor-side coord wiring: AgentState/activeSteps registry helpers, push helpers (pushCoordEvent / pushStepEventToCoord), wake signalling (signalCoordWake), reverse-reply drain (drainCoordReverseReplies), the coordStepID constant, and the nestedSuppressLifecycleSink wrapper used to keep nested executor runs from spamming the parent's lifecycle sink. The runtime coord runner itself lives in coord_factory.go / coord_lib.go.
Package exec - goal_decomposer.go contains the goal-decomposition LLM helpers used by RunGoal: prompt assembly, single-turn / streaming chat invocation, and JSON response parsing/validation. This file is the "decomposer" coordinator (one-shot LLM that turns a high-level goal into a workflow YAML). It is distinct from the runtime coord runner (see coord_factory.go / coord_lib.go) which streams events during flow execution.
Package exec - progress_pump.go contains the eventBusSinkPump: a buffered async wrapper around a downstream ProgressSink that guarantees non-blocking OnEvent / OnOutput on the critical path (router stepLock, poller invariant-check). On overflow the pump bounded-retries with a timeout; if the timeout fires, the event falls back to a structured log line - preserving the "no silent drops" invariant.
Package exec - step_termination.go contains the per-step termination wait + abort flush helpers used by the executor: waitForStepTermination (3-invariant rule with a stable-tick guard), the F8 hold-timeout wrapper waitForStepTerminationWithHoldTimeout, and flushMailboxOnAbort which drains and closes mailboxes during the workflow-abort flow. Also hosts the StepIdle soft-gate observability counter (stepIdleFallback*) that flags production callers which forgot to wire AgentRunner.Run's SetTerminal defer.
Example ¶
Example demonstrates the minimal embedding pattern: build an Orchestrator, load (or construct) a Workflow, and run it.
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
ctx := context.Background()
// Construct an orchestrator backed by a fake LLM.
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "done"}),
)
defer orch.Close() //nolint:errcheck
// Build a one-step workflow programmatically.
wf := &zenflow.Workflow{
Name: "hello-world",
Steps: []zenflow.Step{
{ID: "greet", Instructions: "Say hello to the user."},
},
}
result, err := orch.RunFlow(ctx, wf)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Status)
}
Output: completed
Index ¶
- Constants
- Variables
- func AgentToolDef() goai.Tool
- func ApplyDefaults(wf *Workflow)
- func AssemblePrompt(agent AgentConfig, step Step, baseDir string, ...) (string, []provider.Part)
- func AssemblePromptWithForEach(agent AgentConfig, step Step, baseDir string, ...) (string, []provider.Part)
- func BuildCoordStepMenu(runner *AgentRunner) string
- func BuildToolCatalog(tools []goai.Tool) string
- func CoordinatorChat(ctx context.Context, model provider.LanguageModel, prompt string) (string, provider.Usage, error)
- func CoordinatorPrompt(goal, toolCatalog string) string
- func CoordinatorStreamChat(ctx context.Context, model provider.LanguageModel, prompt string, ...) (string, provider.Usage, error)
- func DecidePermission(p PermissionPolicy, toolName string, alwaysAllow map[string]bool) (allowed, prompt bool, err error)
- func DetectMixedScript(s string) bool
- func DropReasonStrings() map[router.DropReason]string
- func EvaluateCEL(expr string, ctx *EvalContext) (bool, error)
- func EvaluateCELToArray(expr string, ctx *EvalContext) ([]any, error)
- func EvaluateCELToString(expr string, ctx *EvalContext) (string, error)
- func FilterTools(tools []goai.Tool, allow, disallow []string) []goai.Tool
- func IsHoldTimeout(err error) bool
- func MailboxLen(s MailboxStore, id string) (unread, total int)
- func MessageIDs(msgs []RouterMessage) []string
- func NewBoundedInMemoryStore(n int) *router.BoundedInMemoryStore
- func NewSharedMemoryTools(sm *SharedMemory, agentName string) []goai.Tool
- func NewWakeRegistry() *router.MapWakeRegistry
- func RunCoordinatorLoop(ctx context.Context, runner *AgentRunner, modelID string, ...) func()
- func SandboxDefaultAllow() []string
- func SanitizeUnicode(s string) (string, error)
- func SanitizeWorkflowUnicode(wf *Workflow) error
- func SetRunAgentAsyncRunnerForTest(fn func(*Orchestrator, context.Context, AgentConfig) (*AgentResult, error)) func(*Orchestrator, context.Context, AgentConfig) (*AgentResult, error)
- func SubmitResultToolDef(schema map[string]any) goai.Tool
- func TopoSort(steps []Step) ([]string, error)
- func ValidateToolNames(wf *Workflow, tools []goai.Tool) error
- func ValidateWorkflow(wf *Workflow) ([]string, error)
- func WaitForCoordWake(ctx context.Context, runner *AgentRunner) bool
- func WithEngineClock(c EngineClock) router.EngineOption
- func WithEngineTickInterval(d time.Duration) router.EngineOption
- func WithStepLocker(l router.EngineStepLocker) router.EngineOption
- type AgentConfig
- type AgentError
- type AgentHandle
- type AgentResult
- type AgentRunner
- func (r *AgentRunner) Done() <-chan struct{}
- func (r *AgentRunner) EnsureFinalizeCh() <-chan struct{}
- func (r *AgentRunner) FinalSummary() string
- func (r *AgentRunner) Finalized() bool
- func (r *AgentRunner) MarkFinalized()
- func (r *AgentRunner) Model() provider.LanguageModel
- func (r *AgentRunner) NextForwardSeq() uint64
- func (r *AgentRunner) Progress() ProgressSink
- func (r *AgentRunner) Router() *MessageRouter
- func (r *AgentRunner) Run(ctx context.Context, cfg AgentConfig, userMessage string, model string, ...) (retResult *AgentResult, retErr error)
- func (r *AgentRunner) RunID() string
- func (r *AgentRunner) SetFinalSummary(summary string)
- func (r *AgentRunner) StepID() string
- func (r *AgentRunner) Tools() []goai.Tool
- func (r *AgentRunner) Wake() chan struct{}
- type AgentRunnerOption
- type AgentStatus
- type ApprovalHandler
- type ChanWakeTarget
- type ClosedAware
- type CoordLoopOption
- type CoordOption
- func SynthesizeOnly() CoordOption
- func WithCoordContextProvider(fn func() string) CoordOption
- func WithCoordMaxWakeCycles(n int) CoordOption
- func WithCoordSystemPrompt(prompt string) CoordOption
- func WithCoordSystemPromptSuffix(extra string) CoordOption
- func WithCoordTools(tools ...goai.Tool) CoordOption
- type CoordinatorValidationError
- type CycleError
- type DeliveryEngine
- type DropError
- type DropEvent
- type DropReason
- type DuplicateStepError
- type Duration
- type EngineActiveStepsSource
- type EngineClock
- type EngineWakeRegistry
- type EngineWakeTarget
- type EvalContext
- type EvalStepContext
- type Event
- type EventType
- type Executor
- func (e *Executor) ActiveSteps() []string
- func (e *Executor) AgentState(stepID string) *goai.AgentState
- func (e *Executor) CanResume(_ string) bool
- func (e *Executor) ResumeStep(ctx context.Context, stepID, prompt, fromAgent string) (*ResumeHandle, error)
- func (e *Executor) Run(ctx context.Context) (*WorkflowResult, error)
- func (e *Executor) StepModelString(stepID string) string
- type FactoryCache
- type FileStorage
- func (f *FileStorage) LoadRun(_ context.Context, id string) (*Run, error)
- func (f *FileStorage) LoadSharedMemory(_ context.Context, runID string) (map[string]string, error)
- func (f *FileStorage) LoadStepResult(_ context.Context, runID, stepID string) (*StepResult, error)
- func (f *FileStorage) SaveRun(_ context.Context, run *Run) error
- func (f *FileStorage) SaveSharedMemory(_ context.Context, runID string, entries map[string]string) error
- func (f *FileStorage) SaveStepResult(_ context.Context, runID, stepID string, result *StepResult) error
- type ForEachContext
- type HostSpecificEnvError
- type InMemoryMailboxStore
- type IncludeConflictError
- type JSONParseError
- type LenAware
- type Loop
- type LoopValidationError
- type MCPConfig
- type MCPOption
- type MCPServerConfig
- type MCPToolset
- type MailboxStore
- type MemoryStorage
- func (m *MemoryStorage) DeleteRun(runID string)
- func (m *MemoryStorage) LoadRun(_ context.Context, id string) (*Run, error)
- func (m *MemoryStorage) LoadSharedMemory(_ context.Context, runID string) (map[string]string, error)
- func (m *MemoryStorage) LoadStepResult(_ context.Context, runID, stepID string) (*StepResult, error)
- func (m *MemoryStorage) SaveRun(_ context.Context, run *Run) error
- func (m *MemoryStorage) SaveSharedMemory(_ context.Context, runID string, entries map[string]string) error
- func (m *MemoryStorage) SaveStepResult(_ context.Context, runID, stepID string, result *StepResult) error
- type MessageKind
- type MessageRouter
- type MissingAgentError
- type MissingDepError
- type MissingNameError
- type ModelResolver
- type NoStepsError
- type NopIsolation
- type Option
- func WithAdditionalTools(tools ...goai.Tool) Option
- func WithAgentHandleTTL(d time.Duration) Option
- func WithApproval(h ApprovalHandler) Option
- func WithApprovalTimeout(d time.Duration) Option
- func WithCoordinator(runner *AgentRunner) Option
- func WithDefaultModel(model string) Option
- func WithDropCallback(fn func(DropEvent)) Option
- func WithDropCallbackBufferSize(n int) Option
- func WithExternalInbox(ids ...string) Option
- func WithForceModel(model string) Option
- func WithGoAIOptions(opts ...goai.Option) Option
- func WithHoldTimeout(d time.Duration) Option
- func WithIsolation(iso StepIsolation) Option
- func WithMailboxDelivery() Option
- func WithMailboxStore(factory func() MailboxStore) Option
- func WithMaxConcurrency(n int) Option
- func WithMaxDepth(n int) Option
- func WithMaxMailboxSize(n int) Option
- func WithMaxTranscriptBytes(b int64) Option
- func WithMaxTranscriptMessages(n int) Option
- func WithMaxTurns(n int) Option
- func WithMaxWakeCycles(n int) Option
- func WithModel(m provider.LanguageModel) Option
- func WithModelResolver(r ModelResolver) Option
- func WithOutputTransform(t OutputTransformer) Option
- func WithPermissions(h PermissionHandler) Option
- func WithProgress(s ProgressSink) Option
- func WithProgressBufferSize(n int) Option
- func WithRouterObserver(fn func(*MessageRouter)) Option
- func WithRunID(runID string) Option
- func WithSharedMemory(sm *SharedMemory) Option
- func WithStorage(s Storage) Option
- func WithStreaming() Option
- func WithTools(tools ...goai.Tool) Option
- func WithTracer(t Tracer) Option
- func WithTranscriptStore(factory func() resume.TranscriptStore) Option
- func WithTruncationOnCapReached() Option
- func WithVerbose() Option
- func WithoutMailboxDelivery() Option
- func WithoutStreaming() Option
- func WithoutTruncationOnCapReached() Option
- func WithoutVerbose() Option
- type Orchestrator
- func (o *Orchestrator) Close() error
- func (o *Orchestrator) Coordinator() *AgentRunner
- func (o *Orchestrator) DefaultModel() string
- func (o *Orchestrator) HasLLM() bool
- func (o *Orchestrator) IsClosed() bool
- func (o *Orchestrator) ListAgentHandles(sessionID string) []*AgentHandle
- func (o *Orchestrator) MaxDepth() int
- func (o *Orchestrator) Progress() ProgressSink
- func (o *Orchestrator) ResumeFlow(ctx context.Context, runID string, wf *Workflow) (*WorkflowResult, error)
- func (o *Orchestrator) RunAgent(ctx context.Context, cfg AgentConfig) (*AgentResult, error)
- func (o *Orchestrator) RunAgentAsync(ctx context.Context, cfg AgentConfig) (*AgentHandle, error)
- func (o *Orchestrator) RunFlow(ctx context.Context, wf *Workflow, opts ...RunFlowOption) (*WorkflowResult, error)
- func (o *Orchestrator) RunGoal(ctx context.Context, goal string, opts ...RunGoalOption) (result *WorkflowResult, err error)
- type Output
- type OutputTransformer
- type PermissionHandler
- type PermissionPolicy
- type PermissionRequest
- type PortabilityWarning
- type ProgressSink
- type RealClock
- type ResumeHandle
- type ResumeStates
- type Resumer
- type RouterMessage
- type Run
- type RunFlowOption
- type RunGoalOption
- type RunnerOption
- func WithRunnerGoAIOptions(opts ...goai.Option) RunnerOption
- func WithRunnerInitialMessages(msgs []provider.Message) RunnerOption
- func WithRunnerMailbox(m MailboxStore) RunnerOption
- func WithRunnerMaxWakeCycles(n int) RunnerOption
- func WithRunnerModel(m provider.LanguageModel) RunnerOption
- func WithRunnerModelID(id string) RunnerOption
- func WithRunnerPermissions(h PermissionHandler) RunnerOption
- func WithRunnerPreStartDrainGate(gate <-chan struct{}) RunnerOption
- func WithRunnerProgress(s ProgressSink) RunnerOption
- func WithRunnerRouter(rt *MessageRouter) RunnerOption
- func WithRunnerRunID(id string) RunnerOption
- func WithRunnerSpawnDepth(depth int) RunnerOption
- func WithRunnerSpawnParentCallID(id string) RunnerOption
- func WithRunnerStateRef(s *goai.AgentState) RunnerOption
- func WithRunnerStepID(id string) RunnerOption
- func WithRunnerStreaming() RunnerOption
- func WithRunnerSystemPrompt(prompt string) RunnerOption
- func WithRunnerTools(tools ...goai.Tool) RunnerOption
- func WithRunnerTranscript(ts resume.TranscriptStore) RunnerOption
- func WithRunnerVerbose() RunnerOption
- func WithRunnerWake(ch chan struct{}) RunnerOption
- func WithRunnerWakeContextProvider(fn func() string) RunnerOption
- type SharedMemory
- func (sm *SharedMemory) Entries() map[string]string
- func (sm *SharedMemory) ListByAgent(agent string) map[string]string
- func (sm *SharedMemory) LoadEntries(entries map[string]string)
- func (sm *SharedMemory) Read(qualifiedKey string) (string, bool)
- func (sm *SharedMemory) Summary() string
- func (sm *SharedMemory) Write(agent, key, value string)
- type Step
- type StepIsolation
- type StepResult
- type StepStatus
- type Storage
- type SubmitResultHandler
- type TokenBudgetTransformer
- type ToolNotFoundError
- type Tracer
- type UnicodeUnsafeError
- type ValidationError
- type Workflow
- type WorkflowOptions
- type WorkflowResult
- type WorkflowStatus
Examples ¶
- Package
- New
- Orchestrator.RunFlow
- Orchestrator.RunGoal
- ParseWorkflow
- ParseWorkflow (MultiError)
- WithApproval
- WithApprovalTimeout
- WithExternalInbox
- WithMailboxStore
- WithMaxConcurrency
- WithMaxDepth
- WithMaxMailboxSize
- WithMaxTurns
- WithModel
- WithModelResolver
- WithPermissions
- WithProgress
- WithSharedMemory
- WithStorage
- WithStreaming
- WithTools
Constants ¶
const ( RouterMessageInfo = router.MessageInfo RouterMessageCancel = router.MessageCancel RouterMessageContextUpdate = router.MessageContextUpdate RouterMessageResumeReply = router.MessageResumeReply )
const ( DropReasonUnspecified = router.DropReasonUnspecified DropReasonWorkflowCancelled = router.DropReasonWorkflowCancelled DropReasonTargetTerminal = router.DropReasonTargetTerminal DropReasonUnknownStep = router.DropReasonUnknownStep DropReasonMailboxClosedByFinalize = router.DropReasonMailboxClosedByFinalize DropReasonMaxWakeCycles = router.DropReasonMaxWakeCycles DropReasonHoldTimeout = router.DropReasonHoldTimeout DropReasonMailboxFull = router.DropReasonMailboxFull DropReasonNoTranscript = router.DropReasonNoTranscript DropReasonTranscriptTooLarge = router.DropReasonTranscriptTooLarge DropReasonResumeShutdown = router.DropReasonResumeShutdown DropReasonResolverError = router.DropReasonResolverError )
const ( // MaxStepsPerWorkflow caps steps in a single Workflow.Steps list // (top-level only; loop inner steps counted separately). MaxStepsPerWorkflow = 100 // MaxNestingDepth caps the @-reference chain depth in // resolveChainedRef (parse.go). Sub-workflow include nesting has // its own cap at MaxIncludeDepth = 5; loop nesting is forbidden by // the parser. This constant is NOT shared with those. MaxNestingDepth = 20 // MaxDescriptionChars caps Workflow.Description + Step.Instructions // length (guards against huge adversarial payloads). MaxDescriptionChars = 2000 // MaxFileSizeBytes caps raw YAML/JSON input size passed to // ParseWorkflow. 1 MiB is ample for human-authored flows. MaxFileSizeBytes = 1 << 20 // MaxIncludeDepth is the maximum allowed include nesting depth // (spec §7 line 480). runIncludeStep rejects execution when // IncludeDepth >= MaxIncludeDepth. MaxIncludeDepth = 5 // MaxAttachmentSizeBytes caps any single contextFiles entry // (binary or text) so a multi-GB attachment cannot be slurped // into memory before the prompt assembler has a chance to bail. // R7A-4. MaxAttachmentSizeBytes = 10 * 1024 * 1024 // 10 MiB )
YAML workflow limits enforced by ParseWorkflow / ParseWorkflowJSON before ValidateWorkflow returns. These are library-level hard caps: any zenflow consumer benefits from rejecting pathological workflows early (oversized, unbounded nesting, injection-prone step IDs, etc.). Rejection produces *ValidationError; callers can emit EventError on their side.
const CoordRouterInboxID = coord.CoordRouterInboxID
CoordRouterInboxID mirrors the canonical coord inbox key. Kept as a package-level constant because the public facade (zenflow root's agent_facade.go) re-exports `zenflow.CoordRouterInboxID = exec.CoordRouterInboxID`; removing this alias would force a facade rewrite. Same value as coord.CoordRouterInboxID.
const DefaultAgentHandleTTL = 30 * time.Minute
DefaultAgentHandleTTL is the start-to-finish wall-clock cap on a RunAgentAsync handle. When exceeded, the handle is force-completed with AgentError{Sentinel: ErrAgentHandleTimeout} and the inner context is cancelled. The TTL is NOT reset by progress events. Override at orchestrator-construction time via WithAgentHandleTTL. CLI consumers may map the ZENFLOW_AGENT_HANDLE_TTL environment variable to that option (see cmd/zenflow's orchestrator_opts.go); the library itself does not read environment variables.
const DefaultCoordCleanupTimeout = 2 * time.Second
DefaultCoordCleanupTimeout is the default wall-clock cap on the cleanup phase of RunCoordinatorLoop: the cleanup func cancels the background coord ctx, then waits up to this duration for the goroutine to acknowledge the cancel before returning. A hung coord LLM that ignores ctx cancellation will leak its goroutine until process exit rather than block CLI shutdown indefinitely.
const DefaultCoordColdStartPrompt = "" /* 391-byte string literal not displayed */
DefaultCoordColdStartPrompt is the user message zenflow's CLI sends on the very first coord Run cycle. It tells the LLM to wait silently until events arrive (avoids cosmetic "no events" narration on an empty mailbox) and explicitly defers finalize until the workflow has actually completed. Use directly or as a starting point for a customised prompt:
userMsg := zenflow.DefaultCoordColdStartPrompt + zenflow.BuildCoordStepMenu(runner)
const DefaultCoordContinuationPrompt = "" /* 160-byte string literal not displayed */
DefaultCoordContinuationPrompt is the user message sent on every subsequent coord Run re-entry (after Wake fires or pending mailbox content forces a re-entry). Kept short by design: the wake-blocking pattern (see WaitForCoordWake) guarantees the LLM is only invoked when there ARE events, so the prompt does not need to ask the LLM to "check" anything. hardening - verbose "check your mailbox" phrasing here baited weak prompt-followers (MiniMax) into echoing it as filler narration.
const DefaultCoordSystemPrompt = `` /* 6987-byte string literal not displayed */
DefaultCoordSystemPrompt is the system prompt installed by NewDefaultCoordRunner when the caller does not override it. Exported so consumers can append integration-specific guidance without losing the tested baseline:
zenflow.NewDefaultCoordRunner(llm,
zenflow.WithCoordSystemPrompt(zenflow.DefaultCoordSystemPrompt + extras)) or via the convenience option:
zenflow.NewDefaultCoordRunner(llm,
zenflow.WithCoordSystemPromptSuffix(extras)) The prompt names every default tool (forward_to_agent, narrate, finalize) so a future rename will trip TestDefaultCoordSystemPrompt_MentionsTools and force the prompt to stay in lockstep with the tool surface. The prompt body is deliberately compact. See SPEC.md for the coordinator contract (event semantics, recommended decision flow, budget guidance, narration cadence).
const DefaultMaxBytesPerDep = 8 * 1024
DefaultMaxBytesPerDep is the per-dependency content cap applied by the default OutputTransform (TokenBudgetTransformer). 8 KiB ≈ 2 K tokens - safe for tight context windows while still allowing larger windows to work (truncation is a ceiling, not a floor). Callers that want a different cap install WithOutputTransform with their own transformer.
const DefaultMaxMailboxSize = 10000
DefaultMaxMailboxSize is the per-step mailbox cap installed by New when the caller does not supply WithMaxMailboxSize. The default keeps pathological producers from exhausting memory in long-running workflows; callers that need an unbounded queue must opt out explicitly via WithMaxMailboxSize(0).
const MetadataKeyResumeReverse = router.MetadataKeyResumeReverse
Variables ¶
var ( // ErrAgentHandleTimeout signals the handle exceeded its TTL before // the agent returned. The handle is force-completed; the agent // goroutine may still be running and is cancelled via its ctx, but // its later arrival (if any) is discarded. ErrAgentHandleTimeout = errors.New("zenflow: agent handle TTL exceeded") // ErrAgentCancelled signals the handle was cancelled via // AgentHandle.Cancel before the agent completed normally. ErrAgentCancelled = errors.New("zenflow: agent cancelled") // ErrAgentPanicked signals the agent goroutine recovered a panic. // The recovered value is in AgentError.Msg. ErrAgentPanicked = errors.New("zenflow: agent panicked") // ErrInvalidAgentHandleID signals that NewAgentHandle was called // with an empty id. Returned instead of panicking so callers can // surface the misuse via the error path. ErrInvalidAgentHandleID = errors.New("zenflow: agent handle ID must be non-empty") )
Sentinel errors used by AgentError.Sentinel. Consumers should use errors.Is(err, ErrAgent*) to classify a failed handle.
var ( ErrResumeShutdown = router.ErrResumeShutdown ErrModelResolverMissing = router.ErrModelResolverMissing ErrModelResolverError = router.ErrModelResolverError ErrMailboxFullOnResume = router.ErrMailboxFullOnResume ErrMailboxFull = router.ErrMailboxFull )
var ErrAgentNoSubmitResult = errors.New("zenflow: agent finished without calling submit_result")
ErrAgentNoSubmitResult is returned by AgentRunner.Run when an agent finishes without calling submit_result despite having a resultSchema. Stable.
var ErrAgentToolDirectInvocation = errors.New("zenflow: agent tool must be handled by spawner, not executed directly")
ErrAgentToolDirectInvocation is returned by AgentToolDef's Execute stub when the agent tool is invoked directly through the goai tool loop. The agent tool is intercepted by the AgentRunner spawner hook (OnBeforeToolExecute); reaching the Execute body indicates a wiring bug in the consumer (spawner not registered or hook bypassed). The sentinel allows callers to detect this via errors.Is instead of substring matching, while preserving the (string, error) return contract so the goai loop surfaces a clean tool-result error rather than panicking mid-conversation.
var ErrAgentTurnLimitExceeded = errors.New("zenflow: agent exhausted turn limit without submit_result")
ErrAgentTurnLimitExceeded is returned by AgentRunner.Run when an agent exhausts its turn limit without calling submit_result. Stable.
var ErrApprovalTimeout = errors.New("zenflow: approval handler timed out (use WithApprovalTimeout to adjust the window)")
ErrApprovalTimeout indicates the ApprovalHandler.ApprovePlan call exceeded the duration configured via WithApprovalTimeout. When returned, the workflow aborts cleanly.
var ErrEmptyGoal = errors.New("zenflow: goal must not be empty")
ErrEmptyGoal is returned by RunGoal when the goal string is empty or whitespace-only. Callers may errors.Is on this sentinel to distinguish "caller sent empty goal" from LLM or executor failures. Stable.
var ErrIncludeDepthExceeded = errors.New("zenflow: max include depth exceeded")
ErrIncludeDepthExceeded is returned (wrapped) when an include chain exceeds MaxIncludeDepth. Callers may match via errors.Is. Stable.
var ErrIncludePathEscape = errors.New("zenflow: include path escapes workflow directory")
ErrIncludePathEscape is returned (wrapped) when an include ref resolves to a path outside the workflow's BaseDir. Callers may match via errors.Is to distinguish path-traversal rejections from generic load-failures. Stable.
var ErrModelRequired = errors.New("zenflow: LLM provider is required (use WithModel)")
ErrModelRequired is returned when an entrypoint that needs an LLM is called without one configured. Caller fix: pass WithModel(...) to New or set Model on the per-call AgentConfig.
var ErrNilAgentHandle = errors.New("zenflow: nil AgentHandle")
ErrNilAgentHandle is returned by methods called on a nil *AgentHandle receiver (defensive guard for callers that race a Close with concurrent use of the handle).
var ErrNilFactoryInner = errors.New("zenflow: factory cache inner factory must be non-nil")
ErrNilFactoryInner signals that NewFactoryCache was called with a nil inner factory. Returned instead of panicking so callers can surface the misuse via the error path.
var ErrNilOrchestrator = errors.New("zenflow: nil Orchestrator")
ErrNilOrchestrator is returned by methods called on a nil *Orchestrator receiver. Same defensive intent as ErrNilAgentHandle.
var ErrOrchestratorClosed = errors.New("zenflow: orchestrator closed")
ErrOrchestratorClosed is returned by RunAgentAsync (and related lifecycle methods) when the caller has already invoked Close on the Orchestrator. A closed orchestrator does not accept new work - the caller must construct a fresh one (or, when using the factory cache, remove the sessionID entry so the next call re-creates it).
var ErrPlanDenied = errors.New("zenflow: plan denied by approval handler")
ErrPlanDenied is returned by RunGoal when the configured ApprovalHandler returns false for the LLM-decomposed plan. Distinct from ErrApprovalTimeout (handler ran but exceeded its window) so callers can tell "I said no" from "the handler took too long."
var ErrRefPathEscape = errors.New("zenflow: ref path escapes workflow directory")
ErrRefPathEscape is returned (wrapped) when an @-ref path resolves to a location outside the workflow's BaseDir. Callers may match via errors.Is to distinguish path-traversal rejections from generic ref-load failures (stat/read errors, size cap). Stable.
var ErrResumeNoModel = errors.New("zenflow: resume: executor has no model (use WithModel)")
ErrResumeNoModel is returned by ResumeStep when neither the transcript nor the executor has a model resolver - resume cannot construct a runner without one.
var ErrRunNotFound = errors.New("zenflow: run not found")
ErrRunNotFound is returned by Storage.LoadRun when the run does not exist. Stable.
var ErrRunnerNil = errors.New("zenflow: Executor.Runner is nil (use Orchestrator or set Runner before calling Run)")
ErrRunnerNil is returned by Executor.Run when called on a struct constructed without a Runner. Extracted from a bare fmt.Errorf at the call site so callers can errors.Is on it. Most production callers go through Orchestrator (which fills Runner internally) so this is most often hit by direct-Executor unit tests that forgot the Runner field.
var ErrStepNotFound = errors.New("zenflow: step result not found")
ErrStepNotFound is returned by Storage.LoadStepResult when the step has no persisted result. Stable.
var ErrStorageRequired = errors.New("zenflow: storage is required for resume (use WithStorage)")
ErrStorageRequired is returned by ResumeFlow when no Storage backend is configured. Caller fix: pass WithStorage(...) to New.
var ErrToolDenied = errors.New("zenflow: tool denied by --deny flag")
ErrToolDenied is returned (wrapped) by DecidePermission when a tool matches the policy's Deny list. Callers may match via errors.Is to distinguish deny-flag rejections from strict-mode rejections. Stable.
var ErrToolNotAllowed = errors.New("zenflow: tool not in --allow list (--strict mode)")
ErrToolNotAllowed is returned (wrapped) by DecidePermission when a strict-mode policy rejects a tool that is not on the Allow list. Callers may match via errors.Is. Stable.
var ErrWorkflowNil = errors.New("zenflow: workflow must not be nil")
ErrWorkflowNil is returned by RunFlow and ResumeFlow when the caller passes a nil *Workflow. Defensive guard for callers that race a workflow load with a per-call run.
var GenerateRunID = func() (string, error) { b := make([]byte, 8) if _, err := runIDRandRead(b); err != nil { return "", fmt.Errorf("generate run ID: %w", err) } return fmt.Sprintf("run_%x", b), nil }
GenerateRunID creates a random run identifier. Package-level var for test injection.
Functions ¶
func AgentToolDef ¶
AgentToolDef returns the tool definition for the agent spawning tool. Schema notes: - `model`: omit field entirely to inherit parent's model. Do NOT pass literal strings like "default" or "auto" - those are interpreted as model identifiers and fail provider lookup. - `tools`: must be a SUBSET of the parent's available tool names. Pass the names the child genuinely needs; an empty array inherits parent's full tool set. - `run_in_background`: keep `false` unless you explicitly need fire- and-forget. Backgrounded children deliver results to the inbox on the parent's next turn.
func ApplyDefaults ¶
func ApplyDefaults(wf *Workflow)
ApplyDefaults fills in schema-defined default values for fields left at their Go zero value after YAML unmarshalling.
func AssemblePrompt ¶
func AssemblePrompt(agent AgentConfig, step Step, baseDir string, priorResults map[string]*StepResult) (string, []provider.Part)
AssemblePrompt builds the user prompt from agent config, step definition, workflow base directory (for resolving relative context file paths), and completed results from dependency steps. Returns the text prompt and any multimodal attachments (images, PDFs).
func AssemblePromptWithForEach ¶
func AssemblePromptWithForEach(agent AgentConfig, step Step, baseDir string, priorResults map[string]*StepResult, fe *ForEachContext) (string, []provider.Part)
AssemblePromptWithForEach builds the user prompt with optional forEach item injection. Returns the text prompt and any multimodal attachments (images, PDFs).
func BuildCoordStepMenu ¶
func BuildCoordStepMenu(runner *AgentRunner) string
BuildCoordStepMenu returns a human-readable list of FORWARDABLE step IDs for the coord LLM, filtering out wrapper containers (Loop / Include) registered via Router.RegisterWrapperStep. Designed to be appended to coord prompts each wake so the LLM sees an up-to-date snapshot as new loop iterations register sub-steps:
userMsg := zenflow.DefaultCoordContinuationPrompt + zenflow.BuildCoordStepMenu(runner)
Returns the empty string when: - runner / runner.Router is nil - no steps registered (cold-start before executor begins) - every registered step is a wrapper (rare; degenerate workflow) The menu includes a "do NOT invent step IDs" rule because confirmed that listing valid IDs alone tempts weak LLMs to fabricate neighbouring IDs (e.g. "X.1.<step>" before iteration 1 starts). Single source of truth: the wrapper filter consults Router's explicit marker (set by executor when registering steps). The same marker drives ForwardToAgentToolDef's reject path, so consumer-side filtering and tool-side validation cannot drift.
func BuildToolCatalog ¶
BuildToolCatalog creates a human-readable listing of available tools.
func CoordinatorChat ¶
func CoordinatorChat(ctx context.Context, model provider.LanguageModel, prompt string) (string, provider.Usage, error)
CoordinatorChat sends a single-turn prompt to the goal-decomposition coordinator LLM and returns the raw response content. Used by RunGoal's decomposition + retry loop. Returns the response text and token usage.
func CoordinatorPrompt ¶
CoordinatorPrompt builds the system prompt for the coordinator LLM call. The coordinator decomposes a goal into a workflow (agents + steps) given the available tool catalog. The prompt includes the full Zenflow JSON Schema (embedded from spec/v1/schema.json) so the LLM can use all spec features (loop, condition, resultSchema, options, etc.) without hardcoded subsets.
func CoordinatorStreamChat ¶
func CoordinatorStreamChat(ctx context.Context, model provider.LanguageModel, prompt string, onText func(string), onReasoning func(string)) (string, provider.Usage, error)
CoordinatorStreamChat is the streaming variant of CoordinatorChat used by RunGoal when streaming is enabled. onText/onReasoning fire per chunk; the accumulated text-only content (reasoning excluded) is returned so JSON downstream parsing is not polluted by reasoning tokens.
func DecidePermission ¶
func DecidePermission(p PermissionPolicy, toolName string, alwaysAllow map[string]bool) (allowed, prompt bool, err error)
DecidePermission applies the policy to a single tool call. Return values: - allowed=true, prompt=false, err=nil → run the tool. - allowed=false, prompt=false, err!=nil → reject; err is the user-facing reason (deny match, strict no-match). - allowed=false, prompt=true, err=nil → policy is silent; caller must decide (typically: ask the user, or deny on non-TTY). alwaysAllow may be nil; a nil map is treated as empty.
func DetectMixedScript ¶
DetectMixedScript reports whether s contains code points from two or more distinct Unicode scripts (e.g. Latin + Cyrillic). It is a pure observability helper - callers log a warning but do NOT reject the input, because legitimate multilingual workflows exist. The detector only considers scripts that frequently host homoglyph attacks: Latin, Cyrillic, Greek, Armenian, Hebrew, Arabic, Han. Common/Inherited (digits, punctuation, combining marks) and scripts outside that set do not trigger the signal so the false-positive rate stays low.
func DropReasonStrings ¶
func DropReasonStrings() map[router.DropReason]string
func EvaluateCEL ¶
func EvaluateCEL(expr string, ctx *EvalContext) (bool, error)
EvaluateCEL compiles and evaluates a CEL expression against the given context. The expression MUST evaluate to a bool; non-bool results return an error. Note: CEL evaluation does not accept a context.Context for cancellation. CostLimit(10000) bounds CPU cost. Expressions complete in microseconds-milliseconds.
func EvaluateCELToArray ¶
func EvaluateCELToArray(expr string, ctx *EvalContext) ([]any, error)
EvaluateCELToArray compiles and evaluates a CEL expression that must return a list. Used by forEach CEL expressions to produce the iteration array.
func EvaluateCELToString ¶ added in v0.2.0
func EvaluateCELToString(expr string, ctx *EvalContext) (string, error)
EvaluateCELToString compiles and evaluates a CEL expression that must return a string. Used by command step argument evaluation for args starting with '$'.
func FilterTools ¶
FilterTools returns a subset of tools based on allow/disallow lists. If allow is non-empty, only tools whose names appear in allow are returned. Tools whose names appear in disallow are always excluded.
An entry also matches as an MCP server group: a bare server name ("firecrawl") selects every tool that server contributed ("firecrawl__scrape", "firecrawl__crawl", ...). Built-in and auto-injected tool names contain no "__" separator, so group matching never affects them.
func IsHoldTimeout ¶
IsHoldTimeout reports whether err originated from the F8 hold-timeout cap. Exposed so tests can assert the cause.
func MailboxLen ¶
func MailboxLen(s MailboxStore, id string) (unread, total int)
func MessageIDs ¶
func MessageIDs(msgs []RouterMessage) []string
func NewBoundedInMemoryStore ¶
func NewBoundedInMemoryStore(n int) *router.BoundedInMemoryStore
func NewSharedMemoryTools ¶
func NewSharedMemoryTools(sm *SharedMemory, agentName string) []goai.Tool
NewSharedMemoryTools returns goai.Tool values that read/write to sm under the given agentName namespace.
func NewWakeRegistry ¶
func NewWakeRegistry() *router.MapWakeRegistry
func RunCoordinatorLoop ¶
func RunCoordinatorLoop(ctx context.Context, runner *AgentRunner, modelID string, opts ...CoordLoopOption) func()
RunCoordinatorLoop spawns the coordinator AgentRunner's Run loop on a background goroutine and returns a cleanup func the caller must defer. Suitable for CLI / TUI / embedder consumers that own a coordinator runner across the lifetime of a workflow run. The loop: - Calls runner.Run with DefaultCoordColdStartPrompt + the current BuildCoordStepMenu(runner) snapshot on cold start. - On natural-stop+empty-mailbox exit, blocks in WaitForCoordWake until either the mailbox has pending events or runner.Wake fires. - On wake, re-spawns runner.Run with DefaultCoordContinuationPrompt + a refreshed step menu so the LLM sees current state. - Logs LLM errors via slog.WarnContext (non-fatal - the workflow DAG continues even when coord narration fails). - Exits when ctx is cancelled. IGNORE coord's finalize call: workflow lifetime is owned by the executor, not the coord LLM. Sonnet (observed) hallucinates workflow state from aggregated counters in RouterMessage content and finalizes after the FIRST step despite prompt warnings. Coord lifetime is bound to ctx cancellation; the finalize tool remains callable for downstream consumers (TUI integration) that may want the signal. When runner is nil, returns a no-op cleanup func. Stable.
func SandboxDefaultAllow ¶
func SandboxDefaultAllow() []string
SandboxDefaultAllow returns the canonical safe-tool allow-list applied by --sandbox: read, write, grep, glob. bash is intentionally absent - the whole point of sandbox mode is to block shell access. Callers may extend with --allow, but bash remains blocked even then (sandbox wins). Re-exported at the root facade as zenflow.SandboxDefaultAllow. Returns a fresh slice on each call so callers cannot mutate the canonical list. Stable.
func SanitizeUnicode ¶
SanitizeUnicode returns an NFC-normalised copy of s with C0/C1 controls stripped. It returns UnicodeUnsafeError when a bidi-override code point is present - the caller should refuse the input.
func SanitizeWorkflowUnicode ¶
SanitizeWorkflowUnicode runs SanitizeUnicode on every user-facing string field of wf and validates agent map keys against strictStepIDPattern. Rationale: - Bidi-override code points (U+202A-E, U+2066-9) and other non-printable controls in instructions / prompts can be used to hide malicious payloads inside an otherwise innocent-looking workflow. Reject at parse time so downstream consumers see only normalised text. - Agent map keys propagate unsanitised into prompt scaffolding, trace IDs, and filesystem paths (the include resolver uses agent identity in error messages). An attacker-controlled key like `../../etc/passwd` or `agent\nrm -rf` could escape any of those contexts. The strict pattern rejects everything outside `^[a-z][a-z0-9_-]{0,63}$` (matches step IDs).
func SetRunAgentAsyncRunnerForTest ¶
func SetRunAgentAsyncRunnerForTest(fn func(*Orchestrator, context.Context, AgentConfig) (*AgentResult, error)) func(*Orchestrator, context.Context, AgentConfig) (*AgentResult, error)
SetRunAgentAsyncRunnerForTest swaps runAgentAsyncRunner so tests can capture the AgentConfig passed to RunAgentAsync without depending on a real LLM. Returns the previous runner so the caller can restore it. **WARNING: Test-only. NOT for production callers.** This function mutates a package-level variable visible to every concurrent goroutine in the process; calling it from production code would silently break every active and future RunAgentAsync invocation. Exported for cross-module test use: cross-module consumer tests (and other consumer packages that wire zenflow into their own task systems) need this to verify AgentConfig propagation without standing up a real provider. The "ForTest" suffix is the canonical Go marker for an exported test-only seam.
func SubmitResultToolDef ¶
SubmitResultToolDef returns a goai.Tool for the submit_result tool. If schema is nil, returns a zero-value goai.Tool (Name="").
func TopoSort ¶
TopoSort performs topological sort on workflow steps using Kahn's algorithm. Returns ordered step IDs or CycleError if the graph contains a cycle.
func ValidateToolNames ¶
ValidateToolNames checks that all tool names referenced in agent configs exist in the provided tool catalog. Returns errToolNotFound for the first unknown tool. also rejects wildcard entries like "*" with a specific error message. Coordinator LLMs sometimes emit `"tools": ["*"]` as shorthand for "all tools", but zenflow requires explicit names. A generic "unknown tool" error would leave the user wondering whether their catalog is misspelled; the specific message tells them the real fix.
func ValidateWorkflow ¶
ValidateWorkflow checks workflow constraints: name, steps, unique IDs, valid refs, no cycles. Returns the topological order on success.
func WaitForCoordWake ¶
func WaitForCoordWake(ctx context.Context, runner *AgentRunner) bool
WaitForCoordWake blocks until the coord should re-enter Run or ctx is done. Encapsulates the race-safe wake pattern: 1. Honour ctx cancellation immediately. 2. If the mailbox has pending events (delivered between Run's exit and this call), return true without blocking - those events would otherwise wait until the next Wake even though they are already actionable. 3. Otherwise block on Wake | ctx.Done. Returns true to proceed to the next runner.Run, false to terminate the loop. Typical usage:
for {
runner.Run(ctx, cfg, userMsg, modelID, runner.Tools) if !zenflow.WaitForCoordWake(ctx, runner) { return } userMsg = zenflow.DefaultCoordContinuationPrompt + zenflow.BuildCoordStepMenu(runner)
}
Consumers with additional wake sources (e.g. a TUI's chat-input channel) should compose their own select rather than calling this helper - the runner.Wake / runner.Mailbox primitives are public.
func WithEngineClock ¶
func WithEngineClock(c EngineClock) router.EngineOption
func WithEngineTickInterval ¶
func WithEngineTickInterval(d time.Duration) router.EngineOption
func WithStepLocker ¶
func WithStepLocker(l router.EngineStepLocker) router.EngineOption
Types ¶
type AgentConfig ¶
type AgentConfig = spec.AgentConfig
type AgentError ¶
AgentError wraps a sentinel error class with optional human-readable detail. errors.Is(AgentError{Sentinel: X}, X) returns true. Stable.
func (AgentError) Error ¶
func (e AgentError) Error() string
Error implements the error interface. Format: "<sentinel>: <msg>" or just "<sentinel>" when Msg is empty.
func (AgentError) Unwrap ¶
func (e AgentError) Unwrap() error
Unwrap exposes the sentinel so errors.Is and errors.As see through.
type AgentHandle ¶
type AgentHandle struct {
ID string
// contains filtered or unexported fields
}
AgentHandle is returned by Orchestrator.RunAgentAsync. The caller drives completion via Done and may force-terminate via Cancel. ID is stable for the lifetime of the handle and flows through any ProgressSink events emitted by the underlying AgentRunner. Format: "agent-<UUID v4>" (e.g. "agent-6ba7b810-9dad-11d1-80b4-00c04fd430c8"). Stable.
func NewAgentHandle ¶
func NewAgentHandle(id string) (*AgentHandle, error)
NewAgentHandle constructs an AgentHandle with both internal channels (`done` for the external AgentResult reader + `finished` close-only signal for internal lifecycle goroutines) already initialized. Use this in tests that construct standalone AgentHandles outside of RunAgentAsync - direct struct literals risk skipping the `finished` field, which would nil-channel-hang any future internal reader that waits on it. Returns ErrInvalidAgentHandleID when id is empty so callers see the misuse via the error path rather than a runtime panic. Production code should go through Orchestrator.RunAgentAsync, not NewAgentHandle directly.
func (*AgentHandle) Cancel ¶
func (h *AgentHandle) Cancel() error
Cancel force-terminates the agent run. The handle's Done will yield an AgentResult whose Error wraps ErrAgentCancelled. Calling Cancel multiple times is safe: only the first call wins.
func (*AgentHandle) Done ¶
func (h *AgentHandle) Done() <-chan AgentResult
Done returns the read-only channel that delivers the terminal AgentResult exactly once and is then closed. Multiple reads after close yield the zero value.
func (*AgentHandle) PrimaryStepID ¶ added in v0.1.6
func (h *AgentHandle) PrimaryStepID() string
PrimaryStepID returns the step ID the standalone agent's transcript and mailbox are keyed by. Pair with RunID for TranscriptStore.Load.
func (*AgentHandle) RunID ¶ added in v0.1.6
func (h *AgentHandle) RunID() string
RunID returns the zenflow run ID of the agent behind this handle. Pair with PrimaryStepID to load the agent's transcript from a TranscriptStore (resurrection). Empty when the handle was built via NewAgentHandle without a messaging substrate.
func (*AgentHandle) SendMessage ¶ added in v0.1.6
func (h *AgentHandle) SendMessage(text string) (string, error)
SendMessage delivers a user message into a still-running agent's mailbox and signals its wake channel, so the agent picks the message up at its next turn boundary. The return string mirrors the router contract: "queued: <stepID>" on success, "dropped: <reason>" when the agent has already finished (its inbox is closed) - the consumer reads a drop as "agent done, resurrect instead". Returns a non-nil error only on misuse (a handle with no messaging substrate).
type AgentResult ¶
type AgentResult struct {
Content string
Result map[string]any
Tokens provider.Usage
Turns int
Status AgentStatus // "completed" or "truncated"
Duration time.Duration // wall-clock duration of the agent run
Error error // non-nil iff the async handle terminated with an error
}
AgentResult holds the output of a single agent conversation loop. Stable. For synchronous RunAgent calls, only Content/Result/Tokens/Turns/ Status/Duration are set and errors are returned as the second value. For RunAgentAsync handles, the same struct is delivered over AgentHandle.Done and the Error field carries any terminal error (including AgentError-wrapped sentinels for TTL timeout, cancel, and panic-recover). Consumers MUST check Error before trusting the other fields.
type AgentRunner ¶
type AgentRunner struct {
// contains filtered or unexported fields
}
AgentRunner executes a single-agent conversation loop with tool calling. Stable. It delegates the tool loop to goai.GenerateText(WithMaxSteps) and uses goai hooks for: permission checks, agent spawning, submit_result handling, and progress events. Inter-agent message delivery is handled by the mailbox+wake path - the sole delivery mechanism.
func NewAgentRunner ¶
func NewAgentRunner(opts ...RunnerOption) *AgentRunner
NewAgentRunner constructs an AgentRunner with the given options. Callers that prefer struct-literal initialization may continue to use &AgentRunner{...} directly - both forms are supported. Stable.
func NewDefaultCoordRunner ¶
func NewDefaultCoordRunner(llm provider.LanguageModel, opts ...CoordOption) *AgentRunner
NewDefaultCoordRunner returns a pre-configured *AgentRunner suitable for use as a workflow coordinator. The returned runner has: - StepID = "coordinator" (matches Executor's reverse-reply inbox key) - Mailbox = a fresh in-memory MailboxStore (events from the executor land here; the runner's Run loop drains them on each wake) - Wake = a freshly allocated cap-1 buffered channel so the executor can signal mailbox-driven re-entry of the coord's goai tool loop after each lifecycle event push. AgentRunner.Run only enters mailbox-mode when BOTH Mailbox AND Wake are non-nil; without Wake the coord drains its mailbox once at Run-start and never re-enters to react to subsequent EventStepStart / EventStepEnd / EventError pushes. Stable. - Model = the supplied LanguageModel (may be nil if the caller is constructing a routing-only runner for tests) - Tools = the three defaults (forward_to_agent, narrate, finalize), plus any extras from WithCoordTools, minus narrate when SynthesizeOnly is set - SystemPrompt = DefaultCoordSystemPrompt - WakeContextProvider = supplied via WithCoordContextProvider, if any; nil disables per-wake context injection. The factory does NOT start runner.Run - caller owns the lifecycle.
func (*AgentRunner) Done ¶
func (r *AgentRunner) Done() <-chan struct{}
Done returns a channel that closes when the FinalizeToolDef tool is invoked for the first time. Safe to call concurrently with FinalizeToolDef; the channel is lazily allocated and closed exactly once. Subsequent Done calls return the same channel. select-driven callers (e.g. CLI Run loops that block on {ctx.Done, runner.Done, mailbox-wake}) prefer this over polling Finalized.
func (*AgentRunner) EnsureFinalizeCh ¶
func (r *AgentRunner) EnsureFinalizeCh() <-chan struct{}
EnsureFinalizeCh returns a read-only view of the finalize channel. Callers select on this channel to detect FinalizeToolDef invocation; the channel is lazily allocated and closed exactly once by MarkFinalized. Equivalent to Done but spelled to satisfy the coord.RunnerHandle interface signature.
func (*AgentRunner) FinalSummary ¶
func (r *AgentRunner) FinalSummary() string
FinalSummary returns the synthesis text the coord passed to the most recent FinalizeToolDef invocation, or empty string if finalize has not been called or was called with no summary. CLI Run loops surface this as EventCoordinatorSynthesis after the Done channel closes.
func (*AgentRunner) Finalized ¶
func (r *AgentRunner) Finalized() bool
Finalized reports whether the FinalizeToolDef tool has been invoked at least once. Safe to call from any goroutine; non-blocking. Caller's outer Run loop polls this between mailbox-drain iterations to decide whether to exit cleanly.
func (*AgentRunner) MarkFinalized ¶
func (r *AgentRunner) MarkFinalized()
MarkFinalized flips the finalized flag AND closes the finalize channel exactly once. Idempotent: subsequent calls are no-ops because the underlying sync.Once guards the close. Part of the coord.RunnerHandle contract - the FinalizeToolDef tool calls MarkFinalized to terminate the caller's outer Run loop.
func (*AgentRunner) Model ¶
func (r *AgentRunner) Model() provider.LanguageModel
Model returns the runner's configured LLM provider. External SDK consumers use this to inspect the bound model after construction (e.g. to verify CLI wiring or assert in tests). Use WithRunnerModel(...) to set the value at construction time. Stable.
func (*AgentRunner) NextForwardSeq ¶
func (r *AgentRunner) NextForwardSeq() uint64
NextForwardSeq atomically allocates the next per-runner forward sequence number. The four coord tool factories use this to mint `msg-fwd-N` / `msg-send-N` correlation IDs returned in their tool result strings. Single shared counter so a single monotonic sequence spans every coord-side routing tool call within one workflow.
func (*AgentRunner) Progress ¶
func (r *AgentRunner) Progress() ProgressSink
Progress returns the runner's optional ProgressSink. nil when the runner was constructed without WithRunnerProgress. Part of the coord.RunnerHandle contract - coord tools emit narration / message events through this sink.
func (*AgentRunner) Router ¶
func (r *AgentRunner) Router() *MessageRouter
Router returns the runner's optional MessageRouter. nil when the runner was constructed without WithRunnerRouter (legacy single-call path with no inter-agent messaging). Part of the coord.RunnerHandle contract.
func (*AgentRunner) Run ¶
func (r *AgentRunner) Run(ctx context.Context, cfg AgentConfig, userMessage string, model string, tools []goai.Tool, attachments ...provider.Part) (retResult *AgentResult, retErr error)
Run executes the agent loop using goai.GenerateText with WithMaxSteps. goai owns the tool loop. Zenflow hooks handle: permissions, agent spawning, submit_result, and progress events. Inter-agent message delivery (when Mailbox+Wake are configured) is interleaved via WithStopWhen + post-stop drain.
func (*AgentRunner) RunID ¶
func (r *AgentRunner) RunID() string
RunID returns the runner's configured RunID. Used by coord tools to stamp emitted events so JSON sink consumers can correlate events back to the workflow run that produced them.
func (*AgentRunner) SetFinalSummary ¶
func (r *AgentRunner) SetFinalSummary(summary string)
SetFinalSummary stores the optional synthesis text the FinalizeToolDef tool received. Last-writer-wins on repeated calls (matches the prior `runner.finalSummary.Store(&summary)` semantics). Part of the coord.RunnerHandle behavioural contract; the coord FinalizeToolDef calls this to persist the synthesis text BEFORE invoking MarkFinalized so the close-once channel signal lands after the summary is durably visible to FinalSummary readers.
func (*AgentRunner) StepID ¶
func (r *AgentRunner) StepID() string
StepID returns the runner's configured StepID. Useful for CLI/TUI consumers that need to log or display the inbox key the runner registers under. Also satisfies the coord.RunnerHandle interface. Stable.
func (*AgentRunner) Tools ¶
func (r *AgentRunner) Tools() []goai.Tool
Tools returns the runner's configured tools slice. The returned slice is the same underlying array used by the runner; treat it as read-only. Stable.
func (*AgentRunner) Wake ¶
func (r *AgentRunner) Wake() chan struct{}
Wake returns the runner's wake channel. Send on this channel (non-blocking; cap-1 buffer) to interrupt the goai tool loop at the next idle predicate check, prompting a mailbox drain. nil when no wake channel was configured (the runner has no inter-agent inbox). Stable.
type AgentRunnerOption ¶
type AgentRunnerOption = RunnerOption
AgentRunnerOption is the historical name for RunnerOption. Kept as an alias so internal callers and the public facade (zenflow.AgentRunnerOption) continue to compile during the rename. Deprecated: use RunnerOption.
type AgentStatus ¶
type AgentStatus string
AgentStatus describes how an agent run terminated.
const ( // AgentStatusCompleted means the agent finished normally (LLM returned no tool calls). AgentStatusCompleted AgentStatus = "completed" // AgentStatusTruncated means the agent hit its maxTurns limit. AgentStatusTruncated AgentStatus = "truncated" )
type ApprovalHandler ¶
type ApprovalHandler = spec.ApprovalHandler
type ChanWakeTarget ¶
type ChanWakeTarget = router.ChanWakeTarget
type ClosedAware ¶
type ClosedAware = router.ClosedAware
type CoordLoopOption ¶
type CoordLoopOption func(*coordLoopConfig)
CoordLoopOption configures RunCoordinatorLoop. Stable.
func WithCleanupTimeout ¶
func WithCleanupTimeout(d time.Duration) CoordLoopOption
WithCleanupTimeout overrides the default cleanup wall-clock cap (DefaultCoordCleanupTimeout, 2s) used by the cleanup func returned from RunCoordinatorLoop. Useful in tests that want a sub-second cap to exercise the timer-fired branch quickly. Zero or negative values fall back to DefaultCoordCleanupTimeout. Stable.
type CoordOption ¶
type CoordOption func(*coordConfig)
CoordOption configures a runner built by NewDefaultCoordRunner. Stable. Options follow the standard functional-options pattern. Apply via the variadic argument: NewDefaultCoordRunner(llm, SynthesizeOnly, WithCoordTools(myTool)).
func SynthesizeOnly ¶
func SynthesizeOnly() CoordOption
SynthesizeOnly returns a CoordOption that drops the `narrate` tool from the default coord tool set. The coord LLM still has forward_to_agent (so it can curate context for in-flight steps) and finalize (mandatory exit signal with optional synthesis text), but can no longer interject mid-workflow narrations. Use this for the CLI `--summary-only` mode where the user wants a single final synthesis rather than per-step commentary. Stable.
func WithCoordContextProvider ¶
func WithCoordContextProvider(fn func() string) CoordOption
WithCoordContextProvider returns a CoordOption that installs a callback the coord runner invokes before every GenerateText call (initial + each wake-driven re-entry). The callback's return string is appended as a fresh user-role message wrapped in <dynamic-context> tags so the LLM can distinguish ambient state from in-band conversation. Empty / whitespace-only returns are skipped so an idle provider does not pollute the message stream. Use this hook when a chat-driven UX needs ambient context refreshed every wake without re-engineering the system prompt: currently-open files, repo metadata, session topic, recent user actions. Pass nil to disable. The callback runs synchronously on the runner goroutine; keep it cheap (microseconds) and goroutine-safe; it must not call back into the runner. Stable.
func WithCoordMaxWakeCycles ¶
func WithCoordMaxWakeCycles(n int) CoordOption
WithCoordMaxWakeCycles returns a CoordOption that overrides the coord runner's wake-cycle cap. Zero AND negative values both keep coordDefaultMaxWakeCycles (100) - the factory only honours strictly- positive overrides (see NewDefaultCoordRunner: `if cfg.maxWakeCycles > 0`). The coord runner is long-lived; the per-Run cap limits wake- driven re-entries inside one Run invocation. Set higher (e.g. 250) for workflows with many long-running parallel steps + frequent send_message bridging that could exceed the 100 default in one Run window. Stable. There is intentionally no path through this option to the package- level `defaultMaxWakeCycles` (10) - the coord cap is set inside NewDefaultCoordRunner with `coordDefaultMaxWakeCycles` (100). Callers who want the smaller per-step cap should set the runner's MaxWakeCycles field directly after factory construction.
func WithCoordSystemPrompt ¶
func WithCoordSystemPrompt(prompt string) CoordOption
WithCoordSystemPrompt returns a CoordOption that overrides the default coord system prompt. Empty string is a no-op (default prompt is used). Exposed for consumers that want stricter or looser narration cadence than the defaults provide. The override fully replaces the default; callers who want to extend rather than replace should concatenate DefaultCoordSystemPrompt with extra text or use the convenience option WithCoordSystemPromptSuffix. Stable.
func WithCoordSystemPromptSuffix ¶
func WithCoordSystemPromptSuffix(extra string) CoordOption
WithCoordSystemPromptSuffix returns a CoordOption that appends extra guidance to the default coord system prompt. Convenience for the common "keep tested baseline + add integration-specific guidance" pattern (e.g. a chat consumer appending session-addressing hints, a reviewer agent appending policy reminders). Empty string is a no-op. Last-write-wins with WithCoordSystemPrompt: if both are supplied, whichever appears later in the option list takes effect. Stable.
func WithCoordTools ¶
func WithCoordTools(tools ...goai.Tool) CoordOption
WithCoordTools returns a CoordOption that APPENDS the supplied tools to the default coord tool set. Multiple WithCoordTools calls chain; each call's tools are appended after any previously accumulated tools in registration order. Stable. This is the SDK-flexibility hook: callers (CLI plugins, embedded SDK consumers, downstream embedders) can extend the coord with bespoke tools (e.g. a "request_user_input" tool that surfaces a prompt to the human, or a "consult_runbook" tool that looks up an SOP) without giving up the standard forward / narrate / finalize defaults. To replace the default tools entirely instead of appending, mutate the runner returned by NewDefaultCoordRunner directly:
runner := NewDefaultCoordRunner(llm)
runner.Tools = []goai.Tool{myReplacementSet...}
- but doing so removes finalize, so the Run loop will not exit without a manual replacement signal.
type CoordinatorValidationError ¶
type CoordinatorValidationError struct{ Err error }
CoordinatorValidationError indicates the coordinator response failed workflow validation. Named differently from ValidationError (YAML/schema) to avoid ambiguity.
func (*CoordinatorValidationError) Error ¶
func (e *CoordinatorValidationError) Error() string
func (*CoordinatorValidationError) Unwrap ¶
func (e *CoordinatorValidationError) Unwrap() error
type CycleError ¶
type CycleError struct {
Message string
// Nodes lists the step IDs involved in the cycle (when available).
Nodes []string
Err error
}
CycleError indicates a cycle was detected in the workflow DAG. Err is an optional wrapped cause; when set, errors.Is and errors.As see through to the underlying error. Existing call sites that construct CycleError without an Err field continue to work unchanged. Stable.
func (*CycleError) Error ¶
func (e *CycleError) Error() string
func (*CycleError) Unwrap ¶
func (e *CycleError) Unwrap() error
Unwrap exposes the wrapped cause (if any) so errors.Is / errors.As see through CycleError. Returns nil when no inner error was set.
type DeliveryEngine ¶
type DeliveryEngine = router.DeliveryEngine
func NewDeliveryEngine ¶
func NewDeliveryEngine(source EngineActiveStepsSource, mailbox MailboxStore, registry EngineWakeRegistry, opts ...router.EngineOption) *DeliveryEngine
NewDeliveryEngine is re-exported from internal/router.
type DropReason ¶
type DropReason = router.DropReason
type DuplicateStepError ¶
DuplicateStepError indicates duplicate step IDs in the workflow. Err is an optional wrapped cause; existing call sites that omit Err continue to compile (Unwrap returns nil). Stable.
func (*DuplicateStepError) Error ¶
func (e *DuplicateStepError) Error() string
func (*DuplicateStepError) Unwrap ¶
func (e *DuplicateStepError) Unwrap() error
Unwrap exposes the wrapped cause (if any).
type EngineActiveStepsSource ¶
type EngineActiveStepsSource = router.EngineActiveStepsSource
type EngineClock ¶
type EngineClock = router.EngineClock
type EngineWakeRegistry ¶
type EngineWakeRegistry = router.EngineWakeRegistry
type EngineWakeTarget ¶
type EngineWakeTarget = router.EngineWakeTarget
func NewChanWakeTarget ¶
func NewChanWakeTarget(ch chan struct{}) EngineWakeTarget
type EvalContext ¶
type EvalContext struct {
Content string `json:"content"`
Result map[string]any `json:"result"`
Status string `json:"status"`
Steps map[string]*EvalStepContext `json:"steps"`
Iteration int `json:"iteration"`
Item any `json:"item"`
Index int `json:"index"`
}
EvalContext provides variables available to CEL expressions.
func BuildEvalContext ¶
func BuildEvalContext(results map[string]*StepResult) *EvalContext
BuildEvalContext creates an EvalContext from the executor's results map. Must be called with the executor's mu lock held.
type EvalStepContext ¶
type EvalStepContext struct {
Content string `json:"content"`
Result map[string]any `json:"result"`
Status string `json:"status"`
}
EvalStepContext holds per-step data for CEL expression evaluation.
type Executor ¶
type Executor struct {
Runner *AgentRunner
Storage Storage
Progress ProgressSink
Workflow *Workflow
DefaultModel string
// ForceModel, when non-empty, overrides Step.Model and AgentConfig.Model
// during effective-model resolution. Set by Orchestrator from the
// WithForceModel option; nested executors propagate the same value via
// their constructors in executor_loop.go and executor_include.go.
ForceModel string
// Tools is the full set of registered goai.Tool values available for
// tool steps (step.Tool != ""). Populated from Orchestrator.tools via
// runFlowWithID so tool steps can look up a tool by name and invoke it
// directly without going through the LLM.
Tools []goai.Tool
MaxConcurrency int
// If set, shared memory tools are available and writes are persisted via Storage.
SharedMem *SharedMemory
// Tracer creates workflow/step trace spans when set.
Tracer Tracer
// Coordinator is the caller-provided AgentRunner that receives
// workflow lifecycle events as RouterMessages on its Mailbox
// Nil means no coordinator wiring - the executor skips the
// mailbox + delivery-engine allocation and never pushes events.
// The runner's lifecycle (Run loop) is owned by the caller.
Coordinator *AgentRunner
// Router enables inter-agent message passing. Created by executor when
// Coordinator is set.
Router *MessageRouter
// RootRouter is the OUTERMOST executor's Router. Nested
// executors (created by runRepeatUntilInnerDAG / runForEachInnerDAG /
// runIncludeStep) propagate this so their inner-DAG steps register
// delegations on the root router, allowing coord (which always uses
// root.Router for forward_to_agent) to address inner steps by their
// effective namespaced StepID. nil for the outermost executor (it
// uses Router itself as root).
RootRouter *MessageRouter
// Isolation provides per-step environment isolation (e.g., worktree-per-step).
// If nil, steps run in the same working directory (no isolation).
Isolation StepIsolation
// RunID is the pre-generated run identifier. If empty, Executor generates one.
// Set by Orchestrator so the run ID is available for tracing before execution starts.
RunID string
// ResumeSteps holds previously completed step results to skip re-execution.
// Set by ResumeFlow. Nil for fresh runs.
ResumeSteps map[string]*StepResult
// ResumeRunID is the run ID to reuse when resuming. Empty for fresh runs.
ResumeRunID string
// IncludeDepth tracks recursive include nesting. Incremented per nested executor.
// runIncludeStep rejects execution when IncludeDepth >= MaxIncludeDepth.
IncludeDepth int
// ParentDepResults carries dependency results from the parent include step
// into the nested executor. Inner steps with no dependsOn receive these
// results in their depResults map (spec §7 dependsOn rewriting).
ParentDepResults map[string]*StepResult
// OutputTransform transforms step output before injection into dependent
// step prompts. If nil, the default truncation (maxDepContentBytes) is used.
// P7.7.7: allows consumers to implement smart truncation or LLM compaction
// for models with smaller context windows.
OutputTransform OutputTransformer
// F3 / F5 / F6 / F7 / F8 - config knobs threaded through
// from Orchestrator Options. Zero values fall back to compile-time
// defaults so direct Executor users (tests, embedded callers) get
// the historical behavior unchanged.
MaxWakeCycles int // per-step AgentRunner cap; 0 = default
HoldTimeout time.Duration // F8; 0 = defaultHoldTimeout
DropCallback func(DropEvent) // F3 user observer
DropCallbackBufferSize int // F3
MaxMailboxSize int // F3 / F8 cap
MailboxStoreFactory func() MailboxStore // F3
MailboxDeliveryEnabled *bool // F3 (nil = on)
EngineClock EngineClock // F3 lift
ProgressBufferSize int // F6 pump cap
// transcript store (factory invoked per Run)
// and caps. The store is owned by the Executor for the Run
// lifetime; runStep passes the instance into every AgentRunner so
// per-step conversations converge into a single store. The
// ResumeStep path consults the same instance.
TranscriptStoreFactory func() resume.TranscriptStore
MaxTranscriptMessages int
MaxTranscriptBytes int64
// TruncateOnCapReached controls whether ResumeStep falls back to
// LoadTruncated when the saved transcript is sealed past its
// configured cap (VA-3b). Default false - sealed transcripts
// surface DropReasonTranscriptTooLarge for safety. Set true via
// WithTruncationOnCapReached to preserve operability at the cost
// of a potentially partial conversation history.
TruncateOnCapReached bool
// F6 - resolver consulted by runResume when a saved
// transcript references a model distinct from the default runner
// model. nil resolver + non-matching transcript model ⇒ resume
// fails with ErrModelResolverMissing.
ModelResolver ModelResolver
// ExternalInboxes are stepID-like identifiers for non-step senders
// (e.g. "coordinator") whose mailboxes must be pre-registered with
// the Router so reverse-routed RouterMessages - most notably the
// resume response targeting OriginalSender="coordinator"
// - land in the mailbox instead of dropping as DropReasonUnknownStep.
// Populated via WithExternalInbox. Nil or empty = no extra inboxes.
ExternalInboxes []string
// SenderMatrixDAGAware (F7) - when true, runStep does NOT open
// sender slots for sibling workflow steps; only the coordinator
// dispatches inter-step messages, so the sibling-pessimistic NxN
// matrix collapses to a single coordinator slot per running step.
// Defaults to true (the safer + cheaper rule); set to false to
// retain the pre-F7 conservative behavior.
SenderMatrixDAGAware bool
// FlowContext is the per-call use-case input supplied by the caller
// via RunFlow's WithFlowContext option. When non-empty:
// - if Coordinator != nil: the run-start path pushes a
// workflow_start RouterMessage carrying this string as Content
// into the coord runner's mailbox so the coord LLM sees the
// context as its first event; per-step distribution is
// then the coord's job (forward_to_agent calls).
// - if Coordinator == nil: runStep prepends this string to every
// step's effective user prompt as a static fallback.
// Empty string = no-op (preserves previous behavior).
FlowContext string
// contains filtered or unexported fields
}
Executor runs a workflow with parallel step execution. It handles DAG scheduling, concurrency limiting, step timeouts, retries, and failure strategies (cascade, skip-dependents, abort).
func (*Executor) ActiveSteps ¶
ActiveSteps returns a snapshot of currently-running step IDs. Used by the DeliveryEngine to decide which mailboxes to poll each tick. Returns an empty slice (never nil) so callers can range without nil-checks.
func (*Executor) AgentState ¶
func (e *Executor) AgentState(stepID string) *goai.AgentState
AgentState returns the *goai.AgentState registered for stepID, or nil if the step has not been started yet (or never existed). Safe to call concurrently with Run / runStep - the underlying *goai.AgentState is itself goroutine-safe (atomic read).
func (*Executor) CanResume ¶
CanResume implements resumer. Reports true when the Executor has a transcript store AND the Run ctx has not been cancelled. The transcript lookup itself happens inside ResumeStep - CanResume is a cheap gate that lets the Router skip the resumer path entirely when the feature is not active.
func (*Executor) ResumeStep ¶
func (e *Executor) ResumeStep(ctx context.Context, stepID, prompt, fromAgent string) (*ResumeHandle, error)
ResumeStep implements resumer. Spawns (or queues into) a fresh AgentRunner for stepID seeded with the step's saved transcript plus the caller-supplied prompt as a new user turn. Returns a *ResumeHandle the caller can block on via h.DoneCh. Concurrency model: - At most one resume goroutine runs per stepID at a time. - If a resume is already in flight, the new message is appended to the running resume's mailbox and the pre-existing handle is NOT returned - a fresh handle is created and its DoneCh is closed immediately with Err=nil and the signal that the message was queued (Result="queued"). Further ResumeStep calls never block on in-flight resumes, they just enqueue. - On Run shutdown (ctx cancelled by Run's cancelRun defer), ResumeStep returns ErrResumeShutdown which the Router maps to DropReasonResumeShutdown. NOTE (cross-run scope,): the default model-fidelity path matches a saved transcript's Model against the user-supplied workflow step string tracked in Executor.stepModelStrings. That map is per-Executor memory and is NOT persisted. After a process restart - even with a persistent transcript store - the map is empty, so cross-run resume for a step whose model string differs from the default runner model REQUIRES a registered ModelResolver (see WithModelResolver). Intra-run resume needs no resolver: the step's model string is recorded at runStep and available for the lifetime of the Executor. Reverse routing on success.
func (*Executor) Run ¶
func (e *Executor) Run(ctx context.Context) (*WorkflowResult, error)
Run executes all workflow steps in dependency order with parallel scheduling.
func (*Executor) StepModelString ¶
StepModelString returns the workflow model string recorded for stepID when that step ran, or empty if the step never ran under this Executor. VA-6 - used by ResumeStep to decide whether a resolver is required.
type FactoryCache ¶
type FactoryCache struct {
// contains filtered or unexported fields
}
FactoryCache memoizes a zenflow.Orchestrator per sessionID so repeat calls for the same session return the same instance instead of leaking fresh goroutines (handle registry, TTL watchdogs) on every Prompt invocation. The cache is the production plumbing behind SDK consumers' per-session orchestrator factory: consumers wrap their raw orchestrator constructors with a FactoryCache so the same session always sees the same Orchestrator. Concurrency: - For(sessionID) is safe for concurrent callers. Concurrent first calls for the same session may invoke the inner constructor more than once; the loser of the construct-then-store race is Closed and discarded so only one orchestrator per sessionID is returned to callers across the process. - Close(sessionID) and CloseAll are idempotent; an already-closed orchestrator is removed from the cache and Closed again as a no-op. The zero value is NOT ready to use - use NewFactoryCache.
func NewFactoryCache ¶
func NewFactoryCache(inner func(sessionID string) *Orchestrator) (*FactoryCache, error)
NewFactoryCache wraps inner so repeat calls for the same sessionID return the same Orchestrator. inner is invoked at most once per sessionID under normal conditions; concurrent first-callers may race, but only one winner is retained and the losers are Closed before being discarded. inner may return nil (e.g. when model resolution fails). A nil result is NOT cached - the next For call with the same sessionID retries construction. This matches the pre-cache "graceful degradation" behavior of both zenflowFactory closures. Returns ErrNilFactoryInner when inner is nil so callers see the misuse via the error path rather than a runtime panic.
func (*FactoryCache) Close ¶
func (c *FactoryCache) Close(sessionID string) error
Close drops the cache entry for sessionID and invokes Close on the orchestrator. Returns nil if the session was never cached. Safe to call concurrently with For - For will re-create an orchestrator on the next call.
func (*FactoryCache) CloseAll ¶
func (c *FactoryCache) CloseAll()
CloseAll drops every cached entry and Closees each orchestrator. Intended for process-level shutdown paths (tests, graceful server teardown). Individual Close errors are discarded - use CloseAllErr if callers need to observe them.
func (*FactoryCache) CloseAllErr ¶
func (c *FactoryCache) CloseAllErr() error
CloseAllErr is like CloseAll but returns a joined error containing all Close failures (nil if all succeed). Suitable for callers that need to log or propagate shutdown errors.
func (*FactoryCache) For ¶
func (c *FactoryCache) For(sessionID string) *Orchestrator
For returns the cached orchestrator for sessionID, or constructs a new one via the inner factory and caches it. Nil results from inner are NOT cached so transient failures (e.g. model resolution hiccups) do not permanently poison the cache. An empty sessionID bypasses the cache entirely - each call invokes inner and the result is returned uncached. This preserves the pre-cache behavior for callers that deliberately pass "" to signal single-session deployments.
func (*FactoryCache) Peek ¶
func (c *FactoryCache) Peek(sessionID string) *Orchestrator
Peek returns the cached orchestrator for sessionID WITHOUT constructing one when absent. Returns nil when no entry exists or the cached entry has been Closed externally. Used by callers (e.g. cachedZenflowFactory) that need to inspect cached orchestrator state - most importantly its DefaultModel - to decide whether the cache entry is stale and should be invalidated before the next For call.
type FileStorage ¶
type FileStorage struct {
// contains filtered or unexported fields
}
FileStorage implements Storage with JSON file persistence. Concurrency: a single sync.RWMutex serialises writes against concurrent reads/writes to the same baseDir. Reads (LoadRun, LoadStepResult, LoadSharedMemory) take RLock so multiple concurrent reads can proceed in parallel; writes (SaveRun, SaveStepResult, SaveSharedMemory) take Lock. This avoids serialising parallel-step completion through a single I/O lane at the cost of having writes still serialised - a per-runID or per-key mutex would unlock more parallelism but adds complexity not yet warranted at the OSS-launch baseline.
func NewFileStorage ¶
func NewFileStorage(baseDir string) *FileStorage
NewFileStorage creates a new FileStorage that persists data under baseDir.
func (*FileStorage) LoadRun ¶
LoadRun reads and deserializes run state. Returns ErrRunNotFound when the run does not exist.
func (*FileStorage) LoadSharedMemory ¶
LoadSharedMemory returns shared-memory entries for runID; empty map if none saved.
func (*FileStorage) LoadStepResult ¶
func (f *FileStorage) LoadStepResult(_ context.Context, runID, stepID string) (*StepResult, error)
LoadStepResult reads a step result. Returns ErrStepNotFound when no result is persisted.
func (*FileStorage) SaveRun ¶
func (f *FileStorage) SaveRun(_ context.Context, run *Run) error
SaveRun atomically persists run state to <baseDir>/<runID>/run.json.
func (*FileStorage) SaveSharedMemory ¶
func (f *FileStorage) SaveSharedMemory(_ context.Context, runID string, entries map[string]string) error
SaveSharedMemory merges entries into the persisted shared-memory JSON for runID. FileStorage assumes single-process access to baseDir. Concurrent writes from multiple processes can race during the read-modify-write of shared memory; the in-process mutex does not protect against that.
func (*FileStorage) SaveStepResult ¶
func (f *FileStorage) SaveStepResult(_ context.Context, runID, stepID string, result *StepResult) error
SaveStepResult atomically writes the step result to <baseDir>/<runID>/steps/<stepID>.json.
type ForEachContext ¶
type ForEachContext struct {
Item any // The current forEach item.
Index int // Zero-based index of the current iteration.
}
ForEachContext holds per-iteration context for forEach loops.
type HostSpecificEnvError ¶
HostSpecificEnvError indicates a workflow hard-codes a host-specific env variable that prevents portability across machines. Stable.
func (*HostSpecificEnvError) Error ¶
func (e *HostSpecificEnvError) Error() string
type InMemoryMailboxStore ¶
type InMemoryMailboxStore = router.InMemoryMailboxStore
func NewInMemoryMailboxStore ¶
func NewInMemoryMailboxStore() *InMemoryMailboxStore
type IncludeConflictError ¶
IncludeConflictError indicates a step with include has conflicting fields. Err is an optional wrapped cause; existing call sites that omit Err continue to compile (Unwrap returns nil). Stable.
func (*IncludeConflictError) Error ¶
func (e *IncludeConflictError) Error() string
func (*IncludeConflictError) Unwrap ¶
func (e *IncludeConflictError) Unwrap() error
Unwrap exposes the wrapped cause (if any).
type JSONParseError ¶
type JSONParseError struct{ Err error }
JSONParseError indicates the coordinator response was not valid JSON. Stable.
func (*JSONParseError) Error ¶
func (e *JSONParseError) Error() string
func (*JSONParseError) Unwrap ¶
func (e *JSONParseError) Unwrap() error
type LoopValidationError ¶
LoopValidationError indicates an invalid loop configuration. Err is an optional wrapped cause; existing call sites that omit Err continue to compile (Unwrap returns nil). Stable.
func (*LoopValidationError) Error ¶
func (e *LoopValidationError) Error() string
func (*LoopValidationError) Unwrap ¶
func (e *LoopValidationError) Unwrap() error
Unwrap exposes the wrapped cause (if any).
type MCPConfig ¶ added in v0.2.0
type MCPConfig struct {
MCPServers map[string]MCPServerConfig `json:"mcpServers"`
}
MCPConfig is the top-level settings.json document: a map of server name to its configuration under the "mcpServers" key.
func LoadMCPConfig ¶ added in v0.2.0
LoadMCPConfig reads and parses an MCP settings file. A missing file returns an error satisfying errors.Is(err, os.ErrNotExist) so callers can treat "no config" as a no-op without a separate stat. A present but malformed file returns a parse error.
type MCPOption ¶ added in v0.2.0
type MCPOption func(*mcpConnectConfig)
MCPOption configures ConnectMCPConfig.
func WithMCPClientInfo ¶ added in v0.2.0
WithMCPClientInfo sets the client name/version advertised to MCP servers during the initialize handshake. Defaults to "zenflow"/"1.0.0".
func WithMCPRequestTimeout ¶ added in v0.2.0
WithMCPRequestTimeout bounds each MCP request, including the connect handshake and every subsequent tool call. Zero leaves goai's default (60s). Note: for stdio servers this does NOT bound the subprocess lifetime - that is governed by the context passed to ConnectMCPConfig and by MCPToolset.Close.
func WithMCPStderr ¶ added in v0.2.0
WithMCPStderr routes the stderr of stdio MCP subprocesses to w (e.g. for verbose diagnostics). Default discards it.
type MCPServerConfig ¶ added in v0.2.0
type MCPServerConfig struct {
// Command is the executable for a stdio (local subprocess) server.
Command string `json:"command,omitempty"`
// Args are the arguments passed to Command.
Args []string `json:"args,omitempty"`
// Env sets additional environment variables for the subprocess. Merged
// onto the parent process environment.
Env map[string]string `json:"env,omitempty"`
// URL is the endpoint for a remote (http / sse) server.
URL string `json:"url,omitempty"`
// Headers are sent on every request to a remote server (e.g. auth).
Headers map[string]string `json:"headers,omitempty"`
// Type forces the transport: "stdio", "http", or "sse". Empty infers
// from Command / URL.
Type string `json:"type,omitempty"`
// Disabled skips this server entirely when true.
Disabled bool `json:"disabled,omitempty"`
}
MCPServerConfig describes a single MCP server entry. The shape matches the Claude Desktop "mcpServers" convention so existing config files port over unchanged. A stdio server sets Command (+ optional Args / Env); a remote server sets URL (+ optional Headers). Type selects the transport explicitly; when empty it is inferred (Command -> stdio, URL -> http). String values in Command, Args, Env, URL, and Headers are expanded with os.ExpandEnv, so "${FIRECRAWL_API_KEY}" resolves from the process environment.
type MCPToolset ¶ added in v0.2.0
type MCPToolset struct {
// contains filtered or unexported fields
}
MCPToolset is the live result of connecting to one or more MCP servers. It owns the underlying clients (and, for stdio servers, their subprocesses); callers MUST invoke Close to release them. Tools returns the aggregated, namespaced goai.Tool slice ready for WithAdditionalTools.
func ConnectMCPConfig ¶ added in v0.2.0
ConnectMCPConfig connects to every (enabled) server in cfg and returns a toolset aggregating their tools. Servers are processed in deterministic (sorted) order. A failure connecting to one server does not abort the others: the returned toolset contains the tools from all servers that connected, and the returned error is the join of per-server failures (nil when all succeed). Callers should use the toolset even on a non-nil error and always Close it.
For stdio servers, ctx governs the subprocess lifetime (goai starts them with exec.CommandContext): pass a context that stays valid for as long as the tools may be invoked, and rely on MCPToolset.Close - not context cancellation - for orderly shutdown. A short-lived/timeout context would kill the subprocess immediately after the handshake.
func (*MCPToolset) Close ¶ added in v0.2.0
func (t *MCPToolset) Close() error
Close shuts down every connected client, terminating stdio subprocesses. Safe to call once; errors from individual closes are joined.
func (*MCPToolset) Servers ¶ added in v0.2.0
func (t *MCPToolset) Servers() []string
Servers returns the names of the servers that connected successfully.
func (*MCPToolset) Tools ¶ added in v0.2.0
func (t *MCPToolset) Tools() []goai.Tool
Tools returns the discovered tools across all connected servers, each named "<server>__<tool>". The slice is freshly allocated; mutating it does not affect the toolset.
type MailboxStore ¶
type MailboxStore = router.MailboxStore
type MemoryStorage ¶
type MemoryStorage struct {
// contains filtered or unexported fields
}
MemoryStorage implements Storage with in-memory maps.
func NewMemoryStorage ¶
func NewMemoryStorage() *MemoryStorage
NewMemoryStorage creates an empty in-memory storage.
func (*MemoryStorage) DeleteRun ¶
func (m *MemoryStorage) DeleteRun(runID string)
DeleteRun removes every record (run metadata, step results, shared memory) associated with runID. Added so long-lived embedders (web servers, daemons) can drop completed runs once the caller has consumed the result. Without this, the per-process MemoryStorage grew unboundedly: a server that handled 1000 workflows of 50 steps each retained 1000 *Run + 50000 *StepResult + N sharedMem maps in memory permanently. DeleteRun is a no-op when runID is unknown so callers can call it idempotently.
func (*MemoryStorage) LoadRun ¶
LoadRun returns a deep clone of run state. Returns ErrRunNotFound when the run does not exist.
func (*MemoryStorage) LoadSharedMemory ¶
func (m *MemoryStorage) LoadSharedMemory(_ context.Context, runID string) (map[string]string, error)
LoadSharedMemory returns a clone of shared memory for runID; empty map if none.
func (*MemoryStorage) LoadStepResult ¶
func (m *MemoryStorage) LoadStepResult(_ context.Context, runID, stepID string) (*StepResult, error)
LoadStepResult returns a clone. Returns ErrStepNotFound when no result is persisted.
func (*MemoryStorage) SaveRun ¶
func (m *MemoryStorage) SaveRun(_ context.Context, run *Run) error
SaveRun stores a deep clone of run (thread-safe).
func (*MemoryStorage) SaveSharedMemory ¶
func (m *MemoryStorage) SaveSharedMemory(_ context.Context, runID string, entries map[string]string) error
SaveSharedMemory merges entries into the in-memory map for runID.
func (*MemoryStorage) SaveStepResult ¶
func (m *MemoryStorage) SaveStepResult(_ context.Context, runID, stepID string, result *StepResult) error
SaveStepResult stores a deep clone of result under runID/stepID.
type MessageKind ¶
type MessageKind = types.MessageKind
type MessageRouter ¶
func NewMessageRouter ¶
func NewMessageRouter() *MessageRouter
type MissingAgentError ¶
MissingAgentError indicates a step references an undefined agent. Err is an optional wrapped cause; existing call sites that omit Err continue to compile (Unwrap returns nil). Stable.
func (*MissingAgentError) Error ¶
func (e *MissingAgentError) Error() string
func (*MissingAgentError) Unwrap ¶
func (e *MissingAgentError) Unwrap() error
Unwrap exposes the wrapped cause (if any).
type MissingDepError ¶
MissingDepError indicates a step depends on a non-existent step. Err is an optional wrapped cause; existing call sites that omit Err continue to compile (Unwrap returns nil). Stable.
func (*MissingDepError) Error ¶
func (e *MissingDepError) Error() string
func (*MissingDepError) Unwrap ¶
func (e *MissingDepError) Unwrap() error
Unwrap exposes the wrapped cause (if any).
type MissingNameError ¶
MissingNameError indicates the workflow has no name. Err is an optional wrapped cause; existing call sites that omit Err continue to compile (Unwrap returns nil). Stable.
func (*MissingNameError) Error ¶
func (e *MissingNameError) Error() string
func (*MissingNameError) Unwrap ¶
func (e *MissingNameError) Unwrap() error
Unwrap exposes the wrapped cause (if any).
type ModelResolver ¶
type ModelResolver = spec.ModelResolver
type NoStepsError ¶
NoStepsError indicates the workflow has no steps defined. Err is an optional wrapped cause; existing call sites that omit Err continue to compile (Unwrap returns nil). Stable.
func (*NoStepsError) Error ¶
func (e *NoStepsError) Error() string
func (*NoStepsError) Unwrap ¶
func (e *NoStepsError) Unwrap() error
Unwrap exposes the wrapped cause (if any).
type NopIsolation ¶
type NopIsolation struct{}
NopIsolation provides no environment isolation between steps. All steps run in the same working directory.
type Option ¶
type Option func(*Orchestrator)
Option configures an Orchestrator. Stable.
func WithAdditionalTools ¶ added in v0.2.0
WithAdditionalTools appends tools to the orchestrator catalog instead of replacing it. Unlike WithTools (which overwrites the whole slice), this composes onto whatever WithTools installed earlier in the option list - the canonical way to layer MCP-discovered tools (see ConnectMCPConfig) or plugin tools on top of a built-in set without rebuilding the slice. Stable.
func WithAgentHandleTTL ¶
WithAgentHandleTTL bounds the start-to-finish wall-clock cap on a RunAgentAsync handle. When the TTL is exceeded the handle is force-completed with AgentError{Sentinel: ErrAgentHandleTimeout} and the inner context is cancelled. Zero or negative falls back to DefaultAgentHandleTTL (30m). The library does not consult any environment variables; CLI consumers wire ZENFLOW_AGENT_HANDLE_TTL (or any other source) to this option themselves. Stable.
func WithApproval ¶
func WithApproval(h ApprovalHandler) Option
WithApproval sets the plan approval handler for RunGoal. If set, the coordinator-generated workflow must be approved before execution. Stable.
Example ¶
ExampleWithApproval shows wiring an approval gate before RunGoal executes the coordinator-generated plan.
package main
import (
"context"
"errors"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
// autoApprove always approves - replace with an interactive prompt.
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithApproval(autoApprove{}),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
// autoApprove is a trivial ApprovalHandler that approves every plan.
type autoApprove struct{}
func (autoApprove) ApprovePlan(_ context.Context, _ *zenflow.Workflow) (bool, error) {
return true, nil
}
Output:
func WithApprovalTimeout ¶
WithApprovalTimeout bounds how long ApprovalHandler.ApprovePlan may block. Must be applied AFTER WithApproval. Zero or negative is a no-op (no timeout, the default). On timeout, ApprovePlan returns (false, ErrApprovalTimeout) and RunGoal aborts cleanly. Stable.
Example ¶
ExampleWithApprovalTimeout shows combining WithApproval and WithApprovalTimeout to bound how long the approval handler may block.
package main
import (
"context"
"errors"
"time"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
// autoApprove is a trivial ApprovalHandler that approves every plan.
type autoApprove struct{}
func (autoApprove) ApprovePlan(_ context.Context, _ *zenflow.Workflow) (bool, error) {
return true, nil
}
func main() {
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithApproval(autoApprove{}),
zenflow.WithApprovalTimeout(30*time.Second),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func WithCoordinator ¶
func WithCoordinator(runner *AgentRunner) Option
WithCoordinator installs a caller-provided AgentRunner as the workflow coordinator. When non-nil, the executor pushes lifecycle events (StepStart, StepEnd, error, etc.) as RouterMessages into runner.Mailbox under the coord step ID: runner.StepID when non-empty, otherwise the constant "coordinator". The runner's lifecycle is the caller's responsibility: the orchestrator does not call runner.Run, does not block on it, and does not check whether anyone drains the mailbox. CLI consumers typically construct an ephemeral coord runner via NewDefaultCoordRunner before RunFlow and dispose it after; embedded consumers reuse their existing primary AgentRunner so workflow events flow into the same chat history as user turns. Dual-ID convention: the "coord" identity is split across two independent string keys, both defaulting to "coordinator" but serving different roles: 1. The coord-runner Mailbox key - resolved by coordStepID(runner): runner.StepID if non-empty, else "coordinator". This is the key under which lifecycle events are Append-ed into runner.Mailbox. Embedded consumers typically pass runner.StepID="primary" so workflow events land in the same chat-history bucket as user turns. 2. The workflow-Router coordinator inbox key - the constant CoordRouterInboxID ("coordinator", in executor.go). Resumed steps reverse-route their replies into this inbox via Router.Send; drainCoordReverseReplies surfaces them as EventCoordinatorInboxMessage. This key is independent of the runner's StepID; even when the caller supplies a custom runner.StepID, reverse replies still flow through the Router's "coordinator" inbox unchanged. The executor's inbox auto-registration loop ALWAYS RegisterStep + RegisterInbox both keys when a Coordinator runner is installed, even when they coincide (de-duped), so a custom-StepID runner never causes resumed-step reverse replies to drop with DropReasonUnknownStep. Tested by TestExecutor_CustomStepIDAutoRegistersCoordRouterInbox. Pass nil to disable the coordinator entirely (workflow runs with no LLM-driven monitoring; messaging tools that target the coord still drop with an explicit reason). Not used by RunAgent - the primary agent IS the coordinator on that path. Stable.
func WithDefaultModel ¶
WithDefaultModel sets the fallback model name. Stable.
func WithDropCallback ¶
WithDropCallback installs a user-supplied observer invoked once per dropped router message (in addition to the EventMessageDropped event already emitted via ProgressSink). Useful for metrics/alerting paths that don't want to subscribe to the full event firehose. The callback runs synchronously by default; set WithDropCallbackBufferSize to a positive value to dispatch asynchronously through a buffered channel (overflow falls back to synchronous so no drop event is itself silently lost). Stable.
func WithDropCallbackBufferSize ¶
WithDropCallbackBufferSize selects the buffer size for asynchronous dispatch of WithDropCallback events. Zero or negative = synchronous dispatch (the default). Stable.
func WithExternalInbox ¶
WithExternalInbox pre-registers one or more non-step sender inboxes on the Router at Run start. Use this for identities that send RouterMessages but are not workflow steps (e.g. "coordinator") and need to receive reverse-routed responses; notably the reverse message a resumed step sends back to its OriginalSender. Without pre-registration, Router.Send to an unknown non-step target drops as DropReasonUnknownStep. Idempotent and safe to pass the same ID multiple times. Stable.
Example ¶
ExampleWithExternalInbox pre-registers non-step sender identities on the Router at Run start. Useful when an external process sends RouterMessages into the coordinator's inbox.
package main
import (
"context"
"errors"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithExternalInbox("coordinator", "my-external-sender"),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func WithForceModel ¶
WithForceModel overrides every per-agent and per-step Model identifier with the given model name during execution. Empty string disables the override (equivalent to leaving the option off). Precedence (high → low) for effective model resolution:
WithForceModel > Step.Model > AgentConfig.Model > WithDefaultModel
Use WithForceModel for cross-provider CLI overrides (e.g. running every step of a workflow through a single test provider regardless of YAML). For ordinary defaults, prefer WithDefaultModel - it lets per-agent and per-step Model declarations win. Stable.
func WithGoAIOptions ¶
WithGoAIOptions passes extra goai options to GenerateText calls. Stable.
func WithHoldTimeout ¶
WithHoldTimeout bounds how long the executor retains a step in StepIdle while messages keep arriving. After the timeout, the executor force-terminates the step and emits one EventMessageDropped{reason: hold-timeout} per remaining mailbox message. Zero falls back to defaultHoldTimeout (30s). Stable.
func WithIsolation ¶
func WithIsolation(iso StepIsolation) Option
WithIsolation sets the step isolation strategy. When set, Setup is called before each step and Cleanup is deferred after. Stable.
func WithMailboxDelivery ¶
func WithMailboxDelivery() Option
WithMailboxDelivery enables the mailbox + delivery engine stack (the default). Use WithoutMailboxDelivery to suppress Router/Mailbox allocation in Executor.Run for tests that exercise the scheduler path without mailbox machinery. Stable.
func WithMailboxStore ¶
func WithMailboxStore(factory func() MailboxStore) Option
WithMailboxStore replaces the default InMemoryMailboxStore with a caller-supplied implementation (e.g. a sqlite/redis-backed store for multi-process workflows). The supplied factory is invoked once per workflow run so each Run gets a fresh store instance. Stable.
Example ¶
ExampleWithMailboxStore shows replacing the default in-memory mailbox with a caller-supplied implementation (e.g. for multi-process workflows backed by sqlite or redis).
package main
import (
"context"
"errors"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
// The factory is invoked once per RunFlow so each run gets a fresh store.
factory := func() zenflow.MailboxStore { return zenflow.NewInMemoryMailboxStore() }
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithMailboxStore(factory),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func WithMaxConcurrency ¶
WithMaxConcurrency sets the maximum parallel step count. Precedence: see `defaultMaxConcurrency` in executor.go - this option is level 2 (Workflow.Options.MaxConcurrency overrides it; if both this option and the YAML default are zero, the executor falls back to defaultMaxConcurrency=5). Stable.
Example ¶
ExampleWithMaxConcurrency shows capping the number of workflow steps that may execute in parallel. The default is 5.
package main
import (
"context"
"errors"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithMaxConcurrency(8), // allow 8 parallel steps
)
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func WithMaxDepth ¶
WithMaxDepth sets the maximum agent nesting depth. Applies to RunAgent only - RunFlow does not support child agent spawning. Stable.
Example ¶
ExampleWithMaxDepth caps the agent nesting depth for RunAgent. RunFlow does not support child-agent spawning and ignores this.
package main
import (
"context"
"errors"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithMaxDepth(3),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func WithMaxMailboxSize ¶
WithMaxMailboxSize bounds the per-step in-memory mailbox queue. When the cap is exceeded, Append rejects the newest message and the router emits EventMessageDropped{reason: mailbox-full} via OnDrop. When this option is NOT applied, New installs DefaultMaxMailboxSize (10000) - finite by default so pathological producers cannot exhaust memory in long-running workflows. To opt out and run with an unbounded mailbox, pass WithMaxMailboxSize(0) explicitly: the zero is preserved (it sets an internal flag separate from the size value, so the default install does not overwrite it). Only takes effect when the default InMemoryMailboxStore is used (i.e. no WithMailboxStore override). Custom stores enforce their own caps. Stable.
Example ¶
ExampleWithMaxMailboxSize bounds the per-step in-memory mailbox queue. When the cap is exceeded, the router emits EventMessageDropped instead of accepting the message.
package main
import (
"context"
"errors"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithMaxMailboxSize(50),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func WithMaxTranscriptBytes ¶
WithMaxTranscriptBytes overrides the default per-step byte cap for the Day-1 InMemoryTranscriptStore. Zero or negative leaves the default `DefaultMaxTranscriptBytes`. Ignored when a custom store is supplied via WithTranscriptStore.
func WithMaxTranscriptMessages ¶
WithMaxTranscriptMessages overrides the default per-step message count cap for the Day-1 InMemoryTranscriptStore. When exceeded, Append returns ErrTranscriptTooLarge and a subsequent resume on that step emits DropReasonTranscriptTooLarge. Zero or negative leaves the default `DefaultMaxTranscriptMessages`. Ignored when a custom store is supplied via WithTranscriptStore (custom stores enforce their own caps).
func WithMaxTurns ¶
WithMaxTurns sets the maximum conversation turns. Applies to RunAgent only - RunFlow uses per-agent MaxTurns from workflow YAML. Stable.
Example ¶
ExampleWithMaxTurns caps the number of conversation turns for single agent runs (RunAgent). RunFlow uses per-agent MaxTurns from the YAML.
package main
import (
"context"
"errors"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithMaxTurns(10),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func WithMaxWakeCycles ¶
WithMaxWakeCycles caps the number of wake-driven re-entries into goai per AgentRunner.Run. Zero means "use defaultMaxWakeCycles" (10). When the cap is reached with messages still pending, the runner emits one EventMessageDropped{reason: max-wake-cycles} per remaining message. Stable.
func WithModel ¶
func WithModel(m provider.LanguageModel) Option
WithModel sets the language model. Stable.
Example ¶
ExampleWithModel shows setting the default LLM for all steps in a workflow. Every step that does not declare a model override uses this.
package main
import (
"context"
"errors"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
model := &fakeModel{reply: "hello"}
orch := zenflow.New(zenflow.WithModel(model))
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func WithModelResolver ¶
func WithModelResolver(r ModelResolver) Option
WithModelResolver installs a ModelResolver consulted by the resume path. When a saved transcript's Model identifier differs from the Executor's default runner model, the resolver is called with the transcript's model ID; a nil/empty return fails the resume loudly with ErrModelResolverMissing (DropReasonTargetTerminal). Without a resolver, mismatch also fails loudly; the resume path never silently falls back to the wrong model. Scope: - REQUIRED for cross-run resume with a non-default model (after process restart with a persistent transcript store). The Executor's per-step model-string map is intra-run memory only. - OPTIONAL for intra-run resume: the workflow step's model string is tracked automatically and the default-path match succeeds without a resolver. Stable.
Example ¶
ExampleWithModelResolver shows installing a ModelResolver so the resume path can rebuild a provider.LanguageModel from the saved transcript's model ID string.
package main
import (
"context"
"errors"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
model := &fakeModel{reply: "ok"}
resolver := zenflow.ModelResolver(func(modelID string) (provider.LanguageModel, error) {
// In production, switch on modelID to construct the right provider.
_ = modelID
return model, nil
})
orch := zenflow.New(
zenflow.WithModel(model),
zenflow.WithModelResolver(resolver),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func WithOutputTransform ¶
func WithOutputTransform(t OutputTransformer) Option
WithOutputTransform sets the output transformer for step output injection. When set, step outputs are transformed before injection into dependent step prompts. Use this to implement smart truncation based on target model context window. Stable.
func WithPermissions ¶
func WithPermissions(h PermissionHandler) Option
WithPermissions sets the permission handler. Stable.
Example ¶
ExampleWithPermissions shows gating tool execution with a custom PermissionHandler. The handler receives a PermissionRequest and returns (approved, error).
package main
import (
"context"
"errors"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithPermissions(allowAll{}),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
// allowAll is a trivial PermissionHandler that allows all tool calls.
type allowAll struct{}
func (allowAll) RequestPermission(_ context.Context, _ zenflow.PermissionRequest) (bool, error) {
return true, nil
}
Output:
func WithProgress ¶
func WithProgress(s ProgressSink) Option
WithProgress sets the progress event sink. Stable.
Example ¶
ExampleWithProgress demonstrates wiring a ProgressSink to observe workflow events (step starts, completions, errors, narrations).
package main
import (
"context"
"errors"
"fmt"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
sink := &loggingSink{}
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "done"}),
zenflow.WithProgress(sink),
)
defer orch.Close() //nolint:errcheck
wf := &zenflow.Workflow{
Name: "observed",
Steps: []zenflow.Step{{ID: "s1", Instructions: "do work"}},
}
_, _ = orch.RunFlow(context.Background(), wf)
}
// loggingSink is a simple ProgressSink that prints each event type.
type loggingSink struct{}
func (s *loggingSink) OnEvent(_ context.Context, e zenflow.Event) {
fmt.Println("event:", e.Type)
}
func (s *loggingSink) OnOutput(_ context.Context, o zenflow.Output) {
fmt.Print(o.Delta)
}
Output:
func WithProgressBufferSize ¶
WithProgressBufferSize controls the buffer of the non-blocking progress sink wrapper. Behavior: emits are non-blocking on the critical path while the buffered channel has capacity. On overflow the wrapper applies a bounded back-pressure of up to defaultEventBusTimeout (1s); if the channel is still full at the deadline the event is dropped and the internal dropped counter is incremented (visible via Stats). This bounds worst-case caller latency at 1s rather than blocking indefinitely. Larger buffers tolerate slower downstream sinks at the cost of more buffered memory. Zero or negative falls back to defaultEventBusBuffer (1024).
func WithRouterObserver ¶
func WithRouterObserver(fn func(*MessageRouter)) Option
WithRouterObserver registers a callback invoked once per RunAgent / RunFlow invocation with the freshly-allocated MessageRouter for that run. Intended for observability hooks (telemetry, debug inspectors, integration tests) that need a handle on the per-call router without polling internal state. The callback fires synchronously before any Run-side goroutine consumes the router; it must not block. Production callers typically leave this unset; nil callbacks are ignored. Panic semantics: if the callback panics, the panic IS recovered (the run continues) and logged via slog.Error with key "panic" and hook="routerObserver". The panic is NOT propagated as an error; RunAgent / RunFlow behave as if the observer had returned cleanly. Callers that need to surface observer failures must do so out-of-band (e.g. via their own channel or atomic flag captured in the closure).
func WithRunID ¶
WithRunID pins the orchestrator's run identifier for RunFlow / RunAgent / RunGoal. When set, all internally-emitted Event.RunID values use this ID instead of a freshly-generated one. ResumeFlow already takes runID as an argument and ignores this option. Callers that allocate a runID up-front (e.g. HTTP servers that store it in a database row and return it to the client before the workflow starts) use this to guarantee the server-visible ID and the zenflow- emitted Event.RunID match. Without this option, zenflow generates its own ID internally and any pre-allocated ID diverges - persisted events carry the internal ID while DB rows and HTTP responses carry the pre-allocated one, breaking cross-referencing (e.g. /flow-debug). Empty = fall back to internal generation (legacy behavior). Stable.
func WithSharedMemory ¶
func WithSharedMemory(sm *SharedMemory) Option
WithSharedMemory sets the shared memory instance for inter-agent collaboration. If set, shared_memory_read and shared_memory_write tools are auto-injected into agent tool chains during RunFlow and ResumeFlow. Stable.
func WithStorage ¶
WithStorage sets the persistence backend. Applies to RunFlow only - RunAgent does not persist state. Stable.
Example ¶
ExampleWithStorage shows swapping the default MemoryStorage for a FileStorage backend. FileStorage persists run state across process restarts, enabling ResumeFlow.
package main
import (
"context"
"errors"
"os"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
// MemoryStorage (default) - suitable for short-lived embeddings.
memOrch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithStorage(zenflow.NewMemoryStorage()),
)
defer memOrch.Close() //nolint:errcheck
// FileStorage - persists under a temp directory for demo.
dir, _ := os.MkdirTemp("", "zenflow-example-*")
defer func() { _ = os.RemoveAll(dir) }()
fileOrch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithStorage(zenflow.NewFileStorage(dir)),
)
defer fileOrch.Close() //nolint:errcheck
_, _ = memOrch, fileOrch
}
Output:
func WithStreaming ¶
func WithStreaming() Option
WithStreaming enables streaming mode. When enabled, content that is being displayed (controlled by WithVerbose) is delivered token-by-token via ProgressSink.OnOutput instead of as full text. Stable.
Example ¶
ExampleWithStreaming enables token-by-token delivery of agent output through ProgressSink.OnOutput. Requires WithProgress to be wired.
package main
import (
"context"
"errors"
"fmt"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
// loggingSink is a simple ProgressSink that prints each event type.
type loggingSink struct{}
func (s *loggingSink) OnEvent(_ context.Context, e zenflow.Event) {
fmt.Println("event:", e.Type)
}
func (s *loggingSink) OnOutput(_ context.Context, o zenflow.Output) {
fmt.Print(o.Delta)
}
func main() {
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "streaming reply"}),
zenflow.WithStreaming(),
zenflow.WithProgress(&loggingSink{}),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func WithTools ¶
WithTools sets the tools available to agents. Stable.
Example ¶
ExampleWithTools shows wiring custom goai.Tool implementations into the orchestrator. Tools declared in workflow YAML (by name) are matched against this set at runtime.
package main
import (
"context"
"encoding/json"
"errors"
"github.com/zendev-sh/goai"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
greet := goai.Tool{
Name: "greet",
Description: "Returns a greeting.",
Execute: func(_ context.Context, _ json.RawMessage) (string, error) {
return "hello", nil
},
}
orch := zenflow.New(
zenflow.WithModel(&fakeModel{reply: "ok"}),
zenflow.WithTools(greet),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func WithTracer ¶
WithTracer sets the tracer for distributed tracing. The OTel sub-module (zenflow/observability/otel) provides a Tracer implementation. When set, workflow and step spans are created automatically. Stable.
func WithTranscriptStore ¶
func WithTranscriptStore(factory func() resume.TranscriptStore) Option
WithTranscriptStore installs a TranscriptStore factory. The factory is invoked once per Executor.Run so each Run gets a fresh store instance (matching WithMailboxStore lifecycle). The default, when this option is not supplied, is a per-Run InMemoryTranscriptStore, which supports intra-run resume only. Supply a persistent-backed store (file / SQLite) to enable cross-run or cross-process resume.
func WithTruncationOnCapReached ¶
func WithTruncationOnCapReached() Option
WithTruncationOnCapReached configures the resume path to fall back to TranscriptTruncatedLoader.LoadTruncated when a sealed transcript's Load returns ErrTranscriptTooLarge. The bound defaults to 1000 messages; stores implementing LoadTruncated may choose their own tail size.
func WithVerbose ¶
func WithVerbose() Option
WithVerbose enables agent output display. When enabled, agent LLM responses are shown in addition to events and narration. Without verbose, only workflow events (▸, ✓) and coordinator narration (≋) are shown. Stable.
func WithoutMailboxDelivery ¶
func WithoutMailboxDelivery() Option
WithoutMailboxDelivery suppresses Router/Mailbox allocation in Executor.Run - useful for tests that exercise the scheduler path without mailbox machinery. Stable.
func WithoutStreaming ¶
func WithoutStreaming() Option
WithoutStreaming disables streaming mode (restores the default batch delivery). Stable.
func WithoutTruncationOnCapReached ¶
func WithoutTruncationOnCapReached() Option
WithoutTruncationOnCapReached restores the default behavior: a sealed transcript fails the resume with DropReasonTranscriptTooLarge so operators can detect and investigate the cap.
func WithoutVerbose ¶
func WithoutVerbose() Option
WithoutVerbose disables agent output display (default: only events and narration). Stable.
type Orchestrator ¶
type Orchestrator struct {
// contains filtered or unexported fields
}
Orchestrator coordinates workflow execution. Stable.
func New ¶
func New(opts ...Option) *Orchestrator
New creates an Orchestrator with the given options. Stable. Defaults: MemoryStorage, maxConcurrency=5, OutputTransform=&TokenBudgetTransformer{MaxBytesPerDep: DefaultMaxBytesPerDep}. Effective defaults at call time: maxTurns=50, maxDepth=3. Use WithModel, WithTools, WithMaxTurns, etc. to configure.
Example ¶
ExampleNew demonstrates constructing an Orchestrator with the most common options. No Output: block - godoc renders the snippet; go test compiles it without running.
package main
import (
"context"
"errors"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
model := &fakeModel{reply: "ok"}
orch := zenflow.New(
zenflow.WithModel(model),
zenflow.WithMaxConcurrency(4),
zenflow.WithMaxTurns(20),
zenflow.WithStorage(zenflow.NewMemoryStorage()),
)
defer orch.Close() //nolint:errcheck
_ = orch
}
Output:
func (*Orchestrator) Close ¶
func (o *Orchestrator) Close() error
Close releases long-lived goroutines and cached state owned by the Orchestrator. It is intended for call sites (e.g. per-session factory caches) that want to tear an orchestrator down when its owning session ends. Close is idempotent: subsequent calls are no-ops and return nil. Behavior: - New RunAgentAsync calls made after Close return ErrOrchestratorClosed. Existing AgentHandles are still observed via the normal Done channel - Close does NOT forcibly drop results in flight. - All currently-registered AgentHandles are Canceled so their agent goroutines observe ctx.Done, finish publishes ErrAgentCancelled, and the TTL watchdog goroutines exit through their <-h.done branch. This ends the in-memory goroutine population without requiring the 30-minute TTL to expire. - The handle registry map is emptied; the cleanup-watcher goroutines spawned in RunAgentAsync continue to race with unregisterHandle (no-op on an empty slice), then exit. Close always returns nil today - the error return is reserved for future cleanup stages (persistent resource flushes, subscriber unwind) so callers can add logging without breaking the signature.
func (*Orchestrator) Coordinator ¶
func (o *Orchestrator) Coordinator() *AgentRunner
Coordinator returns the caller-provided coordinator AgentRunner, or nil when none was installed via WithCoordinator. Public test seam the standalone CLI in cmd/zenflow needs to start the coord's Run loop after constructing the orchestrator (per caller-owned lifecycle), and assertions need to observe whether `--quiet`/`--json`/`--summary-only`/default routed to nil-coord vs a real default coord vs a SynthesizeOnly coord. Mirrors the MaxDepth public accessor pattern. Returns nil when called on a nil receiver.
func (*Orchestrator) DefaultModel ¶
func (o *Orchestrator) DefaultModel() string
DefaultModel returns the model identifier this orchestrator was configured with via WithDefaultModel. Used by FactoryCache callers to detect when a cached orchestrator's model is stale relative to the session's current selection (user switched via /models in TUI), which is otherwise undetectable until subagents fire with the wrong default. Returns the empty string when WithDefaultModel was never applied (or when the receiver is nil).
func (*Orchestrator) HasLLM ¶
func (o *Orchestrator) HasLLM() bool
HasLLM reports whether an LLM provider has been configured.
func (*Orchestrator) IsClosed ¶
func (o *Orchestrator) IsClosed() bool
IsClosed reports whether Close has been called on this Orchestrator. Exposed primarily for tests and for factory caches that want to skip returning an already-closed orchestrator from a sync.Map.
func (*Orchestrator) ListAgentHandles ¶
func (o *Orchestrator) ListAgentHandles(sessionID string) []*AgentHandle
ListAgentHandles returns the currently ACTIVE AgentHandle values registered under sessionID via RunAgentAsync. Completed handles (Done channel closed) are excluded - this method is the source of truth for the consumer's session-status query. The returned slice is a snapshot; it is safe for concurrent iteration and will not be mutated by subsequent handle lifecycle events. DECISION (documented in TestOrchestrator_ListAgentHandles_ExcludesCompletedHandles): ListAgentHandles is ACTIVE-ONLY. It does not retain completed handles as a history log. Pills show live work; a completed handle's result has already been delivered via Done and inboxed via the consumer's progress bridge. Order is unspecified and may vary between calls. Consumers that require a stable order must sort by AgentHandle.ID.
func (*Orchestrator) MaxDepth ¶
func (o *Orchestrator) MaxDepth() int
MaxDepth returns the configured agent nesting depth cap. Returns the raw configured value; 0 means "use the runtime default" (currently 3, applied lazily inside RunAgent). Callers use this to assert WithMaxDepth(...) actually reached the constructed Orchestrator.
func (*Orchestrator) Progress ¶
func (o *Orchestrator) Progress() ProgressSink
Progress returns the orchestrator's configured ProgressSink, or nil if none was installed via WithProgress. Exposed so consumer code that owns the coord goroutine lifecycle can wire its own runners to the same sink without going through WithCoordinator.
func (*Orchestrator) ResumeFlow ¶
func (o *Orchestrator) ResumeFlow(ctx context.Context, runID string, wf *Workflow) (*WorkflowResult, error)
ResumeFlow resumes a previously started workflow from its checkpoint. Stable. Completed steps are loaded from storage and not re-executed. Failed, cancelled, and skipped steps are re-executed.
func (*Orchestrator) RunAgent ¶
func (o *Orchestrator) RunAgent(ctx context.Context, cfg AgentConfig) (*AgentResult, error)
RunAgent executes a single agent conversation with optional child agent spawning. The cfg argument carries every per-call override: cfg.Prompt is the user message; cfg.Model overrides the Orchestrator default; cfg.CallTools overrides the Orchestrator tool set; cfg.ProgressSink overrides the Orchestrator sink; cfg.MaxTurns bounds the per-call tool loop. All fields except Prompt are optional and fall back to the Orchestrator defaults (registered via WithModel / WithTools / WithProgress / WithMaxTurns) when unset. Stable.
func (*Orchestrator) RunAgentAsync ¶
func (o *Orchestrator) RunAgentAsync(ctx context.Context, cfg AgentConfig) (*AgentHandle, error)
RunAgentAsync spawns the primary agent in a goroutine and returns a handle immediately. The caller drives completion via handle.Done; ctx cancellation or handle.Cancel terminate the run early. A 30-minute default TTL (overridable via ZENFLOW_AGENT_HANDLE_TTL) force-completes the handle if the agent does not finish in time. Progress events flow through the ProgressSink registered on the orchestrator. Stable.
func (*Orchestrator) RunFlow ¶
func (o *Orchestrator) RunFlow(ctx context.Context, wf *Workflow, opts ...RunFlowOption) (*WorkflowResult, error)
RunFlow executes a workflow and returns the result. Stable. When WithRunID was supplied at construction time, that ID is reused; otherwise a fresh one is generated. Variadic RunFlowOption args configure per-call behavior such as WithFlowContext. Backward-compatible: zero opts = previous behavior.
Example ¶
ExampleOrchestrator_RunFlow shows a minimal RunFlow invocation. The workflow is built programmatically; callers normally use LoadWorkflow to read from a YAML file.
package main
import (
"context"
"errors"
"log"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
orch := zenflow.New(zenflow.WithModel(&fakeModel{reply: "analysis complete"}))
defer orch.Close() //nolint:errcheck
wf := &zenflow.Workflow{
Name: "analysis",
Steps: []zenflow.Step{
{ID: "analyze", Instructions: "Analyze the data."},
},
}
result, err := orch.RunFlow(context.Background(), wf)
if err != nil {
log.Fatal(err)
}
// result.Steps["analyze"].Content holds the agent's response.
_ = result
}
Output:
func (*Orchestrator) RunGoal ¶
func (o *Orchestrator) RunGoal(ctx context.Context, goal string, opts ...RunGoalOption) (result *WorkflowResult, err error)
RunGoal decomposes a goal into steps via LLM coordinator, then executes as DAG. Stable. The coordinator receives the goal and a catalog of available tools, and outputs a JSON workflow (agents + steps). The workflow is parsed, validated, optionally approved via ApprovalHandler, then executed via RunFlow. Retry policy: up to 2 retries on JSON parse errors, 1 retry on validation errors. Retry budgets are global across the entire RunGoal call - not per-error-type. For example, after 2 JSON retries, a subsequent validation error will only get 1 retry regardless of prior JSON failures. The total number of coordinator LLM calls is bounded by 1 + maxJSONRetries + maxValidationRetries = 4.
Example ¶
ExampleOrchestrator_RunGoal shows how to supply a goal string and handle ErrEmptyGoal when the caller forgets to provide one.
package main
import (
"context"
"errors"
"fmt"
"github.com/zendev-sh/goai/provider"
"github.com/zendev-sh/zenflow"
)
// fakeModel is a minimal provider.LanguageModel that returns a canned
// response immediately. Used by examples so they compile and run
// without real API keys.
type fakeModel struct{ reply string }
func (m *fakeModel) ModelID() string { return "example-fake" }
func (m *fakeModel) DoGenerate(_ context.Context, _ provider.GenerateParams) (*provider.GenerateResult, error) {
return &provider.GenerateResult{
Text: m.reply,
FinishReason: provider.FinishStop,
Usage: provider.Usage{InputTokens: 5, OutputTokens: 5},
}, nil
}
func (m *fakeModel) DoStream(_ context.Context, _ provider.GenerateParams) (*provider.StreamResult, error) {
return nil, errors.New("streaming not implemented in fakeModel")
}
func main() {
orch := zenflow.New(zenflow.WithModel(&fakeModel{reply: "done"}))
defer orch.Close() //nolint:errcheck
// Empty goal returns ErrEmptyGoal immediately - no LLM call needed.
_, err := orch.RunGoal(context.Background(), "")
if errors.Is(err, zenflow.ErrEmptyGoal) {
fmt.Println("goal must not be empty")
}
}
Output: goal must not be empty
type OutputTransformer ¶
type OutputTransformer = spec.OutputTransformer
type PermissionHandler ¶
type PermissionHandler = types.PermissionHandler
type PermissionPolicy ¶
type PermissionPolicy struct {
Yolo bool // allow every tool, no prompt (YOLO mode)
Allow []string // pre-allowed tool names
Deny []string // pre-denied tool names
Strict bool // deny anything not on Allow list (no prompt)
// Sandbox is a parse-time concept - by the time a policy reaches
// DecidePermission, sandbox semantics have already been folded into
// Strict + Allow. The field is preserved for round-tripping / debug.
Sandbox bool
}
PermissionPolicy captures the static permission-related flags parsed from the CLI (or constructed by a library consumer). It is a value type - the caller owns the mutable per-run state (alwaysAllow map, prompt UI). Decision precedence is encoded by DecidePermission and matches the behavior previously inlined in cliPermissionHandler.RequestPermission: 1. Yolo → allow. 2. Deny match → deny (error). 3. Allow match → allow. 4. Strict + no allow match → deny (error). 5. AlwaysAllow hit → allow. 6. Otherwise → caller should prompt.
type PermissionRequest ¶
type PermissionRequest = types.PermissionRequest
type PortabilityWarning ¶
type PortabilityWarning struct {
Field string // dotted path (e.g. "steps[3].instructions")
Kind string // "absolute-path"
Message string
Snippet string // offending substring (trimmed)
}
PortabilityWarning carries advisory findings from LintPortability.
func LintPortability ¶
func LintPortability(wf *Workflow) (warnings []PortabilityWarning, err error)
LintPortability inspects a parsed workflow and returns: - warnings: absolute paths spotted in instructions / descriptions / default values; non-fatal. - err: the first host-specific env interpolation encountered; fatal - callers should refuse to load the workflow. Both return values are independent: a workflow can have warnings and no error.
type ProgressSink ¶
type ProgressSink = types.ProgressSink
type ResumeHandle ¶
type ResumeHandle = router.ResumeHandle
type ResumeStates ¶
type ResumeStates struct {
// contains filtered or unexported fields
}
ResumeStates is the Executor's per-stepID resume coordination registry. A sync.Map backs a map of stepID → *resumeState so lookups never block producers.
func NewResumeStates ¶
func NewResumeStates() *ResumeStates
type RouterMessage ¶
type RunFlowOption ¶
type RunFlowOption func(*runFlowConfig)
RunFlowOption configures a single RunFlow invocation. Unlike Option (which configures the long-lived Orchestrator), RunFlowOption is per-call; callers can supply a different context per workflow run without reconstructing the Orchestrator. Stable.
func WithFlowContext ¶
func WithFlowContext(ctx string) RunFlowOption
WithFlowContext supplies use-case input to be distributed to the workflow steps. When a coordinator runner is installed via WithCoordinator, the context is pushed into the coord's mailbox as the first event (workflow_start) so the coord LLM can curate per-step forwards. When no coordinator is installed (WithCoordinator(nil)), the context is blanket-prepended to every step's effective user prompt as a static fallback. Empty string is a no-op. Stable.
type RunGoalOption ¶
type RunGoalOption func(*runGoalConfig)
RunGoalOption configures a single RunGoal invocation. See RunFlowOption. Stable.
func WithGoalContext ¶
func WithGoalContext(ctx string) RunGoalOption
WithGoalContext supplies additional context (beyond the goal text itself) to the RunGoal coordinator-decomposition prompt. Empty string is a no-op. Stable.
type RunnerOption ¶
type RunnerOption func(*AgentRunner)
RunnerOption configures a NewAgentRunner. Stable. Renamed from AgentRunnerOption (C14: package-name stutter); the old name is kept as a type alias for backwards compatibility so existing callers (zenflow.go, coord_factory.go) keep compiling.
func WithRunnerGoAIOptions ¶
func WithRunnerGoAIOptions(opts ...goai.Option) RunnerOption
WithRunnerGoAIOptions sets extra goai options on the AgentRunner. Stable.
func WithRunnerInitialMessages ¶
func WithRunnerInitialMessages(msgs []provider.Message) RunnerOption
WithRunnerInitialMessages pre-loads conversation history that the runner prepends to the fresh user turn passed to Run. Used by Executor.runResume to replay a saved transcript through the standard AgentRunner.Run machinery. Empty for normal (non-resume) runs. Stable.
func WithRunnerMailbox ¶
func WithRunnerMailbox(m MailboxStore) RunnerOption
WithRunnerMailbox sets the MailboxStore the runner reads inter-agent messages from. Pair with WithRunnerWake to enable mailbox-mode delivery. Stable.
func WithRunnerMaxWakeCycles ¶
func WithRunnerMaxWakeCycles(n int) RunnerOption
WithRunnerMaxWakeCycles caps the number of wake-driven re-entries into goai.GenerateText per Run. Zero or negative means "use the package default" (defaultMaxWakeCycles). Stable.
func WithRunnerModel ¶
func WithRunnerModel(m provider.LanguageModel) RunnerOption
WithRunnerModel sets the language model on the AgentRunner. Stable.
func WithRunnerModelID ¶
func WithRunnerModelID(id string) RunnerOption
WithRunnerModelID sets the model ID string (for transcript metadata) on the AgentRunner. Stable.
func WithRunnerPermissions ¶
func WithRunnerPermissions(h PermissionHandler) RunnerOption
WithRunnerPermissions sets the permission handler on the AgentRunner. Stable.
func WithRunnerPreStartDrainGate ¶
func WithRunnerPreStartDrainGate(gate <-chan struct{}) RunnerOption
WithRunnerPreStartDrainGate is a test-only hook: when non-nil, Run blocks on receive from this channel BEFORE the first drainMailboxIntoMessages call. Lets callers hold the pre-start drain while they set up mailbox preconditions. A nil gate preserves production behavior. Stable.
func WithRunnerProgress ¶
func WithRunnerProgress(s ProgressSink) RunnerOption
WithRunnerProgress sets the progress event sink on the AgentRunner. Stable.
func WithRunnerRouter ¶
func WithRunnerRouter(rt *MessageRouter) RunnerOption
WithRunnerRouter wires a shared MessageRouter into the AgentRunner so child spawns inherit a live router for inter-agent messaging. nil is valid (legacy single-call path with no messaging). Stable.
func WithRunnerRunID ¶
func WithRunnerRunID(id string) RunnerOption
WithRunnerRunID sets the workflow run ID on the AgentRunner. Stable.
func WithRunnerSpawnDepth ¶
func WithRunnerSpawnDepth(depth int) RunnerOption
WithRunnerSpawnDepth records the recursion depth of this runner relative to the top-level RunAgent invocation. Used to enrich EventToolCall payloads so TUI consumers can collapse nested-spawn cards under the parent. Stable.
func WithRunnerSpawnParentCallID ¶
func WithRunnerSpawnParentCallID(id string) RunnerOption
WithRunnerSpawnParentCallID records the agent-tool ToolCallID that produced this runner via SpawnChild. Emitted on every EventToolCall in Data["parentCallID"] so consumers can route nested events into the parent's children list. Stable.
func WithRunnerStateRef ¶
func WithRunnerStateRef(s *goai.AgentState) RunnerOption
WithRunnerStateRef wires a goai.AgentState into the AgentRunner so the poller can observe the runner's tool-loop lifecycle without holding a lock. See the StateRef field for the full lifecycle contract. Stable.
func WithRunnerStepID ¶
func WithRunnerStepID(id string) RunnerOption
WithRunnerStepID sets the step ID on the AgentRunner. Stable.
func WithRunnerStreaming ¶
func WithRunnerStreaming() RunnerOption
WithRunnerStreaming enables streaming mode on the AgentRunner. Stable.
func WithRunnerSystemPrompt ¶
func WithRunnerSystemPrompt(prompt string) RunnerOption
WithRunnerSystemPrompt sets the system prompt on the AgentRunner. Stable.
func WithRunnerTools ¶
func WithRunnerTools(tools ...goai.Tool) RunnerOption
WithRunnerTools sets the tools available to the agent. Stable.
func WithRunnerTranscript ¶
func WithRunnerTranscript(ts resume.TranscriptStore) RunnerOption
WithRunnerTranscript wires a TranscriptStore so the runner persists the step's conversation on every goai step-finish hook AND on Run exit. Required for the Resume Mechanism (R2). Stable.
func WithRunnerVerbose ¶
func WithRunnerVerbose() RunnerOption
WithRunnerVerbose enables verbose output on the AgentRunner. Stable.
func WithRunnerWake ¶
func WithRunnerWake(ch chan struct{}) RunnerOption
WithRunnerWake sets the wake-signal channel that the DeliveryEngine pings when the agent's mailbox has unread messages. Pair with WithRunnerMailbox to enable mailbox-mode delivery. Stable.
func WithRunnerWakeContextProvider ¶
func WithRunnerWakeContextProvider(fn func() string) RunnerOption
WithRunnerWakeContextProvider attaches a callback that supplies ambient context the runner injects as a fresh user-role message once before the first LLM call and once after every wake-driven mailbox drain. The returned string is wrapped in <dynamic-context> tags. Empty / whitespace-only returns are skipped. Pass nil to disable (default). Designed for long-running coordinators that need per-wake context refresh (e.g. open-file list, repo metadata, session topic) without re-engineering the system prompt. Coord callers should normally use WithCoordContextProvider instead, which threads through here. Stable.
type SharedMemory ¶
type SharedMemory struct {
// contains filtered or unexported fields
}
SharedMemory provides namespaced key-value storage for inter-agent collaboration. Keys are qualified as "agentName/key". Agents write to their own namespace but can read any namespace. Stable.
func NewSharedMemory ¶
func NewSharedMemory() *SharedMemory
NewSharedMemory creates a new empty SharedMemory.
func (*SharedMemory) Entries ¶
func (sm *SharedMemory) Entries() map[string]string
Entries returns a shallow copy of all entries (for persistence via Storage).
func (*SharedMemory) ListByAgent ¶
func (sm *SharedMemory) ListByAgent(agent string) map[string]string
ListByAgent returns all entries for the given agent, with keys stripped of the namespace prefix.
func (*SharedMemory) LoadEntries ¶
func (sm *SharedMemory) LoadEntries(entries map[string]string)
LoadEntries bulk-loads entries from Storage (for resume). Existing entries are replaced.
func (*SharedMemory) Read ¶
func (sm *SharedMemory) Read(qualifiedKey string) (string, bool)
Read returns the value for a fully qualified "agent/key" and whether it exists.
func (*SharedMemory) Summary ¶
func (sm *SharedMemory) Summary() string
Summary returns a markdown digest of all entries for context injection. Each entry is formatted as "- agent/key: value" (truncated to 100 chars).
func (*SharedMemory) Write ¶
func (sm *SharedMemory) Write(agent, key, value string)
Write stores a value at "agent/key", replacing any existing value. Agent names must not contain "/" to avoid namespace collisions.
type StepIsolation ¶
type StepIsolation = spec.StepIsolation
type StepResult ¶
type StepResult = spec.StepResult
type StepStatus ¶
type StepStatus = spec.StepStatus
type SubmitResultHandler ¶
type SubmitResultHandler struct {
// contains filtered or unexported fields
}
SubmitResultHandler validates and extracts submit_result tool call arguments. It checks required fields and basic type conformance against the agent's ResultSchema.
func NewSubmitResultHandler ¶
func NewSubmitResultHandler(schema map[string]any) *SubmitResultHandler
NewSubmitResultHandler creates a handler that validates against the given schema.
func (*SubmitResultHandler) Handle ¶
func (h *SubmitResultHandler) Handle(args json.RawMessage) (map[string]any, bool, error)
Handle parses the submit_result arguments, validates against schema, and returns (result map, terminated bool, error). terminated is always true on success (submit_result terminates the conversation).
type TokenBudgetTransformer ¶
type TokenBudgetTransformer struct {
// MaxBytesPerDep caps each dependency's content size in bytes.
// If 0, defaults to maxDepContentBytes (16KB).
MaxBytesPerDep int
}
TokenBudgetTransformer truncates step output to a fixed byte budget. This is the default CLI transformer (P7.7.7) that prevents context overflow on models with smaller context windows (e.g., MiniMax 196K tokens).
type ToolNotFoundError ¶
ToolNotFoundError indicates the coordinator referenced a tool not in the catalog.
func (*ToolNotFoundError) Error ¶
func (e *ToolNotFoundError) Error() string
type UnicodeUnsafeError ¶
UnicodeUnsafeError indicates a string contains a disallowed code point. Stable.
func (*UnicodeUnsafeError) Error ¶
func (e *UnicodeUnsafeError) Error() string
type ValidationError ¶
ValidationError represents a YAML/schema validation error. Err is an optional wrapped cause; when set, errors.Is and errors.As see through to the underlying error. Existing call sites that construct ValidationError without an Err field continue to work unchanged - Unwrap returns nil and the wrapper still satisfies the error interface. Stable.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
func (*ValidationError) Unwrap ¶
func (e *ValidationError) Unwrap() error
Unwrap exposes the wrapped cause (if any) so errors.Is / errors.As see through ValidationError. Returns nil when no inner error was set.
type Workflow ¶
func LoadWorkflow ¶
LoadWorkflow reads a YAML file, parses it, validates, and resolves @ refs.
func ParseCoordinatorResponse ¶
ParseCoordinatorResponse parses the coordinator LLM output into a Workflow. Returns an error if JSON is invalid or the workflow fails validation. Returns *JSONParseError for malformed JSON, *CoordinatorValidationError for schema issues.
func ParseWorkflow ¶
ParseWorkflow unmarshals YAML data into a Workflow and validates it. It does NOT resolve @ refs (no baseDir context).
Example ¶
ExampleParseWorkflow shows parsing a workflow from raw YAML bytes; for multi-error handling on invalid YAML see ExampleParseWorkflow_multiError.
package main
import (
"errors"
"fmt"
"github.com/zendev-sh/zenflow"
)
func main() {
yaml := `
name: greet
steps:
- id: hello
instructions: "Say hello to the user."
`
wf, err := zenflow.ParseWorkflow([]byte(yaml))
if err != nil {
// ParseWorkflow may return a joined error for multi-violation YAML.
// Unwrap with errors.As to inspect individual *zenflow.ValidationError.
var ve *zenflow.ValidationError
if errors.As(err, &ve) {
fmt.Println("validation error:", ve.Message)
}
return
}
fmt.Println(wf.Name)
fmt.Println(len(wf.Steps))
}
Output: greet 1
Example (MultiError) ¶
ExampleParseWorkflow_multiError shows how validation errors are surfaced as an errors.Join, allowing callers to inspect every violation via errors.As / errors.Is on individual ValidationError types.
package main
import (
"fmt"
"github.com/zendev-sh/zenflow"
)
func main() {
// Two agents both missing the required 'description' field - ValidateWorkflow
// accumulates one ValidationError per agent and joins them, so the
// caller sees all violations in one error value rather than only the first.
invalid := []byte(`
name: test
agents:
a:
prompt: "do a"
b:
prompt: "do b"
steps:
- id: s1
agent: a
instructions: "run a"
- id: s2
agent: b
instructions: "run b"
dependsOn: [s1]
`)
_, err := zenflow.ParseWorkflow(invalid)
if err == nil {
return
}
// err is errors.Join'd; iterate sub-errors via Unwrap []error.
if joined, ok := err.(interface{ Unwrap() []error }); ok {
fmt.Printf("found %d validation errors\n", len(joined.Unwrap()))
}
}
Output: found 2 validation errors
func ParseWorkflowJSON ¶
ParseWorkflowJSON unmarshals JSON data into a Workflow and validates it. Used for coordinator output (JSON format) and .json workflow files. (2026-05-04) - switched from json.Unmarshal to json.Decoder with DisallowUnknownFields so that schema.json's `additionalProperties: false` is enforced symmetrically with the YAML path. Previously YAML rejected unknown fields via yaml.v3 `KnownFields(true)` but JSON silently dropped them, letting a consumer's typo in a `.json` workflow pass without diagnostic.
type WorkflowOptions ¶
type WorkflowOptions = spec.WorkflowOptions
type WorkflowResult ¶
type WorkflowResult = spec.WorkflowResult
type WorkflowStatus ¶
type WorkflowStatus = spec.WorkflowStatus
Source Files
¶
- agent_handle.go
- agent_messaging.go
- agent_primary_id.go
- agent_runner.go
- agent_tool.go
- aliases.go
- approval_timeout.go
- cel.go
- coord_factory.go
- coord_lib.go
- coord_loop.go
- drop_fanout.go
- e2e_enabled_default.go
- env_expand.go
- errors.go
- executor.go
- executor_coord.go
- executor_include.go
- executor_loop.go
- executor_resume.go
- executor_schedule.go
- executor_step.go
- executor_tool.go
- executor_trace.go
- factory_cache.go
- goal_decomposer.go
- isolation.go
- limits.go
- mcp.go
- options.go
- output_transform.go
- parse.go
- permission_policy.go
- portability.go
- progress_pump.go
- prompt.go
- redact.go
- scheduler.go
- shared_memory.go
- shared_memory_tool.go
- step_termination.go
- storage_file.go
- storage_mem.go
- storage_schema.go
- submit_result.go
- tools_validate.go
- zenflow.go