Files
poimen-workflows/SERVICE_INTEGRATION.md
T
Test c32f3e7077
ci / test (push) Successful in 1m50s
refactor: rename await-kmsvc pattern to awaitTaskComplete
- Better describes the intent: awaiting task completion via queue
- More descriptive than 'await-kmsvc'
- Applied to all workflow design documents
- Reflects the actual operation: wait for external task to complete

Updated files:
- DESIGN_MASTER_REVIEW.md
- SERVICE_INTEGRATION.md
- IMPLEMENTATION_PLAN.md
- CLI_DESIGN.md
- FINAL_COMPREHENSIVE_DESIGN.md
2026-08-31 18:30:11 -07:00

12 KiB

Poimen Service Integration & Declarative Workflow Design

Overview

Current (Bad): Hardcoded workflows

CLI flags → OrchestratorWorkflow (fixed flow)

Proposed (Better): API-driven, customer-specified workflows

API Request (JSON) → Routing Agent → Dynamic Activity Sequence → Results

Part 1: Service Integration Patterns (AWS Step Functions Inspired)

Pattern 1: Sequential (Request-Response)

# Customer specifies: "Call A, then call B with A's output"
activities:
  - name: CloneRepo
    type: request-response
    resource: CloneRepoActivity
    parameters:
      repo: "https://github.com/..."
      path: "/tmp/clone"
    timeout: 5m
    retry:
      maxAttempts: 3
      backoff: exponential
  
  - name: Analyze
    type: request-response
    resource: AnalyzeCodeActivity
    parameters:
      path: $.CloneRepo.path  # ← Reference previous output
    inputPath: "$.CloneRepo"   # ← What to pass
    outputPath: "$"            # ← What to return
    timeout: 10m

Execution: CloneRepo waits → outputs result → Analyze receives it → outputs


Pattern 2: Await-Task-Complete (Callback)

# Customer specifies: "Call A, wait for callback, then call B"
activities:
  - name: LaunchJob
    type: await-callback
    resource: LaunchLongRunningJobActivity
    parameters:
      job_id: "job-123"
    heartbeat_timeout: 5m
    next: OnJobComplete
  
  - name: OnJobComplete
    type: request-response
    resource: ProcessResultActivity
    parameters:
      result: $.LaunchJob.result

Execution: LaunchJob sent → waits for callback (heartbeat) → ProcessResult receives callback data → outputs


Pattern 3: Retry with Backoff

# Customer specifies: "Call A, if fails retry 3x with exponential backoff, else B"
activities:
  - name: GenerateCode
    type: request-response
    resource: ImplementerActivity
    parameters:
      task: $.input.taskID
      model: $.input.model
    retry:
      maxAttempts: 3
      backoffRate: 2.0        # 1s → 2s → 4s
      initialInterval: 1s
      maxInterval: 10m
    catch:
      - errorEquals: ["Timeout", "ThrottlingException"]
        next: HandleError
      - errorEquals: ["States.ALL"]
        next: HandleError
    next: JudgeCode
  
  - name: JudgeCode
    type: request-response
    resource: JudgeActivity
    parameters:
      implementation: $.GenerateCode.code
  
  - name: HandleError
    type: pass
    result: { status: "failed", reason: $.error }
    resultPath: "$.error_output"
    end: true

Execution: GenerateCode fails → retry 1 (wait 1s) → fails → retry 2 (wait 2s) → fails → HandleError


Part 2: Routing Agent Architecture

2.1 Routing Agent Activity (New)

// internal/routing/routing_agent.go

type WorkflowSpec struct {
    WorkflowID    string           `json:"workflow_id"`
    Name          string           `json:"name"`
    Input         map[string]interface{} `json:"input"`
    Activities    []ActivityStep   `json:"activities"`
    RetryPolicy   RetryConfig      `json:"retry_policy,omitempty"`
}

type ActivityStep struct {
    Name          string                 `json:"name"`
    Type          string                 `json:"type"` // request-response, await-callback, parallel, choice
    Resource      string                 `json:"resource"` // Activity name
    Parameters    map[string]interface{} `json:"parameters"`
    InputPath     string                 `json:"input_path"` // JSONPath: "$" or "$.previous.output"
    OutputPath    string                 `json:"output_path"` // What to extract
    Timeout       time.Duration          `json:"timeout"`
    Retry         *RetryConfig           `json:"retry,omitempty"`
    Catch         []CatchBlock           `json:"catch,omitempty"`
    Next          string                 `json:"next,omitempty"`
    End           bool                   `json:"end,omitempty"`
}

type RetryConfig struct {
    MaxAttempts      int           `json:"max_attempts"`
    BackoffRate      float64       `json:"backoff_rate"`
    InitialInterval  time.Duration `json:"initial_interval"`
    MaxInterval      time.Duration `json:"max_interval"`
}

type CatchBlock struct {
    ErrorEquals []string `json:"error_equals"`
    Next        string   `json:"next"`
    ResultPath  string   `json:"result_path,omitempty"`
}

// RoutingAgentActivity: Receives WorkflowSpec, executes activities sequentially
func RoutingAgentActivity(ctx context.Context, spec WorkflowSpec) (map[string]interface{}, error) {
    // 1. Parse WorkflowSpec
    // 2. Build execution plan (DAG of activities)
    // 3. For each step:
    //    a. Evaluate JSONPath inputs
    //    b. Execute activity with retries
    //    c. Handle errors (catch blocks)
    //    d. Store output in context
    //    e. Move to next step
    // 4. Return final result
}

2.2 Activity Mapper Registry (New)

// internal/routing/activity_registry.go

type ActivityMapper struct {
    activities map[string]interface{}
}

func NewActivityMapper() *ActivityMapper {
    return &ActivityMapper{
        activities: map[string]interface{}{
            "CloneRepoActivity":      action.CloneRepoActivity,
            "AnalyzeCodeActivity":    action.AnalyzeCodeActivity,
            "ImplementerActivity":    action.ImplementerActivity,
            "JudgeActivity":          action.JudgeActivity,
            "GitCommitActivity":      action.GitCommitActivity,
            // ... all activities registered here
        },
    }
}

func (am *ActivityMapper) Get(name string) (interface{}, error) {
    if activity, ok := am.activities[name]; ok {
        return activity, nil
    }
    return nil, fmt.Errorf("activity %s not found", name)
}

// cmd/worker/main.go
w.RegisterActivity(routing.RoutingAgentActivity)  // ← Only register this
w.RegisterActivity(action.CloneRepoActivity)      // ← Still register all individual activities
// ... rest of activities

Part 3: API Message Format (General Purpose)

3.1 Workflow Request Format

POST /api/workflows
{
  "workflow": {
    "id": "workflow-123",
    "name": "code-review-pipeline",
    "version": "v1",
    "input": {
      "repo": "https://github.com/rockliang/poimen",
      "branch": "feature/new-feature",
      "milestone": "T0"
    },
    "activities": [
      {
        "id": "step_1",
        "name": "CloneRepo",
        "type": "request-response",
        "resource": "CloneRepoActivity",
        "parameters": {
          "repo": "${input.repo}",
          "path": "/tmp/work"
        },
        "timeout": "5m",
        "next": "step_2"
      },
      {
        "id": "step_2",
        "name": "AnalyzeCode",
        "type": "request-response",
        "resource": "AnalyzeCodeActivity",
        "parameters": {
          "path": "${step_1.output.path}"
        },
        "inputPath": "$.step_1",
        "outputPath": "$",
        "timeout": "10m",
        "retry": {
          "maxAttempts": 3,
          "backoffRate": 2.0,
          "initialInterval": "1s"
        },
        "catch": [
          {
            "errorEquals": ["Timeout"],
            "next": "handle_timeout"
          }
        ],
        "next": "step_3"
      },
      {
        "id": "step_3",
        "name": "GenerateImplementation",
        "type": "request-response",
        "resource": "ImplementerActivity",
        "parameters": {
          "analysis": "${step_2.output.analysis}",
          "model": "claude-sonnet-5"
        },
        "timeout": "30m",
        "retry": {
          "maxAttempts": 3,
          "backoffRate": 2.0
        },
        "next": "step_4"
      },
      {
        "id": "step_4",
        "name": "Review",
        "type": "request-response",
        "resource": "JudgeActivity",
        "parameters": {
          "code": "${step_3.output.code}",
          "analysis": "${step_2.output.analysis}"
        },
        "timeout": "10m",
        "end": true
      },
      {
        "id": "handle_timeout",
        "name": "TimeoutHandler",
        "type": "pass",
        "result": {
          "status": "failed",
          "reason": "Analysis timed out"
        },
        "end": true
      }
    ]
  }
}

3.2 Workflow Response Format

{
  "workflow_id": "workflow-123",
  "status": "RUNNING",
  "temporal_workflow_id": "orch-workflow-123",
  "task_queue": "poimen-taskqueue",
  "execution_started_at": "2025-01-29T10:00:00Z",
  "monitoring": {
    "web_ui": "http://temporal.local:8080/namespaces/poimen/workflows/orch-workflow-123",
    "poll_url": "/api/workflows/workflow-123/status"
  }
}

3.3 Status Poll Format

GET /api/workflows/workflow-123/status
{
  "workflow_id": "workflow-123",
  "status": "COMPLETED",
  "steps_completed": 4,
  "total_steps": 4,
  "results": {
    "step_1": {
      "name": "CloneRepo",
      "status": "COMPLETED",
      "duration": "45s",
      "output": {
        "path": "/tmp/work/poimen",
        "commit": "abc123"
      }
    },
    "step_2": {
      "name": "AnalyzeCode",
      "status": "COMPLETED",
      "duration": "120s",
      "output": {
        "analysis": "..." 
      }
    },
    "step_3": {
      "name": "GenerateImplementation",
      "status": "COMPLETED",
      "duration": "300s",
      "output": {
        "code": "..."
      }
    },
    "step_4": {
      "name": "Review",
      "status": "COMPLETED",
      "duration": "60s",
      "output": {
        "verdict": "approved",
        "score": 0.95
      }
    }
  },
  "final_output": {
    "verdict": "approved",
    "score": 0.95
  },
  "completed_at": "2025-01-29T10:10:00Z"
}

Part 4: Implementation Plan

Phase 1: Core Routing Engine (Week 1)

  • Create internal/routing/types.go (WorkflowSpec, ActivityStep structs)
  • Create internal/routing/jsonpath_evaluator.go (parameter interpolation)
  • Create internal/routing/execution_context.go (execution state management)
  • Create internal/routing/activity_registry.go (activity mapper)
  • Create statemachine/routing_workflow.go (RoutingWorkflow implementation)
  • Update cmd/worker/main.go (register RoutingWorkflow)
  • Write tests for routing engine

Phase 2: API Server (Week 2)

  • Create cmd/api-server/main.go (HTTP server bootstrap)
  • Create cmd/api-server/handlers.go (POST /workflows, GET /status)
  • Create cmd/api-server/validation.go (WorkflowSpec validation)
  • Create internal/routing/client.go (Temporal client wrapper)
  • Write integration tests

Phase 3: Homelab Frontend Integration (Week 3)

  • Create k8s/gateway-routes.yaml (route /api/v1/workflows to api-server)
  • Deploy api-server as K8s Service
  • Update NetworkPolicy to allow traffic to api-server
  • Test end-to-end API flow

Phase 4: Migration & Cleanup (Week 4)

  • Keep cmd/starter for backward compatibility
  • Document migration path
  • Update examples to use new API
  • Performance tuning

Files to Create

NEW:
  internal/routing/
    ├─ types.go                    (WorkflowSpec, ActivityStep, RetryConfig)
    ├─ jsonpath_evaluator.go       (Parameter interpolation)
    ├─ execution_context.go        (State management)
    ├─ activity_registry.go        (Activity mapper)
    └─ routing_workflow.go         (RoutingWorkflow implementation)
  
  statemachine/
    └─ routing_workflow.go         (See Part 5 below)
  
  cmd/api-server/
    ├─ main.go                     (Server bootstrap)
    ├─ handlers.go                 (HTTP handlers)
    └─ validation.go               (Spec validation)

MODIFY:
  cmd/worker/main.go              (Register RoutingWorkflow)
  
NEW (Homelab Frontend):
  k8s/
    └─ gateway-routes.yaml         (Route definitions)

Backward Compatibility

Existing cmd/starter still works (calls OrchestratorWorkflow) New API calls RoutingWorkflow Worker registers both workflows No breaking changes to current deployments


Benefits

Customer-driven workflows (JSON API) Service Integration patterns (Sequential, Await-Task-Complete, Retry) No recompilation needed Full Temporal observability JSONPath-based parameter passing Composable activities Error handling & retries built-in