spi

package
v0.47.0 Latest Latest
Warning

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

Go to latest
Published: Aug 23, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package spi is the L0 contract — the "service provider interface" — of echopoint-runner. It owns the wire-facing and execution-result types that cross process and repo boundaries: echopoint's openapi.yaml binds its enums here via x-go-type, and the control plane decodes flow results shaped by these types over SSE and the database.

spi depends on nothing inside echopoint-runner (only the standard library), so it sits at the bottom of the import graph: pkg/node, pkg/extractors, pkg/executionevents and pkg/engine all depend on spi, never the reverse. These contract types are referenced directly as spi.* throughout the runner; spi is the single source of truth, with no re-export or alias layer in between.

The JSON struct tags and enum string values in this package are a cross-repo contract; changing them is a breaking wire change.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func As

func As[T AnyResult](result AnyResult) (T, bool)

As safely casts an AnyResult to a concrete result type T (e.g. As[*request.RequestExecutionResult](result)). It reports false instead of panicking when the dynamic type does not match.

func MustAs

func MustAs[T AnyResult](result AnyResult) T

MustAs casts an AnyResult to a concrete result type T, panicking when the dynamic type does not match. Use only where the type is an invariant.

Types

type AnyResult

type AnyResult interface {
	GetNodeID() string
	GetDisplayName() string
	GetNodeType() Kind
	GetInputs() map[string]any
	GetOutputs() map[string]any
	GetError() error
	GetExecutedAt() time.Time
	// contains filtered or unexported methods
}

AnyResult is the interface for all node execution results (polymorphic).

type AssertionResult

type AssertionResult struct {
	Index     int    `json:"index"`
	Extractor string `json:"extractor"`
	Operator  string `json:"operator"`
	Expected  any    `json:"expected"`
	Actual    any    `json:"actual"`
	Passed    bool   `json:"passed"`
	Error     string `json:"error,omitempty"`
}

AssertionResult records the outcome of evaluating a single node assertion, captured whether it passed or failed so the full result can be reported.

type BaseExecutionResult

type BaseExecutionResult struct {
	NodeID        string         `json:"node_id"`
	DisplayName   string         `json:"display_name"`
	NodeType      Kind           `json:"node_type"`
	RunWhen       RunWhen        `json:"run_when,omitempty"`
	Inputs        map[string]any `json:"inputs"`
	Outputs       map[string]any `json:"outputs"`
	Error         error          `json:"-"` // Don't serialize Go error
	ErrorCode     *string        `json:"error_code,omitempty"`
	ErrorMsg      *string        `json:"error_message,omitempty"`
	SkipReason    *string        `json:"skip_reason,omitempty"`
	MissingInputs []string       `json:"missing_inputs,omitempty"`
	ExecutedAt    time.Time      `json:"executed_at"`

	// AssertionResults records every assertion evaluated on this node (pass or
	// fail). It lives on the shared base so the engine-level assertion pass fills
	// it uniformly for every node kind. The JSON tag is a wire contract consumed
	// by echopoint.
	AssertionResults []AssertionResult `json:"assertion_results,omitempty"`
}

BaseExecutionResult provides common fields for all execution results. Its JSON tags are a wire contract consumed by echopoint.

func (*BaseExecutionResult) Fail

func (b *BaseExecutionResult) Fail(err error, code string)

Fail marks the result as failed: it stores the Go error and surfaces a user-facing message and stable code on the wire. A nil err is ignored.

func (*BaseExecutionResult) GetDisplayName

func (b *BaseExecutionResult) GetDisplayName() string

GetDisplayName returns the node display name.

func (*BaseExecutionResult) GetError

func (b *BaseExecutionResult) GetError() error

GetError returns the error if any.

func (*BaseExecutionResult) GetExecutedAt

func (b *BaseExecutionResult) GetExecutedAt() time.Time

GetExecutedAt returns the execution timestamp.

func (*BaseExecutionResult) GetInputs

func (b *BaseExecutionResult) GetInputs() map[string]any

GetInputs returns the inputs map.

func (*BaseExecutionResult) GetNodeID

func (b *BaseExecutionResult) GetNodeID() string

GetNodeID returns the node ID.

func (*BaseExecutionResult) GetNodeType

func (b *BaseExecutionResult) GetNodeType() Kind

GetNodeType returns the node type.

func (*BaseExecutionResult) GetOutputs

func (b *BaseExecutionResult) GetOutputs() map[string]any

GetOutputs returns the outputs map.

func (*BaseExecutionResult) MergeOutputs

func (b *BaseExecutionResult) MergeOutputs(outputs map[string]any)

MergeOutputs copies the given outputs into the result's Outputs map, lazily initializing it. Existing keys are overwritten.

func (*BaseExecutionResult) SetAssertionResults

func (b *BaseExecutionResult) SetAssertionResults(results []AssertionResult)

SetAssertionResults records the evaluated assertions on the result. Used by the engine-level assertion pass so every node kind reports them uniformly.

type DynamicResolver

type DynamicResolver interface {
	Resolve(name string, args []string) (string, error)
}

DynamicResolver resolves a {{$name:args}} dynamic template variable to a generated value. Implemented by pkg/dynamicvars.

type EventType

type EventType string

EventType identifies a runner execution/progress event on the wire. The helper sets (progress vs terminal) live with the consumers in pkg/executionevents; this package owns only the wire identifiers.

const (
	EventFlowStarted   EventType = "flow.started"
	EventNodeStarted   EventType = "node.started"
	EventNodeCompleted EventType = "node.completed"
	EventNodeFailed    EventType = "node.failed"
	EventFlowCompleted EventType = "flow.completed"
	EventFlowFailed    EventType = "flow.failed"
)

Execution/progress event types.

type ExecutionContext

type ExecutionContext struct {
	// Ctx is the request-scoped context for the execution. Nodes use it for
	// cancellation and deadlines (e.g. the HTTP request honors it). May be nil,
	// in which case callers should treat it as context.Background().
	Ctx context.Context
	// Inputs contains all the data this node declared it needs in InputSchema().
	// Keys are in format "nodeId.outputKey" (e.g., "create-user.userId").
	Inputs map[string]any
	// FlowInputs contains the full effective inputs for the current flow execution,
	// including inherited inputs, static overrides, and any initial input values.
	FlowInputs map[string]any
	// AllOutputs exposes a read-only snapshot of outputs from nodes that completed
	// before the current scheduling batch started.
	AllOutputs OutputView
	// ModuleResolver exposes the additional flow definitions available to module
	// nodes during nested execution.
	ModuleResolver ModuleResolver
	// ModuleExecutor runs nested flows for module nodes.
	ModuleExecutor ModuleExecutor
	// DynamicVars resolves {{$name}} template variables (fake-data generators).
	// May be nil, in which case {{$...}} references are left untouched.
	DynamicVars DynamicResolver
}

ExecutionContext provides inputs and context for a node's execution.

func (ExecutionContext) Context

func (c ExecutionContext) Context() context.Context

Context returns the execution context, defaulting to context.Background() when none was provided so callers never need a nil check.

type ExtractorType

type ExtractorType string

ExtractorType identifies an output/assertion extractor on the wire. The extractor implementations live in pkg/extractors and self-register; this is just the wire identifier the contract binds to.

const (
	ExtractorTypeJSONPath   ExtractorType = "jsonPath"
	ExtractorTypeXMLPath    ExtractorType = "xmlPath"
	ExtractorTypeStatusCode ExtractorType = "statusCode"
	ExtractorTypeHeader     ExtractorType = "header"
	ExtractorTypeBody       ExtractorType = "body"
)

Built-in extractor types.

type FlowExecutionResult

type FlowExecutionResult struct {
	ExecutionResults map[string]AnyResult `json:"execution_results"` // Polymorphic results!
	FinalOutputs     map[string]any       `json:"final_outputs"`     // All outputs flattened ("nodeId.outputKey": value)
	Success          bool                 `json:"success"`
	Error            error                `json:"-"`
	ErrorCode        *string              `json:"error_code,omitempty"`
	ErrorMsg         *string              `json:"error_message,omitempty"`
	DurationMS       int64                `json:"duration_ms"`
}

FlowExecutionResult contains the complete trace of a flow execution.

type Kind

type Kind string

Kind identifies a node kind on the wire. The node-kind registry, not a fixed set, decides which kinds are valid; the constants below are the built-ins.

const (
	KindRequest     Kind = "request"
	KindDelay       Kind = "delay"
	KindModule      Kind = "module"
	KindSetVariable Kind = "set_variable"
	KindLoop        Kind = "loop"
	KindPoll        Kind = "poll"
	KindAssert      Kind = "assert"
	KindBranch      Kind = "branch"
	KindSse         Kind = "sse"
)

Built-in node kinds.

type ModuleExecutionRequest

type ModuleExecutionRequest struct {
	FlowID         string
	FlowDefinition []byte
	Inputs         map[string]any
}

ModuleExecutionRequest is a request to run a nested flow for a module node.

type ModuleExecutor

type ModuleExecutor interface {
	ExecuteModule(request ModuleExecutionRequest) (*FlowExecutionResult, error)
}

ModuleExecutor runs nested flows for module nodes.

type ModuleResolver

type ModuleResolver interface {
	ResolveFlow(flowID string) (ResolvedModuleFlow, bool)
}

ModuleResolver exposes the additional flow definitions available to module nodes during nested execution.

type Node

type Node interface {
	GetID() string
	GetDisplayName() string
	GetType() Kind
	GetRunWhen() RunWhen
	InputSchema() []string

	// OutputSchema defines what this node produces
	// Examples: []string{"statusCode", "userId", "responseBody"}
	OutputSchema() []string

	// Execute performs the node's action with provided inputs and returns the
	// polymorphic result. Error indicates execution failure.
	Execute(ctx ExecutionContext) (AnyResult, error)
}

Node is the engine's core view of any flow node — the capability-agnostic surface the scheduler drives. The full authoring interface (node.AnyNode) embeds this and adds the assertion/output accessors, which carry concrete extractor decode/eval behavior and so live in pkg/node.

type OutputView

type OutputView interface {
	HasNode(nodeID string) bool
	Get(nodeID, outputKey string) (any, bool)
	// Node returns a defensive copy of the requested node outputs.
	Node(nodeID string) map[string]any
}

OutputView is a read-only snapshot of outputs from already-completed nodes.

type ResolvedModuleFlow

type ResolvedModuleFlow struct {
	FlowDefinition []byte
	InputOverrides map[string]any
}

ResolvedModuleFlow is a flow definition (plus input overrides) made available to a module node for nested execution.

type RoutingResult

type RoutingResult interface {
	AnyResult
	// RoutedTargets returns the successor node IDs this result routes execution
	// to. An empty slice means no successor was chosen (every successor edge is
	// dead).
	RoutedTargets() []string
}

RoutingResult is implemented by node results that select which successor edges are taken; the engine skips the untaken successors' subtrees. A routing node (e.g. the branch node) returns the IDs of the successor nodes it routed TO; the engine treats every other successor edge as dead and cascades the skip through that subtree.

type RunWhen

type RunWhen string

RunWhen controls whether a node runs only on the success path or also after the main phase has already failed.

const (
	RunWhenOnSuccess RunWhen = "on_success"
	RunWhenAlways    RunWhen = "always"
)

RunWhen phases.

type UserError

type UserError struct {
	Code    string
	Message string
	Cause   error
}

UserError marks a node failure that is caused by the flow author or the system they are calling — an unreachable or misconfigured target endpoint, invalid node input, a failed assertion — rather than by a fault in the runner itself.

It carries a stable Code and a user-facing Message that node results surface to the caller (error_code / error_message), and it preserves the underlying Cause for errors.Is/errors.As. The engine logs UserErrors at debug, reserving error level for genuine runner faults, because a UserError is an expected outcome already reported back to the user in the execution result.

func AsUserError

func AsUserError(err error) (*UserError, bool)

AsUserError reports whether err is (or wraps) a UserError, returning it when so.

func NewUserError

func NewUserError(code, message string, cause error) *UserError

NewUserError builds a UserError with a stable code, a user-facing message, and the underlying cause (which may be nil).

func (*UserError) Error

func (e *UserError) Error() string

func (*UserError) Unwrap

func (e *UserError) Unwrap() error

Jump to

Keyboard shortcuts

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