ports

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Mar 28, 2026 License: EUPL-1.2 Imports: 7 Imported by: 0

Documentation

Overview

Package ports defines the boundary interfaces for the hexagonal architecture.

Port interfaces establish contracts between the domain layer and external concerns, enabling dependency inversion and testability. All adapters in the infrastructure layer implement these ports, ensuring the domain layer remains pure and agnostic of external implementation details.

Architecture Role

In the hexagonal architecture pattern:

  • Domain layer defines port interfaces (this package)
  • Infrastructure layer implements ports as adapters (repositories, executors, stores)
  • Application layer orchestrates domain operations through ports
  • Domain layer depends on nothing; all dependencies point inward

This inverted dependency structure allows:

  • Testing domain logic with mock implementations
  • Swapping infrastructure implementations without domain changes
  • Clear boundaries between business logic and technical concerns

Port Interfaces by Concern

## Repository Ports

Workflow and template persistence:

  • WorkflowRepository: Load, list, and check existence of workflow definitions
  • TemplateRepository: Load, list, and check existence of workflow templates

## State Persistence Ports

Runtime state and history management:

  • StateStore: Save, load, delete, and list workflow execution states
  • HistoryStore: Record, query, and cleanup workflow execution history
  • PluginStore: Persist plugin state across sessions
  • PluginConfig: Manage plugin enabled state and configuration
  • PluginStateStore: Combined persistence and configuration interface

## Execution Ports

Command and step execution contracts:

  • CommandExecutor: Execute shell commands via detected shell ($SHELL, fallback /bin/sh) with streaming support
  • CLIExecutor: Execute external binaries directly without shell interpretation
  • StepExecutor: Execute a single workflow step (used by parallel execution)
  • ParallelExecutor: Execute multiple branches concurrently with strategy support

## Agent Integration Ports

AI agent CLI invocation and conversation management:

  • AgentProvider: Execute AI agent prompts (single-shot and conversation modes)
  • AgentRegistry: Register, retrieve, and list available agent providers
  • Tokenizer: Count tokens for context window management (exact or approximate)

## Plugin System Ports

Plugin lifecycle and operation management:

  • Plugin: Base interface all plugins must implement (init, shutdown)
  • PluginManager: Discover, load, initialize, and shutdown plugins
  • OperationProvider: Execute plugin-provided operations
  • PluginRegistry: Register and unregister plugin operations
  • PluginLoader: Discover and load plugins from filesystem

## Interactive Mode Ports

User interaction during workflow execution:

  • InteractivePrompt: Step-by-step execution control (run, skip, retry, abort, edit)
  • InputCollector: Pre-execution collection of missing workflow inputs

## Expression Evaluation Ports

Runtime expression handling:

  • ExpressionEvaluator: Evaluate boolean and integer expressions with runtime context
  • ExpressionValidator: Validate expression syntax at compile-time

## Logging Ports

Structured logging abstraction:

  • Logger: Domain logging interface (debug, info, warn, error) with context support

Usage Patterns

Ports are consumed by:

  1. Application services (dependency injection)
  2. Domain entities (passed as parameters when needed)
  3. Test code (mock implementations)

Example: Injecting ports into application service

type WorkflowService struct {
    repo   ports.WorkflowRepository
    store  ports.StateStore
    logger ports.Logger
}

func NewWorkflowService(
    repo ports.WorkflowRepository,
    store ports.StateStore,
    executor ports.Executor,
    logger ports.Logger,
    validator ports.ExpressionValidator,
) *WorkflowService {
    return &WorkflowService{
        repo:      repo,
        store:     store,
        executor:  executor,
        logger:    logger,
        validator: validator,
    }
}

Example: Testing with mock implementations

func TestWorkflowExecution(t *testing.T) {
    mockRepo := testutil.NewMockWorkflowRepository()
    mockStore := testutil.NewMockStateStore()
    mockLogger := testutil.NewMockLogger()

    service := NewWorkflowService(mockRepo, mockStore, mockExecutor, mockLogger, mockValidator)
    // Test service behavior
}

Port Design Principles

When implementing port interfaces:

  • Accept context.Context for cancellation and timeout support
  • Return domain errors (not infrastructure errors) when possible
  • Use domain types in signatures (workflow.Workflow, not YAML structs)
  • Keep interfaces small and focused (Interface Segregation Principle)
  • Document contract expectations and error conditions
  • internal/domain/workflow: Core domain entities (Workflow, Step, State, Context)
  • internal/application: Services that consume ports
  • internal/infrastructure: Concrete implementations of ports
  • internal/testutil: Mock implementations for testing

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AgentProvider

type AgentProvider interface {
	// Execute invokes the agent with the given prompt and options.
	// Returns AgentResult containing output, parsed response, token usage, and any errors.
	Execute(ctx context.Context, prompt string, options map[string]any) (*workflow.AgentResult, error)

	// ExecuteConversation invokes the agent with conversation history for multi-turn interactions.
	// The state parameter contains the conversation history (turns) to send to the agent.
	// Returns ConversationResult containing the updated conversation state, final output, and token usage.
	ExecuteConversation(ctx context.Context, state *workflow.ConversationState, prompt string, options map[string]any) (*workflow.ConversationResult, error)

	// Name returns the provider identifier (e.g., "claude", "codex", "gemini").
	Name() string

	// Validate checks if the provider is properly configured and available.
	// Returns error if the agent CLI binary is not found or misconfigured.
	Validate() error
}

AgentProvider defines the contract for executing AI agent CLI commands. Implementations adapt specific agent CLIs (Claude, Codex, Gemini, etc.) to this unified interface.

type AgentRegistry

type AgentRegistry interface {
	// Register adds a provider to the registry.
	// Returns error if a provider with the same name already exists.
	Register(provider AgentProvider) error

	// Get retrieves a provider by name.
	// Returns error if provider is not found.
	Get(name string) (AgentProvider, error)

	// List returns all registered provider names.
	List() []string

	// Has checks if a provider with the given name is registered.
	Has(name string) bool
}

AgentRegistry manages available agent providers and resolves them by name.

type AuditTrailWriter

type AuditTrailWriter interface {
	Write(ctx context.Context, event *workflow.AuditEvent) error
	Close() error
}

AuditTrailWriter defines the contract for appending audit trail entries.

type CLIExecutor

type CLIExecutor interface {
	// Run executes a binary with given arguments.
	// Returns stdout and stderr as byte slices, plus any execution error.
	//
	// The context allows cancellation and timeout control.
	// If the context is cancelled, the execution should be terminated.
	//
	// Error cases:
	// - Binary not found: error != nil
	// - Non-zero exit code: error != nil (error should contain exit code info)
	// - Context cancelled/timeout: error will be context.Canceled or context.DeadlineExceeded
	Run(ctx context.Context, name string, args ...string) (stdout, stderr []byte, err error)
}

CLIExecutor defines the contract for executing external CLI binaries. Unlike CommandExecutor (shell execution via detected shell), this executes binaries directly without shell interpretation.

This interface is designed for testing agent providers that invoke external CLI tools (claude, gemini, codex, etc.) by allowing test code to inject mock implementations that return predefined responses.

type Command

type Command struct {
	Program      string
	Dir          string
	Env          map[string]string
	IsScriptFile bool
	Stdout       io.Writer
	Stderr       io.Writer
}

Command represents a command to execute.

When IsScriptFile is false (default), Program is passed to the user's detected shell (via $SHELL, fallback /bin/sh) as a shell command string, allowing shell features like pipes and redirects.

When IsScriptFile is true and Program starts with a shebang (#!), the content is written to a temporary file, made executable, and executed directly — letting the kernel dispatch the correct interpreter. If no shebang is present, execution falls back to $SHELL -c for backward compatibility.

Use ShellEscape() from pkg/interpolation for user-provided values.

type CommandExecutor

type CommandExecutor interface {
	Execute(ctx context.Context, cmd *Command) (*CommandResult, error)
}

type CommandResult

type CommandResult struct {
	Stdout   string
	Stderr   string
	ExitCode int
}

type ErrorFormatter

type ErrorFormatter interface {
	// FormatError converts a StructuredError into a formatted string representation.
	// The format depends on the implementation (JSON, human-readable, etc.).
	//
	// Parameters:
	//   - err: The structured error to format
	//
	// Returns:
	//   - Formatted error string according to the implementation's output mode
	//
	// Example:
	//
	//	// JSON formatter:
	//	// {"error_code":"USER.INPUT.MISSING_FILE","message":"workflow file not found",...}
	//
	//	// Human formatter:
	//	// [USER.INPUT.MISSING_FILE] workflow file not found
	FormatError(err *errors.StructuredError) string
}

ErrorFormatter defines the contract for formatting structured errors into different output representations (JSON, human-readable, etc.). Implementations live in infrastructure layer.

ErrorFormatter enables:

  • Machine-readable JSON output for CI/CD pipelines
  • Human-readable CLI output with color and formatting
  • Consistent error presentation across output modes

Example usage:

formatter := infrastructure.NewJSONErrorFormatter()
output := formatter.FormatError(structuredErr)

type ExpressionEvaluator

type ExpressionEvaluator interface {
	// EvaluateBool evaluates a boolean expression against the provided context.
	// Returns the boolean result of the expression, or an error if evaluation fails.
	// Example: "inputs.count > 5" → true/false
	EvaluateBool(expr string, ctx *interpolation.Context) (bool, error)

	// EvaluateInt evaluates an arithmetic expression against the provided context.
	// Returns the integer result of the expression, or an error if evaluation fails.
	// The result is coerced to int type regardless of the underlying numeric type.
	// Example: "20 / 4" → 5
	EvaluateInt(expr string, ctx *interpolation.Context) (int, error)
}

ExpressionEvaluator defines the contract for evaluating runtime expressions. This port abstracts expression evaluation to maintain domain layer purity by avoiding direct dependencies on external expression libraries.

Unlike ExpressionValidator (which validates syntax at compile-time), ExpressionEvaluator handles runtime evaluation with actual data context.

type ExpressionValidator

type ExpressionValidator interface {
	// Compile validates the syntax of an expression string.
	// Returns nil if the expression is syntactically valid, error otherwise.
	// This method does NOT evaluate the expression, only checks if it can be compiled.
	Compile(expression string) error
}

ExpressionValidator defines the contract for validating expression syntax. This port abstracts expression compilation to maintain domain layer purity by avoiding direct dependencies on external expression libraries.

type HistoryStore

type HistoryStore interface {
	Record(ctx context.Context, record *workflow.ExecutionRecord) error
	List(ctx context.Context, filter *workflow.HistoryFilter) ([]*workflow.ExecutionRecord, error)
	GetStats(ctx context.Context, filter *workflow.HistoryFilter) (*workflow.HistoryStats, error)
	Cleanup(ctx context.Context, olderThan time.Duration) (int, error)
	Close() error
}

HistoryStore defines the contract for persisting workflow execution history.

type InputCollector

type InputCollector interface {
	// PromptForInput prompts the user to provide a value for a single workflow input.
	//
	// Behavior by input type:
	//   - Required inputs: Empty value triggers error and re-prompt
	//   - Optional inputs: Empty value returns input.Default or nil
	//   - Enum inputs: Display numbered list (1-9) for selection
	//   - Validated inputs: Apply workflow.InputValidation constraints, re-prompt on error
	//
	// Type coercion is applied based on input.Type:
	//   - "string": Return value as-is
	//   - "integer": Parse string to int
	//   - "boolean": Parse "true"/"false" to bool
	//
	// Returns:
	//   - Validated input value (typed as any for flexibility)
	//   - Error if user cancels (Ctrl+C, EOF) or validation fails repeatedly
	//
	// Implementation notes:
	//   - Check stdin is terminal before calling (fail if non-interactive)
	//   - Handle io.EOF as cancellation, not panic
	//   - Display validation errors with constraint details
	PromptForInput(ctx context.Context, input *workflow.Input) (any, error)
}

InputCollector defines the contract for collecting missing workflow inputs interactively. This port is used for pre-execution input collection when required inputs are missing from command-line arguments.

Key differences from InteractivePrompt:

  • InputCollector: Pre-execution input collection (before workflow starts)
  • InteractivePrompt: During-execution step control (while workflow runs)

Implementation requirements:

  • Display enum options as numbered list when Validation.Enum is present
  • Validate input values against workflow.InputValidation constraints
  • Re-prompt on validation errors with specific error messages
  • Handle optional inputs by accepting empty values and using defaults
  • Support graceful cancellation (Ctrl+C, Ctrl+D/EOF)

Example usage:

collector := cli.NewCLIInputCollector(os.Stdin, os.Stdout, colorizer)
value, err := collector.PromptForInput(&workflow.Input{
    Name:        "environment",
    Type:        "string",
    Description: "Deployment environment",
    Required:    true,
    Validation: &workflow.InputValidation{
        Enum: []string{"dev", "staging", "prod"},
    },
})
if err != nil {
    return fmt.Errorf("input collection failed: %w", err)
}

type InteractivePrompt

type InteractivePrompt interface {
	StepPresenter
	StatusPresenter
	UserInteraction
}

InteractivePrompt defines the composite contract for interactive mode user interaction. Implementations handle UI rendering and user input during step-by-step execution. This interface embeds StepPresenter, StatusPresenter, and UserInteraction for backward compatibility.

type Logger

type Logger interface {
	Debug(msg string, fields ...any)
	Info(msg string, fields ...any)
	Warn(msg string, fields ...any)
	Error(msg string, fields ...any)
	WithContext(ctx map[string]any) Logger
}

type OperationProvider

type OperationProvider interface {
	// GetOperation returns an operation by name.
	GetOperation(name string) (*pluginmodel.OperationSchema, bool)
	// ListOperations returns all available operations.
	ListOperations() []*pluginmodel.OperationSchema
	// Execute runs a plugin operation.
	Execute(ctx context.Context, name string, inputs map[string]any) (*pluginmodel.OperationResult, error)
}

OperationProvider supplies plugin-provided operations.

type ParallelExecutor

type ParallelExecutor interface {
	// Execute runs multiple branches concurrently according to the given strategy.
	// It respects the MaxConcurrent limit via semaphore and applies the strategy
	// to determine overall success/failure.
	Execute(
		ctx context.Context,
		wf *workflow.Workflow,
		branches []string,
		config workflow.ParallelConfig,
		execCtx *workflow.ExecutionContext,
		stepExecutor StepExecutor,
	) (*workflow.ParallelResult, error)
}

ParallelExecutor defines the contract for executing parallel branches.

type Plugin

type Plugin interface {
	// Name returns the unique plugin identifier.
	Name() string
	// Version returns the plugin version.
	Version() string
	// Init initializes the plugin with configuration.
	Init(ctx context.Context, config map[string]any) error
	// Shutdown gracefully stops the plugin.
	Shutdown(ctx context.Context) error
}

Plugin defines the contract that all plugins must implement.

type PluginConfig

type PluginConfig interface {
	// SetEnabled enables or disables a plugin by name.
	SetEnabled(ctx context.Context, name string, enabled bool) error
	// IsEnabled returns whether a plugin is enabled.
	// Default is true: plugins not explicitly disabled are considered enabled.
	// Only plugins whose name appears in the disabled set return false.
	// Unknown plugin names return true (default-enabled contract).
	IsEnabled(name string) bool
	// GetConfig returns the stored configuration for a plugin.
	GetConfig(name string) map[string]any
	// SetConfig stores configuration for a plugin.
	SetConfig(ctx context.Context, name string, config map[string]any) error
}

PluginConfig manages plugin configuration and enabled state.

type PluginLoader

type PluginLoader interface {
	// DiscoverPlugins scans a directory for plugins and returns their info.
	// Each subdirectory with a plugin.yaml is considered a plugin.
	DiscoverPlugins(ctx context.Context, pluginsDir string) ([]*pluginmodel.PluginInfo, error)
	// LoadPlugin loads a single plugin from a directory path.
	LoadPlugin(ctx context.Context, pluginDir string) (*pluginmodel.PluginInfo, error)
	// ValidatePlugin checks if a discovered plugin is valid and compatible.
	ValidatePlugin(info *pluginmodel.PluginInfo) error
}

PluginLoader discovers and loads plugins from the filesystem.

type PluginManager

type PluginManager interface {
	// Discover finds plugins in the plugins directory.
	Discover(ctx context.Context) ([]*pluginmodel.PluginInfo, error)
	// Load loads a plugin by name.
	Load(ctx context.Context, name string) error
	// Init initializes a loaded plugin.
	Init(ctx context.Context, name string, config map[string]any) error
	// Shutdown stops a running plugin.
	Shutdown(ctx context.Context, name string) error
	// ShutdownAll stops all running plugins.
	ShutdownAll(ctx context.Context) error
	// Get returns plugin info by name.
	Get(name string) (*pluginmodel.PluginInfo, bool)
	// List returns all known plugins.
	List() []*pluginmodel.PluginInfo
}

PluginManager handles plugin lifecycle operations.

type PluginRegistry

type PluginRegistry interface {
	// RegisterOperation adds a plugin operation.
	RegisterOperation(op *pluginmodel.OperationSchema) error
	// UnregisterOperation removes a plugin operation.
	UnregisterOperation(name string) error
	// Operations returns all registered operations.
	Operations() []*pluginmodel.OperationSchema
}

PluginRegistry manages registration of plugin-provided extensions.

type PluginStateStore

type PluginStateStore interface {
	PluginStore
	PluginConfig
}

PluginStateStore combines persistence and configuration interfaces. Maintains backward compatibility while enabling consumers to use narrower interfaces.

type PluginStore

type PluginStore interface {
	// Save persists all plugin states to storage.
	Save(ctx context.Context) error
	// Load reads plugin states from storage.
	Load(ctx context.Context) error
	// GetState returns the full state for a plugin, or nil if not found.
	GetState(name string) *pluginmodel.PluginState
	// ListDisabled returns names of all explicitly disabled plugins.
	ListDisabled() []string
}

PluginStore handles plugin state persistence.

type StateStore

type StateStore interface {
	Save(ctx context.Context, state *workflow.ExecutionContext) error
	Load(ctx context.Context, workflowID string) (*workflow.ExecutionContext, error)
	Delete(ctx context.Context, workflowID string) error
	List(ctx context.Context) ([]string, error)
}

StateStore defines the contract for persisting workflow execution state.

type StatusPresenter

type StatusPresenter interface {
	// ShowAborted displays a message indicating workflow was aborted.
	ShowAborted()

	// ShowSkipped displays a message indicating step was skipped.
	ShowSkipped(stepName string, nextStep string)

	// ShowCompleted displays a message indicating workflow completed.
	ShowCompleted(status workflow.ExecutionStatus)

	// ShowError displays an error message.
	ShowError(err error)
}

StatusPresenter defines the contract for displaying terminal and outcome states. Implementations handle rendering of workflow completion, abortion, and errors.

type StepExecutor

type StepExecutor interface {
	// ExecuteStep runs a single step and returns the result.
	// The step is looked up by name from the workflow.
	ExecuteStep(
		ctx context.Context,
		wf *workflow.Workflow,
		stepName string,
		execCtx *workflow.ExecutionContext,
	) (*workflow.BranchResult, error)
}

StepExecutor defines the contract for executing a single workflow step. This is used by parallel execution to delegate individual branch execution.

type StepPresenter

type StepPresenter interface {
	// ShowHeader displays the interactive mode header with workflow name.
	ShowHeader(workflowName string)

	// ShowStepDetails displays step information before execution.
	ShowStepDetails(info *workflow.InteractiveStepInfo)

	// ShowExecuting displays a message indicating step execution is in progress.
	ShowExecuting(stepName string)

	// ShowStepResult displays the outcome of step execution.
	ShowStepResult(state *workflow.StepState, nextStep string)
}

StepPresenter defines the contract for displaying step lifecycle information. Implementations handle rendering of step execution progress and outcomes.

type TemplateRepository

type TemplateRepository interface {
	GetTemplate(ctx context.Context, name string) (*workflow.Template, error)
	ListTemplates(ctx context.Context) ([]string, error)
	Exists(ctx context.Context, name string) bool
}

type Tokenizer

type Tokenizer interface {
	// CountTokens returns the number of tokens in the given text.
	// The count may be exact (tiktoken) or approximate (character-based)
	// depending on the implementation.
	CountTokens(text string) (int, error)

	// CountTurnsTokens returns the total token count across multiple conversation turns.
	// This allows optimizations for batch counting in some implementations.
	CountTurnsTokens(turns []string) (int, error)

	// IsEstimate returns true if this tokenizer produces approximate counts.
	// Used to set TokensEstimated flag in conversation results.
	IsEstimate() bool

	// ModelName returns the tokenizer model identifier (e.g., "cl100k_base", "approximation").
	// Used for debugging and logging.
	ModelName() string
}

Tokenizer defines the contract for counting tokens in text. Implementations provide model-specific or approximation-based token counting for context window management.

type UserInteraction

type UserInteraction interface {
	// PromptAction prompts the user for an action and returns their choice.
	// hasRetry indicates whether the [r]etry option should be available.
	PromptAction(ctx context.Context, hasRetry bool) (workflow.InteractiveAction, error)

	// EditInput prompts the user to edit an input value.
	// Returns the new value and any error from parsing.
	EditInput(ctx context.Context, name string, current any) (any, error)

	// ShowContext displays the current runtime context (inputs, states).
	ShowContext(ctx *workflow.RuntimeContext)
}

UserInteraction defines the contract for interactive user input and context display. Implementations handle user prompts, input editing, and runtime context visualization.

type WorkflowRepository

type WorkflowRepository interface {
	Load(ctx context.Context, name string) (*workflow.Workflow, error)
	List(ctx context.Context) ([]string, error)
	Exists(ctx context.Context, name string) (bool, error)
}

Jump to

Keyboard shortcuts

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