spi

package
v0.50.0 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 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 ErrorCode added in v0.50.0

func ErrorCode(err error) string

ErrorCode returns a UserError's stable code, or "" for a runner fault.

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.

func SafeErrorMessage added in v0.50.0

func SafeErrorMessage(err error) string

SafeErrorMessage returns the part of err that may be written to a log line.

For a UserError that is its authored Message alone. The Cause is dropped: for a request or SSE failure it is the raw Go transport error, which embeds the full target URL — query string, and therefore any secret carried in it, included. Logs are not redacted, so the cause never reaches them; it stays on the error for errors.Is/errors.As, and the failure is reported to the user through the (redacted) node result instead.

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