# 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) ```yaml # 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) ```yaml # 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 ```yaml # 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) ```go // 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) ```go // 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 ```json 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 ```json { "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 ```json 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