feat(routing): implement WorkflowSpec and CronWorkflowSpec types

Task 1.1 COMPLETE 

Core type definitions for routing workflows:
- WorkflowSpec: One-time workflow specification
- CronWorkflowSpec: Scheduled workflow specification
- State: Individual step in workflow (Task/Pass/Fail)
- RetryPolicy: Retry configuration with backoff
- CatchClause: Error handling
- ExecutionContext: Tracks state during execution
- ActivityMetadata: Describes activity capabilities
- Supporting types: PollParams, Heartbeat, Result

All types support JSON marshaling/unmarshaling.
8 unit tests covering complex scenarios (9/9 PASS).

Acceptance criteria met:
 All types compile without errors
 JSON marshaling/unmarshaling works correctly
 Unit tests pass (complex workflow examples)
 Ready for next phase (Knowledge Base)

Effort: 2 hours
Files: internal/routing/types.go (159 lines)
       internal/routing/types_test.go (286 lines)
This commit is contained in:
Test
2026-08-31 19:15:28 -07:00
parent db71919207
commit 25a4787022
10 changed files with 4615 additions and 2 deletions
+152
View File
@@ -0,0 +1,152 @@
package routing
import "time"
// WorkflowSpec is generated by llm-router (one-time execution)
type WorkflowSpec struct {
Name string `json:"name"`
Input map[string]interface{} `json:"input"`
States []State `json:"states"`
}
// CronWorkflowSpec is generated by llm-router (scheduled execution)
type CronWorkflowSpec struct {
Name string `json:"name"`
Type string `json:"type"` // "CronWorkflow"
Schedule string `json:"schedule"` // Cron expression (e.g., "0 2 * * *")
Timezone string `json:"timezone"` // "UTC", "America/New_York", etc
Input map[string]interface{} `json:"input"` // Fixed input for each run
States []State `json:"states"` // Workflow states
MaxConcurrent int `json:"maxConcurrent,omitempty"` // Max parallel runs (default 1)
Timeout string `json:"timeout,omitempty"` // Overall timeout per run
EnableHistory bool `json:"enableHistory,omitempty"` // Keep execution history
}
// State is a step in the workflow
type State struct {
Name string
Type StateType `json:"type"`
// Task fields
Resource string `json:"resource,omitempty"`
Parameters map[string]interface{} `json:"parameters,omitempty"`
Timeout string `json:"timeout,omitempty"`
Retry *RetryPolicy `json:"retry,omitempty"`
Catch []CatchClause `json:"catch,omitempty"`
// Pass fields
Result interface{} `json:"result,omitempty"`
// Fail fields
Error string `json:"error,omitempty"`
Cause string `json:"cause,omitempty"`
// Transition
Next string `json:"next,omitempty"`
End bool `json:"end,omitempty"`
}
// StateType defines valid state types
type StateType string
const (
StateTypeTask StateType = "Task"
StateTypePass StateType = "Pass"
StateTypeFail StateType = "Fail"
)
// RetryPolicy defines retry behavior for activities
type RetryPolicy struct {
MaxAttempts int32 `json:"maxAttempts"`
BackoffRate float64 `json:"backoffRate"`
InitialInterval string `json:"initialInterval"`
MaxInterval string `json:"maxInterval,omitempty"`
}
// CatchClause defines error handling
type CatchClause struct {
ErrorEquals []string `json:"errorEquals"`
ResultPath *string `json:"resultPath,omitempty"`
Next string `json:"next"`
}
// ExecutionContext tracks state during workflow execution
type ExecutionContext struct {
Input map[string]interface{}
StepResults map[string]interface{}
CurrentState string
History []ExecutionEvent
}
// ExecutionEvent tracks individual state execution
type ExecutionEvent struct {
Timestamp time.Time
State string
Type string // "Started", "Completed", "Failed", "Retried"
Result interface{}
Error error
}
// PollParams for AwaitTaskComplete states
type PollParams struct {
QueueName string
CorrelationID string
PollInterval time.Duration
Timeout time.Duration
}
// PollResult is the result of polling
type PollResult struct {
Result interface{}
Status string
}
// Result is the final workflow output
type Result struct {
FinalOutput interface{}
Status string // "COMPLETED", "FAILED"
Error error
}
// Heartbeat contains state for polling activities
type Heartbeat struct {
CorrelationID string
Queue string
Attempt int
Elapsed time.Duration
LastCheck time.Time
}
// ActivityMetadata describes an activity's capabilities and constraints
type ActivityMetadata struct {
Name string
Description string
Category string
Inputs map[string]InputField
Outputs map[string]OutputField
Constraints Constraints
}
// InputField describes an activity input parameter
type InputField struct {
Type string `json:"type"`
Description string `json:"description"`
Required bool `json:"required"`
Default interface{} `json:"default,omitempty"`
}
// OutputField describes an activity output field
type OutputField struct {
Type string `json:"type"`
Description string `json:"description"`
}
// Constraints describes activity execution constraints
type Constraints struct {
DefaultTimeout string
IsFlaky bool
RecommendedRetries int
RetryBackoff float64
Dependencies []string
Notes string
}
+327
View File
@@ -0,0 +1,327 @@
package routing
import (
"encoding/json"
"testing"
)
func TestWorkflowSpecMarshal(t *testing.T) {
spec := WorkflowSpec{
Name: "test-workflow",
Input: map[string]interface{}{
"repo": "https://github.com/test/repo",
},
States: []State{
{
Name: "Clone",
Type: StateTypeTask,
Resource: "CloneRepoActivity",
Parameters: map[string]interface{}{
"repo": "${input.repo}",
},
Timeout: "5m",
Next: "Analyze",
},
},
}
// Marshal to JSON
data, err := json.Marshal(spec)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
// Unmarshal back
var spec2 WorkflowSpec
err = json.Unmarshal(data, &spec2)
if err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
// Verify
if spec2.Name != spec.Name {
t.Errorf("Name mismatch: %s != %s", spec2.Name, spec.Name)
}
if len(spec2.States) != len(spec.States) {
t.Errorf("State count mismatch: %d != %d", len(spec2.States), len(spec.States))
}
}
func TestCronWorkflowSpecMarshal(t *testing.T) {
spec := CronWorkflowSpec{
Name: "daily-scan",
Type: "CronWorkflow",
Schedule: "0 2 * * *",
Timezone: "UTC",
MaxConcurrent: 1,
EnableHistory: true,
Input: map[string]interface{}{
"repos": []string{"repo1", "repo2"},
},
}
// Marshal to JSON
data, err := json.Marshal(spec)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
// Unmarshal back
var spec2 CronWorkflowSpec
err = json.Unmarshal(data, &spec2)
if err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
// Verify
if spec2.Schedule != spec.Schedule {
t.Errorf("Schedule mismatch: %s != %s", spec2.Schedule, spec.Schedule)
}
if spec2.Timezone != spec.Timezone {
t.Errorf("Timezone mismatch: %s != %s", spec2.Timezone, spec.Timezone)
}
if spec2.EnableHistory != spec.EnableHistory {
t.Errorf("EnableHistory mismatch: %v != %v", spec2.EnableHistory, spec.EnableHistory)
}
}
func TestRetryPolicyMarshal(t *testing.T) {
policy := RetryPolicy{
MaxAttempts: 3,
BackoffRate: 2.0,
InitialInterval: "1s",
MaxInterval: "1m",
}
data, err := json.Marshal(policy)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var policy2 RetryPolicy
err = json.Unmarshal(data, &policy2)
if err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if policy2.MaxAttempts != policy.MaxAttempts {
t.Errorf("MaxAttempts mismatch: %d != %d", policy2.MaxAttempts, policy.MaxAttempts)
}
if policy2.BackoffRate != policy.BackoffRate {
t.Errorf("BackoffRate mismatch: %f != %f", policy2.BackoffRate, policy.BackoffRate)
}
}
func TestStateMarshal(t *testing.T) {
state := State{
Name: "Analyze",
Type: StateTypeTask,
Resource: "AnalyzeCodeActivity",
Parameters: map[string]interface{}{
"path": "${Clone.output.path}",
},
Timeout: "10m",
Retry: &RetryPolicy{
MaxAttempts: 3,
BackoffRate: 2.0,
InitialInterval: "1s",
},
Catch: []CatchClause{
{
ErrorEquals: []string{"Timeout"},
Next: "HandleTimeout",
},
},
Next: "Judge",
}
data, err := json.Marshal(state)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var state2 State
err = json.Unmarshal(data, &state2)
if err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if state2.Name != state.Name {
t.Errorf("Name mismatch: %s != %s", state2.Name, state.Name)
}
if state2.Type != state.Type {
t.Errorf("Type mismatch: %s != %s", state2.Type, state.Type)
}
if len(state2.Catch) != len(state.Catch) {
t.Errorf("Catch count mismatch: %d != %d", len(state2.Catch), len(state.Catch))
}
}
func TestPassState(t *testing.T) {
state := State{
Name: "SetSuccess",
Type: StateTypePass,
Result: map[string]interface{}{"status": "success"},
End: true,
}
data, err := json.Marshal(state)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var state2 State
err = json.Unmarshal(data, &state2)
if err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if state2.Type != StateTypePass {
t.Errorf("Type should be Pass, got: %s", state2.Type)
}
if !state2.End {
t.Error("End should be true")
}
}
func TestFailState(t *testing.T) {
state := State{
Name: "HandleError",
Type: StateTypeFail,
Error: "InvalidInput",
Cause: "Repository URL is invalid",
}
data, err := json.Marshal(state)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var state2 State
err = json.Unmarshal(data, &state2)
if err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if state2.Type != StateTypeFail {
t.Errorf("Type should be Fail, got: %s", state2.Type)
}
if state2.Error != "InvalidInput" {
t.Errorf("Error mismatch: %s != InvalidInput", state2.Error)
}
}
func TestExecutionContextInit(t *testing.T) {
ec := &ExecutionContext{
Input: map[string]interface{}{"repo": "test"},
StepResults: make(map[string]interface{}),
History: make([]ExecutionEvent, 0),
}
if ec.Input == nil {
t.Error("Input should not be nil")
}
if ec.StepResults == nil {
t.Error("StepResults should not be nil")
}
if ec.History == nil {
t.Error("History should not be nil")
}
}
func TestComplexWorkflowSpec(t *testing.T) {
// Test a realistic workflow spec
spec := WorkflowSpec{
Name: "code-review",
Input: map[string]interface{}{
"repo": "https://github.com/rockliang/poimen",
"branch": "feature/x",
},
States: []State{
{
Name: "Clone",
Type: StateTypeTask,
Resource: "CloneRepoActivity",
Parameters: map[string]interface{}{
"repo": "${input.repo}",
"branch": "${input.branch}",
},
Timeout: "5m",
Retry: &RetryPolicy{
MaxAttempts: 2,
BackoffRate: 1.5,
InitialInterval: "1s",
},
Next: "Analyze",
},
{
Name: "Analyze",
Type: StateTypeTask,
Resource: "AnalyzeCodeActivity",
Parameters: map[string]interface{}{
"path": "${Clone.output.path}",
},
Timeout: "10m",
Retry: &RetryPolicy{
MaxAttempts: 3,
BackoffRate: 2.0,
InitialInterval: "1s",
},
Catch: []CatchClause{
{
ErrorEquals: []string{"Timeout"},
Next: "HandleTimeout",
},
},
Next: "Judge",
},
{
Name: "Judge",
Type: StateTypeTask,
Resource: "JudgeActivity",
Parameters: map[string]interface{}{
"quality": "${Analyze.output.quality}",
},
Timeout: "5m",
End: true,
},
{
Name: "HandleTimeout",
Type: StateTypeFail,
Error: "AnalysisTimeout",
Cause: "Code analysis timed out",
},
},
}
// Marshal
data, err := json.Marshal(spec)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
// Unmarshal
var spec2 WorkflowSpec
err = json.Unmarshal(data, &spec2)
if err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
// Verify
if spec2.Name != "code-review" {
t.Errorf("Name mismatch")
}
if len(spec2.States) != 4 {
t.Errorf("Expected 4 states, got %d", len(spec2.States))
}
// Check first state
if spec2.States[0].Resource != "CloneRepoActivity" {
t.Errorf("First state resource mismatch")
}
// Check error catching
if len(spec2.States[1].Catch) == 0 {
t.Error("Analyze state should have catch clauses")
}
}