- Add RoutingWorkflow: generic state machine executor for WorkflowSpec - Add LLM Router: natural language → WorkflowSpec generation - Add RetrieveMemoryActivity: query poimen-memory for context - Add activities: AnalyzeCode, SecurityScan, GenerateReport, Notify, etc. - Add agent-prompts/router: LLM prompt documentation - Extend starter with --route flag for routing workflows - Remove orchestrator job (trigger via API/message instead) - Clean up: move docs to Desktop, add .gitignore for *.md
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Ignore markdown docs except agent-prompts
|
||||
*.md
|
||||
!agent-prompts/*.md
|
||||
!agent-prompts/**/*.md
|
||||
# Binaries
|
||||
starter
|
||||
worker
|
||||
poimen
|
||||
@@ -1,394 +0,0 @@
|
||||
# 🎉 **PROJECT COMPLETE: ALL 48 TASKS DELIVERED (T0-T4)** 🎉
|
||||
|
||||
## 📊 FINAL COMPLETION STATUS
|
||||
|
||||
```
|
||||
T0: 9/9 ✅ COMPLETE (100%) [Foundation]
|
||||
T1: 8/8 ✅ COMPLETE (100%) [Production Hardening]
|
||||
T2: 8/8 ✅ COMPLETE (100%) [Scale & Performance]
|
||||
T3: 8/8 ✅ COMPLETE (100%) [Feature Expansion]
|
||||
T4: 8/8 ✅ COMPLETE (100%) [Advanced Operations & Analytics]
|
||||
────────────────────────────────────────────────
|
||||
TOTAL: 48/48 (100%) ✅ ALL MILESTONES COMPLETE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏆 T4 ADVANCED OPERATIONS & ANALYTICS (8/8 ✅)
|
||||
|
||||
### T4.1: Real-Time Metrics Dashboard
|
||||
- **Package**: `internal/dashboard`
|
||||
- **Tests**: 13
|
||||
- **Features**:
|
||||
- MetricsAggregator for time-series data collection
|
||||
- Percentile calculations (p50, p95, p99)
|
||||
- Min/max/average aggregation
|
||||
- Metric-level statistics tracking
|
||||
- Time-range queries
|
||||
|
||||
### T4.2: Workflow Visualization & DAG Rendering
|
||||
- **Package**: `internal/visualization`
|
||||
- **Tests**: 12
|
||||
- **Features**:
|
||||
- DAGRenderer for dependency graphs
|
||||
- DOT format generation for Graphviz
|
||||
- Critical path highlighting
|
||||
- Topological sorting with Kahn's algorithm
|
||||
- HTML visualization
|
||||
- Parallel task grouping
|
||||
|
||||
### T4.3: Advanced Search & Filtering
|
||||
- **Package**: `internal/search`
|
||||
- **Tests**: 18
|
||||
- **Features**:
|
||||
- Full-text indexing with word-based lookup
|
||||
- Filter by status, assignee, tag, date
|
||||
- Regex pattern matching
|
||||
- Saved filter persistence
|
||||
- Case-insensitive search
|
||||
- Multi-word search support
|
||||
|
||||
### T4.4: Cost Tracking & Optimization
|
||||
- **Package**: `internal/cost`
|
||||
- **Tests**: 16
|
||||
- **Features**:
|
||||
- LLM API cost tracking (per token)
|
||||
- Git operation cost tracking
|
||||
- Compute resource cost tracking (per duration)
|
||||
- Cost aggregation by type/workflow
|
||||
- Cost optimization recommendations
|
||||
- Configurable rate settings
|
||||
|
||||
### T4.5: Automated Alerting & Anomaly Detection
|
||||
- **Package**: `internal/alerting`
|
||||
- **Tests**: 12
|
||||
- **Features**:
|
||||
- AlertManager for rule-based alerts
|
||||
- Alert levels (warning, error, critical)
|
||||
- Threshold-based alert triggering
|
||||
- Alert history tracking
|
||||
- Rule management
|
||||
- Active alert queries
|
||||
|
||||
### T4.6: Workflow Profiling & Bottleneck Analysis
|
||||
- **Package**: `internal/profiling`
|
||||
- **Tests**: 11
|
||||
- **Features**:
|
||||
- WorkflowProfiler for execution metrics
|
||||
- Per-task CPU/memory/duration tracking
|
||||
- Identify slow tasks (top N slowest)
|
||||
- High CPU/memory task detection
|
||||
- Optimization suggestions
|
||||
- Throughput calculation
|
||||
|
||||
### T4.7: Multi-Cluster Orchestration
|
||||
- **Package**: `internal/clusters`
|
||||
- **Tests**: 13
|
||||
- **Features**:
|
||||
- ClusterManager for K8s cluster management
|
||||
- Register/unregister clusters
|
||||
- Health checking
|
||||
- Task allocation with load balancing
|
||||
- Capacity tracking
|
||||
- Find best cluster by available capacity
|
||||
|
||||
### T4.8: Self-Deployment (Orchestrator Deploys Itself)
|
||||
- **Package**: `internal/deployment`
|
||||
- **Tests**: 12
|
||||
- **Features**:
|
||||
- SelfDeployer for automated deployment
|
||||
- Docker container build tracking
|
||||
- Image push to registry
|
||||
- K8s manifest generation
|
||||
- Deployment status management
|
||||
- Rollback support
|
||||
|
||||
---
|
||||
|
||||
## 📈 COMPLETE PROJECT STATISTICS
|
||||
|
||||
### Code Metrics
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Total Packages | 29 internal packages |
|
||||
| Total Tests | 546 unit tests |
|
||||
| Test Pass Rate | 100% |
|
||||
| Lines of Code | ~28,000+ |
|
||||
| Compilation Status | ✅ Zero errors |
|
||||
| Git Commits | 40+ atomic commits |
|
||||
| Branches Merged | 25 feature branches |
|
||||
|
||||
### Test Breakdown
|
||||
- T0: 50+ tests
|
||||
- T1: 199 tests
|
||||
- T2: 159 tests
|
||||
- T3: 131 tests
|
||||
- T4: 98 tests
|
||||
- **Total**: 546+ tests ✅
|
||||
|
||||
### Packages by Milestone
|
||||
|
||||
**T0-T1 (17 packages)**:
|
||||
- approval, audit, batching, board
|
||||
- cache, composition, config, dispatch
|
||||
- external, graph, health, history
|
||||
- indexing, judge, locking, logging
|
||||
- metrics, pause, plugins, recovery
|
||||
- templates, tuning
|
||||
|
||||
**T4 New (8 packages)**:
|
||||
- alerting, clusters, cost, dashboard
|
||||
- deployment, profiling, search, visualization
|
||||
|
||||
---
|
||||
|
||||
## 🎯 KEY FEATURES BY CATEGORY
|
||||
|
||||
### 🛡️ Reliability & Observability (T1)
|
||||
✅ Multi-layer error recovery (Retry, Deadletter, Checkpoint)
|
||||
✅ Structured logging (JSON in prod, colored in dev)
|
||||
✅ Prometheus metrics with 20+ metric types
|
||||
✅ Immutable audit trail with hash chaining
|
||||
✅ Pause/resume with state snapshots
|
||||
✅ K8s health checks (readiness + liveness)
|
||||
✅ Auto-healing of board state
|
||||
|
||||
### ⚡ Performance & Scale (T2)
|
||||
✅ Activity result caching (eliminates redundant calls)
|
||||
✅ Parallel task execution (9x speedup verified)
|
||||
✅ Template caching (<100ms render latency)
|
||||
✅ Lessons indexing (<10ms O(1) lookups)
|
||||
✅ Git operation batching (N-1 round trip savings)
|
||||
✅ LLM request batching (90%+ cost reduction)
|
||||
✅ Distributed locking (Redis/etcd/local backends)
|
||||
✅ Memory-efficient history pruning
|
||||
|
||||
### 🚀 Extensibility (T3)
|
||||
✅ Custom skill plugins with dynamic loading
|
||||
✅ YAML-based workflow templates
|
||||
✅ Task dependency graphs with cycle detection
|
||||
✅ Human-in-the-loop approval gates
|
||||
✅ Custom judge implementations
|
||||
✅ Nested workflow composition
|
||||
✅ External task system integration
|
||||
|
||||
### 📊 Operations & Analytics (T4)
|
||||
✅ Real-time metrics dashboard (percentiles, aggregation)
|
||||
✅ Workflow visualization with DAG rendering
|
||||
✅ Full-text search with regex support
|
||||
✅ Cost tracking (LLM + git + compute)
|
||||
✅ Automated alerting with rule engine
|
||||
✅ Bottleneck analysis and profiling
|
||||
✅ Multi-cluster orchestration
|
||||
✅ Self-deployment with rollback
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ ARCHITECTURE HIGHLIGHTS
|
||||
|
||||
### Design Principles
|
||||
✅ **Modularity**: 29 independent packages, zero cross-dependencies
|
||||
✅ **Thread Safety**: All shared state protected by RWMutex
|
||||
✅ **Persistence**: JSON/JSONL for audit trail and recovery
|
||||
✅ **Extensibility**: Interface-based design for plugins/backends
|
||||
✅ **Observability**: Structured logging + metrics export
|
||||
✅ **Performance**: Caching, batching, parallelization
|
||||
✅ **Reliability**: Multi-layer recovery + state snapshots
|
||||
✅ **Kubernetes Ready**: Health checks, graceful shutdown
|
||||
|
||||
### Technical Achievements
|
||||
- **9x parallelization** speedup (verified with benchmarks)
|
||||
- **90%+ cost reduction** via LLM batching (30→3 API calls)
|
||||
- **<10ms queries** for lesson indexing (O(1) hash tables)
|
||||
- **<100ms template** rendering with LRU caching
|
||||
- **Constant memory** despite 1000s of tasks (pruning)
|
||||
- **N-1 network** round trip savings via batching
|
||||
- **Multi-pod safe** distributed locking
|
||||
- **100% test coverage** across 546 tests
|
||||
|
||||
---
|
||||
|
||||
## 📊 COMPLETE MILESTONE OVERVIEW
|
||||
|
||||
### T0: Foundation (9/9) ✅
|
||||
Core planner/judge/implementer orchestration with git workflow
|
||||
|
||||
### T1: Production Hardening (8/8) ✅
|
||||
- Error Recovery (40 tests)
|
||||
- Observability (21 tests)
|
||||
- Timeout Tuning (36 tests)
|
||||
- State Validation (29 tests)
|
||||
- Pause/Resume (34 tests)
|
||||
- Integration Tests (15 tests)
|
||||
- Audit Logging (14 tests)
|
||||
- K8s Health (10 tests)
|
||||
|
||||
### T2: Scale & Performance (8/8) ✅
|
||||
- Result Caching (13 tests)
|
||||
- Parallel Dispatch (15 tests)
|
||||
- Template Caching (17 tests)
|
||||
- Lessons Indexing (20 tests)
|
||||
- Git Batching (24 tests)
|
||||
- LLM Batching (29 tests)
|
||||
- History Pruning (17 tests)
|
||||
- Distributed Locks (24 tests)
|
||||
|
||||
### T3: Feature Expansion (8/8) ✅
|
||||
- Skill Plugins (48 tests)
|
||||
- Workflow Templates (26 tests)
|
||||
- Dependency Graph (23 tests)
|
||||
- Approval Gates (16 tests)
|
||||
- Custom Judges (5 tests)
|
||||
- Immutable Audit (4 tests)
|
||||
- Workflow Composition (4 tests)
|
||||
- External Systems (5 tests)
|
||||
|
||||
### T4: Advanced Operations (8/8) ✅
|
||||
- Metrics Dashboard (13 tests)
|
||||
- DAG Visualization (12 tests)
|
||||
- Search & Filtering (18 tests)
|
||||
- Cost Tracking (16 tests)
|
||||
- Alerting (12 tests)
|
||||
- Profiling (11 tests)
|
||||
- Multi-Cluster (13 tests)
|
||||
- Self-Deployment (12 tests)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 PRODUCTION READINESS CHECKLIST
|
||||
|
||||
- [x] All 48 tasks complete
|
||||
- [x] 546+ unit tests (100% pass rate)
|
||||
- [x] Zero compilation errors
|
||||
- [x] All 29 packages tested
|
||||
- [x] Thread-safe concurrency
|
||||
- [x] Production code quality
|
||||
- [x] Comprehensive test coverage
|
||||
- [x] Performance benchmarks verified
|
||||
- [x] Kubernetes deployment ready
|
||||
- [x] Error recovery implemented
|
||||
- [x] Observability integrated
|
||||
- [x] Cost optimization verified
|
||||
- [x] Multi-cluster support
|
||||
- [x] Automated deployment
|
||||
- [x] Git history clean
|
||||
- [x] Documentation complete
|
||||
|
||||
---
|
||||
|
||||
## 📁 FINAL REPOSITORY STATE
|
||||
|
||||
```
|
||||
Repository: /Users/rockliang/workplace/Poimen/workflows
|
||||
Branch: main
|
||||
Status: ✅ PRODUCTION READY
|
||||
|
||||
Structure:
|
||||
├── internal/
|
||||
│ ├── approval/ # T3.4: Approval gates (16 tests)
|
||||
│ ├── alerting/ # T4.5: Alert management (12 tests)
|
||||
│ ├── audit/ # T1.7 + T3.6: Audit logging (18 tests)
|
||||
│ ├── batching/ # T2.5-2.6: Batching (53 tests)
|
||||
│ ├── board/ # T1.4: State validation (29 tests)
|
||||
│ ├── cache/ # T2.1: Result caching (13 tests)
|
||||
│ ├── clusters/ # T4.7: Multi-cluster (13 tests)
|
||||
│ ├── composition/ # T3.7: Composition (4 tests)
|
||||
│ ├── cost/ # T4.4: Cost tracking (16 tests)
|
||||
│ ├── dashboard/ # T4.1: Metrics dashboard (13 tests)
|
||||
│ ├── deployment/ # T4.8: Self-deployment (12 tests)
|
||||
│ ├── dispatch/ # T2.2: Parallelization (15 tests)
|
||||
│ ├── external/ # T3.8: External systems (5 tests)
|
||||
│ ├── graph/ # T3.3: Dependency graph (23 tests)
|
||||
│ ├── health/ # T1.8: K8s health (10 tests)
|
||||
│ ├── history/ # T2.7: History pruning (17 tests)
|
||||
│ ├── indexing/ # T2.4: Lessons index (20 tests)
|
||||
│ ├── judge/ # T3.5: Custom judges (5 tests)
|
||||
│ ├── locking/ # T2.8: Distributed locks (24 tests)
|
||||
│ ├── logging/ # T1.2: Structured logs (8 tests)
|
||||
│ ├── metrics/ # T1.2: Prometheus (13 tests)
|
||||
│ ├── pause/ # T1.5: Pause/resume (34 tests)
|
||||
│ ├── plugins/ # T3.1: Plugin system (48 tests)
|
||||
│ ├── profiling/ # T4.6: Profiling (11 tests)
|
||||
│ ├── recovery/ # T1.1: Error recovery (40 tests)
|
||||
│ ├── search/ # T4.3: Search & filter (18 tests)
|
||||
│ ├── templates/ # T2.3 + T3.2: Templates (43 tests)
|
||||
│ ├── tuning/ # T1.3: Timeout tuning (36 tests)
|
||||
│ └── visualization/ # T4.2: DAG rendering (12 tests)
|
||||
├── cmd/
|
||||
├── statemachine/
|
||||
├── tasks/
|
||||
├── tests/
|
||||
├── FINAL_SESSION_SUMMARY.md
|
||||
├── COMPLETE_T4_SUMMARY.md
|
||||
└── ... (config, docs, manifests)
|
||||
|
||||
Tests: 546+
|
||||
Commits: 40+
|
||||
Lines: 28,000+
|
||||
Status: ✅ PRODUCTION READY
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 PERFORMANCE VERIFIED
|
||||
|
||||
| Feature | Metric | Achievement |
|
||||
|---------|--------|-------------|
|
||||
| Parallelization | Speedup | 9x verified |
|
||||
| LLM Batching | Cost Reduction | 90%+ reduction |
|
||||
| Indexing | Query Latency | <10ms (O(1)) |
|
||||
| Templates | Render Time | <100ms |
|
||||
| History | Memory Growth | Constant (pruning) |
|
||||
| Locks | Multi-pod Safety | ✅ Verified |
|
||||
| Distributed | Cluster Failover | ✅ Supported |
|
||||
| Alerting | Rule Evaluation | <1ms per rule |
|
||||
|
||||
---
|
||||
|
||||
## 🎓 LESSONS LEARNED
|
||||
|
||||
1. **Modularity Enables Scale**: 29 independent packages with zero dependencies
|
||||
2. **Interface Design is Essential**: Pluggable backends, mock implementations critical
|
||||
3. **Thread Safety Matters**: RWMutex prevents subtle concurrent bugs
|
||||
4. **Performance Optimization is Multi-layered**: Caching + batching + parallelization
|
||||
5. **Testing is Not Optional**: 546 tests catch regressions early
|
||||
6. **Observability is Critical**: Metrics + logs essential for production
|
||||
7. **State Management is Hard**: Snapshots + persistence ensure recovery
|
||||
8. **Distributed Systems Need Care**: Locks, health checks, failover planning
|
||||
|
||||
---
|
||||
|
||||
## 🚀 DEPLOYMENT READY
|
||||
|
||||
This implementation is ready for production deployment:
|
||||
|
||||
✅ **Reliability**: Multi-layer recovery, health checks, state management
|
||||
✅ **Observability**: Structured logging, metrics export, audit trail
|
||||
✅ **Performance**: Caching, batching, parallelization, indexing
|
||||
✅ **Scalability**: Multi-cluster support, distributed locks, load balancing
|
||||
✅ **Operability**: Self-deployment, cost tracking, bottleneck analysis
|
||||
✅ **Testing**: 546+ tests, 100% pass rate, comprehensive coverage
|
||||
✅ **Documentation**: Task specs, performance metrics, architecture docs
|
||||
✅ **Git History**: 40+ atomic commits with clear narratives
|
||||
|
||||
---
|
||||
|
||||
## 📞 NEXT STEPS (OPTIONAL T5+)
|
||||
|
||||
If extending beyond T4, consider:
|
||||
- **T5**: Web UI Dashboard (real-time metrics visualization)
|
||||
- **T6**: Advanced Scheduling (optimal task ordering)
|
||||
- **T7**: Resource Quota Management (CPU/memory limits)
|
||||
- **T8**: Workflow DAG Optimization (automatic parallelization)
|
||||
- **T9**: Advanced Analytics (ML-based anomaly detection)
|
||||
|
||||
---
|
||||
|
||||
**🎉 ALL 48 TASKS COMPLETE - PROJECT PRODUCTION READY** 🎉
|
||||
|
||||
**Repository**: `/Users/rockliang/workplace/Poimen/workflows`
|
||||
**Branch**: `main`
|
||||
**Status**: ✅ Complete and Merged
|
||||
**Tests**: 546+/546+ Passing
|
||||
**Build**: ✅ Successful
|
||||
**Deploy**: ✅ Ready for production
|
||||
@@ -1,369 +0,0 @@
|
||||
# Memory-Service Integration: Completion Summary
|
||||
|
||||
**Date**: August 29, 2026
|
||||
**Status**: ✅ **COMPLETE** — All implementation, testing, and planning done
|
||||
**Commits**: 2 major commits (memory activities + architecture planning)
|
||||
|
||||
---
|
||||
|
||||
## Deliverables Completed
|
||||
|
||||
### 1. Memory Service Integration (12 Temporal Activities)
|
||||
|
||||
**Package**: `internal/memory/`
|
||||
**Files**: 6 core files + tests
|
||||
|
||||
✅ **Activities Implemented** (all tested, 23/23 passing):
|
||||
- `CreateKnowledgeActivity` — Create L1/L2/reference records
|
||||
- `UpdateKnowledgeActivity` — Update existing knowledge
|
||||
- `SearchKnowledgeActivity` — Hybrid semantic+lexical search
|
||||
- `GetContextActivity` — Three-tier retrieval (signature→vector→reference)
|
||||
- `GetVaultActivity` — Browse vault files
|
||||
- `HealthCheckActivity` — Service health monitoring
|
||||
- `LearnFromExecutionActivity` — Learn from task results
|
||||
- `DiagnoseIssueActivity` — Diagnose failures
|
||||
- `AnalyzeErrorActivity` — Find recovery paths
|
||||
- `DocumentDecisionActivity` — Record milestones
|
||||
- `SearchAndApplyActivity` — Search & apply selectively
|
||||
- `RefreshMemoryActivity` — Periodic refresh
|
||||
|
||||
**Key Features**:
|
||||
- 3x retry policy (1s → 2s → 4s exponential backoff)
|
||||
- Per-activity timeout configuration
|
||||
- Full Temporal test suite integration
|
||||
- Error handling with activity context
|
||||
- Logging with Temporal metadata
|
||||
|
||||
**Test Coverage**:
|
||||
```
|
||||
✅ 10 Activity tests (Temporal test suite)
|
||||
✅ 13 Client/service tests (HTTP layer)
|
||||
PASS: 23/23 tests (0.315s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Architecture Documentation
|
||||
|
||||
**4 Major Planning Documents** (3,889 lines total):
|
||||
|
||||
#### A. MEMORY_DRIVEN_ARCHITECTURE.md (24 KB)
|
||||
Comprehensive integration plan:
|
||||
- Current Poimen state machine (10 phases, 80 tasks)
|
||||
- Memory service integration points (6 diagrams)
|
||||
- Activity usage per phase (T0-T10)
|
||||
- Prompt optimization with memory context
|
||||
- Retry policy enhancement via memory
|
||||
- State machine consumption model (Rust code examples)
|
||||
- Memory-skills matrix
|
||||
- Flow diagrams for lifecycle
|
||||
|
||||
#### B. TOOL_USAGE_AND_SKILLS.md (18 KB)
|
||||
Tool landscape & ingestion strategy:
|
||||
- 6 tool categories (workflow, state machine, execution, verification, model, storage)
|
||||
- Tool-skill dependencies
|
||||
- YAML skills registry example
|
||||
- 4-phase ingestion strategy
|
||||
- Skills ingest code example
|
||||
- Tool-skill dependency matrix
|
||||
- End-to-end execution scenario
|
||||
|
||||
#### C. REGISTERED_ACTIVITIES.md (10 KB)
|
||||
Activity reference & calling conventions:
|
||||
- All 12 activities with signatures
|
||||
- Default retry/timeout policies
|
||||
- Activity naming convention (camelCase)
|
||||
- Integration code example
|
||||
- Activity flow diagram
|
||||
- Runtime listing methods
|
||||
|
||||
#### D. MEMORY_INTEGRATION.md (8 KB)
|
||||
High-level integration overview:
|
||||
- How to register in worker
|
||||
- How to use in workflows
|
||||
- Workflow patterns (8 examples)
|
||||
- Configuration guide
|
||||
- Error handling patterns
|
||||
|
||||
---
|
||||
|
||||
### 3. Source Code (internal/memory/)
|
||||
|
||||
**File Structure**:
|
||||
```
|
||||
internal/memory/
|
||||
├── activities.go (240 lines) → 10 activity implementations
|
||||
├── activities_test.go (320 lines) → 10 activity tests
|
||||
├── worker_setup.go (310 lines) → Registration + wrappers + retry config
|
||||
├── workflow_examples.go (260 lines) → 8 workflow patterns
|
||||
├── client.go (250 lines) → HTTP client (12 endpoints)
|
||||
├── client_test.go (150 lines) → Client HTTP tests
|
||||
├── service.go (180 lines) → High-level service wrapper
|
||||
├── service_test.go (170 lines) → Service tests
|
||||
├── example_activity.go (130 lines) → Activity usage examples
|
||||
└── README.md (400 lines) → Full API documentation
|
||||
```
|
||||
|
||||
**Total**: ~2,400 lines of production-ready code + tests
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Memory-Driven Workflow Loop
|
||||
|
||||
```
|
||||
Poimen Workflow (10 Phases)
|
||||
↓
|
||||
For Each Step:
|
||||
├─ 1. GetContextActivity (retrieve lessons)
|
||||
├─ 2. Optimize prompt (add learned facts + skills)
|
||||
├─ 3. Execute with agent
|
||||
├─ 4a. Success → LearnFromExecutionActivity
|
||||
├─ 4b. Failure → AnalyzeErrorActivity
|
||||
├─ 5. Always → DocumentDecisionActivity
|
||||
└─ 6. Continue or retry (with memory guidance)
|
||||
|
||||
Memory Service (PostgreSQL + OpenSearch + Vault)
|
||||
├─ L1 Knowledge: Task execution results
|
||||
├─ L2 Knowledge: Verified patterns & decisions
|
||||
├─ R (Reference): Docs, skill examples
|
||||
└─ Vault: Organized by tool/phase/domain
|
||||
```
|
||||
|
||||
### Skills & Context Flow
|
||||
|
||||
```
|
||||
Workflow Execution
|
||||
↓
|
||||
Tools Used ─────────→ Skills Retrieved from Memory
|
||||
├─ WorkflowDefBuilder ──→ IR canonicalization rules
|
||||
├─ EventLog ────────────→ State machine patterns
|
||||
├─ RunExecutor ─────────→ Attempt lifecycle
|
||||
├─ Verifier Port ───────→ Rubric design
|
||||
├─ Judge Port ──────────→ Decision logic
|
||||
├─ ModelProvider ───────→ Prompt optimization
|
||||
└─ Storage Ports ───────→ Retention policies
|
||||
↓
|
||||
Skills Guide Execution ─→ Results Learned
|
||||
├─ Success patterns (L1)
|
||||
├─ Failure recovery (L1)
|
||||
├─ Verified practices (L2)
|
||||
└─ Vault enriched
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Phase 1: Core Integration
|
||||
✅ **Completed**:
|
||||
- 12 activities implemented & tested
|
||||
- Worker registration function
|
||||
- Activity wrapper functions with retry policy
|
||||
- Workflow execution examples
|
||||
- Full documentation
|
||||
|
||||
🔄 **Next (Phase 2)**:
|
||||
- Wire activities into RunExecutor
|
||||
- Add pre/post-execution hooks in state machine
|
||||
- Ingest skill YAML → memory vault
|
||||
- Prompt optimization with context
|
||||
|
||||
### Phase 2: Optimization (Next Sprint)
|
||||
- Enhanced prompt generation with memory lessons
|
||||
- Retry policy improvement via learned limits
|
||||
- Budget tracking with learned constraints
|
||||
- Phase composition gate improvements
|
||||
|
||||
### Phase 3: Observability (2 Sprints)
|
||||
- Memory usage metrics per phase
|
||||
- Context relevance scoring
|
||||
- Skill suggestion effectiveness
|
||||
- Orchestrator dashboard integration
|
||||
|
||||
---
|
||||
|
||||
## Technical Highlights
|
||||
|
||||
### Error Handling
|
||||
- Graceful degradation (continue without memory if unavailable)
|
||||
- Activity-context-aware error wrapping
|
||||
- Retryable vs non-retryable error classification
|
||||
- Timeout handling per activity type
|
||||
|
||||
### Performance
|
||||
- Parallel context retrieval (async)
|
||||
- 3-tier retrieval (signature → ML → reference)
|
||||
- Budget-aware response assembly
|
||||
- Non-blocking learn/document operations
|
||||
|
||||
### Observability
|
||||
- Temporal activity logging with metadata
|
||||
- Per-activity attempt tracking
|
||||
- Context budget usage monitoring
|
||||
- Vault hit rate metrics
|
||||
|
||||
---
|
||||
|
||||
## Files & Commits
|
||||
|
||||
### Local Changes Committed
|
||||
|
||||
**Commit 1**: Temporal Activities Integration
|
||||
```
|
||||
feat(memory): add Temporal activities integration for memory service
|
||||
|
||||
- Implement 12 Temporal activities for memory operations
|
||||
- Full retry/timeout configuration with observability
|
||||
- Activity registration and worker setup
|
||||
- Workflow patterns and examples
|
||||
- All tests passing (23/23)
|
||||
```
|
||||
|
||||
**Commit 2**: Architecture Planning
|
||||
```
|
||||
docs(architecture): add memory-driven architecture & tool usage planning
|
||||
|
||||
- MEMORY_DRIVEN_ARCHITECTURE.md (24 KB)
|
||||
- TOOL_USAGE_AND_SKILLS.md (18 KB)
|
||||
- Complete integration roadmap
|
||||
```
|
||||
|
||||
### Documentation Files
|
||||
|
||||
| File | Size | Purpose |
|
||||
|------|------|---------|
|
||||
| MEMORY_DRIVEN_ARCHITECTURE.md | 24 KB | State machine integration plan |
|
||||
| TOOL_USAGE_AND_SKILLS.md | 18 KB | Tool landscape & skills strategy |
|
||||
| REGISTERED_ACTIVITIES.md | 10 KB | Activity reference |
|
||||
| MEMORY_INTEGRATION.md | 8 KB | Integration overview |
|
||||
| MEMORY_ACTIVITIES.md | 11 KB | Temporal activities reference |
|
||||
| REGISTERED_ACTIVITIES.md | 9.7 KB | Activities registry |
|
||||
|
||||
**Total Documentation**: ~80 KB (extensive, production-ready)
|
||||
|
||||
---
|
||||
|
||||
## How to Deploy
|
||||
|
||||
### 1. Register Activities in Worker
|
||||
|
||||
```go
|
||||
// In cmd/worker/main.go
|
||||
import "github.com/rockliang/poimen/workflows/internal/memory"
|
||||
|
||||
func main() {
|
||||
c, _ := client.Dial(client.Options{
|
||||
HostPort: "temporal-frontend.temporal:7233",
|
||||
Namespace: "poimen-harness",
|
||||
})
|
||||
defer c.Close()
|
||||
|
||||
w := worker.New(c, "poimen-taskqueue", worker.Options{})
|
||||
|
||||
// Register memory activities
|
||||
memSvc := memory.NewService(
|
||||
os.Getenv("MEMORY_SERVICE_URL"),
|
||||
os.Getenv("MEMORY_SERVICE_TOKEN"),
|
||||
"poimen",
|
||||
)
|
||||
memory.RegisterMemoryActivities(w, memSvc)
|
||||
|
||||
w.Start()
|
||||
defer w.Stop()
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Ingest Skills
|
||||
|
||||
```bash
|
||||
# From YAML
|
||||
cat prompts/skills.yaml | memory-ingest --level L2
|
||||
|
||||
# From Rust docs
|
||||
cargo doc --extract-comments | memory-ingest --level L2
|
||||
```
|
||||
|
||||
### 3. Use in RunExecutor
|
||||
|
||||
```go
|
||||
// In run_executor.rs (Rust)
|
||||
fn execute_step(...) {
|
||||
// Pre-execution
|
||||
let context = self.memory_svc
|
||||
.retrieve_context("planner", "step-id", budget)
|
||||
.await?;
|
||||
|
||||
// Optimize prompt
|
||||
let prompt = optimize_with_context(base_prompt, context);
|
||||
|
||||
// Execute
|
||||
let output = agent.execute(prompt);
|
||||
|
||||
// Post-execution
|
||||
self.memory_svc
|
||||
.learn_from_execution("step-id", output, tags)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Run all tests:
|
||||
```bash
|
||||
cd ~/workplace/Poimen/workflows
|
||||
go test ./internal/memory -v
|
||||
# Output: PASS: 23/23 tests (0.315s)
|
||||
```
|
||||
|
||||
Run specific activity:
|
||||
```bash
|
||||
go test ./internal/memory -v -run TestActivityCreateKnowledge
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Ready to Implement
|
||||
1. ✅ Activities defined & tested
|
||||
2. ✅ Full documentation complete
|
||||
3. ✅ Integration patterns documented
|
||||
4. 🔄 Deploy to cluster
|
||||
5. 🔄 Wire into RunExecutor
|
||||
6. 🔄 Ingest skills YAML
|
||||
|
||||
### Roadmap
|
||||
- **Week 1**: Deploy to cluster, test with real workflows
|
||||
- **Week 2**: Integrate into RunExecutor, test pre/post execution hooks
|
||||
- **Week 3**: Skills ingestion & prompt optimization
|
||||
- **Week 4**: Observability & metrics
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
✅ **Complete end-to-end memory service integration** for Poimen workflows:
|
||||
|
||||
- 12 production-ready Temporal activities
|
||||
- 23/23 tests passing
|
||||
- Comprehensive architecture planning
|
||||
- 3,889 lines of documentation
|
||||
- Integration roadmap for deployment
|
||||
- Skills ingestion strategy
|
||||
- Tool landscape mapping
|
||||
- State machine consumption model
|
||||
|
||||
**The system is ready for production deployment and will enable Poimen to:**
|
||||
- Learn from every execution (L1 knowledge)
|
||||
- Improve prompts with context (Tier 2/3 lessons)
|
||||
- Recover from failures faster (diagnose + suggest)
|
||||
- Document decisions for compliance (audit trail)
|
||||
- Organize skills and patterns (vault by domain)
|
||||
- Scale across phases (cross-phase pattern reuse)
|
||||
|
||||
**Every run improves the next run.** 🚀
|
||||
@@ -1,252 +0,0 @@
|
||||
# Cron Jobs in Poimen Workflows
|
||||
|
||||
## Quick Summary
|
||||
|
||||
**llm-router** detects if user is asking for scheduled/recurring work and generates either:
|
||||
- **WorkflowSpec** (one-time execution)
|
||||
- **CronWorkflowSpec** (scheduled execution)
|
||||
|
||||
---
|
||||
|
||||
## CRON EXAMPLES
|
||||
|
||||
### Example 1: Daily Security Scan
|
||||
|
||||
**User says**: "Run security scan on all repos every day at 2 AM"
|
||||
|
||||
**llm-router detects**:
|
||||
- is_scheduled: true
|
||||
- schedule: "0 2 * * *" (2 AM every day)
|
||||
- activities_needed: [Clone, SecurityScan, SendAlert]
|
||||
|
||||
**llm-router generates**:
|
||||
```json
|
||||
{
|
||||
"type": "CronWorkflow",
|
||||
"name": "daily-security-scan",
|
||||
"schedule": "0 2 * * *",
|
||||
"timezone": "UTC",
|
||||
"input": {"repos": [...]},
|
||||
"states": [
|
||||
{"name": "Clone", "type": "Task", "resource": "CloneRepoActivity", ...},
|
||||
{"name": "SecurityScan", "type": "Task", "resource": "SecurityScanActivity", ...},
|
||||
{"name": "SendAlert", "type": "Task", "resource": "SendNotificationActivity", ...}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Temporal** executes this:
|
||||
- At 2 AM UTC every day
|
||||
- Runs RoutingWorkflow with this spec
|
||||
- Each run is independent (tracks execution history)
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Hourly Health Check
|
||||
|
||||
**User says**: "Check API health every hour"
|
||||
|
||||
**llm-router generates**:
|
||||
```json
|
||||
{
|
||||
"type": "CronWorkflow",
|
||||
"schedule": "0 * * * *",
|
||||
"timezone": "UTC",
|
||||
"states": [
|
||||
{"name": "HealthCheck", "type": "Task", "resource": "HealthCheckActivity", ...},
|
||||
{"name": "RecordMetrics", "type": "Task", "resource": "RecordMetricsActivity", ...}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Execution**: Every hour, automatically
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Weekly Performance Baseline
|
||||
|
||||
**User says**: "Compare performance with baseline every Sunday at 3 AM"
|
||||
|
||||
**llm-router generates**:
|
||||
```json
|
||||
{
|
||||
"type": "CronWorkflow",
|
||||
"schedule": "0 3 * * 0",
|
||||
"timezone": "America/New_York",
|
||||
"states": [...]
|
||||
}
|
||||
```
|
||||
|
||||
**Execution**: Every Sunday at 3 AM in New York timezone
|
||||
|
||||
---
|
||||
|
||||
## CRON SCHEDULE SYNTAX
|
||||
|
||||
```
|
||||
┌───────────── minute (0 - 59)
|
||||
│ ┌───────────── hour (0 - 23)
|
||||
│ │ ┌───────────── day of month (1 - 31)
|
||||
│ │ │ ┌───────────── month (1 - 12)
|
||||
│ │ │ │ ┌───────────── day of week (0 - 6, 0 = Sunday)
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
* * * * *
|
||||
|
||||
Common Examples:
|
||||
0 2 * * * → Every day at 2:00 AM
|
||||
0 */6 * * * → Every 6 hours
|
||||
0 9 * * 1-5 → Weekdays at 9 AM (Mon-Fri)
|
||||
0 0 1 * * → First day of month at midnight
|
||||
0 0 * * 0 → Every Sunday at midnight
|
||||
*/15 * * * * → Every 15 minutes
|
||||
30 2 * * 0 → Every Sunday at 2:30 AM
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION TRACKING
|
||||
|
||||
Temporal automatically tracks all cron executions:
|
||||
|
||||
```
|
||||
Workflow: daily-security-scan
|
||||
|
||||
Run 1: 2025-02-01 02:00:00 UTC → COMPLETED (5m 32s)
|
||||
Run 2: 2025-02-02 02:00:00 UTC → COMPLETED (4m 58s)
|
||||
Run 3: 2025-02-03 02:00:00 UTC → FAILED (timeout in SecurityScan)
|
||||
Run 4: 2025-02-04 02:00:00 UTC → COMPLETED (6m 15s)
|
||||
Run 5: 2025-02-05 02:00:00 UTC → COMPLETED (5m 01s)
|
||||
```
|
||||
|
||||
Retrieve with:
|
||||
```bash
|
||||
temporal workflow list --query "ExecutionStatus='Completed' AND WorkflowType='RoutingWorkflow'"
|
||||
temporal workflow describe --workflow-id daily-security-scan
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HOW llm-router DETECTS CRON
|
||||
|
||||
LLM looks for keywords in user message:
|
||||
|
||||
```
|
||||
"every day at 2 AM" → "0 2 * * *"
|
||||
"every 6 hours" → "0 */6 * * *"
|
||||
"daily" → "0 0 * * *"
|
||||
"weekly" → "0 0 * * 0" (Sunday)
|
||||
"every Monday" → "0 0 * * 1"
|
||||
"every 15 minutes" → "*/15 * * * *"
|
||||
"weekdays at 9 AM" → "0 9 * * 1-5"
|
||||
"first of month" → "0 0 1 * *"
|
||||
"midnight" → "0 0"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DIFFERENCES: One-Time vs Cron
|
||||
|
||||
| Aspect | One-Time | Cron |
|
||||
|--------|----------|------|
|
||||
| **Type** | WorkflowSpec | CronWorkflowSpec |
|
||||
| **Triggered by** | API call, CLI | Schedule |
|
||||
| **Execution** | Runs once, returns immediately | Runs on schedule, indefinitely |
|
||||
| **Input** | Varies per call | Fixed for all runs |
|
||||
| **History** | Single execution | Multiple executions tracked |
|
||||
| **Cancellation** | Can't cancel | Can stop/restart cron |
|
||||
| **Use Case** | Ad-hoc analysis | Background monitoring |
|
||||
|
||||
---
|
||||
|
||||
## API ENDPOINTS (Proposed)
|
||||
|
||||
### Submit Cron Workflow
|
||||
```bash
|
||||
POST /api/v1/cron/workflows
|
||||
{
|
||||
"type": "CronWorkflow",
|
||||
"schedule": "0 2 * * *",
|
||||
"timezone": "UTC",
|
||||
"states": [...]
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"workflow_id": "daily-security-scan",
|
||||
"schedule": "0 2 * * *",
|
||||
"next_run": "2025-02-02 02:00:00 UTC",
|
||||
"created": "2025-02-01 15:30:00 UTC"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Cron Status
|
||||
```bash
|
||||
GET /api/v1/cron/workflows/daily-security-scan/status
|
||||
|
||||
Response:
|
||||
{
|
||||
"workflow_id": "daily-security-scan",
|
||||
"schedule": "0 2 * * *",
|
||||
"is_active": true,
|
||||
"last_run": {
|
||||
"time": "2025-02-01 02:00:00 UTC",
|
||||
"status": "COMPLETED",
|
||||
"duration": "5m 32s"
|
||||
},
|
||||
"next_run": "2025-02-02 02:00:00 UTC",
|
||||
"execution_history": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### List Cron Workflows
|
||||
```bash
|
||||
GET /api/v1/cron/workflows
|
||||
|
||||
Response:
|
||||
{
|
||||
"workflows": [
|
||||
{
|
||||
"workflow_id": "daily-security-scan",
|
||||
"schedule": "0 2 * * *",
|
||||
"is_active": true,
|
||||
"created": "2025-02-01 15:30:00 UTC"
|
||||
},
|
||||
{
|
||||
"workflow_id": "hourly-health-check",
|
||||
"schedule": "0 * * * *",
|
||||
"is_active": true,
|
||||
"created": "2025-02-01 16:00:00 UTC"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Cancel Cron Workflow
|
||||
```bash
|
||||
DELETE /api/v1/cron/workflows/daily-security-scan
|
||||
|
||||
Response:
|
||||
{
|
||||
"status": "cancelled",
|
||||
"workflow_id": "daily-security-scan",
|
||||
"cancelled_at": "2025-02-01 16:30:00 UTC"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SUMMARY
|
||||
|
||||
✅ **llm-router handles both**:
|
||||
- One-time workflows (instant execution)
|
||||
- Cron workflows (scheduled, recurring)
|
||||
|
||||
✅ **Same RoutingWorkflow executor** for both
|
||||
|
||||
✅ **Temporal manages scheduling** (native support)
|
||||
|
||||
✅ **Full execution history** tracked
|
||||
|
||||
This completes the architecture! 🎉
|
||||
|
||||
@@ -1,888 +0,0 @@
|
||||
# Poimen Service Integration & Dynamic Workflows — MASTER REVIEW DOCUMENT
|
||||
|
||||
**Status**: Ready for Approval
|
||||
**Version**: 1.0
|
||||
**Date**: 2025-01-31
|
||||
**Duration**: 52-58 hours (4 weeks)
|
||||
|
||||
---
|
||||
|
||||
## EXECUTIVE SUMMARY (5 MINUTES)
|
||||
|
||||
### Problem
|
||||
Current workflows are **hardcoded in Go**. Changing activity sequences requires:
|
||||
- Code modification
|
||||
- Recompilation
|
||||
- K8s deployment
|
||||
- Pod restart
|
||||
- **Total: 5-10 minutes**
|
||||
|
||||
### Solution
|
||||
New **RoutingWorkflow** reads JSON WorkflowSpec and executes dynamically:
|
||||
- **Three entry points**: CLI + HTTP API + Legacy CLI (backward compatible)
|
||||
- **Three patterns**: Sequential, Await-Task-Complete (KMSvc queue), Retry with error handling
|
||||
- **JSONPath parameters**: `${step1.output.path}` instead of hardcoded values
|
||||
- **Result**: Workflow changes in **seconds** (API call only)
|
||||
|
||||
### Impact
|
||||
✅ Instant changes | ✅ Three interfaces | ✅ Full compatibility | ❌ +KMSvc complexity
|
||||
|
||||
---
|
||||
|
||||
## PART 1: THE THREE PATTERNS
|
||||
|
||||
### Pattern 1: Sequential (A → B → C)
|
||||
|
||||
Activities execute in sequence. Each step passes output to next via JSONPath.
|
||||
|
||||
```json
|
||||
{
|
||||
"activities": [
|
||||
{
|
||||
"name": "Clone",
|
||||
"resource": "CloneRepoActivity",
|
||||
"parameters": {"repo": "${input.repo}"},
|
||||
"timeout": "5m",
|
||||
"next": "Analyze"
|
||||
},
|
||||
{
|
||||
"name": "Analyze",
|
||||
"resource": "AnalyzeCodeActivity",
|
||||
"parameters": {"path": "${Clone.output.path}"},
|
||||
"timeout": "10m",
|
||||
"next": "Judge"
|
||||
},
|
||||
{
|
||||
"name": "Judge",
|
||||
"resource": "JudgeActivity",
|
||||
"parameters": {"code": "${Analyze.output.code}"},
|
||||
"timeout": "5m",
|
||||
"end": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Execution Flow**:
|
||||
```
|
||||
Clone ──(output: {path: /tmp/repo})──> Analyze ──(output: {code: ...})──> Judge ──(final result)
|
||||
```
|
||||
|
||||
**Use Case**: Code review pipeline (clone → analyze → judge)
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Await-Task-Complete (Launch → Poll Queue → Process)
|
||||
|
||||
Launch long-running job, poll KMSvc queue for result with correlation ID matching, then proceed.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "LaunchJob",
|
||||
"type": "awaitTaskComplete",
|
||||
"resource": "LaunchJobActivity",
|
||||
"parameters": {"job_id": "job-123"},
|
||||
"queue": "job-completions",
|
||||
"correlation_key": "${LaunchJob.output.correlation_id}",
|
||||
"timeout": "5m",
|
||||
"next": "ProcessResult"
|
||||
}
|
||||
```
|
||||
|
||||
**Execution Flow**:
|
||||
```
|
||||
LaunchJobActivity ──(returns {correlation_id: "xyz"})──> Poll Queue "job-completions"
|
||||
↓
|
||||
[External system completes job]
|
||||
[Publishes {correlation_id: "xyz", result: ...}]
|
||||
↓
|
||||
[Workflow receives message]
|
||||
[Proceeds to ProcessResult]
|
||||
```
|
||||
|
||||
**Note**: Pattern 2 type name is `awaitTaskComplete` (awaits task completion via queue)
|
||||
|
||||
**Use Case**: Long-running batch jobs (model training, data processing)
|
||||
|
||||
---
|
||||
|
||||
### Pattern 3: Retry with Error Handling (Retry N times with backoff)
|
||||
|
||||
Activity retries N times with exponential backoff. On failure, jumps to catch block.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Generate",
|
||||
"resource": "ImplementerActivity",
|
||||
"parameters": {"task": "${input.task}"},
|
||||
"timeout": "30m",
|
||||
"retry": {
|
||||
"maxAttempts": 3,
|
||||
"backoffRate": 2.0,
|
||||
"initialInterval": "1s"
|
||||
},
|
||||
"catch": [
|
||||
{"errorEquals": ["Timeout"], "next": "HandleTimeout"}
|
||||
],
|
||||
"next": "Verify"
|
||||
}
|
||||
```
|
||||
|
||||
**Execution Flow**:
|
||||
```
|
||||
Generate (attempt 1) ──TIMEOUT──> [Wait 1s]
|
||||
Generate (attempt 2) ──TIMEOUT──> [Wait 2s]
|
||||
Generate (attempt 3) ──TIMEOUT──> [Wait 4s]
|
||||
Max retries exceeded ──> Jump to HandleTimeout
|
||||
```
|
||||
|
||||
**Use Case**: Code generation (retry on timeout, handle gracefully)
|
||||
|
||||
---
|
||||
|
||||
## PART 2: THE THREE ENTRY POINTS
|
||||
|
||||
### Entry Point 1: CLI (New Command-Line)
|
||||
|
||||
```bash
|
||||
# Submit workflow from JSON file
|
||||
$ poimen-cli submit workflow.json
|
||||
Workflow submitted: wf-abc123
|
||||
Poll status: poimen-cli status wf-abc123
|
||||
|
||||
# Submit and wait for results
|
||||
$ poimen-cli submit workflow.json --wait
|
||||
Waiting for workflow to complete...
|
||||
✅ Workflow completed
|
||||
Output: {...}
|
||||
|
||||
# Execute template immediately (sync, no polling)
|
||||
$ poimen-cli execute template code-review-v1 --input-file input.json
|
||||
Executing template...
|
||||
Step 1/3: Clone ... OK (45s)
|
||||
Step 2/3: Analyze ... OK (120s)
|
||||
Step 3/3: Judge ... OK (30s)
|
||||
✅ COMPLETED
|
||||
Output: {...}
|
||||
|
||||
# Check status later
|
||||
$ poimen-cli status wf-abc123
|
||||
Workflow ID: wf-abc123
|
||||
Status: COMPLETED
|
||||
Started: 2025-01-31 15:04:05
|
||||
Completed: 2025-01-31 15:05:00
|
||||
|
||||
# List templates
|
||||
$ poimen-cli template list
|
||||
Available Templates:
|
||||
- code-review-v1
|
||||
- clone-analyze-v1
|
||||
```
|
||||
|
||||
**Implementation**: `cmd/cli/` (new directory)
|
||||
|
||||
---
|
||||
|
||||
### Entry Point 2: HTTP API (New)
|
||||
|
||||
#### Route 1: Submit Workflow (Async)
|
||||
```bash
|
||||
curl -X POST http://api.example.com/api/v1/workflows \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @workflow.json
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"workflow_id": "wf-abc123",
|
||||
"status": "RUNNING",
|
||||
"polling_url": "/api/v1/workflows/wf-abc123/status"
|
||||
}
|
||||
```
|
||||
|
||||
#### Check Status
|
||||
```bash
|
||||
curl http://api.example.com/api/v1/workflows/wf-abc123/status
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"workflow_id": "wf-abc123",
|
||||
"status": "COMPLETED",
|
||||
"steps_completed": 3,
|
||||
"results": {
|
||||
"Clone": { "status": "COMPLETED", "output": {...} },
|
||||
"Analyze": { "status": "COMPLETED", "output": {...} },
|
||||
"Judge": { "status": "COMPLETED", "output": {...} }
|
||||
},
|
||||
"final_output": {...}
|
||||
}
|
||||
```
|
||||
|
||||
#### Route 2: Execute Template (Sync)
|
||||
```bash
|
||||
curl -X POST http://api.example.com/api/v1/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"template": "code-review-v1",
|
||||
"input": {"repo": "https://github.com/...", "branch": "feature/x"},
|
||||
"timeout": "2m"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response** (immediate):
|
||||
```json
|
||||
{
|
||||
"status": "COMPLETED",
|
||||
"results": {...},
|
||||
"final_output": {...}
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `cmd/api-server/` (new directory)
|
||||
|
||||
---
|
||||
|
||||
### Entry Point 3: Legacy CLI (Unchanged)
|
||||
|
||||
```bash
|
||||
# Old way still works, completely backward compatible
|
||||
$ go run ./cmd/starter \
|
||||
--repo https://github.com/rockliang/poimen \
|
||||
--remote file:///tmp/poimen-output \
|
||||
--milestone T0 \
|
||||
--planner-model ornith \
|
||||
--judge-model ornith \
|
||||
--implementer-model claude-sonnet-5
|
||||
|
||||
Workflow submitted: orch-poimen
|
||||
Status: RUNNING
|
||||
```
|
||||
|
||||
**Implementation**: `cmd/starter/` (existing, unchanged)
|
||||
|
||||
---
|
||||
|
||||
## PART 3: ARCHITECTURE OVERVIEW
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ THREE ENTRY POINTS │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ CLI (JSON file) HTTP API (JSON POST) Legacy CLI │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌─────────┐ │
|
||||
│ │ poimen-cli │ │ /api/v1/ │ │ cmd/ │ │
|
||||
│ │ submit │ │ workflows │ │ starter │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └────┬────┘ │
|
||||
│ │ │ │ │
|
||||
└─────────┼─────────────────────────┼─────────────────────┼────────┘
|
||||
│ │ │
|
||||
└─────────────────────────┼─────────────────────┘
|
||||
↓
|
||||
┌───────────────────────────┐
|
||||
│ RoutingWorkflow │
|
||||
│ (NEW - generic) │
|
||||
│ │
|
||||
│ 1. Parse WorkflowSpec │
|
||||
│ 2. For each activity: │
|
||||
│ - Resolve JSONPath │
|
||||
│ - Execute activity │
|
||||
│ - Handle errors │
|
||||
│ - Store result │
|
||||
│ 3. Return results │
|
||||
└───────────┬───────────────┘
|
||||
↓
|
||||
┌───────────────────────────┐
|
||||
│ Temporal Server │
|
||||
│ │
|
||||
│ Worker Pool: │
|
||||
│ - Executes activities │
|
||||
│ - Returns results │
|
||||
│ - Retries with backoff │
|
||||
└───────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PART 4: BEFORE vs AFTER
|
||||
|
||||
### Before (Hardcoded Workflows)
|
||||
|
||||
```bash
|
||||
# To change activity sequence:
|
||||
1. Modify Go code (OrchestratorWorkflow in statemachine/)
|
||||
2. Compile: go build ./cmd/worker
|
||||
3. Push docker image
|
||||
4. Deploy to K8s: kubectl set image deployment/poimen-worker
|
||||
5. Wait for rollout
|
||||
Time: 5-10 minutes
|
||||
```
|
||||
|
||||
**Workflow definition**:
|
||||
```go
|
||||
// Hard to change, tight coupling to specific sequence
|
||||
if err := workflow.ExecuteActivity(ctx, "CloneRepoActivity", params).Get(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := workflow.ExecuteActivity(ctx, "PlanningActivity", params2).Get(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
// ... more activities hardcoded
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### After (Dynamic Workflows)
|
||||
|
||||
```bash
|
||||
# To change activity sequence:
|
||||
1. Edit workflow.json (define sequence)
|
||||
2. Call API or CLI
|
||||
3. Done
|
||||
Time: Seconds
|
||||
```
|
||||
|
||||
**Workflow definition**:
|
||||
```json
|
||||
{
|
||||
"activities": [
|
||||
{"name": "Clone", "resource": "CloneRepoActivity", "parameters": {...}, "next": "Analyze"},
|
||||
{"name": "Analyze", "resource": "AnalyzeCodeActivity", "parameters": {...}, "next": "Judge"},
|
||||
{"name": "Judge", "resource": "JudgeActivity", "parameters": {...}, "end": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PART 5: IMPLEMENTATION PHASES
|
||||
|
||||
### Phase 1: Routing Engine (Week 1) — 20-22 Hours
|
||||
|
||||
**Goal**: Core RoutingWorkflow that executes JSON sequences dynamically
|
||||
|
||||
**Files to Create**:
|
||||
```
|
||||
internal/routing/
|
||||
├─ types.go (2h) - WorkflowSpec, ActivityStep
|
||||
├─ jsonpath_evaluator.go (3h) - ${step1.output.path} resolution
|
||||
├─ execution_context.go (2h) - State tracking (vars, step results)
|
||||
├─ activity_registry.go (2h) - Activity name → function mapping
|
||||
├─ kmsvc_client.go (2-3h)- KMSvc queue polling (awaitTaskComplete)
|
||||
└─ *_test.go (tests)
|
||||
|
||||
statemachine/
|
||||
├─ routing_workflow.go (4-5h)- ⭐ Main workflow logic
|
||||
└─ routing_workflow_test.go (2h) - Tests
|
||||
|
||||
cmd/worker/main.go: Register RoutingWorkflow (1h)
|
||||
```
|
||||
|
||||
**Deliverable**: RoutingWorkflow executes activity sequences with:
|
||||
- ✅ Sequential execution (A → B → C)
|
||||
- ✅ Await-Task-Complete pattern (KMSvc queue polling)
|
||||
- ✅ Retry with exponential backoff
|
||||
- ✅ Error catch blocks
|
||||
- ✅ JSONPath parameter resolution
|
||||
|
||||
**Risk**: Low-Medium (KMSvc queue polling adds complexity)
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: API Server (Week 2) — 12-14 Hours
|
||||
|
||||
**Goal**: HTTP endpoints for workflow submission and status polling
|
||||
|
||||
**Files to Create**:
|
||||
```
|
||||
cmd/api-server/
|
||||
├─ main.go (2h) - Server bootstrap
|
||||
├─ handlers.go (4h) - Routes: /workflows, /execute, status
|
||||
├─ validation.go (2h) - Validate WorkflowSpec
|
||||
├─ template_loader.go (2-3h)- Load predefined templates
|
||||
└─ *_test.go (2h) - Tests
|
||||
```
|
||||
|
||||
**Deliverable**:
|
||||
- `POST /api/v1/workflows` — Submit workflow (async)
|
||||
- `GET /api/v1/workflows/{id}/status` — Check status
|
||||
- `POST /api/v1/execute` — Execute template (sync)
|
||||
|
||||
**Risk**: Low (isolated, no breaking changes)
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: CLI (Week 2) — 8-10 Hours
|
||||
|
||||
**Goal**: Command-line interface for workflow submission
|
||||
|
||||
**Files to Create**:
|
||||
```
|
||||
cmd/cli/
|
||||
├─ main.go (2h) - Entry point, command routing
|
||||
└─ commands/
|
||||
├─ submit.go (2h) - poimen-cli submit workflow.json
|
||||
├─ status.go (2h) - poimen-cli status wf-id
|
||||
├─ template.go (1h) - poimen-cli template list/show
|
||||
├─ execute.go (1h) - poimen-cli execute template
|
||||
└─ history.go (1h) - poimen-cli history wf-id
|
||||
```
|
||||
|
||||
**Deliverable**:
|
||||
- `poimen-cli submit workflow.json [--wait] [--watch]`
|
||||
- `poimen-cli status wf-id [--wait]`
|
||||
- `poimen-cli execute template name --input-file input.json`
|
||||
- `poimen-cli template list/show`
|
||||
|
||||
**Risk**: Low
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Kubernetes Integration (Week 3) — 5 Hours
|
||||
|
||||
**Goal**: Deploy api-server to K8s, expose via ingress
|
||||
|
||||
**Files to Create**:
|
||||
```
|
||||
k8s/
|
||||
├─ api-server-deployment.yaml (2h) - 2 replicas, Temporal connection
|
||||
├─ api-server-service.yaml (1h) - Port 8080
|
||||
└─ api-server-networkpolicy.yaml(1h) - Allow ingress traffic
|
||||
|
||||
homelab-frontend/k8s/
|
||||
└─ gateway-routes.yaml (1h) - Route /api/v1/workflows to api-server
|
||||
```
|
||||
|
||||
**Deliverable**: API exposed at `https://api.riotpiao.com/api/v1/workflows`
|
||||
|
||||
**Risk**: Medium (network policy changes)
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Documentation (Week 4) — 7 Hours
|
||||
|
||||
**Files to Create**:
|
||||
```
|
||||
docs/
|
||||
├─ API.md (2h) - Complete API reference
|
||||
├─ CLI.md (2h) - CLI commands + examples
|
||||
├─ TEMPLATES.md (1h) - How to create/manage templates
|
||||
├─ MIGRATION.md (1h) - Migration from cmd/starter
|
||||
└─ examples/
|
||||
├─ sequential-workflow.json
|
||||
├─ retry-workflow.json
|
||||
└─ template-execute.json
|
||||
```
|
||||
|
||||
**Deliverable**: Complete documentation with working examples
|
||||
|
||||
**Risk**: Low
|
||||
|
||||
---
|
||||
|
||||
### Timeline Summary
|
||||
|
||||
```
|
||||
Phase 1: Routing Engine 20-22 hours (Week 1)
|
||||
Phase 2: API Server 12-14 hours (Week 2)
|
||||
Phase 3: CLI 8-10 hours (Week 2)
|
||||
Phase 4: K8s Integration 5 hours (Week 3)
|
||||
Phase 5: Documentation 7 hours (Week 4)
|
||||
─────────────────────────────────────────────────────
|
||||
TOTAL 52-58 hours (~1.5 weeks)
|
||||
|
||||
Team: 2-3 engineers
|
||||
Duration: 4 weeks (with overlapping phases)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PART 6: CRITICAL KMSVC QUESTIONS
|
||||
|
||||
These answers **block implementation** of Phase 1:
|
||||
|
||||
### Q1: What is KMSvc?
|
||||
- Is it Kafka? Redis? AWS SQS? Custom system?
|
||||
- What Go client library should we use?
|
||||
- Example: `github.com/segmentio/kafka-go`?
|
||||
|
||||
### Q2: Message Format
|
||||
- Is it JSON?
|
||||
- What fields are required? (e.g., `correlation_id`, `result`, `status`?)
|
||||
- Example:
|
||||
```json
|
||||
{
|
||||
"correlation_id": "xyz123",
|
||||
"status": "completed",
|
||||
"result": {...}
|
||||
}
|
||||
```
|
||||
|
||||
### Q3: Correlation ID Management
|
||||
- Who generates the correlation_id? (LaunchJobActivity? Or RoutingWorkflow?)
|
||||
- Is it returned in LaunchJobActivity output?
|
||||
- Example: LaunchJobActivity returns:
|
||||
```json
|
||||
{
|
||||
"correlation_id": "abc123-generated-by-activity",
|
||||
"job_id": "job-123"
|
||||
}
|
||||
```
|
||||
|
||||
### Q4: Queue Polling Strategy
|
||||
- Active polling loop (pull messages every N seconds)? OR
|
||||
- Temporal Signal (push-based, external system sends signal to workflow)?
|
||||
- If polling: What interval? (1s, 5s, 10s?)
|
||||
|
||||
### Q5: Message Timeout Behavior
|
||||
- If no message arrives within timeout (e.g., 5m), what happens?
|
||||
- Option A: Jump to catch block (error handler)
|
||||
- Option B: Fail entire workflow
|
||||
- Option C: Infinite wait (only human intervention can stop)
|
||||
|
||||
### Q6: Consumer Group Strategy
|
||||
- Single shared consumer (all workflows share one connection)?
|
||||
- Per-workflow consumer (each workflow gets own connection)?
|
||||
- Dead-letter queue for unmatched messages?
|
||||
|
||||
---
|
||||
|
||||
## PART 7: KEY DESIGN DECISIONS
|
||||
|
||||
### Decision 1: Three Patterns (Sequential, Await-Task-Complete, Retry)
|
||||
**Rationale**: Covers 90% of use cases. Retry critical for production. Await-Task-Complete enables long-running jobs.
|
||||
**Alternative Rejected**: Just Sequential (too limited)
|
||||
|
||||
### Decision 2: KMSvc Queue for Await-Task-Complete Pattern
|
||||
**Rationale**: Decouples external systems from Temporal. External systems only need queue client, not Temporal SDK.
|
||||
**Alternative Rejected**: Temporal callback/signal (tight coupling)
|
||||
|
||||
### Decision 3: JSONPath for Parameters
|
||||
**Rationale**: Standard notation (AWS Step Functions, Kubernetes). Supports nested refs and arrays.
|
||||
**Alternative Rejected**: Go templating (overkill, security risks)
|
||||
|
||||
### Decision 4: RoutingWorkflow (not Activity)
|
||||
**Rationale**: Workflows can call activities with retries. Durable execution + replay guarantee.
|
||||
**Alternative Rejected**: RoutingActivity (can't call other activities, no retries)
|
||||
|
||||
### Decision 5: Static Activity Registry
|
||||
**Rationale**: Type-safe, simpler. Worker pod must compile all activities anyway.
|
||||
**Alternative Rejected**: Dynamic registration (complex, unsafe)
|
||||
|
||||
### Decision 6: Keep cmd/starter Forever
|
||||
**Rationale**: No need to break existing deployments. Let customers choose.
|
||||
**Alternative Rejected**: Deprecate (breaking change)
|
||||
|
||||
### Decision 7: Shared api-server (2 replicas)
|
||||
**Rationale**: Simpler, resource-efficient, easier to scale.
|
||||
**Alternative Rejected**: Per-namespace servers (overkill)
|
||||
|
||||
---
|
||||
|
||||
## PART 8: RISKS & MITIGATIONS
|
||||
|
||||
| Risk | Severity | Mitigation |
|
||||
|------|----------|-----------|
|
||||
| JSONPath evaluation bugs | Medium | Unit tests (>90% coverage), property-based testing |
|
||||
| **KMSvc queue reliability** | **High** | Dead-letter queue, timeouts, persistence checks |
|
||||
| **Correlation ID mismatch** | **Medium** | Strict matching, schema validation, detailed logging |
|
||||
| **Consumer group conflicts** | **Medium** | Clear strategy, rebalancing, message redelivery |
|
||||
| Activity registry mismatch | Low | Validate at spec submission time |
|
||||
| Temporal network issues | Low | Retry + timeout in api-server |
|
||||
| Cache staleness (api-server) | Low | 24h TTL, event-driven invalidation |
|
||||
| Performance regression | Low | Measure baseline: target <200ms submit, <100ms poll |
|
||||
|
||||
**Total Risk Score**: Medium-High (up from Low-Medium due to KMSvc)
|
||||
|
||||
---
|
||||
|
||||
## PART 9: SUCCESS CRITERIA
|
||||
|
||||
### Functional ✅
|
||||
- RoutingWorkflow executes activity sequences dynamically
|
||||
- JSONPath parameters resolve correctly (nested, arrays)
|
||||
- Error catch blocks jump to handlers
|
||||
- Retry logic with exponential backoff (1s → 2s → 4s)
|
||||
- API validates WorkflowSpec (no dangling refs)
|
||||
- CLI reads JSON (no hardcoded flags)
|
||||
- Status polling returns step-by-step results
|
||||
- Backward compatible (cmd/starter unchanged)
|
||||
|
||||
### Non-Functional ✅
|
||||
- Performance: <200ms workflow submit, <100ms poll
|
||||
- Uptime: 99.9% (2 replicas, rolling updates)
|
||||
- Test coverage: >90%
|
||||
- Documentation: Complete + 3 working examples
|
||||
|
||||
---
|
||||
|
||||
## PART 10: BACKWARD COMPATIBILITY
|
||||
|
||||
### What Stays Unchanged ✅
|
||||
- `cmd/starter/` (old CLI works exactly as before)
|
||||
- All existing activities
|
||||
- All existing workflows (OrchestratorWorkflow, TaskUnitWorkflow)
|
||||
- Temporal worker registration
|
||||
|
||||
### What's New ✅
|
||||
- `cmd/cli/` (new CLI)
|
||||
- `cmd/api-server/` (new HTTP API)
|
||||
- `internal/routing/` (new routing engine)
|
||||
- `statemachine/routing_workflow.go` (new workflow)
|
||||
|
||||
### Migration Path ✅
|
||||
- Customers can use OLD way or NEW way
|
||||
- No forced upgrades
|
||||
- No breaking changes
|
||||
|
||||
---
|
||||
|
||||
## PART 11: FILES SUMMARY
|
||||
|
||||
### New Files (Phase 1-5)
|
||||
|
||||
**Routing Engine** (Phase 1):
|
||||
```
|
||||
internal/routing/types.go
|
||||
internal/routing/jsonpath_evaluator.go
|
||||
internal/routing/execution_context.go
|
||||
internal/routing/activity_registry.go
|
||||
internal/routing/kmsvc_client.go
|
||||
statemachine/routing_workflow.go
|
||||
```
|
||||
|
||||
**API Server** (Phase 2):
|
||||
```
|
||||
cmd/api-server/main.go
|
||||
cmd/api-server/handlers.go
|
||||
cmd/api-server/validation.go
|
||||
cmd/api-server/template_loader.go
|
||||
```
|
||||
|
||||
**CLI** (Phase 3):
|
||||
```
|
||||
cmd/cli/main.go
|
||||
cmd/cli/commands/submit.go
|
||||
cmd/cli/commands/status.go
|
||||
cmd/cli/commands/template.go
|
||||
cmd/cli/commands/execute.go
|
||||
cmd/cli/commands/history.go
|
||||
```
|
||||
|
||||
**K8s** (Phase 4):
|
||||
```
|
||||
k8s/api-server-deployment.yaml
|
||||
k8s/api-server-service.yaml
|
||||
k8s/api-server-networkpolicy.yaml
|
||||
homelab-frontend/k8s/gateway-routes.yaml
|
||||
```
|
||||
|
||||
**Docs** (Phase 5):
|
||||
```
|
||||
docs/API.md
|
||||
docs/CLI.md
|
||||
docs/TEMPLATES.md
|
||||
docs/MIGRATION.md
|
||||
docs/examples/*.json
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
- `cmd/worker/main.go` (register RoutingWorkflow)
|
||||
- `homelab-frontend/k8s/network-policy.yaml` (already done ✅)
|
||||
|
||||
---
|
||||
|
||||
## PART 12: COMPARISON TABLE
|
||||
|
||||
| Aspect | Before (Hardcoded) | After (Dynamic) |
|
||||
|--------|---|---|
|
||||
| **Workflow Definition** | Go code (OrchestratorWorkflow) | JSON (customer-specified) |
|
||||
| **Sequencing** | Fixed (Clone→Plan→Impl→Judge) | Any (customer defines in JSON) |
|
||||
| **Parameters** | Direct: `in.RemoteURL` | JSONPath: `${input.repo}` |
|
||||
| **Change Time** | 5-10 min (recompile+deploy) | Seconds (API call) |
|
||||
| **Entry Points** | CLI only (hardcoded flags) | CLI + API + Legacy CLI |
|
||||
| **Error Handling** | Try-catch (per-activity) | Catch blocks (conditional jumps) |
|
||||
| **Retry Policy** | Temporal default | Configurable per-activity |
|
||||
| **Long-Running Jobs** | Not supported | awaitTaskComplete pattern |
|
||||
| **Backward Compat** | N/A | 100% (cmd/starter unchanged) |
|
||||
| **Code Coupling** | Tight (to specific sequence) | Loose (generic RoutingWorkflow) |
|
||||
|
||||
---
|
||||
|
||||
## PART 13: APPROVAL CHECKLIST
|
||||
|
||||
### Must Answer (Blocks Phase 1)
|
||||
- [ ] Q1: What is KMSvc system?
|
||||
- [ ] Q2: Message format for KMSvc?
|
||||
- [ ] Q3: Who generates correlation_id?
|
||||
- [ ] Q4: Polling strategy (active or signal)?
|
||||
- [ ] Q5: Timeout behavior?
|
||||
- [ ] Q6: Consumer group strategy?
|
||||
|
||||
### Should Approve
|
||||
- [ ] Timeline OK? (52-58 hours)
|
||||
- [ ] Architecture OK? (RoutingWorkflow + 3 entry points)
|
||||
- [ ] Risk level acceptable? (Medium-High)
|
||||
- [ ] Backward compatibility OK? (Keep cmd/starter)
|
||||
|
||||
### Sign-Off
|
||||
- [ ] Tech Lead approval
|
||||
- [ ] Stakeholder sign-off
|
||||
- [ ] Ready to start Phase 1
|
||||
|
||||
---
|
||||
|
||||
## PART 14: NEXT STEPS
|
||||
|
||||
1. **Review This Document** (1-2 hours)
|
||||
- Read Parts 1-7 (understanding)
|
||||
- Read Parts 8-9 (risks & success)
|
||||
- Read Part 13 (approval checklist)
|
||||
|
||||
2. **Answer Critical Questions** (Q1-Q6)
|
||||
- Email or Slack responses required
|
||||
- Needed before Phase 1 can start
|
||||
|
||||
3. **Stakeholder Approval**
|
||||
- Architecture review meeting
|
||||
- Risk acknowledgment
|
||||
- Sign-off on timeline
|
||||
|
||||
4. **Start Implementation**
|
||||
- Week 1: Phase 1 (RoutingWorkflow)
|
||||
- Week 2: Phase 2 (API) + Phase 3 (CLI)
|
||||
- Week 3: Phase 4 (K8s deploy)
|
||||
- Week 4: Phase 5 (Docs)
|
||||
|
||||
---
|
||||
|
||||
## APPENDIX: EXAMPLE WORKFLOW JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "code-review-pipeline",
|
||||
"input": {
|
||||
"repo": "https://github.com/rockliang/poimen",
|
||||
"branch": "feature/new-cli"
|
||||
},
|
||||
"activities": [
|
||||
{
|
||||
"name": "Clone",
|
||||
"resource": "CloneRepoActivity",
|
||||
"parameters": {
|
||||
"repo": "${input.repo}",
|
||||
"branch": "${input.branch}"
|
||||
},
|
||||
"timeout": "5m",
|
||||
"next": "Analyze"
|
||||
},
|
||||
{
|
||||
"name": "Analyze",
|
||||
"resource": "AnalyzeCodeActivity",
|
||||
"parameters": {
|
||||
"path": "${Clone.output.path}"
|
||||
},
|
||||
"timeout": "10m",
|
||||
"retry": {
|
||||
"maxAttempts": 3,
|
||||
"backoffRate": 2.0,
|
||||
"initialInterval": "1s"
|
||||
},
|
||||
"catch": [
|
||||
{
|
||||
"errorEquals": ["Timeout"],
|
||||
"next": "HandleTimeout"
|
||||
}
|
||||
],
|
||||
"next": "Judge"
|
||||
},
|
||||
{
|
||||
"name": "Judge",
|
||||
"resource": "JudgeActivity",
|
||||
"parameters": {
|
||||
"code": "${Analyze.output.code}",
|
||||
"analysis": "${Analyze.output.analysis}"
|
||||
},
|
||||
"timeout": "5m",
|
||||
"end": true
|
||||
},
|
||||
{
|
||||
"name": "HandleTimeout",
|
||||
"type": "pass",
|
||||
"result": {
|
||||
"status": "failed",
|
||||
"reason": "Analysis timed out after 10m"
|
||||
},
|
||||
"end": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## APPENDIX: CLI USAGE EXAMPLES
|
||||
|
||||
```bash
|
||||
# Submit workflow from JSON file
|
||||
$ poimen-cli submit workflow.json
|
||||
Workflow submitted: wf-abc123
|
||||
Poll status: poimen-cli status wf-abc123
|
||||
|
||||
# Submit and wait for results
|
||||
$ poimen-cli submit workflow.json --wait
|
||||
Waiting for workflow to complete...
|
||||
✅ Workflow completed
|
||||
Output:
|
||||
{
|
||||
"verdict": "approved",
|
||||
"score": 0.95,
|
||||
"issues": []
|
||||
}
|
||||
|
||||
# Check status
|
||||
$ poimen-cli status wf-abc123
|
||||
Workflow ID: wf-abc123
|
||||
Status: COMPLETED
|
||||
Started: 2025-01-31 15:04:05
|
||||
Completed: 2025-01-31 15:05:00
|
||||
|
||||
# Execute template immediately (sync)
|
||||
$ poimen-cli execute template code-review-v1 --input-file input.json
|
||||
Executing template: code-review-v1
|
||||
[15:04:05] Step 1/3: Clone ... OK (45s)
|
||||
[15:04:50] Step 2/3: Analyze ... OK (120s)
|
||||
[15:05:50] Step 3/3: Judge ... OK (30s)
|
||||
✅ COMPLETED
|
||||
Output: {...}
|
||||
|
||||
# List templates
|
||||
$ poimen-cli template list
|
||||
Available Templates:
|
||||
- code-review-v1
|
||||
- clone-analyze-v1
|
||||
- simple-test-v1
|
||||
|
||||
# Show template
|
||||
$ poimen-cli template show code-review-v1
|
||||
Template: code-review-v1
|
||||
Activities: 3
|
||||
|
||||
1. Clone (CloneRepoActivity)
|
||||
Timeout: 5m
|
||||
2. Analyze (AnalyzeCodeActivity)
|
||||
Timeout: 10m
|
||||
Retry: 3 attempts, backoff 2.0
|
||||
3. Judge (JudgeActivity)
|
||||
Timeout: 5m
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**END OF MASTER REVIEW DOCUMENT**
|
||||
|
||||
For detailed technical documentation, see existing design docs in the repo.
|
||||
|
||||
@@ -1,423 +0,0 @@
|
||||
# 🎉 FINAL SESSION SUMMARY: Complete T1, T2, T3 Milestones
|
||||
|
||||
## 📊 OVERALL COMPLETION STATUS
|
||||
|
||||
```
|
||||
T0: 9/9 ✅ COMPLETE (100%) [Foundation]
|
||||
T1: 8/8 ✅ COMPLETE (100%) [Production Hardening]
|
||||
T2: 8/8 ✅ COMPLETE (100%) [Scale & Performance]
|
||||
T3: 8/8 ✅ COMPLETE (100%) [Feature Expansion]
|
||||
────────────────────────────────────────
|
||||
TOTAL: 40/40 (100%) ✅ ALL MILESTONES COMPLETE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 DELIVERABLES
|
||||
|
||||
### Code Statistics
|
||||
- **Lines of code**: ~24,000+ (production + tests)
|
||||
- **Internal packages**: 22 fully integrated packages
|
||||
- **Test files**: 70+ test files
|
||||
- **Total tests**: 520+ tests passing
|
||||
- **Compilation**: ✅ Zero errors
|
||||
- **Test pass rate**: 100%
|
||||
- **Code branches merged**: 25 branches → 1 main
|
||||
|
||||
### Repository Structure
|
||||
```
|
||||
internal/
|
||||
├── approval/ # T3.4 - Human-in-the-loop approval gates
|
||||
├── audit/ # T1.7 + T3.6 - Immutable audit trail
|
||||
├── batching/ # T2.5, T2.6 - Git & LLM batching
|
||||
├── board/ # T1.4 - State validation & healing
|
||||
├── cache/ # T2.1 - Result caching
|
||||
├── composition/ # T3.7 - Workflow composition
|
||||
├── config/ # Configuration management
|
||||
├── dispatch/ # T2.2 - Parallel executor
|
||||
├── external/ # T3.8 - External task import
|
||||
├── graph/ # T3.3 - Dependency graph
|
||||
├── health/ # T1.8 - K8s health probes
|
||||
├── history/ # T2.7 - History pruning
|
||||
├── indexing/ # T2.4 - Lessons indexing
|
||||
├── judge/ # T3.5 - Custom judges
|
||||
├── locking/ # T2.8 - Distributed locks
|
||||
├── lock/ # (deprecated)
|
||||
├── logging/ # T1.2 - Structured logging
|
||||
├── metrics/ # T1.2 - Prometheus metrics
|
||||
├── pause/ # T1.5 - Pause/resume
|
||||
├── plugins/ # T3.1 - Plugin system
|
||||
├── recovery/ # T1.1 - Error recovery
|
||||
├── templates/ # T2.3 + T3.2 - Caching & templates
|
||||
├── tuning/ # T1.3 - Timeout automation
|
||||
└── lock.go # (placeholder)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 T1: PRODUCTION HARDENING (8/8 ✅)
|
||||
|
||||
### T1.1 - Error Recovery & Deadletter Handling
|
||||
- **Tests**: 40
|
||||
- **Components**: Retry, Deadletter, Checkpoint
|
||||
- **Features**:
|
||||
- Exponential backoff with jitter
|
||||
- Max retry policies
|
||||
- Deadletter for permanent failures
|
||||
- Checkpoint for recovery state
|
||||
- Three-layer recovery strategy
|
||||
|
||||
### T1.2 - Structured Logging & Prometheus Metrics
|
||||
- **Tests**: 8 (logging) + 13 (metrics)
|
||||
- **Features**:
|
||||
- JSON logging in production
|
||||
- Colored output in development
|
||||
- Prometheus gauge/counter/histogram metrics
|
||||
- Activity tracking
|
||||
- Error rate monitoring
|
||||
|
||||
### T1.3 - Activity Timeout Tuning Automation
|
||||
- **Tests**: 36
|
||||
- **Features**:
|
||||
- P99 latency analysis
|
||||
- Confidence scoring (40% sample size + 60% reliability)
|
||||
- Historical lesson tracking
|
||||
- Automatic timeout adjustment
|
||||
- Learning from past executions
|
||||
|
||||
### T1.4 - Board State Validation & Auto-Healing
|
||||
- **Tests**: 29
|
||||
- **Features**:
|
||||
- Format validation (task names, types)
|
||||
- Semantic validation (references, types)
|
||||
- Automatic healing of common issues
|
||||
- State tracking
|
||||
- Consistency guarantees
|
||||
|
||||
### T1.5 - Workflow Pause/Resume with State Snapshots
|
||||
- **Tests**: 34
|
||||
- **Features**:
|
||||
- Snapshot persistence
|
||||
- Signal-based pause/resume
|
||||
- Cross-pod recovery
|
||||
- State restoration
|
||||
- TTL-based snapshot cleanup
|
||||
|
||||
### T1.6 - Comprehensive Integration Tests
|
||||
- **Tests**: 15
|
||||
- **Features**:
|
||||
- Concurrent workflow execution
|
||||
- Temporal backend simulation
|
||||
- Full workflow lifecycle
|
||||
|
||||
### T1.7 - Immutable Audit Logging
|
||||
- **Tests**: 8 + 4 (immutable_log)
|
||||
- **Features**:
|
||||
- Write-once audit trail
|
||||
- Timestamp tracking
|
||||
- Event immutability
|
||||
- Metadata storage
|
||||
- Hash chain integrity
|
||||
|
||||
### T1.8 - Health Checks for Kubernetes
|
||||
- **Tests**: 10
|
||||
- **Features**:
|
||||
- Separate health server on port 8081
|
||||
- /health/ready endpoint
|
||||
- /health/live endpoint
|
||||
- Dependency probes
|
||||
- Graceful degradation
|
||||
|
||||
---
|
||||
|
||||
## 🚀 T2: SCALE & PERFORMANCE (8/8 ✅)
|
||||
|
||||
### T2.1 - Activity Result Caching
|
||||
- **Tests**: 13
|
||||
- **Performance**: Eliminates redundant API calls
|
||||
- **Features**:
|
||||
- MD5-based cache keys
|
||||
- TTL support
|
||||
- FIFO eviction
|
||||
- Disk persistence
|
||||
- Query by task/activity/failure pattern
|
||||
|
||||
### T2.2 - Parallel Task Dispatcher
|
||||
- **Tests**: 15
|
||||
- **Performance**: 9x speedup for parallel execution
|
||||
- **Features**:
|
||||
- Semaphore-based concurrency
|
||||
- Result aggregation
|
||||
- Timing metrics
|
||||
- Wall-clock speedup verification
|
||||
|
||||
### T2.3 - Prompt Template Caching
|
||||
- **Tests**: 17
|
||||
- **Performance**: <100ms render latency
|
||||
- **Features**:
|
||||
- Pre-compiled Go templates
|
||||
- LRU eviction
|
||||
- Per-template statistics
|
||||
- Cache metrics
|
||||
|
||||
### T2.4 - Lessons File Indexing
|
||||
- **Tests**: 20
|
||||
- **Performance**: <10ms O(1) lookups for 10k entries
|
||||
- **Features**:
|
||||
- Multi-field indexing (task, activity, failure, pattern)
|
||||
- Time range queries
|
||||
- Similarity search
|
||||
- Incremental updates
|
||||
|
||||
### T2.5 - Git Operation Batching
|
||||
- **Tests**: 24
|
||||
- **Performance**: N-1 round trip savings
|
||||
- **Features**:
|
||||
- Batch commit combining
|
||||
- Auto-flush on size/time
|
||||
- Status tracking
|
||||
- Network cost calculation
|
||||
|
||||
### T2.6 - LLM Request Batching
|
||||
- **Tests**: 29
|
||||
- **Performance**: 90%+ cost reduction (3 requests → 1 API call)
|
||||
- **Features**:
|
||||
- Grouping by type & model
|
||||
- Async result delivery
|
||||
- Token counting
|
||||
- Execution time tracking
|
||||
|
||||
### T2.7 - Workflow History Pruning
|
||||
- **Tests**: 17
|
||||
- **Performance**: Constant memory growth
|
||||
- **Features**:
|
||||
- Size-based pruning (100MB default)
|
||||
- Age-based pruning (24h default)
|
||||
- Count-based pruning (1000 default)
|
||||
- Archive to disk
|
||||
|
||||
### T2.8 - Distributed Lock Optimization
|
||||
- **Tests**: 24
|
||||
- **Features**:
|
||||
- Pluggable backends (Redis/etcd/local)
|
||||
- LocalLockBackend fallback
|
||||
- Multi-pod safe
|
||||
- Lock renewal
|
||||
- Deadlock prevention
|
||||
|
||||
---
|
||||
|
||||
## ✨ T3: FEATURE EXPANSION (8/8 ✅)
|
||||
|
||||
### T3.1 - Custom Skill Plugins
|
||||
- **Tests**: 48
|
||||
- **Features**:
|
||||
- SkillPlugin interface
|
||||
- PluginRegistry
|
||||
- plugin:// URL scheme
|
||||
- Dynamic loading
|
||||
- Plugin validation
|
||||
|
||||
### T3.2 - Workflow Templates
|
||||
- **Tests**: 26
|
||||
- **Features**:
|
||||
- YAML-based templates
|
||||
- Task dependency validation
|
||||
- Save/load functionality
|
||||
- Usage tracking
|
||||
|
||||
### T3.3 - Task Dependency Graph
|
||||
- **Tests**: 23
|
||||
- **Features**:
|
||||
- Cycle detection
|
||||
- Topological sorting (Kahn's algorithm)
|
||||
- Critical path analysis
|
||||
- Dependency validation
|
||||
|
||||
### T3.4 - Human-in-the-Loop Approval Gates
|
||||
- **Tests**: 16
|
||||
- **Features**:
|
||||
- ApprovalGate for workflow gating
|
||||
- Status tracking (pending/approved/rejected/expired)
|
||||
- TTL-based expiration
|
||||
- Multiple approval requirement
|
||||
- History tracking
|
||||
|
||||
### T3.5 - Custom Judge Implementations
|
||||
- **Tests**: 5
|
||||
- **Features**:
|
||||
- Judge interface for domain-specific validators
|
||||
- CustomJudgeRegistry
|
||||
- Register/unregister at runtime
|
||||
- Default judge support
|
||||
|
||||
### T3.6 - Immutable Audit Trail (Enhanced)
|
||||
- **Tests**: 4
|
||||
- **Features**:
|
||||
- SHA256 hash chaining
|
||||
- Integrity verification
|
||||
- Append-only entries
|
||||
- Metadata tracking
|
||||
- Tamper-proof logging
|
||||
|
||||
### T3.7 - Workflow Composition
|
||||
- **Tests**: 4
|
||||
- **Features**:
|
||||
- WorkflowComposer for nested workflows
|
||||
- ChildOrchestrator management
|
||||
- Parent-child relationships
|
||||
- Hierarchy queries
|
||||
|
||||
### T3.8 - External Task System Integration
|
||||
- **Tests**: 5
|
||||
- **Features**:
|
||||
- TaskImporter for GitHub/Linear/JIRA
|
||||
- Source tracking
|
||||
- Status synchronization
|
||||
- External ID mapping
|
||||
|
||||
---
|
||||
|
||||
## 📈 TEST COVERAGE SUMMARY
|
||||
|
||||
| Milestone | Packages | Tests | Status |
|
||||
|-----------|----------|-------|--------|
|
||||
| T1 | 9 | 199 | ✅ Pass |
|
||||
| T2 | 8 | 159 | ✅ Pass |
|
||||
| T3 | 5 | 62 | ✅ Pass |
|
||||
| **TOTAL** | **22** | **520+** | **✅ 100%** |
|
||||
|
||||
### Test Distribution
|
||||
- Unit tests: 480+
|
||||
- Integration tests: 15
|
||||
- Concurrency tests: 15+
|
||||
- Benchmark tests: 10+
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ ARCHITECTURE HIGHLIGHTS
|
||||
|
||||
### Design Principles
|
||||
✅ **Modularity**: Each task is independent package with zero cross-dependencies
|
||||
✅ **Thread Safety**: All shared state protected by RWMutex
|
||||
✅ **Persistence**: JSON/JSONL for audit trail and recovery
|
||||
✅ **Extensibility**: Interface-based design for plugins and backends
|
||||
✅ **Observability**: Structured logging + Prometheus metrics
|
||||
✅ **Performance**: Caching, batching, parallelization optimizations
|
||||
✅ **Reliability**: Multi-layer error recovery and state snapshots
|
||||
✅ **Kubernetes Ready**: Health checks, graceful shutdown, distributed locks
|
||||
|
||||
### Key Technical Achievements
|
||||
- **9x parallelization speedup** verified with benchmarks
|
||||
- **90%+ LLM cost reduction** via batching (30 tasks → 3 API calls)
|
||||
- **<10ms query latency** for lesson indexing (O(1) hash tables)
|
||||
- **<100ms template rendering** with LRU caching
|
||||
- **Constant memory** despite thousands of tasks (pruning strategy)
|
||||
- **N-1 network round trip savings** via git operation batching
|
||||
- **Multi-pod safe** distributed locking with Redis/etcd/local backends
|
||||
- **100% test pass rate** across 520+ tests
|
||||
|
||||
---
|
||||
|
||||
## 📋 GIT HISTORY
|
||||
|
||||
### Merged Branches (25 total)
|
||||
```
|
||||
T1 (8 branches): task/T1.1 → task/T1.8
|
||||
T2 (8 branches): task/T2.1 → task/T2.8
|
||||
T3 (9 branches): task/T3.1 → task/T3.5-T3.8 (consolidated)
|
||||
```
|
||||
|
||||
### Recent Commits
|
||||
```
|
||||
00f1dad fix(T3.4): simplify approval gate tests for better isolation
|
||||
cb94314 feat(T3.5-T3.8): complete feature expansion tasks
|
||||
75a01a9 feat(T3.4): implement human-in-the-loop approval gates
|
||||
e00762b feat(T3.3): implement task dependency graph
|
||||
b0313ae feat(T3.2): implement workflow templates system
|
||||
cb8a3fe feat(T3.1): implement custom skill plugin system
|
||||
00d40e3 feat(T2.8): implement distributed lock optimization
|
||||
9ed6c26 feat(T2.7): implement workflow history pruning
|
||||
b2cebe1 feat(T2.6): implement LLM request batching
|
||||
d8fe3f5 feat(T2.5): implement git operation batching
|
||||
87ceea3 feat(T2.4): implement fast lessons file indexing
|
||||
8baf16a feat(T2.3): implement prompt template caching engine
|
||||
b77c7b5 feat(T2.2): implement parallel task dispatcher
|
||||
9315fa6 feat(T2.1): implement activity result caching
|
||||
e3f3b35 feat(T1.6, T1.7): comprehensive integration tests and audit logging
|
||||
37d7aea feat(T1.5): implement workflow pause/resume with state snapshots
|
||||
b1e3136 feat(T1.4): implement board state validation and auto-healing
|
||||
927835c feat(T1.3): implement activity timeout tuning automation
|
||||
60f9ca2 feat(T1.1): implement error recovery, retry policies, and deadletter handling
|
||||
59a1eee feat(T1.2): implement structured logging and Prometheus metrics
|
||||
90fcd6a feat(T1.8): implement health checks for Kubernetes deployment
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ VERIFICATION CHECKLIST
|
||||
|
||||
- [x] All 40 milestone tasks complete
|
||||
- [x] 520+ unit tests passing (100% pass rate)
|
||||
- [x] Zero compilation errors
|
||||
- [x] All 22 internal packages tested
|
||||
- [x] Thread-safe concurrent implementations
|
||||
- [x] Production-ready code quality
|
||||
- [x] Comprehensive test coverage
|
||||
- [x] Performance benchmarks verified
|
||||
- [x] Kubernetes deployment ready
|
||||
- [x] Error recovery implemented
|
||||
- [x] Observability integrated (logging + metrics)
|
||||
- [x] Git history clean and merged to main
|
||||
- [x] Documentation complete
|
||||
|
||||
---
|
||||
|
||||
## 🚀 NEXT STEPS
|
||||
|
||||
### Immediate
|
||||
1. Deploy to staging environment
|
||||
2. Run integration tests against real Temporal backend
|
||||
3. Monitor metrics and logs in production
|
||||
4. Validate health checks in K8s cluster
|
||||
5. Test failover scenarios
|
||||
|
||||
### Future Enhancements
|
||||
1. T4: Advanced Features (if roadmap extends)
|
||||
2. Performance tuning based on production data
|
||||
3. Dashboard implementation for metrics
|
||||
4. Advanced workflow visualization
|
||||
5. Multi-tenancy support
|
||||
|
||||
---
|
||||
|
||||
## 📊 SESSION STATISTICS
|
||||
|
||||
- **Total development time**: Single comprehensive session
|
||||
- **Code commits**: 25+ atomic commits
|
||||
- **Files created**: 100+ (production + tests)
|
||||
- **Lines written**: ~24,000+
|
||||
- **Packages implemented**: 22 internal packages
|
||||
- **Test coverage**: 520+ tests, 100% pass rate
|
||||
- **Production readiness**: Full ✅
|
||||
|
||||
---
|
||||
|
||||
## 🎓 ARCHITECTURAL LESSONS LEARNED
|
||||
|
||||
1. **Modularity wins**: Independent packages enable parallel development and testing
|
||||
2. **Interface-based design**: Essential for testability and extensibility
|
||||
3. **Observability first**: Structured logging + metrics catch issues early
|
||||
4. **Thread safety matters**: RWMutex and proper synchronization prevent subtle bugs
|
||||
5. **Performance by design**: Batching, caching, and parallelization must be planned
|
||||
6. **Error recovery layering**: Multiple strategies (retry, deadletter, checkpoint) essential
|
||||
7. **State management**: Snapshots and persistence enable cross-pod recovery
|
||||
8. **Testing strategy**: Concurrent access, edge cases, and benchmarks all necessary
|
||||
|
||||
---
|
||||
|
||||
**🎉 ALL 40 TASKS COMPLETE - PROJECT PRODUCTION READY** 🎉
|
||||
|
||||
Repository: `/Users/rockliang/workplace/Poimen/workflows`
|
||||
Branch: `main` (all features merged)
|
||||
Status: ✅ Ready for deployment
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,498 +0,0 @@
|
||||
# Poimen Memory Service — Temporal Activities Integration
|
||||
|
||||
## Summary
|
||||
|
||||
Memory service fully integrated as **Temporal Activities** for workflows. All operations (create, update, retrieve, diagnose) are now first-class Temporal activities with retries, timeouts, logging, and error handling.
|
||||
|
||||
**Status**: ✅ 23/23 tests passing, 10 activities implemented, production-ready.
|
||||
|
||||
---
|
||||
|
||||
## What Changed
|
||||
|
||||
### Before
|
||||
```go
|
||||
// Raw service calls (no Temporal integration)
|
||||
svc := memory.NewService(...)
|
||||
id, err := svc.CreateKnowledge(ctx, record)
|
||||
```
|
||||
|
||||
### After
|
||||
```go
|
||||
// Temporal activity (automatic retries, logging, observability)
|
||||
id, err := memory.ExecuteCreateKnowledge(ctx, record, nil)
|
||||
// With custom retry policy:
|
||||
opts := &memory.ActivityOptions{
|
||||
RetryAttempts: 5,
|
||||
RetryBackoff: time.Second,
|
||||
}
|
||||
id, err := memory.ExecuteCreateKnowledge(ctx, record, opts)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Activities Implemented
|
||||
|
||||
| Activity | Purpose | Input | Output | Retries |
|
||||
|----------|---------|-------|--------|---------|
|
||||
| **CreateKnowledgeActivity** | Create L1/L2/reference records | `KnowledgeRecord` | `string` (ID) | 3x default |
|
||||
| **UpdateKnowledgeActivity** | Update existing knowledge | `KnowledgeRecord` | `string` (ID) | 3x |
|
||||
| **SearchKnowledgeActivity** | Hybrid search (semantic+lexical) | `string` query, `RetrievalOptions` | `[]KnowledgeRecord` | 3x |
|
||||
| **GetContextActivity** | Three-tier retrieval (Tier 1→2→3) | tool, task, budget | `*ServiceContext` | 3x |
|
||||
| **GetVaultActivity** | Browse vault files | (none) | `[]VaultInfo` | 3x |
|
||||
| **HealthCheckActivity** | Check service health | (none) | `bool` | 3x |
|
||||
| **LearnFromExecutionActivity** | Learn from task results | taskID, result, tags | `string` (ID) | 3x |
|
||||
| **DiagnoseIssueActivity** | Diagnose tool/task issues | tool, issue | `[]string` (recommendations) | 3x |
|
||||
| **AnalyzeErrorActivity** | Analyze errors, find solutions | errorMsg | `[]KnowledgeRecord` | 3x |
|
||||
| **DocumentDecisionActivity** | Record workflow decisions | decisionType, decision, reasoning | `string` (ID) | 3x |
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Register in Worker
|
||||
|
||||
```go
|
||||
import "github.com/rockliang/poimen/workflows/internal/memory"
|
||||
|
||||
// In worker setup
|
||||
svc := memory.NewService(baseURL, token, project)
|
||||
memory.RegisterMemoryActivities(w, svc)
|
||||
```
|
||||
|
||||
### 2. Use in Workflows
|
||||
|
||||
```go
|
||||
func MyWorkflow(ctx workflow.Context) error {
|
||||
// Simple call (default retry policy)
|
||||
id, err := memory.ExecuteCreateKnowledge(
|
||||
ctx,
|
||||
&memory.KnowledgeRecord{
|
||||
Level: "L1",
|
||||
Content: "...",
|
||||
},
|
||||
nil, // Use defaults
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Custom retry policy
|
||||
recommendations, err := memory.ExecuteDiagnoseIssue(
|
||||
ctx,
|
||||
"kubectl",
|
||||
"pod-crash",
|
||||
&memory.ActivityOptions{
|
||||
RetryAttempts: 5,
|
||||
RetryBackoff: time.Second * 2,
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
internal/memory/
|
||||
├── activities.go (240 lines) — Activity implementations
|
||||
├── activities_test.go (320 lines) — 10 activity tests
|
||||
├── worker_setup.go (310 lines) — Registration + wrappers + retry config
|
||||
├── workflow_examples.go (260 lines) — 8 workflow patterns
|
||||
├── client.go (250 lines) — HTTP client (unchanged)
|
||||
├── service.go (180 lines) — High-level wrapper (unchanged)
|
||||
├── client_test.go (150 lines) — Client tests (unchanged)
|
||||
├── service_test.go (170 lines) — Service tests (unchanged)
|
||||
├── README.md (400 lines) — Full API + examples
|
||||
└── example_activity.go (130 lines) — Legacy examples (deprecated)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Activity Features
|
||||
|
||||
### Automatic Retries
|
||||
|
||||
Each activity retries on failure (default 3 attempts, exponential backoff):
|
||||
|
||||
```go
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: backoff,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaximumInterval: 30 * time.Second,
|
||||
MaximumAttempts: 3,
|
||||
NonRetryableErrorTypes: [],
|
||||
}
|
||||
```
|
||||
|
||||
### Configurable Timeouts
|
||||
|
||||
Per-activity timeout control:
|
||||
|
||||
```go
|
||||
opts := &memory.ActivityOptions{
|
||||
RetryAttempts: 5,
|
||||
RetryBackoff: time.Second,
|
||||
StartTimeout: 30 * time.Second,
|
||||
HeartbeatRate: 10 * time.Second,
|
||||
}
|
||||
```
|
||||
|
||||
### Built-in Logging
|
||||
|
||||
All activities log:
|
||||
- Activity start + parameters
|
||||
- Success + result
|
||||
- Errors + stack trace
|
||||
|
||||
Example log output:
|
||||
```
|
||||
INFO Creating knowledge title="Pod Debugging"
|
||||
INFO Knowledge created id=chunk-123
|
||||
ERROR Failed to create knowledge error="connection refused"
|
||||
```
|
||||
|
||||
### Health Monitoring
|
||||
|
||||
Activities can check service health:
|
||||
|
||||
```go
|
||||
healthy, err := memory.ExecuteHealthCheck(ctx, nil)
|
||||
if !healthy {
|
||||
return fmt.Errorf("memory service unavailable")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow Patterns
|
||||
|
||||
### Pattern 1: Learning Workflow
|
||||
|
||||
Learn from task execution, persist knowledge:
|
||||
|
||||
```go
|
||||
func LearnWorkflow(ctx workflow.Context, taskID string) (string, error) {
|
||||
result := "Task succeeded"
|
||||
|
||||
knowledgeID, err := memory.ExecuteLearnFromExecution(
|
||||
ctx,
|
||||
taskID,
|
||||
result,
|
||||
[]string{"success"},
|
||||
nil,
|
||||
)
|
||||
return knowledgeID, err
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Diagnostic Workflow
|
||||
|
||||
Diagnose issues, retrieve recommendations:
|
||||
|
||||
```go
|
||||
func DiagnoseWorkflow(ctx workflow.Context, tool, issue string) ([]string, error) {
|
||||
return memory.ExecuteDiagnoseIssue(
|
||||
ctx,
|
||||
tool,
|
||||
issue,
|
||||
&memory.ActivityOptions{RetryAttempts: 5},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Error Recovery
|
||||
|
||||
Analyze error, find recovery path:
|
||||
|
||||
```go
|
||||
func RecoveryWorkflow(ctx workflow.Context, errorMsg string) ([]string, error) {
|
||||
records, err := memory.ExecuteAnalyzeError(ctx, errorMsg, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use L1 records (high confidence)
|
||||
recovery := make([]string, 0)
|
||||
for _, rec := range records {
|
||||
if rec.Level == "L1" {
|
||||
recovery = append(recovery, rec.Content)
|
||||
}
|
||||
}
|
||||
return recovery, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Context-Aware Decision
|
||||
|
||||
Make decisions based on memory context:
|
||||
|
||||
```go
|
||||
func ContextualDecisionWorkflow(ctx workflow.Context, tool, task string) (string, error) {
|
||||
// Get context
|
||||
svcCtx, err := memory.ExecuteGetContext(ctx, tool, task, 8192, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract best lesson
|
||||
decision := ""
|
||||
if len(svcCtx.Lessons) > 0 {
|
||||
decision = svcCtx.Lessons[0].Text
|
||||
}
|
||||
|
||||
// Document decision
|
||||
docID, err := memory.ExecuteDocumentDecision(
|
||||
ctx,
|
||||
tool,
|
||||
decision,
|
||||
"From memory context",
|
||||
nil,
|
||||
)
|
||||
return docID, err
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 5: Multi-Step Workflow
|
||||
|
||||
Multiple memory operations in sequence:
|
||||
|
||||
```go
|
||||
func MultiStepWorkflow(ctx workflow.Context, topic string) error {
|
||||
// Step 1: Create knowledge
|
||||
id, err := memory.ExecuteCreateKnowledge(ctx, &memory.KnowledgeRecord{
|
||||
Content: "Initial fact",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 2: Search related knowledge
|
||||
records, err := memory.ExecuteSearchKnowledge(ctx, topic, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 3: Get context
|
||||
svcCtx, err := memory.ExecuteGetContext(ctx, "workflow", topic, 8192, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 4: Document findings
|
||||
_, err = memory.ExecuteDocumentDecision(
|
||||
ctx,
|
||||
"workflow_complete",
|
||||
fmt.Sprintf("Found %d records, tier %d context", len(records), svcCtx.Tier),
|
||||
"Completed multi-step",
|
||||
nil,
|
||||
)
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
All 23 tests pass (10 activity + 13 client/service tests):
|
||||
|
||||
```bash
|
||||
cd ~/workplace/Poimen/workflows
|
||||
go test ./internal/memory -v
|
||||
|
||||
# Output:
|
||||
# === RUN TestActivityCreateKnowledge
|
||||
# --- PASS: TestActivityCreateKnowledge (0.04s)
|
||||
# ...
|
||||
# PASS: 23/23 tests (0.452s)
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
**Activity Tests** (10):
|
||||
- ✅ CreateKnowledgeActivity
|
||||
- ✅ SearchKnowledgeActivity
|
||||
- ✅ GetContextActivity
|
||||
- ✅ DiagnoseIssueActivity
|
||||
- ✅ AnalyzeErrorActivity
|
||||
- ✅ HealthCheckActivity
|
||||
- ✅ LearnFromExecutionActivity
|
||||
- ✅ DocumentDecisionActivity
|
||||
- ✅ ActivityOptions
|
||||
- ✅ ActivityError
|
||||
|
||||
**Client Tests** (5):
|
||||
- ✅ Ingest
|
||||
- ✅ Query
|
||||
- ✅ Context
|
||||
- ✅ Vault
|
||||
- ✅ Health
|
||||
|
||||
**Service Tests** (6):
|
||||
- ✅ CreateKnowledge
|
||||
- ✅ UpdateKnowledge
|
||||
- ✅ RetrieveKnowledge
|
||||
- ✅ RetrieveContext
|
||||
- ✅ GetVault
|
||||
- ✅ IsHealthy
|
||||
|
||||
---
|
||||
|
||||
## Observability
|
||||
|
||||
### Activity Logging
|
||||
|
||||
Automatic logging with activity context:
|
||||
|
||||
```
|
||||
INFO Creating knowledge ActivityID=0 ActivityType=CreateKnowledgeActivity Attempt=1 title="Pod Debugging"
|
||||
INFO Knowledge created ActivityID=0 ActivityType=CreateKnowledgeActivity Attempt=1 id=chunk-123
|
||||
ERROR Failed to create knowledge ActivityID=0 ActivityType=CreateKnowledgeActivity Attempt=2 error="service unavailable"
|
||||
```
|
||||
|
||||
### Metrics Tracked
|
||||
|
||||
- Activity execution count
|
||||
- Retry attempts
|
||||
- Latency per operation
|
||||
- Success/failure rates
|
||||
- Timeouts
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Activity Errors
|
||||
|
||||
All errors include context:
|
||||
|
||||
```go
|
||||
type MemoryActivityError struct {
|
||||
ActivityName string
|
||||
Attempt int
|
||||
Err error
|
||||
}
|
||||
|
||||
// Example: "memory activity create-knowledge (attempt 2): connection refused"
|
||||
```
|
||||
|
||||
### Retry Strategy
|
||||
|
||||
- Default: 3 attempts, exponential backoff (1s → 2s → 4s → ...)
|
||||
- Max interval: 30 seconds
|
||||
- Non-retryable: None (all errors retry)
|
||||
|
||||
Example with custom retry:
|
||||
|
||||
```go
|
||||
opts := &memory.ActivityOptions{
|
||||
RetryAttempts: 5,
|
||||
RetryBackoff: time.Second,
|
||||
}
|
||||
id, err := memory.ExecuteCreateKnowledge(ctx, record, opts)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
Typical latencies (from logs):
|
||||
|
||||
- CreateKnowledgeActivity: 20-50ms
|
||||
- SearchKnowledgeActivity: 100-200ms
|
||||
- GetContextActivity: 150-250ms
|
||||
- DiagnoseIssueActivity: 100-300ms
|
||||
- HealthCheckActivity: 10-20ms
|
||||
|
||||
Rate limits (per JWT identity):
|
||||
- Ingest: 100/hr
|
||||
- Query: 1000/hr
|
||||
- Context: 100/hr
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Worker Registration
|
||||
|
||||
```go
|
||||
// In your worker setup
|
||||
svc := memory.NewService(
|
||||
os.Getenv("MEMORY_SERVICE_URL"),
|
||||
os.Getenv("MEMORY_SERVICE_TOKEN"),
|
||||
"poimen",
|
||||
)
|
||||
memory.RegisterMemoryActivities(w, svc)
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
MEMORY_SERVICE_URL=http://memory-service.poimen.svc.cluster.local:8080
|
||||
MEMORY_SERVICE_TOKEN=<jwt-token-from-authentik>
|
||||
```
|
||||
|
||||
### Activity Defaults
|
||||
|
||||
```go
|
||||
&memory.ActivityOptions{
|
||||
RetryAttempts: 3,
|
||||
RetryBackoff: time.Second,
|
||||
StartTimeout: 30 * time.Second,
|
||||
HeartbeatRate: 10 * time.Second,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files Summary
|
||||
|
||||
| File | Lines | Purpose |
|
||||
|------|-------|---------|
|
||||
| `activities.go` | 240 | 10 Temporal activity implementations |
|
||||
| `activities_test.go` | 320 | Activity unit tests (Temporal test suite) |
|
||||
| `worker_setup.go` | 310 | Activity registration + wrapper functions + retry config |
|
||||
| `workflow_examples.go` | 260 | 8 workflow patterns using activities |
|
||||
| `client.go` | 250 | HTTP client (HTTP layer) |
|
||||
| `service.go` | 180 | High-level service wrapper |
|
||||
| `client_test.go` | 150 | HTTP client tests |
|
||||
| `service_test.go` | 170 | Service tests |
|
||||
| `README.md` | 400 | Full API documentation + examples |
|
||||
| **TOTAL** | **2,280** | Production-ready Temporal integration |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Deploy to cluster**: Update worker Pod to register activities
|
||||
2. **Use in workflows**: Import and call activities from workflow code
|
||||
3. **Monitor**: Track activity execution in Temporal UI
|
||||
4. **Optimize**: Adjust retry policy based on production metrics
|
||||
|
||||
---
|
||||
|
||||
## Documentation Links
|
||||
|
||||
- Full API: `internal/memory/README.md`
|
||||
- Workflow patterns: `internal/memory/workflow_examples.go`
|
||||
- Worker setup: `internal/memory/worker_setup.go`
|
||||
- Memory service API: `~/workplace/Poimen/memory/CLAUDE.md`
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
✅ **Complete & Production-Ready**
|
||||
|
||||
- 23/23 tests passing
|
||||
- 10 activities implemented
|
||||
- Full Temporal integration
|
||||
- Retry + timeout handling
|
||||
- Built-in logging
|
||||
- Error handling
|
||||
- Documentation complete
|
||||
|
||||
Ready for workflow integration.
|
||||
@@ -1,683 +0,0 @@
|
||||
# Memory-Driven Architecture for Poimen Workflows
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Poimen state machine (10 phases, 80 tasks, 10 composition gates) will consume Memory Service context & skills to:
|
||||
- **Learn** from execution attempts (L1 knowledge)
|
||||
- **Diagnose** failures using memory (three-tier retrieval)
|
||||
- **Document** decisions for future runs (L2 knowledge)
|
||||
- **Optimize** prompts with relevant context before agent execution
|
||||
- **Track** tools, skills, and pattern usage across the harness lifecycle
|
||||
|
||||
This document outlines how Temporal activities integrate with the existing state machine to create a memory-driven, self-improving workflow system.
|
||||
|
||||
---
|
||||
|
||||
## Current State Machine Architecture
|
||||
|
||||
```
|
||||
Poimen Harness (Rust + JSON-RPC)
|
||||
├─ Kernel (Event Log + State Machine)
|
||||
├─ 10 Phases (T0-T10)
|
||||
├─ 80 Tasks (70 build + 10 composition gates)
|
||||
├─ WorkflowDef IR (YAML + Rust builder)
|
||||
└─ 3 Ports (Verifier, Judge, ModelProvider)
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
**WorkflowDef (IR)**: Canonical hash of workflow definition
|
||||
- YAML declares: steps, transitions, retry policy, budgets
|
||||
- Rust implements: verifier logic, judge logic, model behavior
|
||||
|
||||
**State Machine**: Event-sourced, immutable audit trail
|
||||
- Events: WorkerEvent enum
|
||||
- Attempts: AttemptState with context partition capture
|
||||
- Folds: re-derive state from event log
|
||||
|
||||
**Run Executor**: Poll-based with:
|
||||
- Retry policy per step
|
||||
- Budget tracking (attempts, tokens, time)
|
||||
- Context partition per attempt
|
||||
|
||||
---
|
||||
|
||||
## Memory Service Integration Points
|
||||
|
||||
### Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Poimen Workflow │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ T1-T10: Task Execution Loop │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ For each step in workflow: │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ 1. RetrieveContext (Memory Service) │ │ │
|
||||
│ │ │ ├─ Tool: executor type (planner/judge/impl) │ │ │
|
||||
│ │ │ ├─ Task: step name │ │ │
|
||||
│ │ │ └─ Returns: tier-1 (signature) + tier-2 (ML) │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ 2. OptimizePrompt (with context) │ │ │
|
||||
│ │ │ ├─ Add learned facts from memory │ │ │
|
||||
│ │ │ ├─ Include skill usage examples │ │ │
|
||||
│ │ │ └─ Attach budget constraints │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ 3. ExecuteStep (ModelProvider) │ │ │
|
||||
│ │ │ └─ Agent uses optimized prompt │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ 4. OnStepComplete: │ │ │
|
||||
│ │ │ ├─ Success? → LearnFromExecution │ │ │
|
||||
│ │ │ ├─ Failure? → AnalyzeError │ │ │
|
||||
│ │ │ └─ DocumentDecision (all paths) │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ └─────────────────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ↕ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Memory Service (PostgreSQL + OpenSearch + Vault) │ │
|
||||
│ │ │ │
|
||||
│ │ ├─ L1 Knowledge: Task execution results │ │
|
||||
│ │ ├─ L2 Knowledge: Verified patterns & decisions │ │
|
||||
│ │ ├─ R (Reference): Docs, skill examples, guides │ │
|
||||
│ │ └─ Vault: Organized facts by tool/phase/domain │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Memory Service Activities Flow
|
||||
|
||||
```
|
||||
Workflow Step Execution → Memory Activities → Response
|
||||
|
||||
1. PRE-EXECUTION (Before step runs)
|
||||
┌─────────────────────────────────┐
|
||||
│ ExecuteGetContext Activity │
|
||||
│ ├─ Input: tool, task, budget │
|
||||
│ ├─ Retrieval: 3-tier │
|
||||
│ │ ├─ Tier 1: Signature match │
|
||||
│ │ │ (exact failure patterns) │
|
||||
│ │ ├─ Tier 2: Vector search │
|
||||
│ │ │ (learned from similar) │
|
||||
│ │ └─ Tier 3: References │
|
||||
│ │ (docs, skill guides) │
|
||||
│ └─ Returns: Lessons + Skills │
|
||||
└─────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────┐
|
||||
│ Prompt Optimization │
|
||||
│ ├─ Add context lessons │
|
||||
│ ├─ Inject skill examples │
|
||||
│ └─ Set budget constraints │
|
||||
└─────────────────────────────────┘
|
||||
|
||||
2. EXECUTION
|
||||
┌─────────────────────────────────┐
|
||||
│ Agent executes with context │
|
||||
│ (planner/judge/implementer) │
|
||||
└─────────────────────────────────┘
|
||||
|
||||
3. POST-EXECUTION (After step completes)
|
||||
┌─────────────────────────────────┐
|
||||
│ if SUCCESS: │
|
||||
│ ExecuteLearnFromExecution │
|
||||
│ ├─ taskID: step name │
|
||||
│ ├─ result: output │
|
||||
│ ├─ tags: [tool, phase] │
|
||||
│ └─ Returns: knowledgeID │
|
||||
├──────────────────────────────────┤
|
||||
│ if FAILURE: │
|
||||
│ ExecuteAnalyzeError │
|
||||
│ ├─ errorMsg: failure message │
|
||||
│ ├─ Returns: recovery steps │
|
||||
│ └─ (helps with retry) │
|
||||
├──────────────────────────────────┤
|
||||
│ ALWAYS: │
|
||||
│ ExecuteDocumentDecision │
|
||||
│ ├─ Type: phase milestone │
|
||||
│ ├─ Decision: action taken │
|
||||
│ └─ Reasoning: why chosen │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Skills and Context in State Machine
|
||||
|
||||
### Skill Types
|
||||
|
||||
**Tool Skills** (Skill Category 1):
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ Tool Skills (Executor capabilities) │
|
||||
├──────────────────────────────────────┤
|
||||
│ • planner-best-practices │ (T1.3: Plan generation)
|
||||
│ • judge-evaluation-patterns │ (T1.5: Rubric application)
|
||||
│ • implementer-code-patterns │ (T2.1: Code generation)
|
||||
│ • verifier-logic-chains │ (T1.4: Verification)
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Domain Skills** (Skill Category 2):
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ Domain Skills (Phase-specific) │
|
||||
├──────────────────────────────────────┤
|
||||
│ • T0: State machine kernel │ Event log, fork, rewind
|
||||
│ • T1: Workflow execution │ Attempt lifecycle, budgets
|
||||
│ • T2: Error recovery │ Crash matrix, checkpoints
|
||||
│ • T3: IR canonicalization │ YAML ↔ Rust equivalence
|
||||
│ • T4-T10: Specialization │ Phase-specific patterns
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Pattern Skills** (Skill Category 3):
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ Pattern Skills (Cross-cutting) │
|
||||
├──────────────────────────────────────┤
|
||||
│ • retry-strategy │ Exponential backoff
|
||||
│ • budget-tracking │ Token/attempt/time limits
|
||||
│ • composition-gates │ Phase completion criteria
|
||||
│ • schema-evolution │ Backward compatibility
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Context Hierarchy
|
||||
|
||||
```
|
||||
WorkflowContext (L0 - Always available)
|
||||
├─ WorkflowDef (IR + hash)
|
||||
├─ PhaseId (T0-T10)
|
||||
├─ StepId (current step)
|
||||
└─ AttemptState (attempt #, budget)
|
||||
├─ Attempt context (attempt-scoped)
|
||||
├─ Decision points (retry/abort)
|
||||
└─ Cost ledger (tokens spent)
|
||||
|
||||
TaskContext (L1 - Learned from execution)
|
||||
├─ Tool type (planner/judge/impl)
|
||||
├─ Execution results (input/output)
|
||||
├─ Failure patterns (error signatures)
|
||||
└─ Retry outcomes (success rates)
|
||||
|
||||
ReferenceContext (L2 - From vault)
|
||||
├─ Skill documentation
|
||||
├─ Best practices (YAML-level)
|
||||
├─ Code patterns (Rust-level)
|
||||
└─ Design rationale
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Activity Usage Per Phase
|
||||
|
||||
### Phase 0-2 (Kernel & Execution Foundation)
|
||||
|
||||
```
|
||||
T0: State Machine Kernel
|
||||
├─ GetContextActivity
|
||||
│ └─ Retrieve lessons on event log patterns
|
||||
├─ LearnFromExecutionActivity
|
||||
│ └─ Record fold/rewind operations
|
||||
└─ DocumentDecisionActivity
|
||||
└─ Track checkpointing decisions
|
||||
|
||||
T1: Attempt Lifecycle
|
||||
├─ GetContextActivity
|
||||
│ ├─ Tier 1: Known retry patterns
|
||||
│ └─ Tier 2: Attempt budget tracking
|
||||
├─ DiagnoseIssueActivity (on failure)
|
||||
│ ├─ Search for "budget exhausted" patterns
|
||||
│ └─ Find recovery step limits
|
||||
└─ LearnFromExecutionActivity
|
||||
└─ Record successful attempt patterns
|
||||
|
||||
T2: Error Recovery
|
||||
├─ GetContextActivity
|
||||
│ └─ Crash matrix lessons
|
||||
├─ AnalyzeErrorActivity
|
||||
│ ├─ Match against crash patterns
|
||||
│ └─ Return recovery procedure
|
||||
└─ DocumentDecisionActivity
|
||||
└─ Log recovery action chosen
|
||||
```
|
||||
|
||||
### Phase 3-5 (IR & Canonicalization)
|
||||
|
||||
```
|
||||
T3: WorkflowDef IR
|
||||
├─ GetContextActivity
|
||||
│ └─ Tier 1: Canonical hash failures
|
||||
├─ SearchKnowledgeActivity
|
||||
│ └─ "YAML builder equivalence" patterns
|
||||
└─ DocumentDecisionActivity
|
||||
└─ IR versioning decisions
|
||||
|
||||
T4: Schema Evolution
|
||||
├─ GetContextActivity
|
||||
│ └─ Backward compatibility lessons
|
||||
├─ DiagnoseIssueActivity
|
||||
│ └─ Upcaster failure diagnosis
|
||||
└─ LearnFromExecutionActivity
|
||||
└─ Schema migration successes
|
||||
|
||||
T5: Storage Abstraction
|
||||
├─ SearchKnowledgeActivity
|
||||
│ └─ DB migration patterns
|
||||
└─ DocumentDecisionActivity
|
||||
└─ Storage backend selection
|
||||
```
|
||||
|
||||
### Phase 6-8 (Orchestration & APIs)
|
||||
|
||||
```
|
||||
T6: Orchestrator
|
||||
├─ GetContextActivity
|
||||
│ ├─ Tool: orchestrator
|
||||
│ ├─ Task: workflow step dispatch
|
||||
│ └─ Returns: step ordering lessons
|
||||
├─ SearchKnowledgeActivity
|
||||
│ └─ Query ordering patterns
|
||||
└─ LearnFromExecutionActivity
|
||||
└─ Successful step sequences
|
||||
|
||||
T7: HTTP API
|
||||
├─ DiagnoseIssueActivity (on API error)
|
||||
│ └─ Match error codes to recovery
|
||||
└─ DocumentDecisionActivity
|
||||
└─ Rate limit/timeout decisions
|
||||
|
||||
T8: Observability
|
||||
├─ SearchKnowledgeActivity
|
||||
│ └─ Logging pattern queries
|
||||
└─ RefreshMemoryActivity
|
||||
└─ Periodic metric snapshots
|
||||
```
|
||||
|
||||
### Phase 9-10 (Delivery & Completion)
|
||||
|
||||
```
|
||||
T9: Deployment
|
||||
├─ GetContextActivity
|
||||
│ ├─ Tool: deployment executor
|
||||
│ └─ Task: artifact rollout
|
||||
├─ DiagnoseIssueActivity (on deployment failure)
|
||||
│ └─ Canary issues, rollback strategies
|
||||
└─ AnalyzeErrorActivity
|
||||
└─ Find deployment-specific solutions
|
||||
|
||||
T10: CLI & Metrics
|
||||
├─ SearchKnowledgeActivity
|
||||
│ └─ Transcript formatting patterns
|
||||
├─ LearnFromExecutionActivity
|
||||
│ └─ User interaction patterns
|
||||
└─ DocumentDecisionActivity
|
||||
└─ Metric collection decisions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prompt Optimization with Memory Context
|
||||
|
||||
### Before (Current)
|
||||
|
||||
```go
|
||||
prompt := fmt.Sprintf(`
|
||||
Execute step: %s
|
||||
Workflow: %s
|
||||
Budget: %d tokens
|
||||
|
||||
Task: %s
|
||||
`)
|
||||
```
|
||||
|
||||
### After (Memory-Optimized)
|
||||
|
||||
```go
|
||||
// 1. Get context from memory
|
||||
ctx, err := ExecuteGetContext(
|
||||
wfCtx,
|
||||
"planner", // tool type
|
||||
"T1.3-run-executor", // task name
|
||||
4096, // budget
|
||||
)
|
||||
if err != nil {
|
||||
log.Warn("memory unavailable, continue without context")
|
||||
ctx = nil
|
||||
}
|
||||
|
||||
// 2. Build prompt with lessons
|
||||
lessons := ""
|
||||
if ctx != nil && len(ctx.Lessons) > 0 {
|
||||
// Add tier-1 (signature matches)
|
||||
for _, lesson := range ctx.Lessons {
|
||||
if lesson.Tier == 1 {
|
||||
lessons += fmt.Sprintf("Known pattern: %s\n", lesson.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Inject skills
|
||||
skills := ""
|
||||
if ctx != nil && len(ctx.Skills) > 0 {
|
||||
for _, skill := range ctx.Skills {
|
||||
skills += fmt.Sprintf("Skill %s: %s\n", skill.Name, skill.Why)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Build optimized prompt
|
||||
prompt := fmt.Sprintf(`
|
||||
Execute step: %s
|
||||
Workflow: %s
|
||||
Budget: %d tokens
|
||||
|
||||
# Learned Patterns
|
||||
%s
|
||||
|
||||
# Skills to Apply
|
||||
%s
|
||||
|
||||
# Instructions
|
||||
%s
|
||||
`, stepName, workflowId, budget, lessons, skills, instructions)
|
||||
|
||||
// 5. Send to agent with enriched context
|
||||
response := agent.Execute(prompt)
|
||||
|
||||
// 6. Learn from result
|
||||
ExecuteLearnFromExecution(
|
||||
wfCtx,
|
||||
stepName,
|
||||
response.Text,
|
||||
[]string{"phase", "tool", "status"},
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Usage Summary
|
||||
|
||||
### Basic Tools
|
||||
|
||||
**Core Workflow Tools**:
|
||||
- `State Machine Events`: Insert events, compute state
|
||||
- `WorkflowDef Builder`: Create IR programmatically
|
||||
- `Run Executor`: Poll and execute steps
|
||||
- `Attempt Lifecycle`: Retry, checkpoint, rewind
|
||||
|
||||
**Testing Tools**:
|
||||
- `Harness`: Verification framework
|
||||
- `Integration Tests`: Phase composition gates
|
||||
- `Verify Script`: Assertion + diff runner
|
||||
|
||||
### Memory-Integrated Tools
|
||||
|
||||
**New with Memory Service**:
|
||||
- `ExecuteGetContext`: Retrieve 3-tier context
|
||||
- `ExecuteLearnFromExecution`: Capture task results
|
||||
- `ExecuteAnalyzeError`: Diagnosis on failure
|
||||
- `ExecuteDocumentDecision`: Log milestones
|
||||
- `ExecuteSearchKnowledge`: Find patterns
|
||||
- `ExecuteHealthCheck`: Verify service readiness
|
||||
|
||||
**Memory Vault Organization**:
|
||||
```
|
||||
vault/
|
||||
├─ tools/
|
||||
│ ├─ planner/
|
||||
│ │ └─ best-practices.md
|
||||
│ ├─ judge/
|
||||
│ │ └─ rubric-patterns.md
|
||||
│ └─ verifier/
|
||||
│ └─ logic-chains.md
|
||||
├─ phases/
|
||||
│ ├─ T0-kernel/
|
||||
│ ├─ T1-execution/
|
||||
│ └─ T2-recovery/
|
||||
├─ patterns/
|
||||
│ ├─ retry-strategies.md
|
||||
│ ├─ budget-tracking.md
|
||||
│ └─ error-signatures.md
|
||||
└─ skills/
|
||||
├─ schema-evolution.md
|
||||
├─ composition-gates.md
|
||||
└─ ir-canonicalization.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Machine Consumption Model
|
||||
|
||||
### Step Execution with Memory
|
||||
|
||||
```rust
|
||||
// In RunExecutor::execute_step()
|
||||
|
||||
fn execute_step(
|
||||
&self,
|
||||
workflow: &WorkflowDef,
|
||||
step: &StepId,
|
||||
attempt: &AttemptState,
|
||||
) -> Result<StepOutput> {
|
||||
// 1. Pre-execution: Retrieve context
|
||||
let context = self.memory_svc
|
||||
.retrieve_context(
|
||||
"tool_type", // planner/judge/implementer
|
||||
format!("{:?}", step), // step name
|
||||
attempt.budget.remaining_tokens,
|
||||
)
|
||||
.await
|
||||
.ok(); // Fail gracefully if memory unavailable
|
||||
|
||||
// 2. Optimize prompt with memory lessons
|
||||
let prompt = self.optimize_prompt(
|
||||
&workflow.def,
|
||||
step,
|
||||
context.as_ref(), // Lessons + skills
|
||||
);
|
||||
|
||||
// 3. Execute step with agent
|
||||
let output = self.model_provider.run(
|
||||
&self.model_id,
|
||||
&prompt,
|
||||
&attempt.budget,
|
||||
).await?;
|
||||
|
||||
// 4. Post-execution: Learn or diagnose
|
||||
if output.status == StepStatus::Success {
|
||||
self.memory_svc
|
||||
.learn_from_execution(
|
||||
format!("{:?}", step),
|
||||
output.text.clone(),
|
||||
vec!["tool", "phase"],
|
||||
)
|
||||
.await
|
||||
.ok(); // Non-blocking
|
||||
} else {
|
||||
self.memory_svc
|
||||
.analyze_error(
|
||||
&output.error_message,
|
||||
)
|
||||
.await
|
||||
.ok(); // Returns recovery suggestions
|
||||
}
|
||||
|
||||
// 5. Document decision
|
||||
self.memory_svc
|
||||
.document_decision(
|
||||
"step_completion",
|
||||
output.text.clone(),
|
||||
format!("Attempt {}", attempt.number),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
```
|
||||
|
||||
### Retry Policy Integration
|
||||
|
||||
```rust
|
||||
// In AttemptState::should_retry()
|
||||
|
||||
fn should_retry(&self, error: &Error) -> bool {
|
||||
// 1. Check budget first
|
||||
if self.budget.attempts_remaining == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Consult memory for pattern
|
||||
let recovery = self.memory_svc
|
||||
.analyze_error(&error.message)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// 3. If memory suggests retry strategy, use it
|
||||
if let Some(recovery_steps) = recovery {
|
||||
for step in recovery_steps {
|
||||
if step.level == "L1" { // High confidence
|
||||
return step.suggests_retry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fall back to default policy
|
||||
self.retry_policy.should_retry(self.number, error)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flow Diagram: Memory-Driven Lifecycle
|
||||
|
||||
```
|
||||
Workflow Initiated
|
||||
│
|
||||
↓
|
||||
┌───────────────┐
|
||||
│ Phase T0-T10 │
|
||||
└───────┬───────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
↓ ↓ ↓
|
||||
┌─────────────┐ ┌──────────┐ ┌─────────┐
|
||||
│ Get Context │ │ Execute │ │ Analyze │
|
||||
│ (Pre-exec) │ │ Step │ │ Result │
|
||||
└──────┬──────┘ └────┬─────┘ └────┬────┘
|
||||
│ │ │
|
||||
├─────────────→ │ (optimize) │
|
||||
│ │ │
|
||||
│ ┌──────────→│◄────────────┤
|
||||
│ │ ↓ │
|
||||
│ │ ┌─────────────┐ │
|
||||
│ │ │ Memory Tier │ │
|
||||
│ │ │ 1/2/3 │ │
|
||||
│ │ └─────────────┘ │
|
||||
│ │ │
|
||||
└───┴────────────────────────┴────→ Learn/Document
|
||||
│
|
||||
↓
|
||||
┌───────────────────┐
|
||||
│ Continue or Retry?│
|
||||
└─────┬─────────────┘
|
||||
│
|
||||
┌───────────┴────────────┐
|
||||
↓ ↓
|
||||
Next Step Attempt Retry
|
||||
│ (with memory
|
||||
│ guidance)
|
||||
│ │
|
||||
└──────────┬─────────────┘
|
||||
↓
|
||||
Phase Complete?
|
||||
│ │
|
||||
Yes ↓ No ↓
|
||||
│ Return to
|
||||
Composition Step Loop
|
||||
Gate
|
||||
│
|
||||
↓
|
||||
All Phases Done?
|
||||
│
|
||||
Yes ↓ No
|
||||
│ └─→ Next Phase
|
||||
Workflow
|
||||
Complete ──→ DocumentDecision
|
||||
(Final)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory-Skills Matrix
|
||||
|
||||
### Which Activities for Which Tools
|
||||
|
||||
```
|
||||
│ Planner │ Judge │ Impl │ Verifier │ Executor
|
||||
─────────┼─────────┼───────┼──────┼──────────┼─────────
|
||||
Create │ ✓ │ ✓ │ ✓ │ ✓ │ ✓
|
||||
Update │ ✓ │ ✓ │ ✓ │ ✓ │ ✓
|
||||
Search │ ✓ │ ✓ │ ✓ │ ✓ │ ✓
|
||||
Context │ ✓ │ ✓ │ ✓ │ ✓ │ ✓
|
||||
Learn │ ✓ │ ✓ │ ✓ │ ✓ │ ✓
|
||||
Diagnose │ ✓ │ ✓ │ ✓ │ ✓ │ ✓
|
||||
Document │ ✓ │ ✓ │ ✓ │ ✓ │ ✓
|
||||
Vault │ ✓ │ ✓ │ ✓ │ ✓ │ ✓
|
||||
Health │ ✓ │ ✓ │ ✓ │ ✓ │ ✓
|
||||
Analyze │ ✓ │ ✓ │ ✓ │ ✓ │ ✓
|
||||
```
|
||||
|
||||
### Context Availability by Phase
|
||||
|
||||
```
|
||||
Phase │ L0 (Workflow) │ L1 (Task) │ L2 (Reference)
|
||||
──────┼───────────────┼───────────┼────────────────
|
||||
T0-1 │ High │ Growing │ Available
|
||||
T2-3 │ High │ High │ High
|
||||
T4-6 │ High │ High │ Very High
|
||||
T7-10 │ High │ Very High│ Very High
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Phase 1: Integration (This Sprint)
|
||||
- ✅ Memory activities implemented (12 activities)
|
||||
- ✅ Temporal test suite passing (23/23 tests)
|
||||
- 🔄 Wire activities into RunExecutor
|
||||
- 🔄 Add memory pre/post-execution hooks
|
||||
- 🔄 Ingest skill YAML → memory vault
|
||||
|
||||
### Phase 2: Optimization (Next Sprint)
|
||||
- 🔄 Prompt optimization with context
|
||||
- 🔄 Retry policy enhancement via memory
|
||||
- 🔄 Budget tracking with learned limits
|
||||
- 🔄 Phase composition gate improvements
|
||||
|
||||
### Phase 3: Observability (2 Sprints)
|
||||
- 🔄 Memory usage metrics per phase
|
||||
- 🔄 Context relevance scoring
|
||||
- 🔄 Skill suggestion effectiveness tracking
|
||||
- 🔄 Orchestrator dashboard with memory stats
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Memory-driven architecture enables Poimen to:
|
||||
|
||||
1. **Learn** from every execution (Tier 1 knowledge)
|
||||
2. **Improve** prompts with context (Tier 2/3 lessons)
|
||||
3. **Recover** from failures faster (diagnose + suggest)
|
||||
4. **Document** decisions for compliance (audit trail)
|
||||
5. **Organize** skills and patterns (vault by domain)
|
||||
6. **Scale** across phases (cross-phase pattern reuse)
|
||||
|
||||
The state machine becomes a **learning system**, not just an executor—every run improves future runs.
|
||||
@@ -1,373 +0,0 @@
|
||||
# Poimen Memory Service Integration
|
||||
|
||||
## Overview
|
||||
|
||||
Poimen workflows now integrate with the **Poimen Memory Service** for:
|
||||
- ✅ **Create** knowledge records (L1/L2/reference)
|
||||
- ✅ **Update** existing knowledge
|
||||
- ✅ **Retrieve** knowledge via hybrid search
|
||||
- ✅ **Context** retrieval (three-tier: signature → vector → reference)
|
||||
|
||||
Package: `internal/memory` → 4 files, 15+ tests, 100% passing
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Workflow Activity
|
||||
↓
|
||||
Service (high-level)
|
||||
↓
|
||||
Client (low-level HTTP)
|
||||
↓
|
||||
Memory Service API (remote)
|
||||
├─ POST /memory/ingest (create knowledge)
|
||||
├─ POST /memory/query (search)
|
||||
├─ POST /memory/context (three-tier retrieval)
|
||||
├─ GET /memory/vault (browse)
|
||||
└─ GET /health (health check)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Import
|
||||
|
||||
```go
|
||||
import "github.com/rockliang/poimen/workflows/internal/memory"
|
||||
```
|
||||
|
||||
### 2. Create Service
|
||||
|
||||
```go
|
||||
svc := memory.NewService(
|
||||
"http://memory-service.poimen.svc.cluster.local:8080",
|
||||
"jwt-token-from-env",
|
||||
"poimen", // project
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Create Knowledge
|
||||
|
||||
```go
|
||||
id, err := svc.CreateKnowledge(ctx, &memory.KnowledgeRecord{
|
||||
Level: "L1",
|
||||
Content: "Pod debugging: kubectl logs <pod>",
|
||||
Source: "workflow://task-123",
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Search Knowledge
|
||||
|
||||
```go
|
||||
records, err := svc.RetrieveKnowledge(ctx, "pod debugging", nil)
|
||||
for _, rec := range records {
|
||||
fmt.Println(rec.Content)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Get Context
|
||||
|
||||
```go
|
||||
svcCtx, err := svc.RetrieveContext(ctx, "kubectl", "debug-pod", 8192)
|
||||
for _, lesson := range svcCtx.Lessons {
|
||||
fmt.Println(lesson.Text)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files Added
|
||||
|
||||
```
|
||||
internal/memory/
|
||||
├── client.go (HTTP client, 250 lines)
|
||||
├── client_test.go (6 tests)
|
||||
├── service.go (High-level API, 180 lines)
|
||||
├── service_test.go (5 tests)
|
||||
├── example_activity.go (Workflow integration examples)
|
||||
└── README.md (Full API docs)
|
||||
```
|
||||
|
||||
### File Purposes
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `client.go` | Low-level HTTP client for memory API endpoints |
|
||||
| `service.go` | High-level wrapper with project-scoped operations |
|
||||
| `example_activity.go` | Temporal workflow activity examples |
|
||||
| `client_test.go` | Client unit tests (mock HTTP server) |
|
||||
| `service_test.go` | Service unit tests |
|
||||
| `README.md` | Complete API reference + examples |
|
||||
|
||||
---
|
||||
|
||||
## Test Results
|
||||
|
||||
```
|
||||
✅ TestClientIngest (Create)
|
||||
✅ TestClientQuery (Search)
|
||||
✅ TestClientContext (Three-tier retrieval)
|
||||
✅ TestClientVault (Browse)
|
||||
✅ TestClientHealth (Health check)
|
||||
✅ TestServiceCreateKnowledge
|
||||
✅ TestServiceRetrieveKnowledge
|
||||
✅ TestServiceRetrieveContext
|
||||
✅ TestServiceGetVault
|
||||
✅ TestServiceIsHealthy
|
||||
✅ TestServiceUpdateKnowledge
|
||||
|
||||
PASS: 11/11 tests (0.317s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints Covered
|
||||
|
||||
| Endpoint | Method | Wrapper | Status |
|
||||
|----------|--------|---------|--------|
|
||||
| `/memory/ingest` | POST | `CreateKnowledge()` | ✅ Implemented |
|
||||
| `/memory/query` | POST | `RetrieveKnowledge()` | ✅ Implemented |
|
||||
| `/memory/context` | POST | `RetrieveContext()` | ✅ Implemented |
|
||||
| `/memory/vault` | GET | `GetVault()` | ✅ Implemented |
|
||||
| `/health` | GET | `IsHealthy()` | ✅ Implemented |
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Learn from Task Execution
|
||||
|
||||
```go
|
||||
// In Temporal workflow/activity:
|
||||
result := executeTask()
|
||||
id, err := svc.CreateKnowledge(ctx, &memory.KnowledgeRecord{
|
||||
Level: "L1",
|
||||
Title: "Task Result",
|
||||
Content: result,
|
||||
Source: "workflow://task-id",
|
||||
})
|
||||
```
|
||||
|
||||
### Example 2: Diagnose Issue
|
||||
|
||||
```go
|
||||
// Retrieve context for debugging
|
||||
svcCtx, err := svc.RetrieveContext(ctx, "kubectl", "pod-crash", 8192)
|
||||
for _, lesson := range svcCtx.Lessons {
|
||||
fmt.Printf("Tier %d: %s\n", lesson.Tier, lesson.Text)
|
||||
}
|
||||
for _, skill := range svcCtx.Skills {
|
||||
fmt.Printf("Skill: %s\n", skill.Name)
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Search Knowledge
|
||||
|
||||
```go
|
||||
records, err := svc.RetrieveKnowledge(ctx, "kubernetes debugging", &memory.RetrievalOptions{
|
||||
Limit: 10,
|
||||
LevelFilter: []string{"L1", "L2"},
|
||||
Floor: 0.7, // Minimum relevance
|
||||
})
|
||||
```
|
||||
|
||||
### Example 4: Update Knowledge
|
||||
|
||||
```go
|
||||
_, err := svc.UpdateKnowledge(ctx, &memory.KnowledgeRecord{
|
||||
ID: "chunk-123",
|
||||
Level: "L2",
|
||||
Content: "Updated facts...",
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow Integration Pattern
|
||||
|
||||
### Pattern 1: Learning Workflow
|
||||
|
||||
```go
|
||||
type LearnWorkflow struct {
|
||||
MemoryService *memory.Service
|
||||
}
|
||||
|
||||
func (w *LearnWorkflow) Run(ctx context.Context, task string) error {
|
||||
// Execute task
|
||||
result, err := executeTask(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Learn from result
|
||||
_, err = w.MemoryService.CreateKnowledge(ctx, &memory.KnowledgeRecord{
|
||||
Content: result,
|
||||
Source: "workflow://learn/" + task,
|
||||
})
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Diagnostic Workflow
|
||||
|
||||
```go
|
||||
func (w *Workflow) Diagnose(ctx context.Context, tool, issue string) error {
|
||||
// Retrieve context
|
||||
svcCtx, err := w.MemoryService.RetrieveContext(ctx, tool, issue, 8192)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use best lesson (tier-1 has highest confidence)
|
||||
if len(svcCtx.Lessons) > 0 {
|
||||
lesson := svcCtx.Lessons[0]
|
||||
fmt.Printf("Recommended action: %s\n", lesson.Text)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Search-Based Workflow
|
||||
|
||||
```go
|
||||
func (w *Workflow) SearchAndApply(ctx context.Context, query string) error {
|
||||
records, err := w.MemoryService.RetrieveKnowledge(ctx, query, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range records {
|
||||
if rec.Level == "L1" { // High confidence
|
||||
applyKnowledge(rec.Content)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Memory service endpoint
|
||||
MEMORY_SERVICE_URL=http://memory-service.poimen.svc.cluster.local:8080
|
||||
|
||||
# JWT token (from Authentik)
|
||||
MEMORY_SERVICE_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGc...
|
||||
|
||||
# Project name
|
||||
MEMORY_PROJECT=poimen
|
||||
```
|
||||
|
||||
### Initialization
|
||||
|
||||
```go
|
||||
// From environment
|
||||
svc := memory.NewService(
|
||||
os.Getenv("MEMORY_SERVICE_URL"),
|
||||
os.Getenv("MEMORY_SERVICE_TOKEN"),
|
||||
os.Getenv("MEMORY_PROJECT"),
|
||||
)
|
||||
|
||||
// Or hardcoded (for testing)
|
||||
svc := memory.NewService(
|
||||
"http://localhost:8080",
|
||||
"test-token",
|
||||
"poimen",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
Common errors:
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| 401 Unauthorized | Invalid/missing JWT | Check token in env |
|
||||
| 403 Forbidden | Token lacks capability | Ensure token has `memory:read`/`memory:write` |
|
||||
| 429 Too Many Requests | Rate limit exceeded | Implement backoff |
|
||||
| 503 Service Unavailable | Memory service down | Retry with exponential backoff |
|
||||
| Timeout | Slow network/remote | Increase timeout or retry |
|
||||
|
||||
Example with retry:
|
||||
|
||||
```go
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
resp, err := svc.RetrieveKnowledge(ctx, query, nil)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
lastErr = err
|
||||
time.Sleep(time.Duration(math.Pow(2, float64(attempt))) * time.Second)
|
||||
}
|
||||
return nil, lastErr
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- **Query**: ~150ms (hybrid search)
|
||||
- **Context**: ~200ms (three-tier retrieval)
|
||||
- **Ingest**: ~10ms (sync), async processing
|
||||
- **Vault**: ~50ms (file listing)
|
||||
|
||||
Rate limits:
|
||||
- Ingest: 100/hour
|
||||
- Query: 1000/hour
|
||||
- Context: 100/hour
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
cd ~/workplace/Poimen/workflows
|
||||
go test ./internal/memory -v
|
||||
```
|
||||
|
||||
### Mock Integration
|
||||
|
||||
Tests use `httptest.NewServer` for mocking. Example:
|
||||
|
||||
```go
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(QueryResponse{...})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := memory.NewClient(server.URL, "test-token")
|
||||
resp, _ := client.Query(context.Background(), &QueryRequest{...})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Add to Temporal activities**: Integrate into workflow activities
|
||||
2. **Configure JWT token**: Set env var in deployment
|
||||
3. **Add error handling**: Implement retry logic
|
||||
4. **Monitor usage**: Track API calls, response times
|
||||
5. **Extend patterns**: Add domain-specific activities
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Memory service API: `~/workplace/Poimen/memory/CLAUDE.md`
|
||||
- Package API docs: `internal/memory/README.md`
|
||||
- Example activities: `internal/memory/example_activity.go`
|
||||
@@ -1,337 +0,0 @@
|
||||
# Multi-Agent Dev Orchestrator (Temporal + Go) — Implementation Plan
|
||||
|
||||
See the full design doc at `/Users/rockliang/.claude/plans/considered-u-are-a-curried-reef.md`.
|
||||
|
||||
## Quick Summary
|
||||
|
||||
Build a Temporal-based orchestrator that drives multi-agent software dev work on arbitrary target repos. Three roles (Planner/reasoning, Judge/reasoning, Implementer/cheaper model) collaborate on hierarchical tasks (`T0` milestone split into `T0.1`-`T0.9` subtasks). Orchestrator owns config (system prompt, skills, activity timeouts/retries), live-updatable via signals. All code runs on shared FS where target repo sits; git concurrency handled via worktrees + advisory lock. System testable with mocked activities + real end-to-end against `temporal.riotpiao.com`.
|
||||
|
||||
## Implementation Track
|
||||
|
||||
**Milestone T0**: Nine subtasks, each with its own verification gate = completion criterion.
|
||||
|
||||
| Task | Scope | Status |
|
||||
|---|---|---|
|
||||
| [T0.1](#t01-repo-scaffold) | Repo scaffold: `go.mod`, `statemachine/`, `action/`, `cmd/`, `prompts/`, `internal/`, `tests/` | [ ] |
|
||||
| [T0.2](#t02-shared-types) | Shared types: `ModelSpec`, `PromptSpec`, `OrchestratorConfig`, `ActivityTuning`, `PiRetryPolicy` | [ ] |
|
||||
| [T0.3](#t03-git-and-locking) | Git & locking: `CloneRepoActivity`, worktrees, squash-merge, `orchestrator.lock` | [ ] |
|
||||
| [T0.4](#t04-pi-and-error-classification) | `PrepareSkillsActivity`, `classifyPiErr` (4xx/5xx/504), stream timeout learning | [ ] |
|
||||
| [T0.5](#t05-llm-agents-and-prompts) | Planner/Judge/Implementer activities, LLM client, prompt templates + live customization | [ ] |
|
||||
| [T0.6](#t06-taskunit-workflow) | TaskUnit workflow: retry loops, timeout escalation, lessons injection | [ ] |
|
||||
| [T0.7](#t07-orchestrator-workflow) | Orchestrator workflow: config state, signals, fan-out/fan-in, `continue-as-new`, 504 learning | [ ] |
|
||||
| [T0.8](#t08-worker-and-starter) | Worker & starter CLIs, env/config loading, Temporal registration | [ ] |
|
||||
| [T0.9](#t09-end-to-end) | Full e2e against real cluster + scratch repo: all 7 verification items | [ ] |
|
||||
|
||||
---
|
||||
|
||||
### T0.1: Repo Scaffold
|
||||
|
||||
Create directory structure, `go.mod`, empty stubs.
|
||||
|
||||
**Verification:** `go build ./...` succeeds; layout matches plan.
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
```
|
||||
/go.mod
|
||||
/cmd/worker/main.go
|
||||
/cmd/starter/main.go
|
||||
/statemachine/types.go types.go signals.go orchestrator.go taskunit.go
|
||||
/action/planner.go implementer.go judge.go git.go skills.go integration_test.go lessons.go llm/client.go
|
||||
/prompts/registry.go planner/default.tmpl judge/default.tmpl implementer/default.tmpl
|
||||
/internal/config/config.go lock/flock.go
|
||||
/tests/taskunit_workflow_test.go orchestrator_workflow_test.go
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### T0.2: Shared Types
|
||||
|
||||
Implement `statemachine/types.go` with all config/input/output structs. Document defaults.
|
||||
|
||||
**Verification:** Unit test asserts all defaults (5m / 2s / 30s / 2.0 / 30s stream / 2m stream-max).
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
- `ModelSpec`: ModelID, Thinking, Effort
|
||||
- `PromptSpec`: TemplateRef, RawTemplate, Variables, Model, LessonsRef
|
||||
- `OrchestratorInput`, `OrchestratorOutput`
|
||||
- `TaskUnitInput`, `TaskUnitOutput`
|
||||
- `ActivityTuning`: ImplementerBaseTimeout, ImplementerMaxRetries, JudgeTimeout, PiRetry
|
||||
- `PiRetryPolicy`: ScheduleToCloseTimeout (5m), InitialInterval (2s), MaximumInterval (30s), BackoffCoefficient (2.0), StreamTimeout (30s), StreamTimeoutMax (2m)
|
||||
- `OrchestratorConfig`: SystemPrompt, Skills, RolePrompts, Tuning
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### T0.3: Git & Locking
|
||||
|
||||
Implement `action/git.go` + `internal/lock/flock.go`.
|
||||
|
||||
**Verification:** Test against local scratch repo: clone-if-empty vs fetch-if-exists, worktree lifecycle (add/commit/remove), squash-merge produces exactly one commit on main.
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
**Activities:**
|
||||
- `CloneRepoActivity(ctx, {RemoteURL, TargetRepoPath}) error` — idempotent `git clone` or `git fetch`
|
||||
- `GitWorktreeAddActivity(ctx, {RepoPath, TaskID}) (string, error)` — returns worktree path, guarded by lock
|
||||
- `GitCommitActivity(ctx, {WorktreePath, Message}) error` — commits in worktree (no lock needed)
|
||||
- `GitPushActivity(ctx, {RepoPath}) error` — guarded by lock
|
||||
- `GitSquashMergeActivity(ctx, {RepoPath, Branches, Message}) error` — guarded by lock
|
||||
|
||||
**Lock helper (`internal/lock/`):**
|
||||
- `Lock(path string) error`, `Unlock(path string) error` using `golang.org/x/sys/unix.Flock` or `fcntl` equivalent
|
||||
|
||||
**Squash-merge sequence:**
|
||||
```
|
||||
fetch origin main
|
||||
checkout main && pull --ff-only origin main
|
||||
for b in branches:
|
||||
merge --squash $b
|
||||
commit -m "T0: squash merge subtasks..."
|
||||
push origin main
|
||||
for b in branches:
|
||||
worktree remove worktrees/$id --force
|
||||
branch -D $b
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### T0.4: Pi & Error Classification
|
||||
|
||||
Implement `action/skills.go` with `PrepareSkillsActivity` and `classifyPiErr`.
|
||||
|
||||
**Verification:** Unit test all three error buckets (4xx/5xx/504) against a mocked `pi` HTTP client.
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
**`PrepareSkillsActivity`:**
|
||||
- Input: `{Skills []SkillRef, StreamTimeout time.Duration}`
|
||||
- For each skill, `pi clone-or-fetch <skill-url>` (idempotent)
|
||||
- Each skill guarded by its own lock
|
||||
|
||||
**Error classification:**
|
||||
```go
|
||||
func classifyPiErr(err error) error {
|
||||
// 4xx -> NonRetryableApplicationError "PiClientError"
|
||||
// 504 -> ApplicationError "PiStreamTimeout"
|
||||
// others -> retryable
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### T0.5: LLM Agents & Prompts
|
||||
|
||||
Implement LLM activities + prompt templates.
|
||||
|
||||
**Verification:** Unit test renders a `PromptSpec` (system prompt + template override + raw template) and calls mock Anthropic client.
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
**Activities:**
|
||||
- `PlanningActivity(ctx, {OrchestratorConfig, BoardState}) (TaskDispatch, error)` — reads board/INDEX.md, calls Planner
|
||||
- `ImplementerActivity(ctx, {PromptSpec, WortkreeePath, Lessons}) (ImplementOutput, error)` — tool-call agent loop
|
||||
- `JudgeActivity(ctx, {PromptSpec, Diff, IntegrationTestResult}) (Verdict, Critique, error)` — reviews correctness
|
||||
- `RunIntegrationTestActivity(ctx, {WortkreeePath, TestCmd}) (pass/fail, logs, error)` — shells out
|
||||
|
||||
**Prompt templates:**
|
||||
- `planner/default.tmpl`: expects `{{.SystemPrompt}}`, `{{.TaskBoard}}`, etc.
|
||||
- `judge/default.tmpl`: expects `{{.SystemPrompt}}`, `{{.Diff}}`, `{{.TestResult}}`
|
||||
- `implementer/default.tmpl`: expects `{{.SystemPrompt}}`, `{{.Task}}`, `{{.Lessons}}`
|
||||
|
||||
**`prompts/registry.go`:**
|
||||
- `go:embed prompts/*.tmpl`
|
||||
- `Render(templateRef string, variables map[string]any) (string, error)`
|
||||
|
||||
**`action/llm/client.go`:**
|
||||
- Thin Anthropic client wrapper
|
||||
- Read `ANTHROPIC_API_KEY` from env
|
||||
- Call `messages.Create` with model/thinking/effort from `ModelSpec`
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### T0.6: TaskUnit Workflow
|
||||
|
||||
Implement `statemachine/taskunit.go` with retry loops & timeout escalation.
|
||||
|
||||
**Verification (Testsuite):**
|
||||
- Pass-first-try
|
||||
- Fail-then-pass-after-lesson-injection
|
||||
- Retries-exhausted
|
||||
- Timeout-escalation (both judges)
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
**Flow:**
|
||||
1. `GitWorktreeAddActivity` → get isolated working tree
|
||||
2. Retry loop:
|
||||
- Track `timeoutAttempt`, `judgeAttempt` separately
|
||||
- `ImplementerActivity` with timeout = `BaseTimeout * timeoutAttempt`
|
||||
- If timeout, increment `timeoutAttempt` and retry (duration grows)
|
||||
- If success, call `RunIntegrationTestActivity`
|
||||
- Call `JudgeActivity`
|
||||
- If judge pass, commit in worktree and return
|
||||
- If judge fail, append to lessons, increment `judgeAttempt`, retry (lessons injected next time)
|
||||
- If retries exhausted, return fail verdict to orchestrator
|
||||
|
||||
**Key detail:** HeartbeatTimeout = (BaseTimeout * timeoutAttempt) / 4, scales with escalation.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### T0.7: Orchestrator Workflow
|
||||
|
||||
Implement `statemachine/orchestrator.go` with config state, signals, fan-out/fan-in, `continue-as-new`, 504 learning.
|
||||
|
||||
**Verification (Testsuite):**
|
||||
- Fan-out/fan-in correctness
|
||||
- Squash-merge triggers on submilestone complete
|
||||
- `continue-as-new` at cycle cap, carries `OrchestratorConfig` forward
|
||||
- `update-*` signals mutate config without touching in-flight TaskUnit
|
||||
- `PiStreamTimeout` doubles `StreamTimeout` and persists it
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
**Per-cycle logic:**
|
||||
1. If `config.Skills` changed, call `PrepareSkillsActivity` once (wraps it for 504 learning)
|
||||
2. Call `PlanningActivity` → get dispatch decision
|
||||
3. Fan out: `workflow.ExecuteChildWorkflow(TaskUnitWorkflow, ...)` for each dispatched T0.x
|
||||
4. Await all via `workflow.Selector`
|
||||
5. Call `PlanningActivity` again to update board + commit + push
|
||||
6. If submilestone complete, call `GitSquashMergeActivity`
|
||||
7. Increment cycle count
|
||||
8. If cycle count >= cap, `workflow.NewContinueAsNewError(ctx, ..., nextInput)`
|
||||
|
||||
**Signal handlers:**
|
||||
- `pause`, `resume`: gate the cycle loop
|
||||
- `abort-task(taskID)`: forward via `SignalExternalWorkflow` to TaskUnit
|
||||
- `inject-lesson`: append to lessons store
|
||||
- `update-system-prompt`, `update-skills`, `update-role-prompt`, `update-tuning`: mutate `config.*`
|
||||
|
||||
**504 learning wrapper (pseudo-code):**
|
||||
```go
|
||||
for {
|
||||
r := config.Tuning.PiRetry
|
||||
err := ExecuteActivity(..., PrepareSkillsActivity, Input{...StreamTimeout: r.StreamTimeout})
|
||||
if isPiStreamTimeout(err) && r.StreamTimeout < r.StreamTimeoutMax {
|
||||
config.Tuning.PiRetry.StreamTimeout *= 2
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### T0.8: Worker & Starter CLIs
|
||||
|
||||
Implement `cmd/worker/main.go` and `cmd/starter/main.go`.
|
||||
|
||||
**Verification:**
|
||||
- `go run ./cmd/worker` connects to `temporal.riotpiao.com:7233` without error
|
||||
- `go run ./cmd/starter --dry-run` starts a workflow that appears in Temporal Web UI
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
**`cmd/worker/main.go`:**
|
||||
```go
|
||||
config := loadConfig() // reads env: TEMPORAL_NAMESPACE, TEMPORAL_TLS_CERT, TEMPORAL_TLS_KEY, ANTHROPIC_API_KEY
|
||||
c, err := client.Dial(client.Options{HostPort: "temporal.riotpiao.com:7233", ...TLS...})
|
||||
w, err := worker.New(c, "default", worker.Options{})
|
||||
// register both workflows
|
||||
w.RegisterWorkflow(statemachine.OrchestratorWorkflow)
|
||||
w.RegisterWorkflow(statemachine.TaskUnitWorkflow)
|
||||
// register all activities
|
||||
w.RegisterActivity(action.CloneRepoActivity)
|
||||
w.RegisterActivity(action.GitWorktreeAddActivity)
|
||||
// ... etc
|
||||
w.Run()
|
||||
```
|
||||
|
||||
**`cmd/starter/main.go`:**
|
||||
```go
|
||||
flag.String("repo", "", "target repo path")
|
||||
flag.String("remote", "", "remote URL")
|
||||
flag.String("milestone", "T0", "milestone ID")
|
||||
flag.Bool("dry-run", false, "disable git push/merge")
|
||||
flag.String("planner-model", "claude-opus-5", "planner model ID")
|
||||
// ... judge, implementer models
|
||||
// build OrchestratorInput, call client.ExecuteWorkflow
|
||||
```
|
||||
|
||||
**`internal/config/config.go`:**
|
||||
- Load Temporal settings from env
|
||||
- Load ANTHROPIC_API_KEY from env
|
||||
- Return filled config struct
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### T0.9: End-to-End Test
|
||||
|
||||
Run against real `temporal.riotpiao.com` + disposable scratch repo.
|
||||
|
||||
**Verification (all 7 items in the plan):**
|
||||
1. Clone bootstrap: fresh clone when repo path empty
|
||||
2. Full cycle: dispatch subtasks, judge pass/fail, commit, squash-merge
|
||||
3. Live signal updates: change prompt/skills mid-run, next dispatch sees them
|
||||
4. 5xx retry-then-succeed + always-503 exhausts at 5m mark
|
||||
5. 504 stream-timeout learning: doubles and is actually used, capped at max
|
||||
6. `continue-as-new` history bounded
|
||||
7. Squash-merge result: main has one squashed commit per submilestone
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
**Fixture repo structure:**
|
||||
```
|
||||
tasks/
|
||||
INDEX.md (guidelines)
|
||||
board.json (task list, T0.1-T0.3 with trivial definitions)
|
||||
```
|
||||
|
||||
**Example subtask:** "Create file `output.txt` with content 'hello world'"
|
||||
|
||||
**Run sequence:**
|
||||
1. `go run ./cmd/starter --repo /tmp/fixture --remote [email protected]:scratch/workflow-test.git --dry-run`
|
||||
2. Monitor Temporal Web UI for workflow progress
|
||||
3. Midway, send signals: `temporal workflow signal --workflow-id orch-... --name update-role-prompt ...`
|
||||
4. Confirm next dispatch uses new prompt (assert marker in output file)
|
||||
5. Confirm board updated, lessons file exists (if any failure happened)
|
||||
6. Remove `--dry-run`, repeat against real remote
|
||||
7. Assert final state: real commits on remote, squash-merge on main
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Approve this scaffold (PLAN.md + tasks/INDEX.md + board)
|
||||
2. Start T0.1 → checkout branch `task/T0.1` → scaffold repo structure
|
||||
3. Each task: implement, test locally, verify against criterion
|
||||
4. Mark on board: [x] when verification passes
|
||||
5. T0.9: final e2e run
|
||||
6. Squash all T0.* branches into main, push
|
||||
@@ -1,170 +0,0 @@
|
||||
# Poimen Routing Workflow - Implementation Progress
|
||||
|
||||
**Last Updated**: August 31, 2025
|
||||
**Current Phase**: Phase 1: Foundation
|
||||
**Overall Progress**: 1 of 27 tasks complete (3.7%)
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: FOUNDATION (8-10 hours)
|
||||
|
||||
### Task 1.1: Create Go Type Definitions ✅ COMPLETE
|
||||
|
||||
**Status**: COMPLETE
|
||||
**Completed**: 2025-08-31
|
||||
**Hours Used**: 2 hours
|
||||
**Effort Estimate**: 2 hours
|
||||
|
||||
**Deliverables**:
|
||||
- ✅ `internal/routing/types.go` (159 lines)
|
||||
- WorkflowSpec (one-time workflows)
|
||||
- CronWorkflowSpec (scheduled workflows)
|
||||
- State (Task/Pass/Fail)
|
||||
- RetryPolicy, CatchClause
|
||||
- ExecutionContext, ExecutionEvent
|
||||
- PollParams, PollResult, Result
|
||||
- ActivityMetadata, InputField, OutputField
|
||||
- Constraints, Heartbeat
|
||||
|
||||
- ✅ `internal/routing/types_test.go` (286 lines)
|
||||
- 8 comprehensive unit tests
|
||||
- JSON marshaling/unmarshaling
|
||||
- Complex workflow scenarios
|
||||
- All tests PASS ✅
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- ✅ All types compile without errors
|
||||
- ✅ JSON marshaling/unmarshaling works correctly
|
||||
- ✅ Unit tests pass (9/9 PASS)
|
||||
- ✅ Ready for next phase
|
||||
|
||||
**Commit**: `05a150b1` - feat(routing): implement WorkflowSpec and CronWorkflowSpec types
|
||||
|
||||
**Notes**: Types are solid and flexible. Ready to proceed with Task 1.2 (ActivityKnowledgeBase.json)
|
||||
|
||||
---
|
||||
|
||||
### Task 1.2: Create ActivityKnowledgeBase.json ⏳ TODO
|
||||
|
||||
**Status**: TODO
|
||||
**Effort Estimate**: 3 hours
|
||||
**Blocker**: None
|
||||
|
||||
**What to build**:
|
||||
- ActivityKnowledgeBase.json with ~7-10 activities
|
||||
- Each activity: name, description, category, inputs, outputs, constraints
|
||||
- Include both flaky (retry 3x) and stable (retry 1x) activities
|
||||
|
||||
---
|
||||
|
||||
### Task 1.3: Create ActivityKnowledgeBase Loader ⏳ TODO
|
||||
|
||||
**Status**: TODO
|
||||
**Effort Estimate**: 2 hours
|
||||
**Blocker**: Depends on Task 1.2
|
||||
|
||||
---
|
||||
|
||||
### Task 1.4: Create WorkflowSpec Validator ⏳ TODO
|
||||
|
||||
**Status**: TODO
|
||||
**Effort Estimate**: 3 hours
|
||||
**Blocker**: Depends on Task 1.1 ✅
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2: LLM-ROUTER (12-15 hours)
|
||||
|
||||
### Task 2.1-2.5 ⏳ TODO
|
||||
|
||||
**Status**: All TODO
|
||||
**Blocker**: Waiting on Phase 1 completion
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3: ROUTINGWORKFLOW (15-18 hours)
|
||||
|
||||
### Task 3.1-3.6 ⏳ TODO
|
||||
|
||||
**Status**: All TODO
|
||||
**Blocker**: Waiting on Phase 1 & 2 completion
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4: API/CLI (12-15 hours)
|
||||
|
||||
### Task 4.1-4.4 ⏳ TODO
|
||||
|
||||
**Status**: All TODO
|
||||
**Blocker**: Waiting on Phase 3 completion
|
||||
|
||||
---
|
||||
|
||||
## PHASE 5: TESTING (8-12 hours)
|
||||
|
||||
### Task 5.1-5.4 ⏳ TODO
|
||||
|
||||
**Status**: All TODO
|
||||
**Blocker**: Waiting on Phase 4 completion
|
||||
|
||||
---
|
||||
|
||||
## PHASE 6: DOCUMENTATION (5-8 hours)
|
||||
|
||||
### Task 6.1-6.4 ⏳ TODO
|
||||
|
||||
**Status**: All TODO
|
||||
**Blocker**: Waiting on Phase 5 completion
|
||||
|
||||
---
|
||||
|
||||
## SUMMARY
|
||||
|
||||
**Completed**: 1/27 tasks (3.7%)
|
||||
**In Progress**: 0 tasks
|
||||
**Blocked**: 0 tasks
|
||||
**Remaining**: 26 tasks (96.3%)
|
||||
|
||||
**Phase 1 Completion**: 25% (1 of 4 tasks done)
|
||||
**Estimated Time to Phase 1 Done**: 6-8 hours (by tomorrow)
|
||||
**Estimated Time to All Done**: 60-70 hours (3-4 weeks)
|
||||
|
||||
**Current Velocity**: 2 hours/task
|
||||
**Est. Daily Capacity**: 8 hours/day
|
||||
**Est. Days to Completion**: 8-10 days (assuming 1 engineer, 8h/day)
|
||||
|
||||
---
|
||||
|
||||
## NEXT IMMEDIATE TASKS
|
||||
|
||||
**Priority 1** (TODAY if possible):
|
||||
- [ ] Task 1.2: Create ActivityKnowledgeBase.json (3h)
|
||||
- [ ] Task 1.3: Create KB Loader (2h)
|
||||
|
||||
**Priority 2** (TOMORROW):
|
||||
- [ ] Task 1.4: Create Validator (3h)
|
||||
- [ ] Phase 1 sign-off complete
|
||||
|
||||
**Priority 3** (BEGIN Phase 2):
|
||||
- [ ] Task 2.1: JSONPath Resolver (3h)
|
||||
- [ ] Task 2.2: llm-router skeleton (2h)
|
||||
|
||||
---
|
||||
|
||||
## BLOCKERS & NOTES
|
||||
|
||||
None currently. Types are solid and ready for next phase.
|
||||
|
||||
---
|
||||
|
||||
## COMMITS THIS SESSION
|
||||
|
||||
| Commit | Message | Files |
|
||||
|--------|---------|-------|
|
||||
| 05a150b1 | feat(routing): implement WorkflowSpec and CronWorkflowSpec types | types.go, types_test.go |
|
||||
|
||||
---
|
||||
|
||||
**Status Indicator**:
|
||||
🟢 ON TRACK - Phase 1 started, Task 1.1 complete, no blockers
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
# Poimen Routing Workflow - Complete Implementation Guide
|
||||
|
||||
## 📚 DOCUMENTATION STRUCTURE
|
||||
|
||||
You now have **4 complete documents** that form a complete specification:
|
||||
|
||||
### 1. `ROUTING_WORKFLOW_SPEC.md` (30+ KB)
|
||||
**The Technical Specification** - Everything about the system design
|
||||
|
||||
Contains:
|
||||
- ActivityKnowledgeBase.json format
|
||||
- llm-router Activity (intelligent workflow generator)
|
||||
- RoutingWorkflow (generic executor)
|
||||
- Go type definitions (copy-paste ready)
|
||||
- Implementation architecture
|
||||
- State types (Task, Pass, Fail)
|
||||
- CronWorkflowSpec (scheduled workflows)
|
||||
- Execution flow examples
|
||||
- Cron syntax reference
|
||||
|
||||
**When to use**: Building the system, understanding architecture
|
||||
|
||||
---
|
||||
|
||||
### 2. `CRON_JOBS_QUICK_REFERENCE.md` (4 KB)
|
||||
**Quick Reference for Cron Jobs**
|
||||
|
||||
Contains:
|
||||
- Cron syntax examples
|
||||
- How llm-router detects scheduled jobs
|
||||
- Execution tracking
|
||||
- API endpoints for cron
|
||||
- One-time vs Cron comparison
|
||||
|
||||
**When to use**: Testing cron features, quick lookup
|
||||
|
||||
---
|
||||
|
||||
### 3. `IMPLEMENTATION_TASKS.md` (30+ KB)
|
||||
**The Complete Task Breakdown** - What to build, in what order
|
||||
|
||||
Contains:
|
||||
- 27 specific, actionable tasks
|
||||
- Effort estimates per task (2-5 hours each)
|
||||
- Acceptance criteria for each task
|
||||
- Dependencies between tasks
|
||||
- Timeline (3 weeks, 1-2 engineers)
|
||||
- Resource allocation
|
||||
- Blockers to watch
|
||||
- Success criteria
|
||||
|
||||
**Structure**:
|
||||
```
|
||||
Phase 1: Foundation (8-10 hours)
|
||||
├─ Task 1.1: Types
|
||||
├─ Task 1.2: Knowledge Base
|
||||
├─ Task 1.3: KB Loader
|
||||
└─ Task 1.4: Validator
|
||||
|
||||
Phase 2: LLM-Router (12-15 hours)
|
||||
├─ Task 2.1: JSONPath Resolver
|
||||
├─ Task 2.2: Activity Skeleton
|
||||
├─ Task 2.3: Intent Analysis
|
||||
├─ Task 2.4: Spec Builder
|
||||
└─ Task 2.5: Cron Builder
|
||||
|
||||
Phase 3: RoutingWorkflow (15-18 hours)
|
||||
├─ Task 3.1-3.6: Executors & State Machine
|
||||
|
||||
Phase 4: API/CLI (12-15 hours)
|
||||
├─ Task 4.1-4.4: Handlers, Commands, Validation
|
||||
|
||||
Phase 5: Testing (8-12 hours)
|
||||
├─ Task 5.1-5.4: Unit, Integration, E2E, Load tests
|
||||
|
||||
Phase 6: Docs & Deployment (5-8 hours)
|
||||
├─ Task 6.1-6.4: API.md, CLI.md, Deployment.md, User Guide
|
||||
```
|
||||
|
||||
**When to use**: Planning sprints, assigning work, tracking progress
|
||||
|
||||
---
|
||||
|
||||
### 4. `DESIGN_MASTER_REVIEW.md` (25+ KB)
|
||||
**Executive Summary for Stakeholders**
|
||||
|
||||
Contains:
|
||||
- Problem/solution
|
||||
- 3 patterns (Sequential, Await-Task-Complete, Retry)
|
||||
- 3 entry points (CLI, API, Legacy)
|
||||
- Phases 1-5 (52-58 hours)
|
||||
- KMSvc questions (Q1-Q6)
|
||||
- Risks & mitigations
|
||||
- Success criteria
|
||||
- Approval checklist
|
||||
|
||||
**When to use**: Stakeholder review, getting buy-in, architecture approval
|
||||
|
||||
---
|
||||
|
||||
## 🎯 THE ARCHITECTURE AT A GLANCE
|
||||
|
||||
```
|
||||
User Message: "Analyze repo for security and quality every day at 2 AM"
|
||||
↓
|
||||
[llm-router Activity]
|
||||
Reads: ActivityKnowledgeBase.json
|
||||
Uses LLM to understand intent
|
||||
Decides: Clone → AnalyzeCode → SecurityScan → Combine → Notify
|
||||
Decides timeouts, retries from knowledge base
|
||||
Detects schedule: "0 2 * * *"
|
||||
Generates: CronWorkflowSpec
|
||||
↓
|
||||
[RoutingWorkflow] (Generic Executor)
|
||||
Registers with Temporal cron: "0 2 * * *"
|
||||
Every day at 2 AM:
|
||||
1. Clone repo
|
||||
2. Analyze code (timeout 10m, retry 3x if flaky)
|
||||
3. Security scan (timeout 15m, retry 2x)
|
||||
4. Combine results
|
||||
5. Send notification
|
||||
Tracks each execution
|
||||
↓
|
||||
[Results]
|
||||
Full execution history
|
||||
Can check status anytime
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✨ KEY FEATURES
|
||||
|
||||
| Feature | Status | Docs | Tasks |
|
||||
|---------|--------|------|-------|
|
||||
| One-time workflows | ✅ | ROUTING_WORKFLOW_SPEC.md | 2.1-2.4, 3.x, 4.x |
|
||||
| Scheduled workflows (cron) | ✅ | CRON_JOBS_QUICK_REFERENCE.md | 2.5, 3.4, 5.x |
|
||||
| Intelligent routing (LLM) | ✅ | ROUTING_WORKFLOW_SPEC.md Part 2 | 2.x |
|
||||
| Smart timeouts | ✅ | ROUTING_WORKFLOW_SPEC.md | 1.2, 2.4 |
|
||||
| Smart retries | ✅ | ROUTING_WORKFLOW_SPEC.md | 1.2, 2.4 |
|
||||
| Error handling | ✅ | ROUTING_WORKFLOW_SPEC.md | 3.4 |
|
||||
| Parameter chaining | ✅ | ROUTING_WORKFLOW_SPEC.md | 2.1 |
|
||||
| Temporal durability | ✅ | ROUTING_WORKFLOW_SPEC.md | 3.4 |
|
||||
| HTTP API | ✅ | ROUTING_WORKFLOW_SPEC.md | 4.1-4.4 |
|
||||
| CLI | ✅ | CRON_JOBS_QUICK_REFERENCE.md | 4.2 |
|
||||
| Execution tracking | ✅ | CRON_JOBS_QUICK_REFERENCE.md | 5.x |
|
||||
|
||||
---
|
||||
|
||||
## 📋 QUICK START FOR IMPLEMENTATION
|
||||
|
||||
### Week 1: Foundation + LLM-Router
|
||||
|
||||
```
|
||||
Day 1-2 (Mon-Tue):
|
||||
Task 1.1: Go types (2h)
|
||||
Task 1.2: Knowledge base JSON (3h)
|
||||
Task 1.3: KB loader (2h)
|
||||
Task 1.4: Validator (3h)
|
||||
→ Deliverable: Core data structures working
|
||||
|
||||
Day 3-5 (Wed-Fri):
|
||||
Task 2.1: JSONPath resolver (3h)
|
||||
Task 2.2: Activity skeleton (2h)
|
||||
Task 2.3: LLM intent analysis (5h)
|
||||
Task 2.4: Spec builder (4h)
|
||||
Task 2.5: Cron builder (2h)
|
||||
→ Deliverable: llm-router generates valid specs
|
||||
```
|
||||
|
||||
### Week 2: RoutingWorkflow + API/CLI
|
||||
|
||||
```
|
||||
Day 1-3 (Mon-Wed):
|
||||
Task 3.1-3.6: RoutingWorkflow & executors (15-18h)
|
||||
Task 3.5: Register in worker
|
||||
→ Deliverable: Workflows execute, can submit via API
|
||||
|
||||
Day 4-5 (Thu-Fri):
|
||||
Task 4.1: API handlers (4h)
|
||||
Task 4.2: CLI commands (5h)
|
||||
Task 4.3: Server bootstrap (2h)
|
||||
Task 4.4: Validation (2h)
|
||||
→ Deliverable: Full HTTP API + CLI working
|
||||
```
|
||||
|
||||
### Week 3: Testing + Documentation
|
||||
|
||||
```
|
||||
Day 1-3 (Mon-Wed):
|
||||
Task 5.1-5.4: All tests (8-12h)
|
||||
→ Deliverable: >90% coverage, all tests pass
|
||||
|
||||
Day 4-5 (Thu-Fri):
|
||||
Task 6.1-6.4: Documentation (5-8h)
|
||||
→ Deliverable: Complete docs, ready to ship
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 HOW TO START TODAY
|
||||
|
||||
### Step 1: Read & Understand (1-2 hours)
|
||||
1. Read `ROUTING_WORKFLOW_SPEC.md` (main spec)
|
||||
2. Read `IMPLEMENTATION_TASKS.md` (what to build)
|
||||
3. Scan `CRON_JOBS_QUICK_REFERENCE.md` (understand cron)
|
||||
|
||||
### Step 2: Assign Tasks
|
||||
1. Engineer 1: Tasks 1.1-1.4, 2.1-2.5, 3.1-3.6
|
||||
2. Engineer 2: Tasks 4.1-4.4, 5.1-5.4, 6.1-6.4
|
||||
|
||||
### Step 3: Start Building
|
||||
1. Begin with Task 1.1 (types.go)
|
||||
2. Follow dependency order
|
||||
3. Daily sync on blockers
|
||||
|
||||
### Step 4: Gate Each Phase
|
||||
- Phase 1 done? → Start Phase 2
|
||||
- Phase 2 done? → Start Phase 3
|
||||
- etc.
|
||||
|
||||
---
|
||||
|
||||
## 📊 EFFORT SUMMARY
|
||||
|
||||
| Phase | Hours | Duration | Parallel |
|
||||
|-------|-------|----------|----------|
|
||||
| Phase 1: Foundation | 8-10 | Mon-Tue | No |
|
||||
| Phase 2: LLM-Router | 12-15 | Wed-Fri + Mon | No |
|
||||
| Phase 3: RoutingWorkflow | 15-18 | Tue-Thu | Can overlap w/ Phase 4 |
|
||||
| Phase 4: API/CLI | 12-15 | Fri-Tue | Can overlap w/ Phase 3 |
|
||||
| Phase 5: Testing | 8-12 | Wed-Fri | Sequential |
|
||||
| Phase 6: Docs | 5-8 | Fri-Mon | Parallel w/ Phase 5 |
|
||||
| **TOTAL** | **60-70** | **3-4 weeks** | **2 engineers** |
|
||||
|
||||
---
|
||||
|
||||
## ✅ SUCCESS CRITERIA
|
||||
|
||||
**Phase 1 Complete**:
|
||||
- All types compile
|
||||
- Knowledge base loads
|
||||
- Validator catches errors
|
||||
- All unit tests pass
|
||||
|
||||
**Phase 2 Complete**:
|
||||
- llm-router generates valid specs
|
||||
- JSONPath resolution works
|
||||
- Cron detection works
|
||||
- Integration tests pass
|
||||
|
||||
**Phase 3 Complete**:
|
||||
- RoutingWorkflow executes any spec
|
||||
- Error handling works
|
||||
- State machine flow correct
|
||||
- Registered in worker
|
||||
|
||||
**Phase 4 Complete**:
|
||||
- HTTP API working (all endpoints)
|
||||
- CLI working (all commands)
|
||||
- Input validation
|
||||
- Can submit and check status
|
||||
|
||||
**Phase 5 Complete**:
|
||||
- >90% code coverage
|
||||
- All scenarios pass
|
||||
- Performance targets met
|
||||
- No flaky tests
|
||||
|
||||
**Phase 6 Complete**:
|
||||
- API documentation complete
|
||||
- CLI documentation complete
|
||||
- Deployment guide complete
|
||||
- User guide with examples
|
||||
|
||||
---
|
||||
|
||||
## 🔗 FILE LOCATIONS
|
||||
|
||||
```
|
||||
Core Specification:
|
||||
~/workplace/Poimen/workflows/ROUTING_WORKFLOW_SPEC.md
|
||||
|
||||
Task Breakdown:
|
||||
~/workplace/Poimen/workflows/IMPLEMENTATION_TASKS.md
|
||||
|
||||
Cron Reference:
|
||||
~/workplace/Poimen/workflows/CRON_JOBS_QUICK_REFERENCE.md
|
||||
|
||||
Stakeholder Review:
|
||||
~/workplace/Poimen/workflows/DESIGN_MASTER_REVIEW.md
|
||||
|
||||
This README:
|
||||
~/workplace/Poimen/workflows/README_IMPLEMENTATION.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 TIPS FOR SUCCESS
|
||||
|
||||
1. **Start with types** (Task 1.1)
|
||||
- Everything depends on these
|
||||
- Make them flexible
|
||||
- Good JSON schema helps
|
||||
|
||||
2. **Knowledge base is critical** (Task 1.2)
|
||||
- LLM decisions are based on this
|
||||
- Make it comprehensive
|
||||
- Document each activity well
|
||||
|
||||
3. **Test llm-router early** (Task 2.3)
|
||||
- This is highest risk
|
||||
- Test with real LLM calls
|
||||
- Validate output quality
|
||||
|
||||
4. **RoutingWorkflow is the heart** (Task 3.4)
|
||||
- Make sure state machine is solid
|
||||
- Test error paths thoroughly
|
||||
- Performance matters
|
||||
|
||||
5. **API/CLI can be simple** (Tasks 4.x)
|
||||
- Just thin wrappers
|
||||
- Focus on DX (developer experience)
|
||||
- Good error messages
|
||||
|
||||
6. **Test everything** (Phase 5)
|
||||
- Unit tests catch bugs early
|
||||
- Integration tests find edge cases
|
||||
- E2E tests validate full flow
|
||||
- Load tests validate performance
|
||||
|
||||
---
|
||||
|
||||
## 🎓 LEARNING RESOURCES
|
||||
|
||||
- **Temporal**: https://docs.temporal.io/
|
||||
- **Cron syntax**: https://crontab.guru/
|
||||
- **JSONPath**: https://goessner.net/articles/JsonPath/
|
||||
- **Go workflow patterns**: https://golang.org/pkg/workflow
|
||||
|
||||
---
|
||||
|
||||
## 📞 DECISION MAKER'S CHECKLIST
|
||||
|
||||
Before starting implementation:
|
||||
|
||||
- [ ] Do we have LLM access? (for llm-router)
|
||||
- [ ] Is Temporal deployed? (task queue "poimen-taskqueue")
|
||||
- [ ] Are activities registered? (CloneRepoActivity, etc)
|
||||
- [ ] Do we have memory service? (for LLM calls)
|
||||
- [ ] Team aligned on architecture?
|
||||
- [ ] Timeline acceptable? (3-4 weeks)
|
||||
- [ ] Resources allocated? (2 engineers)
|
||||
|
||||
All yes? → Ready to build! 🚀
|
||||
|
||||
---
|
||||
|
||||
**This is a complete, implementable specification.**
|
||||
Start with Phase 1, Task 1.1 today!
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
# Registered Memory Service Activities
|
||||
|
||||
## Summary
|
||||
|
||||
**Total Activities Registered**: 12
|
||||
**Package**: `github.com/rockliang/poimen/workflows/internal/memory`
|
||||
**Registration Method**: `RegisterMemoryActivities(worker, service)`
|
||||
**Task Queue**: `poimen-taskqueue`
|
||||
**Namespace**: `poimen-harness`
|
||||
|
||||
---
|
||||
|
||||
## Registered Activities List
|
||||
|
||||
### 1. CreateKnowledgeActivity
|
||||
- **Function**: `CreateKnowledgeActivity(ctx context.Context, record *KnowledgeRecord) (string, error)`
|
||||
- **Input**: `KnowledgeRecord` (level, title, content, source, metadata)
|
||||
- **Output**: Knowledge ID (string)
|
||||
- **Timeout**: 1 minute (default)
|
||||
- **Retries**: 3 attempts (default)
|
||||
- **Purpose**: Create L1/L2/reference knowledge records
|
||||
- **Call in Workflow**: `memory.ExecuteCreateKnowledge(ctx, record, opts)`
|
||||
|
||||
---
|
||||
|
||||
### 2. UpdateKnowledgeActivity
|
||||
- **Function**: `UpdateKnowledgeActivity(ctx context.Context, record *KnowledgeRecord) (string, error)`
|
||||
- **Input**: `KnowledgeRecord` (with ID)
|
||||
- **Output**: Knowledge ID (string)
|
||||
- **Timeout**: 1 minute
|
||||
- **Retries**: 3 attempts
|
||||
- **Purpose**: Update existing knowledge records
|
||||
- **Call in Workflow**: `memory.ExecuteUpdateKnowledge(ctx, record, opts)` (not implemented yet)
|
||||
|
||||
---
|
||||
|
||||
### 3. SearchKnowledgeActivity
|
||||
- **Function**: `SearchKnowledgeActivity(ctx context.Context, query string, opts *RetrievalOptions) ([]KnowledgeRecord, error)`
|
||||
- **Input**: Query string + retrieval options (limit, levelFilter, floor, scope)
|
||||
- **Output**: Array of `KnowledgeRecord`
|
||||
- **Timeout**: 2 minutes
|
||||
- **Retries**: 3 attempts
|
||||
- **Purpose**: Hybrid search (semantic + lexical)
|
||||
- **Call in Workflow**: `memory.ExecuteSearchKnowledge(ctx, query, opts, activityOpts)`
|
||||
|
||||
---
|
||||
|
||||
### 4. GetContextActivity
|
||||
- **Function**: `GetContextActivity(ctx context.Context, tool, task string, budget int) (*ServiceContext, error)`
|
||||
- **Input**: Tool name, task name, budget (bytes)
|
||||
- **Output**: `ServiceContext` (tier, lessons, skills, budget)
|
||||
- **Timeout**: 2 minutes
|
||||
- **Retries**: 3 attempts
|
||||
- **Purpose**: Three-tier retrieval (signature → vector → reference)
|
||||
- **Call in Workflow**: `memory.ExecuteGetContext(ctx, tool, task, budget, opts)`
|
||||
|
||||
---
|
||||
|
||||
### 5. GetVaultActivity
|
||||
- **Function**: `GetVaultActivity(ctx context.Context) ([]VaultInfo, error)`
|
||||
- **Input**: None
|
||||
- **Output**: Array of `VaultInfo` (path, title, level, updatedAt, recordCount)
|
||||
- **Timeout**: 1 minute
|
||||
- **Retries**: 3 attempts
|
||||
- **Purpose**: Browse vault files and structure
|
||||
- **Call in Workflow**: Use via service: `service.GetVault(ctx)`
|
||||
|
||||
---
|
||||
|
||||
### 6. HealthCheckActivity
|
||||
- **Function**: `HealthCheckActivity(ctx context.Context) (bool, error)`
|
||||
- **Input**: None
|
||||
- **Output**: Boolean (healthy or not)
|
||||
- **Timeout**: 30 seconds
|
||||
- **Retries**: 3 attempts
|
||||
- **Purpose**: Check memory service availability
|
||||
- **Call in Workflow**: `memory.ExecuteHealthCheck(ctx, opts)`
|
||||
|
||||
---
|
||||
|
||||
### 7. LearnFromExecutionActivity
|
||||
- **Function**: `LearnFromExecutionActivity(ctx context.Context, taskID string, result string, tags []string) (string, error)`
|
||||
- **Input**: Task ID, execution result, tags (optional)
|
||||
- **Output**: Knowledge record ID
|
||||
- **Timeout**: 1 minute
|
||||
- **Retries**: 3 attempts
|
||||
- **Purpose**: Learn from task execution results
|
||||
- **Call in Workflow**: `memory.ExecuteLearnFromExecution(ctx, taskID, result, tags, opts)`
|
||||
|
||||
---
|
||||
|
||||
### 8. DiagnoseIssueActivity
|
||||
- **Function**: `DiagnoseIssueActivity(ctx context.Context, tool, issue string) ([]string, error)`
|
||||
- **Input**: Tool name, issue description
|
||||
- **Output**: Array of recommendation strings
|
||||
- **Timeout**: 1 minute
|
||||
- **Retries**: 3 attempts
|
||||
- **Purpose**: Diagnose issues using memory context
|
||||
- **Call in Workflow**: `memory.ExecuteDiagnoseIssue(ctx, tool, issue, opts)`
|
||||
|
||||
---
|
||||
|
||||
### 9. AnalyzeErrorActivity
|
||||
- **Function**: `AnalyzeErrorActivity(ctx context.Context, errorMsg string) ([]KnowledgeRecord, error)`
|
||||
- **Input**: Error message
|
||||
- **Output**: Array of `KnowledgeRecord` (solutions)
|
||||
- **Timeout**: 1 minute
|
||||
- **Retries**: 3 attempts
|
||||
- **Purpose**: Analyze errors and find recovery paths
|
||||
- **Call in Workflow**: `memory.ExecuteAnalyzeError(ctx, errorMsg, opts)`
|
||||
|
||||
---
|
||||
|
||||
### 10. DocumentDecisionActivity
|
||||
- **Function**: `DocumentDecisionActivity(ctx context.Context, decisionType, decision, reasoning string) (string, error)`
|
||||
- **Input**: Decision type, decision, reasoning
|
||||
- **Output**: Knowledge record ID
|
||||
- **Timeout**: 1 minute
|
||||
- **Retries**: 3 attempts
|
||||
- **Purpose**: Record workflow decisions (L2 knowledge)
|
||||
- **Call in Workflow**: `memory.ExecuteDocumentDecision(ctx, decisionType, decision, reasoning, opts)`
|
||||
|
||||
---
|
||||
|
||||
### 11. SearchAndApplyActivity
|
||||
- **Function**: `SearchAndApplyActivity(ctx context.Context, query string, selector func(record *KnowledgeRecord) bool) ([]string, error)`
|
||||
- **Input**: Query string, optional selector function
|
||||
- **Output**: Array of applied content strings
|
||||
- **Timeout**: 1 minute
|
||||
- **Retries**: 3 attempts
|
||||
- **Purpose**: Search knowledge and apply selective results
|
||||
- **Call in Workflow**: Use via service
|
||||
|
||||
---
|
||||
|
||||
### 12. RefreshMemoryActivity
|
||||
- **Function**: `RefreshMemoryActivity(ctx context.Context) (map[string]interface{}, error)`
|
||||
- **Input**: None
|
||||
- **Output**: Map with vault stats and health
|
||||
- **Timeout**: 1 minute
|
||||
- **Retries**: 3 attempts
|
||||
- **Purpose**: Periodic memory context refresh
|
||||
- **Call in Workflow**: `memory.ExecuteRefreshMemory(ctx, opts)`
|
||||
|
||||
---
|
||||
|
||||
## Registration Code
|
||||
|
||||
```go
|
||||
// In cmd/worker/main.go or similar
|
||||
import "github.com/rockliang/poimen/workflows/internal/memory"
|
||||
|
||||
func setupWorker() {
|
||||
// Create memory service
|
||||
memoryService := memory.NewService(
|
||||
os.Getenv("MEMORY_SERVICE_URL"),
|
||||
os.Getenv("MEMORY_SERVICE_TOKEN"),
|
||||
"poimen",
|
||||
)
|
||||
|
||||
// Register all memory activities
|
||||
memory.RegisterMemoryActivities(workerInstance, memoryService)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Activity Naming Convention
|
||||
|
||||
Temporal activity names (as seen in logs/UI):
|
||||
|
||||
```
|
||||
- CreateKnowledgeActivity → createKnowledgeActivity
|
||||
- UpdateKnowledgeActivity → updateKnowledgeActivity
|
||||
- SearchKnowledgeActivity → searchKnowledgeActivity
|
||||
- GetContextActivity → getContextActivity
|
||||
- GetVaultActivity → getVaultActivity
|
||||
- HealthCheckActivity → healthCheckActivity
|
||||
- LearnFromExecutionActivity → learnFromExecutionActivity
|
||||
- DiagnoseIssueActivity → diagnoseIssueActivity
|
||||
- AnalyzeErrorActivity → analyzeErrorActivity
|
||||
- DocumentDecisionActivity → documentDecisionActivity
|
||||
- SearchAndApplyActivity → searchAndApplyActivity
|
||||
- RefreshMemoryActivity → refreshMemoryActivity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Default Retry Policy
|
||||
|
||||
```
|
||||
InitialInterval: 1 second
|
||||
BackoffCoefficient: 2.0
|
||||
MaximumInterval: 30 seconds
|
||||
MaximumAttempts: 3
|
||||
NonRetryableErrors: (empty - all errors retry)
|
||||
```
|
||||
|
||||
**Timeline**: 1s → 2s → 4s → fail
|
||||
|
||||
---
|
||||
|
||||
## Default Timeouts
|
||||
|
||||
| Activity | Schedule-to-Close | Start-to-Close |
|
||||
|----------|-------------------|----------------|
|
||||
| CreateKnowledge | 2 min | 1 min |
|
||||
| SearchKnowledge | 3 min | 2 min |
|
||||
| GetContext | 3 min | 2 min |
|
||||
| DiagnoseIssue | 2 min | 1 min |
|
||||
| AnalyzeError | 2 min | 1 min |
|
||||
| LearnFromExecution | 2 min | 1 min |
|
||||
| DocumentDecision | 2 min | 1 min |
|
||||
| HealthCheck | 1 min | 30s |
|
||||
| GetVault | 2 min | 1 min |
|
||||
| RefreshMemory | 2 min | 1 min |
|
||||
|
||||
---
|
||||
|
||||
## How to List Activities at Runtime
|
||||
|
||||
### Option 1: Check Logs
|
||||
```bash
|
||||
kubectl -n poimen logs -f deployment/poimen-worker | grep "ActivityType"
|
||||
```
|
||||
|
||||
### Option 2: In Workflow Test
|
||||
```go
|
||||
suite := &testsuite.WorkflowTestSuite{}
|
||||
env := suite.NewTestActivityEnvironment()
|
||||
|
||||
activities := memory.NewActivities(service)
|
||||
env.RegisterActivity(activities.CreateKnowledgeActivity)
|
||||
// ... etc
|
||||
|
||||
// Run test - activities are registered
|
||||
```
|
||||
|
||||
### Option 3: Via Temporal CLI (when connected)
|
||||
```bash
|
||||
temporal task-queue describe --namespace poimen-harness --task-queue poimen-taskqueue
|
||||
```
|
||||
|
||||
### Option 4: Temporal Web UI
|
||||
```
|
||||
http://temporal.riotpiao.com (or local Temporal UI)
|
||||
→ Namespace: poimen-harness
|
||||
→ Task Queue: poimen-taskqueue
|
||||
→ View registered worker versions with activities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Activity Flow Diagram
|
||||
|
||||
```
|
||||
Workflow
|
||||
↓
|
||||
ExecuteCreateKnowledge(ctx, record, opts)
|
||||
↓
|
||||
Temporal Worker polls poimen-taskqueue
|
||||
↓
|
||||
CreateKnowledgeActivity runs with retry policy
|
||||
↓
|
||||
Memory Service HTTP call (with Bearer token)
|
||||
↓
|
||||
Result → Workflow continues
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with Worker
|
||||
|
||||
```go
|
||||
// cmd/worker/main.go
|
||||
|
||||
func main() {
|
||||
c, _ := client.Dial(client.Options{
|
||||
HostPort: "temporal-frontend.temporal:7233",
|
||||
Namespace: "poimen-harness",
|
||||
})
|
||||
defer c.Close()
|
||||
|
||||
w := worker.New(c, "poimen-taskqueue", worker.Options{})
|
||||
|
||||
// Register memory activities
|
||||
memSvc := memory.NewService(
|
||||
"http://memory-service:8080",
|
||||
os.Getenv("MEMORY_TOKEN"),
|
||||
"poimen",
|
||||
)
|
||||
memory.RegisterMemoryActivities(w, memSvc)
|
||||
|
||||
// Start worker
|
||||
w.Start()
|
||||
defer w.Stop()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| # | Activity | Input | Output | Timeout |
|
||||
|---|----------|-------|--------|---------|
|
||||
| 1 | CreateKnowledge | KnowledgeRecord | string | 1m |
|
||||
| 2 | UpdateKnowledge | KnowledgeRecord | string | 1m |
|
||||
| 3 | SearchKnowledge | string, opts | []KnowledgeRecord | 2m |
|
||||
| 4 | GetContext | tool, task, budget | ServiceContext | 2m |
|
||||
| 5 | GetVault | — | []VaultInfo | 1m |
|
||||
| 6 | HealthCheck | — | bool | 30s |
|
||||
| 7 | LearnFromExecution | taskID, result, tags | string | 1m |
|
||||
| 8 | DiagnoseIssue | tool, issue | []string | 1m |
|
||||
| 9 | AnalyzeError | errorMsg | []KnowledgeRecord | 1m |
|
||||
| 10 | DocumentDecision | type, decision, reason | string | 1m |
|
||||
| 11 | SearchAndApply | query, selector | []string | 1m |
|
||||
| 12 | RefreshMemory | — | map[string]interface{} | 1m |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Activities defined & registered
|
||||
2. ✅ All 12 activities implemented
|
||||
3. 🔄 Deploy worker to cluster
|
||||
4. 🔄 Verify registration in Temporal UI
|
||||
5. 🔄 Use in workflows
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,269 +0,0 @@
|
||||
# Temporal Integration for Poimen Workflows
|
||||
|
||||
## Overview
|
||||
|
||||
This project uses **Temporal** for distributed workflow orchestration. Instead of connecting directly to Temporal ports, we use the **REST API Gateway** at `https://api.riotpiao.com/workflow`.
|
||||
|
||||
**Reference Documentation**: See `~/workplace/homelab-frontend/TEMPORAL_USAGE.md` for full API details.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Configuration
|
||||
|
||||
The Temporal connection is configured via environment variables:
|
||||
|
||||
```bash
|
||||
TEMPORAL_NAMESPACE=poimen-harness # Default namespace
|
||||
TEMPORAL_HOSTPORT=api.riotpiao.com/workflow # REST API gateway (CI only)
|
||||
# Direct gRPC in K8s:
|
||||
TEMPORAL_HOSTPORT=temporal-frontend.temporal:7233 # K8s DNS
|
||||
```
|
||||
|
||||
### For CI/CD (Proper Authentication via PAT Token)
|
||||
|
||||
The CI runner uses a PAT (Personal Access Token) for Forgejo authentication. Integration tests gracefully handle Temporal availability:
|
||||
|
||||
1. **Git authentication configured** in CI:
|
||||
- `.gitea/workflows/ci.yaml` uses `${{ secrets.REGISTRY_PAT }}` token
|
||||
- Enables private module access and authenticated requests
|
||||
|
||||
2. **Integration tests behavior**:
|
||||
```bash
|
||||
go test -v ./... # Runs all tests
|
||||
```
|
||||
- If Temporal accessible: ✅ Tests run
|
||||
- If Temporal unavailable: ⏭️ Tests skip gracefully
|
||||
|
||||
3. **Local development** (with Temporal access):
|
||||
```bash
|
||||
go test -v -run TestTemporal ./tests
|
||||
```
|
||||
|
||||
4. **Graceful fallback**:
|
||||
```go
|
||||
// tests/temporal_integration_test.go
|
||||
if err != nil {
|
||||
t.Skipf("skipping: Temporal not accessible - %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rest API Gateway Usage
|
||||
|
||||
### Base URL
|
||||
```
|
||||
https://api.riotpiao.com/workflow
|
||||
```
|
||||
|
||||
### Example: Start a Workflow (from CI)
|
||||
|
||||
Instead of:
|
||||
```go
|
||||
// ❌ This fails in CI (no direct access)
|
||||
c, err := client.Dial(client.Options{
|
||||
HostPort: "127.0.0.1:7233",
|
||||
Namespace: "poimen-harness",
|
||||
})
|
||||
```
|
||||
|
||||
Use HTTP REST calls:
|
||||
```bash
|
||||
curl -X POST https://api.riotpiao.com/workflow \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"action": "START_WORKFLOW",
|
||||
"namespace": "poimen-harness",
|
||||
"payload": {
|
||||
"workflow_id": "test-workflow",
|
||||
"workflow_type": "OrchestratorWorkflow",
|
||||
"task_queue": "poimen-taskqueue",
|
||||
"input": {}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Operations Available
|
||||
|
||||
All standard Temporal operations:
|
||||
- `START_WORKFLOW` - Launch new workflow
|
||||
- `DESCRIBE_WORKFLOW` - Get workflow status
|
||||
- `LIST_WORKFLOWS` - List executions
|
||||
- `GET_WORKFLOW_HISTORY` - View event history
|
||||
- `SIGNAL_WORKFLOW` - Send signals to running workflows
|
||||
- `QUERY_WORKFLOW` - Query workflow state
|
||||
- `TERMINATE_WORKFLOW` - Stop workflow
|
||||
- `CANCEL_WORKFLOW` - Graceful cancellation
|
||||
|
||||
See `~/workplace/homelab-frontend/TEMPORAL_USAGE.md` for full operation reference.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── cmd/
|
||||
│ ├── starter/ - CLI to start workflows (requires Temporal access)
|
||||
│ └── worker/ - Worker that processes tasks
|
||||
├── tests/
|
||||
│ ├── git_test.go - Unit tests (run in CI ✅)
|
||||
│ ├── types_test.go - Unit tests (run in CI ✅)
|
||||
│ └── temporal_integration_test.go - Integration tests (skipped in CI, local only)
|
||||
├── statemachine/
|
||||
│ ├── orchestrator.go - Main workflow definition
|
||||
│ └── taskunit.go - Sub-workflow for tasks
|
||||
└── action/
|
||||
├── git.go - Git operations (activities)
|
||||
├── planner.go - Planning activity
|
||||
├── implementer.go - Implementation activity
|
||||
└── judge.go - Judgment activity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Unit Tests (CI Compatible)
|
||||
```bash
|
||||
go test -v ./tests # ✅ Passes in CI
|
||||
```
|
||||
|
||||
### Integration Tests (Local Only)
|
||||
```bash
|
||||
# Requires TEMPORAL_HOSTPORT to point to accessible Temporal
|
||||
go test -v -run TestTemporal ./tests
|
||||
|
||||
# Or in K8s environment:
|
||||
kubectl exec -it deployment/poimen-worker -- \
|
||||
go test -v ./tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Worker Deployment
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
# Start worker (requires Temporal access)
|
||||
TEMPORAL_HOSTPORT=localhost:7233 go run ./cmd/worker
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
```bash
|
||||
kubectl apply -k k8s/
|
||||
# Workers connect to temporal-frontend.temporal:7233 (K8s DNS)
|
||||
```
|
||||
|
||||
### Configuration
|
||||
See `k8s/configmap.yaml`:
|
||||
```yaml
|
||||
TEMPORAL_NAMESPACE: "poimen-harness"
|
||||
TEMPORAL_HOSTPORT: "temporal-frontend.temporal:7233"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
The `.gitea/workflows/ci.yaml` runs:
|
||||
|
||||
1. **Git Auth** - Configure Forgejo PAT token for authentication
|
||||
2. **Checkout** - Pull code
|
||||
3. **Dependencies** - `go mod download`
|
||||
4. **Tests** - `go test -v ./...`
|
||||
- Unit tests: ✅ Always pass
|
||||
- Integration tests: ✅ Run if Temporal accessible, ⏭️ skip if not
|
||||
5. **Build** - `go build ./cmd/...`
|
||||
6. **Vet** - `go vet ./...`
|
||||
|
||||
✅ **Always passes** - Proper authentication + graceful test fallback
|
||||
|
||||
---
|
||||
|
||||
## Accessing the Temporal UI
|
||||
|
||||
### Web UI
|
||||
```
|
||||
https://api.riotpiao.com (UI frontend)
|
||||
```
|
||||
|
||||
### Metrics
|
||||
```bash
|
||||
curl https://api.riotpiao.com/workflow/metrics
|
||||
```
|
||||
|
||||
### Health Check
|
||||
```bash
|
||||
curl https://api.riotpiao.com/workflow/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables Reference
|
||||
|
||||
| Variable | Default | Usage | CI |
|
||||
|----------|---------|-------|----|
|
||||
| `TEMPORAL_NAMESPACE` | `poimen-harness` | Workflow namespace | ✅ |
|
||||
| `TEMPORAL_HOSTPORT` | `localhost:7233` | Server address | ✅ (configurable) |
|
||||
| `ANTHROPIC_API_KEY` | (required) | LLM for AI agents | ✅ (secret) |
|
||||
| `GOPRIVATE` | (empty) | Private module auth | ✅ |
|
||||
| `REGISTRY_PAT` | (required) | Forgejo auth token | ✅ (secret) |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "connection refused" in CI
|
||||
✅ **Expected & OK** - Integration tests gracefully skip if Temporal unavailable
|
||||
```bash
|
||||
# Check: integration tests handle connection errors
|
||||
go test -v ./tests
|
||||
# Output: SKIP temporal_integration_test.go:32 (Temporal not accessible)
|
||||
```
|
||||
|
||||
### Tests fail locally with "connection refused"
|
||||
Ensure Temporal is accessible:
|
||||
```bash
|
||||
# Check connectivity
|
||||
curl https://api.riotpiao.com/workflow/health
|
||||
|
||||
# Or for local Temporal:
|
||||
nc -zv localhost 7233
|
||||
```
|
||||
|
||||
### Worker can't reach Temporal in K8s
|
||||
Verify:
|
||||
```bash
|
||||
# Check configmap
|
||||
kubectl get cm poimen-config -o yaml
|
||||
|
||||
# Check pod logs
|
||||
kubectl logs deployment/poimen-worker
|
||||
|
||||
# Verify DNS from pod
|
||||
kubectl exec -it deployment/poimen-worker -- \
|
||||
nslookup temporal-frontend.temporal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ CI tests pass with proper authentication (PAT token)
|
||||
2. ✅ Integration tests run when Temporal accessible, skip otherwise
|
||||
3. 🔄 Local development: access Temporal for full integration test coverage
|
||||
4. 📦 K8s deployment: workers connect to Temporal service
|
||||
5. 📊 Monitor via REST API: `https://api.riotpiao.com/workflow`
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **Full API**: `~/workplace/homelab-frontend/TEMPORAL_USAGE.md`
|
||||
- **K8s Config**: `./k8s/configmap.yaml`
|
||||
- **CI Config**: `.gitea/workflows/ci.yaml`
|
||||
- **Worker Code**: `./cmd/worker/main.go`
|
||||
- **Workflows**: `./statemachine/orchestrator.go`
|
||||
@@ -1,585 +0,0 @@
|
||||
# Tool Usage & Skills Ingestion Strategy
|
||||
|
||||
## Poimen Tool Landscape
|
||||
|
||||
### Category 1: Workflow Definition Tools
|
||||
|
||||
**Tool**: `WorkflowDef Builder` (Rust)
|
||||
```rust
|
||||
let workflow = WorkflowDef::builder()
|
||||
.name("poimen")
|
||||
.phase(T0::phases())?
|
||||
.step(StepId::from("T0.1-identity"))?
|
||||
.transition_to(StepId::from("T0.2-kernel"))?
|
||||
.build()?;
|
||||
```
|
||||
|
||||
**Skill Usage**:
|
||||
- Know when to use builder vs YAML
|
||||
- Understand phase dependencies
|
||||
- Handle schema version mismatches
|
||||
|
||||
**Memory Integration**:
|
||||
```
|
||||
IngestActivity {
|
||||
level: "L2",
|
||||
title: "WorkflowDef Builder Pattern",
|
||||
content: "Use builder for Rust workflows. YAML for runtime customization.",
|
||||
tags: ["T3-canonicalization", "IR"],
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Category 2: State Machine Tools
|
||||
|
||||
**Tool**: `Event Log` (immutable JSONL)
|
||||
```
|
||||
{"attempt_id": "1", "step": "T0.1", "event": "WorkerEvent::Started"}
|
||||
{"attempt_id": "1", "step": "T0.1", "event": "WorkerEvent::Completed"}
|
||||
{"attempt_id": "1", "step": "T0.2", "event": "WorkerEvent::Attempted"}
|
||||
```
|
||||
|
||||
**Skills**:
|
||||
- Event log format and ordering
|
||||
- Atomic commit protocol for writes
|
||||
- Fold + re-derive pattern
|
||||
|
||||
**Memory Integration**:
|
||||
```
|
||||
SearchActivity {
|
||||
query: "event log corruption recovery",
|
||||
returns: ["Verify checksum", "Replay from marker", "Fork + rewind"]
|
||||
}
|
||||
```
|
||||
|
||||
**Tool**: `Fold & Re-derive`
|
||||
```rust
|
||||
fn fold_state(state: &mut AttemptState, event: &WorkerEvent) {
|
||||
match event {
|
||||
WorkerEvent::Started => state.status = Running,
|
||||
WorkerEvent::Completed => state.status = Success,
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Skills**:
|
||||
- Deterministic state transitions
|
||||
- No side effects in fold
|
||||
- Time-ordered replay
|
||||
|
||||
**Memory Integration**:
|
||||
```
|
||||
DiagnoseIssueActivity {
|
||||
issue: "state divergence after event log replay",
|
||||
returns: [
|
||||
"Tier 1: Check for non-deterministic fold",
|
||||
"Tier 2: Verify event order",
|
||||
"Tier 3: See fold/re-derive docs"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Category 3: Execution Tools
|
||||
|
||||
**Tool**: `Run Executor` (polling)
|
||||
```rust
|
||||
loop {
|
||||
let task = queue.wait_for_task(timeout)?;
|
||||
let output = executor.execute_step(&task)?;
|
||||
queue.mark_complete(&task, &output)?;
|
||||
}
|
||||
```
|
||||
|
||||
**Skills**:
|
||||
- Long-poll timeouts
|
||||
- Task queue semantics
|
||||
- Backpressure handling
|
||||
|
||||
**Memory Integration**:
|
||||
```
|
||||
IngestActivity {
|
||||
level: "L1",
|
||||
title: "Executor Timeout Pattern",
|
||||
content: "20s task queue poll, 30s step timeout, exponential backoff",
|
||||
tags: ["executor", "T1-execution"],
|
||||
}
|
||||
```
|
||||
|
||||
**Tool**: `Attempt Lifecycle`
|
||||
```rust
|
||||
pub struct AttemptState {
|
||||
number: u32, // 1st, 2nd, 3rd attempt
|
||||
started_at: SystemTime,
|
||||
budget: Budget, // tokens, attempts, time
|
||||
context: PartitionedContext, // input for this attempt
|
||||
retry_policy: RetryPolicy,
|
||||
}
|
||||
```
|
||||
|
||||
**Skills**:
|
||||
- Budget exhaustion detection
|
||||
- Retry condition evaluation
|
||||
- Context capture per attempt
|
||||
|
||||
**Memory Integration**:
|
||||
```
|
||||
ContextActivity {
|
||||
tool: "executor",
|
||||
task: "attempt-lifecycle",
|
||||
returns: {
|
||||
tier_1: "Known budget limits per phase",
|
||||
tier_2: "Learned attempt success rates",
|
||||
tier_3: "Docs on RetryPolicy tuning",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Category 4: Verification Tools
|
||||
|
||||
**Tool**: `Verifier Port` (pluggable)
|
||||
```rust
|
||||
pub trait Verifier {
|
||||
fn verify(&self, output: &Output, rubric: &Rubric) -> Result<bool>;
|
||||
}
|
||||
```
|
||||
|
||||
**Skills**:
|
||||
- Rubric definition (JSON/YAML)
|
||||
- Verification logic chains
|
||||
- Failure categorization
|
||||
|
||||
**Memory Integration**:
|
||||
```
|
||||
SearchActivity {
|
||||
query: "rubric evaluation patterns",
|
||||
returns: [
|
||||
"Multi-level rubric structure",
|
||||
"Failure classification system",
|
||||
"Score aggregation methods"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Tool**: `Judge Port` (decision logic)
|
||||
```rust
|
||||
pub trait Judge {
|
||||
fn decide(&self, attempt: &AttemptState) -> Decision;
|
||||
// → Approve | Reject | RequestRevision | Retry
|
||||
}
|
||||
```
|
||||
|
||||
**Skills**:
|
||||
- Decision thresholds
|
||||
- Evidence combination
|
||||
- Feedback injection
|
||||
|
||||
**Memory Integration**:
|
||||
```
|
||||
DiagnoseIssueActivity {
|
||||
issue: "judge consistently rejects step output",
|
||||
returns: [
|
||||
"Tier 1: Check rubric alignment",
|
||||
"Tier 2: Review judge logic history",
|
||||
"Tier 3: See judge tuning guide"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Category 5: Model Provider Tools
|
||||
|
||||
**Tool**: `ModelProvider Port`
|
||||
```rust
|
||||
pub trait ModelProvider {
|
||||
fn run(&self, model_id: &str, prompt: &str, budget: &Budget) -> Result<Output>;
|
||||
}
|
||||
```
|
||||
|
||||
**Skills**:
|
||||
- Model selection (when to use which model)
|
||||
- Prompt engineering
|
||||
- Token budgeting
|
||||
- Error handling per model
|
||||
|
||||
**Memory Integration - Prompt Optimization**:
|
||||
```
|
||||
GetContextActivity {
|
||||
tool: "model-provider",
|
||||
task: "planner-step-generation",
|
||||
returns: {
|
||||
tier_1: "Known failure patterns for this step",
|
||||
tier_2: "Successful prompt patterns",
|
||||
tier_3: "Model capability guide",
|
||||
}
|
||||
}
|
||||
// Use returned context to optimize prompt:
|
||||
optimized_prompt = inject_learned_lessons(
|
||||
base_prompt,
|
||||
context.lessons, // "Always include edge cases for T1.3"
|
||||
context.skills, // "Skill: planning-with-constraints"
|
||||
)
|
||||
```
|
||||
|
||||
**Skill Example: Prompt Template**:
|
||||
```yaml
|
||||
title: "Planner Step with Constraint Handling"
|
||||
level: "L2"
|
||||
content: |
|
||||
You are a step planner for workflow execution.
|
||||
|
||||
# Constraints (learned):
|
||||
- Never generate steps without verification steps
|
||||
- Include retry limits in plan
|
||||
- Budget awareness required
|
||||
|
||||
# Examples from memory (tier-2):
|
||||
- Previous successful T1.3 outputs show pattern X
|
||||
- Failed attempts shared pattern Y to avoid
|
||||
|
||||
# Instructions:
|
||||
Generate plan with these considerations...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Category 6: Storage Tools
|
||||
|
||||
**Tool**: `EventLog Port` (redb implementation)
|
||||
```rust
|
||||
pub trait EventLog {
|
||||
fn append(&mut self, event: WorkerEvent) -> Result<u64>;
|
||||
fn read(&self, range: Range<u64>) -> Result<Vec<WorkerEvent>>;
|
||||
}
|
||||
```
|
||||
|
||||
**Skills**:
|
||||
- Event serialization format
|
||||
- Atomic writes
|
||||
- Recovery from incomplete commits
|
||||
|
||||
**Memory Integration**:
|
||||
```
|
||||
LearnFromExecutionActivity {
|
||||
taskID: "T0.5-eventlog-persistence",
|
||||
result: "Redb backend successfully persisted 10K events",
|
||||
tags: ["storage", "T0", "persistence"]
|
||||
}
|
||||
```
|
||||
|
||||
**Tool**: `BlobStore Port` (prompt/output capture)
|
||||
```rust
|
||||
pub trait BlobStore {
|
||||
fn write(&self, path: &str, data: &[u8]) -> Result<()>;
|
||||
fn read(&self, path: &str) -> Result<Vec<u8>>;
|
||||
}
|
||||
```
|
||||
|
||||
**Skills**:
|
||||
- Path conventions (/{attempt_id}/{step_id}/prompt.txt)
|
||||
- Compression strategies
|
||||
- Retention policies
|
||||
|
||||
**Memory Integration**:
|
||||
```
|
||||
DocumentDecisionActivity {
|
||||
decisionType: "blob-retention",
|
||||
decision: "Archive attempts > 30 days to cold storage",
|
||||
reasoning: "Balance audit trail with cost"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Skills Ingestion Strategy
|
||||
|
||||
### Phase 1: YAML Skills Registry
|
||||
|
||||
**File**: `prompts/skills.yaml`
|
||||
```yaml
|
||||
skills:
|
||||
- id: "kernel-state-machine"
|
||||
category: "T0-kernel"
|
||||
level: "L2"
|
||||
title: "State Machine Kernel Patterns"
|
||||
content: |
|
||||
Key patterns for T0:
|
||||
- Event log append-only design
|
||||
- Atomic commit with 2PC
|
||||
- Fold determinism for state derivation
|
||||
- Fork/rewind for attempt recovery
|
||||
|
||||
- id: "attempt-lifecycle"
|
||||
category: "T1-execution"
|
||||
level: "L2"
|
||||
title: "Attempt Lifecycle Management"
|
||||
content: |
|
||||
Execution loop patterns:
|
||||
- Poll-based task queue
|
||||
- Budget tracking (tokens, attempts, time)
|
||||
- Retry policy evaluation
|
||||
- Context capture per attempt
|
||||
|
||||
- id: "prompt-optimization"
|
||||
category: "model-provider"
|
||||
level: "L2"
|
||||
title: "Memory-Based Prompt Optimization"
|
||||
content: |
|
||||
Best practices:
|
||||
- Retrieve 3-tier context before execution
|
||||
- Inject learned facts from tier-1 (exact matches)
|
||||
- Include tier-2 patterns (ML-similar)
|
||||
- Reference tier-3 docs (general guidance)
|
||||
- Set budget constraints from experience
|
||||
```
|
||||
|
||||
### Phase 2: Ingest Skills on Startup
|
||||
|
||||
```go
|
||||
// In cmd/starter/main.go
|
||||
|
||||
func ingestSkills(memSvc *memory.Service) error {
|
||||
skillsYAML, err := ioutil.ReadFile("prompts/skills.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var skillsConfig struct {
|
||||
Skills []struct {
|
||||
ID string `yaml:"id"`
|
||||
Category string `yaml:"category"`
|
||||
Level string `yaml:"level"`
|
||||
Title string `yaml:"title"`
|
||||
Content string `yaml:"content"`
|
||||
} `yaml:"skills"`
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(skillsYAML, &skillsConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, skill := range skillsConfig.Skills {
|
||||
_, err := memSvc.CreateKnowledge(ctx, &memory.KnowledgeRecord{
|
||||
Level: skill.Level,
|
||||
Title: skill.Title,
|
||||
Content: skill.Content,
|
||||
Source: fmt.Sprintf("skills:///%s", skill.ID),
|
||||
Metadata: map[string]interface{}{
|
||||
"skill_id": skill.ID,
|
||||
"category": skill.Category,
|
||||
"type": "skill",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("Failed to ingest skill %s: %v", skill.ID, err))
|
||||
continue
|
||||
}
|
||||
log.Info(fmt.Sprintf("Ingested skill: %s", skill.Title))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Reference Docs Ingestion
|
||||
|
||||
**File**: `poimen/crates/doc/` (Rust doc comments)
|
||||
|
||||
```rust
|
||||
/// # Attempt Lifecycle Pattern
|
||||
///
|
||||
/// Every step execution follows this sequence:
|
||||
/// 1. Check budget (tokens, attempts, time remaining)
|
||||
/// 2. Retrieve context from memory (3-tier)
|
||||
/// 3. Optimize prompt with lessons & skills
|
||||
/// 4. Execute with ModelProvider
|
||||
/// 5. Evaluate with Verifier
|
||||
/// 6. Decide with Judge
|
||||
/// 7. Learn (success) or Diagnose (failure)
|
||||
/// 8. Retry or proceed to next step
|
||||
///
|
||||
/// # Budget Tracking
|
||||
/// - Tokens: Count LLM input/output tokens
|
||||
/// - Attempts: Number of retries allowed
|
||||
/// - Time: Wall-clock timeout per step
|
||||
///
|
||||
/// # Retry Policy
|
||||
/// - Exponential backoff: 1s → 2s → 4s
|
||||
/// - Max attempts: 3 (configurable)
|
||||
/// - Non-retryable: Syntax errors, auth failures
|
||||
pub struct AttemptState { ... }
|
||||
```
|
||||
|
||||
**Ingest Docs**:
|
||||
```go
|
||||
// Extract doc comments and ingest as L2 knowledge
|
||||
// Run during build/startup:
|
||||
// $ cargo doc --extract-comments | memory-ingest --level L2
|
||||
```
|
||||
|
||||
### Phase 4: Execution Pattern Capture
|
||||
|
||||
```go
|
||||
// In RunExecutor::execute_step()
|
||||
|
||||
func (e *Executor) execute_step(ctx *WorkflowContext, step *StepId) error {
|
||||
// ... execution logic ...
|
||||
|
||||
// Capture pattern on success
|
||||
if output.status == Success {
|
||||
memSvc.CreateKnowledge(ctx, &memory.KnowledgeRecord{
|
||||
Level: "L1",
|
||||
Title: fmt.Sprintf("Successful %s execution", step),
|
||||
Content: fmt.Sprintf(
|
||||
"Step %s completed with output:\n%s",
|
||||
step, output.text,
|
||||
),
|
||||
Source: fmt.Sprintf("workflow://execution/%s", step),
|
||||
Metadata: map[string]interface{}{
|
||||
"step_id": step.String(),
|
||||
"phase": ctx.PhaseId,
|
||||
"attempt": ctx.AttemptState.Number,
|
||||
"tokens_used": output.tokens,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool-Skill Mapping Matrix
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ Tool → Skill Dependencies │
|
||||
├──────────────────────┬──────────────────────────────────────────┤
|
||||
│ Tool │ Skills Needed (from memory) │
|
||||
├──────────────────────┼──────────────────────────────────────────┤
|
||||
│ WorkflowDef Builder │ • Phase dependencies │
|
||||
│ │ • IR canonicalization rules │
|
||||
│ │ • Schema versioning │
|
||||
├──────────────────────┼──────────────────────────────────────────┤
|
||||
│ Event Log │ • Event ordering guarantees │
|
||||
│ │ • Atomic commit protocol │
|
||||
│ │ • Checksum validation │
|
||||
├──────────────────────┼──────────────────────────────────────────┤
|
||||
│ Run Executor │ • Attempt lifecycle patterns │
|
||||
│ │ • Budget exhaustion detection │
|
||||
│ │ • Retry policy evaluation │
|
||||
├──────────────────────┼──────────────────────────────────────────┤
|
||||
│ Verifier Port │ • Rubric structure design │
|
||||
│ │ • Failure categorization │
|
||||
│ │ • Score aggregation rules │
|
||||
├──────────────────────┼──────────────────────────────────────────┤
|
||||
│ Judge Port │ • Decision thresholds │
|
||||
│ │ • Evidence combination logic │
|
||||
│ │ • Feedback injection patterns │
|
||||
├──────────────────────┼──────────────────────────────────────────┤
|
||||
│ ModelProvider │ • Prompt engineering best practices │
|
||||
│ │ • Token budget awareness │
|
||||
│ │ • Model-specific quirks │
|
||||
├──────────────────────┼──────────────────────────────────────────┤
|
||||
│ EventLog Storage │ • Serialization format choices │
|
||||
│ │ • Compression strategies │
|
||||
│ │ • Recovery procedures │
|
||||
├──────────────────────┼──────────────────────────────────────────┤
|
||||
│ BlobStore │ • Path naming conventions │
|
||||
│ │ • Retention policies │
|
||||
│ │ • Archive triggers │
|
||||
└──────────────────────┴──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Basic Tool Usage Example
|
||||
|
||||
### Scenario: Planner Step Fails Repeatedly
|
||||
|
||||
**User Command**:
|
||||
```bash
|
||||
poimen plan my-workflow.yaml --phase T1 --retry-with-memory
|
||||
```
|
||||
|
||||
**Tool Execution Chain**:
|
||||
|
||||
```
|
||||
1. LOAD WORKFLOW
|
||||
WorkflowDefBuilder.from_yaml("my-workflow.yaml")
|
||||
→ Memory: Retrieve "IR-canonicalization" skills
|
||||
→ Validate against stored L2 knowledge
|
||||
|
||||
2. INIT EXECUTOR
|
||||
RunExecutor.new()
|
||||
→ Memory: Get "attempt-lifecycle" context
|
||||
→ Load retry policy from memory lessons
|
||||
|
||||
3. EXECUTE PLANNER STEP
|
||||
for attempt in 1..max_attempts:
|
||||
a) GetContextActivity
|
||||
- Tool: "planner"
|
||||
- Task: "step-generation"
|
||||
- Returns: lessons + skills
|
||||
|
||||
b) OptimizePrompt
|
||||
- Inject learned facts (tier-1)
|
||||
- Add pattern examples (tier-2)
|
||||
- Set budget from history
|
||||
|
||||
c) ModelProvider.run(optimized_prompt)
|
||||
- Send to planner agent
|
||||
- Wait for output
|
||||
|
||||
d) Verifier.verify(output)
|
||||
- Check against rubric
|
||||
- Score output quality
|
||||
|
||||
e) Judge.decide(output)
|
||||
- Approve | Retry | Reject
|
||||
|
||||
f) On Success: LearnFromExecutionActivity
|
||||
- Store successful output pattern (L1)
|
||||
|
||||
g) On Failure: AnalyzeErrorActivity
|
||||
- Search for similar failures
|
||||
- Return recovery suggestions
|
||||
|
||||
h) DocumentDecisionActivity
|
||||
- Log decision and reasoning
|
||||
|
||||
4. COMPLETED
|
||||
✅ Plan generated (or user feedback required)
|
||||
→ Memory: Ingest execution pattern
|
||||
→ Next phase starts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary: Tool & Skill Flow
|
||||
|
||||
```
|
||||
Workflow Execution
|
||||
↓
|
||||
Tools Used ────────────────→ Skills Retrieved from Memory
|
||||
├─ WorkflowDefBuilder ├─ IR canonicalization rules
|
||||
├─ EventLog ├─ State machine patterns
|
||||
├─ RunExecutor ├─ Attempt lifecycle
|
||||
├─ Verifier Port ├─ Rubric design
|
||||
├─ Judge Port ├─ Decision logic
|
||||
├─ ModelProvider ├─ Prompt optimization
|
||||
└─ Storage Ports └─ Retention policies
|
||||
|
||||
Skills Guide Execution ──────→ Results Learned
|
||||
├─ Success patterns (L1)
|
||||
├─ Failure recovery (L1)
|
||||
├─ Verified practices (L2)
|
||||
└─ Vault enriched for next run
|
||||
```
|
||||
|
||||
This creates a **virtuous cycle**: Each execution improves the memory, which improves the next execution.
|
||||
@@ -0,0 +1,296 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AnalyzeCodeInput is input for AnalyzeCodeActivity
|
||||
type AnalyzeCodeInput struct {
|
||||
Path string `json:"path"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Depth int `json:"depth,omitempty"`
|
||||
}
|
||||
|
||||
// AnalyzeCodeOutput is output from AnalyzeCodeActivity
|
||||
type AnalyzeCodeOutput struct {
|
||||
Quality float64 `json:"quality"`
|
||||
Metrics map[string]interface{} `json:"metrics"`
|
||||
Issues []string `json:"issues"`
|
||||
Summary string `json:"summary"`
|
||||
}
|
||||
|
||||
// AnalyzeCodeActivity analyzes code quality using available tools
|
||||
func AnalyzeCodeActivity(ctx context.Context, in AnalyzeCodeInput) (AnalyzeCodeOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("AnalyzeCodeActivity started", "path", in.Path)
|
||||
|
||||
output := AnalyzeCodeOutput{
|
||||
Quality: 0.0,
|
||||
Metrics: make(map[string]interface{}),
|
||||
Issues: []string{},
|
||||
}
|
||||
|
||||
// Verify path exists
|
||||
if _, err := os.Stat(in.Path); os.IsNotExist(err) {
|
||||
return output, fmt.Errorf("path does not exist: %s", in.Path)
|
||||
}
|
||||
|
||||
// Count files and lines
|
||||
var totalFiles, totalLines int
|
||||
err := filepath.Walk(in.Path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // Skip errors
|
||||
}
|
||||
if info.IsDir() {
|
||||
// Skip hidden and vendor directories
|
||||
if strings.HasPrefix(info.Name(), ".") || info.Name() == "vendor" || info.Name() == "node_modules" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := filepath.Ext(path)
|
||||
if isCodeFile(ext) {
|
||||
totalFiles++
|
||||
if lines, err := countLines(path); err == nil {
|
||||
totalLines += lines
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn("Error walking path", "error", err)
|
||||
}
|
||||
|
||||
output.Metrics["totalFiles"] = totalFiles
|
||||
output.Metrics["totalLines"] = totalLines
|
||||
|
||||
// Try to run go vet if it's a Go project
|
||||
if _, err := os.Stat(filepath.Join(in.Path, "go.mod")); err == nil {
|
||||
cmd := exec.CommandContext(ctx, "go", "vet", "./...")
|
||||
cmd.Dir = in.Path
|
||||
vetOutput, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
issues := strings.Split(string(vetOutput), "\n")
|
||||
for _, issue := range issues {
|
||||
if strings.TrimSpace(issue) != "" {
|
||||
output.Issues = append(output.Issues, issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
output.Metrics["goVetRan"] = true
|
||||
}
|
||||
|
||||
// Calculate quality score (simple heuristic)
|
||||
issueCount := len(output.Issues)
|
||||
if totalFiles > 0 {
|
||||
issuesPerFile := float64(issueCount) / float64(totalFiles)
|
||||
output.Quality = max(0, 1.0 - (issuesPerFile * 0.1))
|
||||
} else {
|
||||
output.Quality = 0.5
|
||||
}
|
||||
|
||||
output.Summary = fmt.Sprintf("Analyzed %d files (%d lines). Found %d issues. Quality score: %.2f",
|
||||
totalFiles, totalLines, issueCount, output.Quality)
|
||||
|
||||
logger.Info("AnalyzeCodeActivity completed", "quality", output.Quality, "issues", issueCount)
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// SecurityScanInput is input for SecurityScanActivity
|
||||
type SecurityScanInput struct {
|
||||
Path string `json:"path"`
|
||||
Severity string `json:"severity,omitempty"` // low, medium, high, critical
|
||||
}
|
||||
|
||||
// SecurityScanOutput is output from SecurityScanActivity
|
||||
type SecurityScanOutput struct {
|
||||
Vulnerabilities []Vulnerability `json:"vulnerabilities"`
|
||||
SecurityScore float64 `json:"securityScore"`
|
||||
RiskLevel string `json:"riskLevel"`
|
||||
}
|
||||
|
||||
// Vulnerability represents a security issue
|
||||
type Vulnerability struct {
|
||||
ID string `json:"id"`
|
||||
Severity string `json:"severity"`
|
||||
Description string `json:"description"`
|
||||
File string `json:"file,omitempty"`
|
||||
Line int `json:"line,omitempty"`
|
||||
}
|
||||
|
||||
// SecurityScanActivity scans code for security vulnerabilities
|
||||
func SecurityScanActivity(ctx context.Context, in SecurityScanInput) (SecurityScanOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("SecurityScanActivity started", "path", in.Path)
|
||||
|
||||
output := SecurityScanOutput{
|
||||
Vulnerabilities: []Vulnerability{},
|
||||
SecurityScore: 100.0,
|
||||
RiskLevel: "low",
|
||||
}
|
||||
|
||||
// Verify path exists
|
||||
if _, err := os.Stat(in.Path); os.IsNotExist(err) {
|
||||
return output, fmt.Errorf("path does not exist: %s", in.Path)
|
||||
}
|
||||
|
||||
// Check for common security issues
|
||||
// 1. Check for hardcoded secrets
|
||||
secretPatterns := []string{
|
||||
"password=",
|
||||
"secret=",
|
||||
"api_key=",
|
||||
"apikey=",
|
||||
"private_key",
|
||||
"AWS_SECRET",
|
||||
}
|
||||
|
||||
err := filepath.Walk(in.Path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isCodeFile(filepath.Ext(path)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
contentStr := strings.ToLower(string(content))
|
||||
for _, pattern := range secretPatterns {
|
||||
if strings.Contains(contentStr, pattern) {
|
||||
output.Vulnerabilities = append(output.Vulnerabilities, Vulnerability{
|
||||
ID: fmt.Sprintf("SEC-%d", len(output.Vulnerabilities)+1),
|
||||
Severity: "high",
|
||||
Description: fmt.Sprintf("Possible hardcoded secret: %s", pattern),
|
||||
File: path,
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn("Error scanning", "error", err)
|
||||
}
|
||||
|
||||
// Try gosec if available and it's a Go project
|
||||
if _, err := os.Stat(filepath.Join(in.Path, "go.mod")); err == nil {
|
||||
if _, err := exec.LookPath("gosec"); err == nil {
|
||||
cmd := exec.CommandContext(ctx, "gosec", "-fmt=json", "-quiet", "./...")
|
||||
cmd.Dir = in.Path
|
||||
// gosec returns non-zero if issues found, so we ignore the error
|
||||
cmd.CombinedOutput()
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate score
|
||||
vulnCount := len(output.Vulnerabilities)
|
||||
if vulnCount == 0 {
|
||||
output.SecurityScore = 100.0
|
||||
output.RiskLevel = "low"
|
||||
} else if vulnCount < 3 {
|
||||
output.SecurityScore = 80.0
|
||||
output.RiskLevel = "medium"
|
||||
} else if vulnCount < 10 {
|
||||
output.SecurityScore = 50.0
|
||||
output.RiskLevel = "high"
|
||||
} else {
|
||||
output.SecurityScore = 20.0
|
||||
output.RiskLevel = "critical"
|
||||
}
|
||||
|
||||
logger.Info("SecurityScanActivity completed", "vulnerabilities", vulnCount, "riskLevel", output.RiskLevel)
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// GenerateReportInput is input for GenerateReportActivity
|
||||
type GenerateReportInput struct {
|
||||
AnalysisResult interface{} `json:"analysisResult"`
|
||||
SecurityResult interface{} `json:"securityResult"`
|
||||
Format string `json:"format,omitempty"` // markdown, html, json
|
||||
}
|
||||
|
||||
// GenerateReportOutput is output from GenerateReportActivity
|
||||
type GenerateReportOutput struct {
|
||||
Report string `json:"report"`
|
||||
ReportPath string `json:"reportPath"`
|
||||
}
|
||||
|
||||
// GenerateReportActivity generates a combined report
|
||||
func GenerateReportActivity(ctx context.Context, in GenerateReportInput) (GenerateReportOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("GenerateReportActivity started")
|
||||
|
||||
format := in.Format
|
||||
if format == "" {
|
||||
format = "markdown"
|
||||
}
|
||||
|
||||
var report strings.Builder
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
|
||||
switch format {
|
||||
case "markdown":
|
||||
report.WriteString("# Analysis Report\n\n")
|
||||
report.WriteString(fmt.Sprintf("Generated: %s\n\n", timestamp))
|
||||
report.WriteString("## Code Analysis\n\n")
|
||||
report.WriteString(fmt.Sprintf("```\n%v\n```\n\n", in.AnalysisResult))
|
||||
report.WriteString("## Security Scan\n\n")
|
||||
report.WriteString(fmt.Sprintf("```\n%v\n```\n\n", in.SecurityResult))
|
||||
case "json":
|
||||
report.WriteString(fmt.Sprintf(`{"timestamp":"%s","analysis":%v,"security":%v}`,
|
||||
timestamp, in.AnalysisResult, in.SecurityResult))
|
||||
default:
|
||||
report.WriteString(fmt.Sprintf("Report generated at %s\n", timestamp))
|
||||
report.WriteString(fmt.Sprintf("Analysis: %v\n", in.AnalysisResult))
|
||||
report.WriteString(fmt.Sprintf("Security: %v\n", in.SecurityResult))
|
||||
}
|
||||
|
||||
// Save to temp file
|
||||
reportPath := filepath.Join(os.TempDir(), fmt.Sprintf("report-%d.%s", time.Now().UnixNano(), format))
|
||||
if err := os.WriteFile(reportPath, []byte(report.String()), 0644); err != nil {
|
||||
logger.Warn("Failed to save report", "error", err)
|
||||
}
|
||||
|
||||
logger.Info("GenerateReportActivity completed", "format", format)
|
||||
return GenerateReportOutput{
|
||||
Report: report.String(),
|
||||
ReportPath: reportPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func isCodeFile(ext string) bool {
|
||||
codeExts := map[string]bool{
|
||||
".go": true, ".py": true, ".js": true, ".ts": true,
|
||||
".java": true, ".c": true, ".cpp": true, ".h": true,
|
||||
".rs": true, ".rb": true, ".php": true, ".swift": true,
|
||||
}
|
||||
return codeExts[ext]
|
||||
}
|
||||
|
||||
func countLines(path string) (int, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(strings.Split(string(content), "\n")), nil
|
||||
}
|
||||
|
||||
func max(a, b float64) float64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzeCodeActivity(t *testing.T) {
|
||||
// Create temp directory with some Go code
|
||||
tmpDir, err := os.MkdirTemp("", "analyze-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create go.mod
|
||||
goMod := `module test
|
||||
go 1.21
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goMod), 0644))
|
||||
|
||||
// Create a simple Go file
|
||||
goCode := `package main
|
||||
|
||||
func main() {
|
||||
println("hello")
|
||||
}
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(goCode), 0644))
|
||||
|
||||
// Run activity
|
||||
output, err := AnalyzeCodeActivity(context.Background(), AnalyzeCodeInput{
|
||||
Path: tmpDir,
|
||||
Depth: 3,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify output
|
||||
require.Greater(t, output.Quality, 0.0)
|
||||
require.NotNil(t, output.Metrics)
|
||||
require.NotEmpty(t, output.Summary)
|
||||
|
||||
totalFiles, ok := output.Metrics["totalFiles"].(int)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 1, totalFiles) // Just main.go (go.mod not counted)
|
||||
}
|
||||
|
||||
func TestAnalyzeCodeActivity_PathNotExist(t *testing.T) {
|
||||
_, err := AnalyzeCodeActivity(context.Background(), AnalyzeCodeInput{
|
||||
Path: "/nonexistent/path",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "does not exist")
|
||||
}
|
||||
|
||||
func TestSecurityScanActivity(t *testing.T) {
|
||||
// Create temp directory
|
||||
tmpDir, err := os.MkdirTemp("", "security-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create a file with potential secret (matches pattern "password=")
|
||||
code := `package main
|
||||
|
||||
var config = map[string]string{
|
||||
"password=": "supersecret123",
|
||||
"api_key=": "sk-12345",
|
||||
}
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(code), 0644))
|
||||
|
||||
// Run activity
|
||||
output, err := SecurityScanActivity(context.Background(), SecurityScanInput{
|
||||
Path: tmpDir,
|
||||
Severity: "medium",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should find the hardcoded password
|
||||
require.Greater(t, len(output.Vulnerabilities), 0)
|
||||
require.Less(t, output.SecurityScore, 100.0)
|
||||
}
|
||||
|
||||
func TestSecurityScanActivity_Clean(t *testing.T) {
|
||||
// Create temp directory with clean code
|
||||
tmpDir, err := os.MkdirTemp("", "security-clean-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create clean code
|
||||
code := `package main
|
||||
|
||||
func main() {
|
||||
println("hello")
|
||||
}
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(code), 0644))
|
||||
|
||||
// Run activity
|
||||
output, err := SecurityScanActivity(context.Background(), SecurityScanInput{
|
||||
Path: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should be clean
|
||||
require.Equal(t, 0, len(output.Vulnerabilities))
|
||||
require.Equal(t, 100.0, output.SecurityScore)
|
||||
require.Equal(t, "low", output.RiskLevel)
|
||||
}
|
||||
|
||||
func TestGenerateReportActivity(t *testing.T) {
|
||||
output, err := GenerateReportActivity(context.Background(), GenerateReportInput{
|
||||
AnalysisResult: map[string]interface{}{"quality": 0.85},
|
||||
SecurityResult: map[string]interface{}{"score": 95.0},
|
||||
Format: "markdown",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, output.Report, "# Analysis Report")
|
||||
require.NotEmpty(t, output.ReportPath)
|
||||
|
||||
// Verify file was created
|
||||
_, err = os.Stat(output.ReportPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cleanup
|
||||
os.Remove(output.ReportPath)
|
||||
}
|
||||
|
||||
func TestGenerateReportActivity_JSON(t *testing.T) {
|
||||
output, err := GenerateReportActivity(context.Background(), GenerateReportInput{
|
||||
AnalysisResult: map[string]interface{}{"quality": 0.85},
|
||||
SecurityResult: map[string]interface{}{"score": 95.0},
|
||||
Format: "json",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, output.Report, `"timestamp"`)
|
||||
|
||||
// Cleanup
|
||||
os.Remove(output.ReportPath)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"go.temporal.io/sdk/activity"
|
||||
)
|
||||
|
||||
// activityLogger provides logging that works both in and outside Temporal context
|
||||
type activityLogger struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func newActivityLogger(ctx context.Context) *activityLogger {
|
||||
return &activityLogger{ctx: ctx}
|
||||
}
|
||||
|
||||
func (l *activityLogger) Info(msg string, args ...interface{}) {
|
||||
if activity.IsActivity(l.ctx) {
|
||||
activity.GetLogger(l.ctx).Info(msg, args...)
|
||||
} else {
|
||||
log.Printf("INFO: "+msg+" %v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *activityLogger) Warn(msg string, args ...interface{}) {
|
||||
if activity.IsActivity(l.ctx) {
|
||||
activity.GetLogger(l.ctx).Warn(msg, args...)
|
||||
} else {
|
||||
log.Printf("WARN: "+msg+" %v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *activityLogger) Error(msg string, args ...interface{}) {
|
||||
if activity.IsActivity(l.ctx) {
|
||||
activity.GetLogger(l.ctx).Error(msg, args...)
|
||||
} else {
|
||||
log.Printf("ERROR: "+msg+" %v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *activityLogger) Debug(msg string, args ...interface{}) {
|
||||
if activity.IsActivity(l.ctx) {
|
||||
activity.GetLogger(l.ctx).Debug(msg, args...)
|
||||
} else {
|
||||
log.Printf("DEBUG: "+msg+" %v", args)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/rockliang/poimen/workflows/internal/memory"
|
||||
)
|
||||
|
||||
// RetrieveMemoryInput input for RetrieveMemoryActivity
|
||||
type RetrieveMemoryInput struct {
|
||||
// Query semantic search query
|
||||
Query string `json:"query"`
|
||||
|
||||
// Project memory project (default: "poimen")
|
||||
Project string `json:"project,omitempty"`
|
||||
|
||||
// Scope retrieval scope: "skills", "lessons", "all" (default: "all")
|
||||
Scope string `json:"scope,omitempty"`
|
||||
|
||||
// Limit max results (default: 10)
|
||||
Limit int `json:"limit,omitempty"`
|
||||
|
||||
// LevelFilter filter by level: L1, L2, R (reference)
|
||||
LevelFilter []string `json:"levelFilter,omitempty"`
|
||||
|
||||
// Tool tool context for skill matching
|
||||
Tool string `json:"tool,omitempty"`
|
||||
|
||||
// Task task description for context retrieval
|
||||
Task string `json:"task,omitempty"`
|
||||
}
|
||||
|
||||
// RetrieveMemoryOutput output from RetrieveMemoryActivity
|
||||
type RetrieveMemoryOutput struct {
|
||||
// Skills relevant skills found
|
||||
Skills []MemorySkill `json:"skills"`
|
||||
|
||||
// Lessons relevant lessons/knowledge found
|
||||
Lessons []MemoryLesson `json:"lessons"`
|
||||
|
||||
// References reference documents found
|
||||
References []MemoryReference `json:"references"`
|
||||
|
||||
// TotalResults total results found
|
||||
TotalResults int `json:"totalResults"`
|
||||
|
||||
// Budget token budget info
|
||||
Budget MemoryBudget `json:"budget"`
|
||||
}
|
||||
|
||||
// MemorySkill skill from memory
|
||||
type MemorySkill struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Why string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
// MemoryLesson lesson from memory
|
||||
type MemoryLesson struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Level string `json:"level"`
|
||||
Score float32 `json:"score"`
|
||||
Breadcrumb string `json:"breadcrumb,omitempty"`
|
||||
}
|
||||
|
||||
// MemoryReference reference document from memory
|
||||
type MemoryReference struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Score float32 `json:"score"`
|
||||
Breadcrumb string `json:"breadcrumb,omitempty"`
|
||||
}
|
||||
|
||||
// MemoryBudget token budget tracking
|
||||
type MemoryBudget struct {
|
||||
Requested int `json:"requested"`
|
||||
Used int `json:"used"`
|
||||
}
|
||||
|
||||
// RetrieveMemoryActivity retrieves relevant knowledge from poimen-memory
|
||||
func RetrieveMemoryActivity(ctx context.Context, in RetrieveMemoryInput) (RetrieveMemoryOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("RetrieveMemoryActivity started", "query", in.Query, "scope", in.Scope)
|
||||
|
||||
output := RetrieveMemoryOutput{
|
||||
Skills: []MemorySkill{},
|
||||
Lessons: []MemoryLesson{},
|
||||
References: []MemoryReference{},
|
||||
}
|
||||
|
||||
// Get memory service URL and token
|
||||
baseURL := os.Getenv("POIMEN_MEMORY_URL")
|
||||
if baseURL == "" {
|
||||
baseURL = "http://poimen-memory.poimen.svc.cluster.local:8080"
|
||||
}
|
||||
|
||||
token := os.Getenv("POIMEN_MEMORY_TOKEN")
|
||||
// Token optional for internal cluster access
|
||||
|
||||
// Set defaults
|
||||
project := in.Project
|
||||
if project == "" {
|
||||
project = "poimen"
|
||||
}
|
||||
|
||||
scope := in.Scope
|
||||
if scope == "" {
|
||||
scope = "all"
|
||||
}
|
||||
|
||||
limit := in.Limit
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
client := memory.NewClient(baseURL, token)
|
||||
|
||||
// If tool/task provided, use Context API for skill matching
|
||||
if in.Tool != "" || in.Task != "" {
|
||||
contextResp, err := client.Context(ctx, &memory.ContextRequest{
|
||||
Project: project,
|
||||
Tool: in.Tool,
|
||||
Task: in.Task,
|
||||
SignatureSource: in.Query,
|
||||
Scope: "tool_context",
|
||||
Budget: 8192,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn("Context retrieval failed, falling back to query", "error", err)
|
||||
} else {
|
||||
// Extract skills
|
||||
for _, skill := range contextResp.Skills {
|
||||
output.Skills = append(output.Skills, MemorySkill{
|
||||
Name: skill.Name,
|
||||
Why: skill.Why,
|
||||
})
|
||||
}
|
||||
|
||||
// Extract lessons
|
||||
for i, lesson := range contextResp.Lessons {
|
||||
output.Lessons = append(output.Lessons, MemoryLesson{
|
||||
ID: fmt.Sprintf("ctx-%d", i),
|
||||
Text: lesson.Text,
|
||||
Level: lesson.Level,
|
||||
Score: lesson.Score,
|
||||
Breadcrumb: "",
|
||||
})
|
||||
}
|
||||
|
||||
output.Budget = MemoryBudget{
|
||||
Requested: contextResp.Budget.Requested,
|
||||
Used: contextResp.Budget.Used,
|
||||
}
|
||||
output.TotalResults = len(output.Skills) + len(output.Lessons)
|
||||
}
|
||||
}
|
||||
|
||||
// Also do semantic query for additional context
|
||||
if scope == "all" || scope == "lessons" || scope == "references" {
|
||||
levelFilter := in.LevelFilter
|
||||
if len(levelFilter) == 0 {
|
||||
levelFilter = []string{"L1", "L2"}
|
||||
}
|
||||
|
||||
queryResp, err := client.Query(ctx, &memory.QueryRequest{
|
||||
Project: project,
|
||||
Query: in.Query,
|
||||
LevelFilter: levelFilter,
|
||||
Limit: limit,
|
||||
Scope: "all",
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn("Query failed", "error", err)
|
||||
} else {
|
||||
for _, result := range queryResp.Results {
|
||||
if result.Level == "R" {
|
||||
output.References = append(output.References, MemoryReference{
|
||||
ID: result.ID,
|
||||
Text: result.Text,
|
||||
Score: result.Score,
|
||||
Breadcrumb: result.Breadcrumb,
|
||||
})
|
||||
} else {
|
||||
// Avoid duplicates from Context call
|
||||
found := false
|
||||
for _, existing := range output.Lessons {
|
||||
if existing.ID == result.ID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
output.Lessons = append(output.Lessons, MemoryLesson{
|
||||
ID: result.ID,
|
||||
Text: result.Text,
|
||||
Level: result.Level,
|
||||
Score: result.Score,
|
||||
Breadcrumb: result.Breadcrumb,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
output.TotalResults = len(output.Skills) + len(output.Lessons) + len(output.References)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("RetrieveMemoryActivity completed",
|
||||
"skills", len(output.Skills),
|
||||
"lessons", len(output.Lessons),
|
||||
"references", len(output.References))
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// FormatMemoryForPrompt formats memory output for LLM prompt injection
|
||||
func FormatMemoryForPrompt(mem RetrieveMemoryOutput) string {
|
||||
if mem.TotalResults == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var result string
|
||||
|
||||
if len(mem.Skills) > 0 {
|
||||
result += "\n## Relevant Skills\n"
|
||||
for _, skill := range mem.Skills {
|
||||
result += fmt.Sprintf("- **%s**: %s\n", skill.Name, skill.Why)
|
||||
}
|
||||
}
|
||||
|
||||
if len(mem.Lessons) > 0 {
|
||||
result += "\n## Relevant Knowledge\n"
|
||||
for _, lesson := range mem.Lessons {
|
||||
result += fmt.Sprintf("- [%s] %s\n", lesson.Level, truncate(lesson.Text, 200))
|
||||
}
|
||||
}
|
||||
|
||||
if len(mem.References) > 0 {
|
||||
result += "\n## Reference Documents\n"
|
||||
for _, ref := range mem.References {
|
||||
result += fmt.Sprintf("- %s\n", truncate(ref.Text, 200))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRetrieveMemoryActivity_Query(t *testing.T) {
|
||||
// Mock memory service
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/memory/query" {
|
||||
resp := map[string]interface{}{
|
||||
"results": []map[string]interface{}{
|
||||
{
|
||||
"id": "doc-1",
|
||||
"level": "L1",
|
||||
"score": 0.95,
|
||||
"text": "Security scanning best practices: always check for hardcoded secrets",
|
||||
},
|
||||
{
|
||||
"id": "doc-2",
|
||||
"level": "L2",
|
||||
"score": 0.85,
|
||||
"text": "Use gosec for Go security analysis",
|
||||
},
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Set env for test
|
||||
t.Setenv("POIMEN_MEMORY_URL", server.URL)
|
||||
|
||||
output, err := RetrieveMemoryActivity(context.Background(), RetrieveMemoryInput{
|
||||
Query: "security scanning",
|
||||
Project: "poimen",
|
||||
Scope: "lessons",
|
||||
Limit: 5,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 2, len(output.Lessons))
|
||||
require.Equal(t, "L1", output.Lessons[0].Level)
|
||||
require.Contains(t, output.Lessons[0].Text, "Security scanning")
|
||||
}
|
||||
|
||||
func TestRetrieveMemoryActivity_Context(t *testing.T) {
|
||||
// Mock memory service with context endpoint
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/memory/context" {
|
||||
resp := map[string]interface{}{
|
||||
"tier": 1,
|
||||
"skills": []map[string]interface{}{
|
||||
{
|
||||
"name": "security-analysis",
|
||||
"why": "User is asking about security scanning",
|
||||
},
|
||||
},
|
||||
"lessons": []map[string]interface{}{
|
||||
{
|
||||
"tier": 1,
|
||||
"level": "L1",
|
||||
"score": 0.9,
|
||||
"text": "Always scan dependencies for vulnerabilities",
|
||||
},
|
||||
},
|
||||
"budget": map[string]interface{}{
|
||||
"requested": 8192,
|
||||
"used": 1024,
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/memory/query" {
|
||||
resp := map[string]interface{}{"results": []interface{}{}}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
t.Setenv("POIMEN_MEMORY_URL", server.URL)
|
||||
|
||||
output, err := RetrieveMemoryActivity(context.Background(), RetrieveMemoryInput{
|
||||
Query: "security scan repo",
|
||||
Tool: "poimen-router",
|
||||
Task: "generate workflow for security scanning",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(output.Skills))
|
||||
require.Equal(t, "security-analysis", output.Skills[0].Name)
|
||||
require.Equal(t, 1, len(output.Lessons))
|
||||
require.Equal(t, 8192, output.Budget.Requested)
|
||||
require.Equal(t, 1024, output.Budget.Used)
|
||||
}
|
||||
|
||||
func TestFormatMemoryForPrompt(t *testing.T) {
|
||||
mem := RetrieveMemoryOutput{
|
||||
Skills: []MemorySkill{
|
||||
{Name: "security-scan", Description: "Run security scanner", Why: "Matches user intent"},
|
||||
},
|
||||
Lessons: []MemoryLesson{
|
||||
{ID: "1", Level: "L1", Text: "Always check dependencies"},
|
||||
},
|
||||
TotalResults: 2,
|
||||
}
|
||||
|
||||
result := FormatMemoryForPrompt(mem)
|
||||
|
||||
require.Contains(t, result, "## Relevant Skills")
|
||||
require.Contains(t, result, "security-scan")
|
||||
require.Contains(t, result, "## Relevant Knowledge")
|
||||
require.Contains(t, result, "Always check dependencies")
|
||||
}
|
||||
|
||||
func TestFormatMemoryForPrompt_Empty(t *testing.T) {
|
||||
mem := RetrieveMemoryOutput{
|
||||
TotalResults: 0,
|
||||
}
|
||||
|
||||
result := FormatMemoryForPrompt(mem)
|
||||
require.Equal(t, "", result)
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NotifyStatusInput is input for NotifyStatusActivity
|
||||
type NotifyStatusInput struct {
|
||||
Channel string `json:"channel"` // slack, email, webhook
|
||||
Status string `json:"status"` // success, failure, warning
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// NotifyStatusOutput is output from NotifyStatusActivity
|
||||
type NotifyStatusOutput struct {
|
||||
NotificationID string `json:"notificationId"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// NotifyStatusActivity sends notifications
|
||||
func NotifyStatusActivity(ctx context.Context, in NotifyStatusInput) (NotifyStatusOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("NotifyStatusActivity started", "channel", in.Channel, "status", in.Status)
|
||||
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
notificationID := fmt.Sprintf("notify-%d", time.Now().UnixNano())
|
||||
|
||||
switch in.Channel {
|
||||
case "slack":
|
||||
if err := sendSlackNotification(ctx, in); err != nil {
|
||||
logger.Warn("Slack notification failed", "error", err)
|
||||
// Don't fail the activity, just log
|
||||
}
|
||||
case "webhook":
|
||||
if err := sendWebhookNotification(ctx, in); err != nil {
|
||||
logger.Warn("Webhook notification failed", "error", err)
|
||||
}
|
||||
case "email":
|
||||
// Email would require SMTP setup - log for now
|
||||
logger.Info("Email notification (logged)", "message", in.Message)
|
||||
default:
|
||||
logger.Info("Notification logged", "channel", in.Channel, "message", in.Message)
|
||||
}
|
||||
|
||||
logger.Info("NotifyStatusActivity completed", "notificationId", notificationID)
|
||||
return NotifyStatusOutput{
|
||||
NotificationID: notificationID,
|
||||
Timestamp: timestamp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func sendSlackNotification(ctx context.Context, in NotifyStatusInput) error {
|
||||
webhookURL := os.Getenv("SLACK_WEBHOOK_URL")
|
||||
if webhookURL == "" {
|
||||
return fmt.Errorf("SLACK_WEBHOOK_URL not set")
|
||||
}
|
||||
|
||||
// Map status to emoji
|
||||
emoji := "ℹ️"
|
||||
switch in.Status {
|
||||
case "success":
|
||||
emoji = "✅"
|
||||
case "failure":
|
||||
emoji = "❌"
|
||||
case "warning":
|
||||
emoji = "⚠️"
|
||||
}
|
||||
|
||||
payload := map[string]string{
|
||||
"text": fmt.Sprintf("%s *%s*: %s", emoji, in.Status, in.Message),
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(payload)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("slack returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendWebhookNotification(ctx context.Context, in NotifyStatusInput) error {
|
||||
webhookURL := os.Getenv("NOTIFICATION_WEBHOOK_URL")
|
||||
if webhookURL == "" {
|
||||
return fmt.Errorf("NOTIFICATION_WEBHOOK_URL not set")
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"channel": in.Channel,
|
||||
"status": in.Status,
|
||||
"message": in.Message,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(payload)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("webhook returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ArchiveResultsInput is input for ArchiveResultsActivity
|
||||
type ArchiveResultsInput struct {
|
||||
ReportPath string `json:"reportPath"`
|
||||
Destination string `json:"destination"` // s3://bucket/path or local path
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ArchiveResultsOutput is output from ArchiveResultsActivity
|
||||
type ArchiveResultsOutput struct {
|
||||
ArchiveURL string `json:"archiveUrl"`
|
||||
ArchiveSize int64 `json:"archiveSize"`
|
||||
}
|
||||
|
||||
// ArchiveResultsActivity archives results to storage
|
||||
func ArchiveResultsActivity(ctx context.Context, in ArchiveResultsInput) (ArchiveResultsOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("ArchiveResultsActivity started", "reportPath", in.ReportPath, "destination", in.Destination)
|
||||
|
||||
// Check if source exists
|
||||
info, err := os.Stat(in.ReportPath)
|
||||
if err != nil {
|
||||
return ArchiveResultsOutput{}, fmt.Errorf("report not found: %s", in.ReportPath)
|
||||
}
|
||||
|
||||
// For now, just copy to local destination or log for cloud
|
||||
var archiveURL string
|
||||
var archiveSize int64
|
||||
|
||||
if len(in.Destination) > 5 && in.Destination[:5] == "s3://" {
|
||||
// Would use AWS SDK - for now just log
|
||||
logger.Info("Would upload to S3", "destination", in.Destination)
|
||||
archiveURL = in.Destination
|
||||
archiveSize = info.Size()
|
||||
} else {
|
||||
// Copy to local destination
|
||||
content, err := os.ReadFile(in.ReportPath)
|
||||
if err != nil {
|
||||
return ArchiveResultsOutput{}, fmt.Errorf("failed to read report: %w", err)
|
||||
}
|
||||
|
||||
destPath := in.Destination
|
||||
if destPath == "" {
|
||||
destPath = fmt.Sprintf("/tmp/archive-%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
if err := os.WriteFile(destPath, content, 0644); err != nil {
|
||||
return ArchiveResultsOutput{}, fmt.Errorf("failed to write archive: %w", err)
|
||||
}
|
||||
|
||||
archiveURL = destPath
|
||||
archiveSize = int64(len(content))
|
||||
}
|
||||
|
||||
logger.Info("ArchiveResultsActivity completed", "archiveUrl", archiveURL, "size", archiveSize)
|
||||
return ArchiveResultsOutput{
|
||||
ArchiveURL: archiveURL,
|
||||
ArchiveSize: archiveSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeploymentPreCheckInput is input for DeploymentPreCheckActivity
|
||||
type DeploymentPreCheckInput struct {
|
||||
Path string `json:"path"`
|
||||
CheckType string `json:"checkType,omitempty"` // lint, test, build, all
|
||||
}
|
||||
|
||||
// DeploymentPreCheckOutput is output from DeploymentPreCheckActivity
|
||||
type DeploymentPreCheckOutput struct {
|
||||
Passed bool `json:"passed"`
|
||||
Failures []string `json:"failures"`
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
// DeploymentPreCheckActivity validates deployment readiness
|
||||
func DeploymentPreCheckActivity(ctx context.Context, in DeploymentPreCheckInput) (DeploymentPreCheckOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("DeploymentPreCheckActivity started", "path", in.Path, "checkType", in.CheckType)
|
||||
|
||||
output := DeploymentPreCheckOutput{
|
||||
Passed: true,
|
||||
Failures: []string{},
|
||||
Warnings: []string{},
|
||||
}
|
||||
|
||||
checkType := in.CheckType
|
||||
if checkType == "" {
|
||||
checkType = "all"
|
||||
}
|
||||
|
||||
// Check if path exists
|
||||
if _, err := os.Stat(in.Path); os.IsNotExist(err) {
|
||||
output.Passed = false
|
||||
output.Failures = append(output.Failures, fmt.Sprintf("path does not exist: %s", in.Path))
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// Check for Go project
|
||||
isGo := false
|
||||
if _, err := os.Stat(fmt.Sprintf("%s/go.mod", in.Path)); err == nil {
|
||||
isGo = true
|
||||
}
|
||||
|
||||
if isGo && (checkType == "all" || checkType == "build") {
|
||||
// Try go build
|
||||
cmd := exec.CommandContext(ctx, "go", "build", "./...")
|
||||
cmd.Dir = in.Path
|
||||
if buildOut, err := cmd.CombinedOutput(); err != nil {
|
||||
output.Passed = false
|
||||
output.Failures = append(output.Failures, fmt.Sprintf("build failed: %s", string(buildOut)))
|
||||
}
|
||||
}
|
||||
|
||||
if isGo && (checkType == "all" || checkType == "test") {
|
||||
// Try go test
|
||||
cmd := exec.CommandContext(ctx, "go", "test", "-short", "./...")
|
||||
cmd.Dir = in.Path
|
||||
if testOut, err := cmd.CombinedOutput(); err != nil {
|
||||
output.Passed = false
|
||||
output.Failures = append(output.Failures, fmt.Sprintf("tests failed: %s", string(testOut)))
|
||||
}
|
||||
}
|
||||
|
||||
if isGo && (checkType == "all" || checkType == "lint") {
|
||||
// Try go vet
|
||||
cmd := exec.CommandContext(ctx, "go", "vet", "./...")
|
||||
cmd.Dir = in.Path
|
||||
if vetOut, err := cmd.CombinedOutput(); err != nil {
|
||||
output.Warnings = append(output.Warnings, fmt.Sprintf("vet issues: %s", string(vetOut)))
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("DeploymentPreCheckActivity completed", "passed", output.Passed, "failures", len(output.Failures))
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// ApproveWorkflowInput is input for ApproveWorkflowActivity
|
||||
type ApproveWorkflowInput struct {
|
||||
WorkflowID string `json:"workflowId"`
|
||||
RequiredApprovals int `json:"requiredApprovals,omitempty"`
|
||||
TimeoutMinutes int `json:"timeoutMinutes,omitempty"`
|
||||
}
|
||||
|
||||
// ApproveWorkflowOutput is output from ApproveWorkflowActivity
|
||||
type ApproveWorkflowOutput struct {
|
||||
Approved bool `json:"approved"`
|
||||
Approver string `json:"approver,omitempty"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// ApproveWorkflowActivity handles approval workflow (auto-approves for now)
|
||||
func ApproveWorkflowActivity(ctx context.Context, in ApproveWorkflowInput) (ApproveWorkflowOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("ApproveWorkflowActivity started", "workflowId", in.WorkflowID)
|
||||
|
||||
// For now, auto-approve
|
||||
// In production, this would wait for human approval via signal or external system
|
||||
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
|
||||
logger.Info("ApproveWorkflowActivity completed (auto-approved)")
|
||||
return ApproveWorkflowOutput{
|
||||
Approved: true,
|
||||
Approver: "system-auto",
|
||||
Timestamp: timestamp,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNotifyStatusActivity(t *testing.T) {
|
||||
// Test with default channel (logs only)
|
||||
output, err := NotifyStatusActivity(context.Background(), NotifyStatusInput{
|
||||
Channel: "log",
|
||||
Status: "success",
|
||||
Message: "Test notification",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEmpty(t, output.NotificationID)
|
||||
require.NotEmpty(t, output.Timestamp)
|
||||
}
|
||||
|
||||
func TestNotifyStatusActivity_AllStatuses(t *testing.T) {
|
||||
statuses := []string{"success", "failure", "warning"}
|
||||
|
||||
for _, status := range statuses {
|
||||
t.Run(status, func(t *testing.T) {
|
||||
output, err := NotifyStatusActivity(context.Background(), NotifyStatusInput{
|
||||
Channel: "log",
|
||||
Status: status,
|
||||
Message: "Test " + status,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, output.NotificationID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveResultsActivity(t *testing.T) {
|
||||
// Create temp source file
|
||||
tmpDir, err := os.MkdirTemp("", "archive-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
srcPath := filepath.Join(tmpDir, "report.txt")
|
||||
require.NoError(t, os.WriteFile(srcPath, []byte("test report content"), 0644))
|
||||
|
||||
// Archive to local destination
|
||||
destPath := filepath.Join(tmpDir, "archive.txt")
|
||||
output, err := ArchiveResultsActivity(context.Background(), ArchiveResultsInput{
|
||||
ReportPath: srcPath,
|
||||
Destination: destPath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, destPath, output.ArchiveURL)
|
||||
require.Greater(t, output.ArchiveSize, int64(0))
|
||||
|
||||
// Verify file was copied
|
||||
content, err := os.ReadFile(destPath)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test report content", string(content))
|
||||
}
|
||||
|
||||
func TestArchiveResultsActivity_NotFound(t *testing.T) {
|
||||
_, err := ArchiveResultsActivity(context.Background(), ArchiveResultsInput{
|
||||
ReportPath: "/nonexistent/file.txt",
|
||||
Destination: "/tmp/archive.txt",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestDeploymentPreCheckActivity(t *testing.T) {
|
||||
// Create temp directory with valid Go code
|
||||
tmpDir, err := os.MkdirTemp("", "precheck-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create go.mod
|
||||
goMod := `module test
|
||||
go 1.21
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goMod), 0644))
|
||||
|
||||
// Create valid Go file
|
||||
goCode := `package main
|
||||
|
||||
func main() {
|
||||
println("hello")
|
||||
}
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(goCode), 0644))
|
||||
|
||||
// Run pre-check
|
||||
output, err := DeploymentPreCheckActivity(context.Background(), DeploymentPreCheckInput{
|
||||
Path: tmpDir,
|
||||
CheckType: "build",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, output.Passed)
|
||||
require.Empty(t, output.Failures)
|
||||
}
|
||||
|
||||
func TestDeploymentPreCheckActivity_PathNotExist(t *testing.T) {
|
||||
output, err := DeploymentPreCheckActivity(context.Background(), DeploymentPreCheckInput{
|
||||
Path: "/nonexistent/path",
|
||||
})
|
||||
require.NoError(t, err) // Activity doesn't error, just reports failure
|
||||
|
||||
require.False(t, output.Passed)
|
||||
require.NotEmpty(t, output.Failures)
|
||||
}
|
||||
|
||||
func TestApproveWorkflowActivity(t *testing.T) {
|
||||
output, err := ApproveWorkflowActivity(context.Background(), ApproveWorkflowInput{
|
||||
WorkflowID: "test-workflow-123",
|
||||
RequiredApprovals: 1,
|
||||
TimeoutMinutes: 60,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Auto-approved
|
||||
require.True(t, output.Approved)
|
||||
require.Equal(t, "system-auto", output.Approver)
|
||||
require.NotEmpty(t, output.Timestamp)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/rockliang/poimen/workflows/internal/routing"
|
||||
"go.temporal.io/sdk/activity"
|
||||
)
|
||||
|
||||
// LLMRouterActivity is the Temporal activity that routes user requests to workflows
|
||||
func LLMRouterActivity(ctx context.Context, input routing.LLMRouterInput) (*routing.LLMRouterOutput, error) {
|
||||
logger := activity.GetLogger(ctx)
|
||||
logger.Info("LLMRouterActivity started", "message", input.Message)
|
||||
|
||||
// Load knowledge base
|
||||
kb, err := routing.LoadKnowledgeBaseFromDefaultPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load knowledge base: %w", err)
|
||||
}
|
||||
|
||||
// Create router
|
||||
router, err := routing.NewLLMRouter(kb)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create router: %w", err)
|
||||
}
|
||||
|
||||
// Route the request
|
||||
output, err := router.Route(ctx, input)
|
||||
if err != nil {
|
||||
logger.Error("LLMRouterActivity failed", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if output.IsCron {
|
||||
logger.Info("LLMRouterActivity completed (cron)",
|
||||
"workflowName", output.CronSpec.Name,
|
||||
"schedule", output.CronSpec.Schedule,
|
||||
"stateCount", len(output.CronSpec.States))
|
||||
} else {
|
||||
logger.Info("LLMRouterActivity completed",
|
||||
"workflowName", output.Spec.Name,
|
||||
"stateCount", len(output.Spec.States))
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// ValidateWorkflowSpecActivity validates a workflow spec before execution
|
||||
func ValidateWorkflowSpecActivity(ctx context.Context, spec routing.WorkflowSpec) (*routing.ValidationResult, error) {
|
||||
logger := activity.GetLogger(ctx)
|
||||
logger.Info("ValidateWorkflowSpecActivity started", "workflowName", spec.Name)
|
||||
|
||||
kb, err := routing.LoadKnowledgeBaseFromDefaultPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load knowledge base: %w", err)
|
||||
}
|
||||
|
||||
validator := routing.NewValidator(kb)
|
||||
result := validator.ValidateWorkflowSpec(&spec)
|
||||
|
||||
logger.Info("ValidateWorkflowSpecActivity completed",
|
||||
"valid", result.Valid,
|
||||
"errorCount", len(result.Errors))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ValidateCronWorkflowSpecActivity validates a cron workflow spec before scheduling
|
||||
func ValidateCronWorkflowSpecActivity(ctx context.Context, spec routing.CronWorkflowSpec) (*routing.ValidationResult, error) {
|
||||
logger := activity.GetLogger(ctx)
|
||||
logger.Info("ValidateCronWorkflowSpecActivity started",
|
||||
"workflowName", spec.Name,
|
||||
"schedule", spec.Schedule)
|
||||
|
||||
kb, err := routing.LoadKnowledgeBaseFromDefaultPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load knowledge base: %w", err)
|
||||
}
|
||||
|
||||
validator := routing.NewValidator(kb)
|
||||
result := validator.ValidateCronWorkflowSpec(&spec)
|
||||
|
||||
logger.Info("ValidateCronWorkflowSpecActivity completed",
|
||||
"valid", result.Valid,
|
||||
"errorCount", len(result.Errors))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
# Agent Prompts
|
||||
|
||||
LLM agent prompts for Poimen RoutingWorkflow.
|
||||
|
||||
## Agents
|
||||
|
||||
| Agent | Model | Purpose | Prompt File |
|
||||
|-------|-------|---------|-------------|
|
||||
| **Router** | `reasoning` | Natural language → WorkflowSpec | [router/AGENTS.md](router/AGENTS.md) |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User Request: "Scan repo X for security issues"
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ RetrieveMemoryActivity │
|
||||
│ Query poimen-memory for relevant skills/lessons│
|
||||
└─────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Router Agent (LLM) │
|
||||
│ Input: message + memory context + activities │
|
||||
│ Output: WorkflowSpec JSON │
|
||||
└─────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ RoutingWorkflow │
|
||||
│ Executes WorkflowSpec as state machine │
|
||||
│ Clone → Scan → Report → Notify │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Integration
|
||||
|
||||
Router receives context from `RetrieveMemoryActivity`:
|
||||
|
||||
```
|
||||
1. User: "scan repo for security"
|
||||
2. RetrieveMemoryActivity queries poimen-memory
|
||||
3. Returns: skills, lessons, references
|
||||
4. Injected into Router prompt
|
||||
5. Router generates smarter WorkflowSpec
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LLM Endpoint
|
||||
|
||||
Default: `https://api.riotpiao.com/v1/chat/completions`
|
||||
|
||||
Override: `LOCAL_LLM_BASE_URL=http://localhost:11434`
|
||||
@@ -0,0 +1,91 @@
|
||||
# Router Agent
|
||||
|
||||
**Purpose**: Intelligent workflow router that analyzes user requests and generates workflow specs.
|
||||
|
||||
**Model**: `reasoning` (via api.riotpiao.com)
|
||||
|
||||
**When Used**: User submits natural language request → Router generates WorkflowSpec/CronWorkflowSpec
|
||||
|
||||
---
|
||||
|
||||
## System Prompt
|
||||
|
||||
```
|
||||
You are an intelligent workflow router. Your job is to:
|
||||
1. Understand what the user wants to accomplish
|
||||
2. Select the appropriate activities from the available list
|
||||
3. Order them correctly based on dependencies
|
||||
4. Extract any parameters mentioned (URLs, branches, etc)
|
||||
5. Detect if user wants scheduled/recurring execution
|
||||
6. Use any relevant knowledge from memory to inform your decisions
|
||||
|
||||
Rules:
|
||||
- Always include CloneRepoActivity first if any analysis activity is needed
|
||||
- Order activities respecting dependencies
|
||||
- If user mentions "daily", "every hour", "weekly", etc → set isCron=true and cronSchedule
|
||||
- Common cron patterns: "0 2 * * *" (2 AM daily), "0 * * * *" (hourly), "0 0 * * 0" (weekly Sunday)
|
||||
- Extract repo URLs, branch names, severity levels from the message
|
||||
- workflowName should be short and descriptive (kebab-case)
|
||||
- If memory context includes relevant skills or lessons, incorporate that knowledge
|
||||
- Skills from memory may suggest specific activity parameters or ordering
|
||||
|
||||
Output ONLY valid JSON.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Prompt Template
|
||||
|
||||
```
|
||||
User request: {{.Message}}
|
||||
{{if .Context}}
|
||||
Provided context: {{.Context}}
|
||||
{{end}}
|
||||
{{if .MemoryContext}}
|
||||
Relevant skills from memory:
|
||||
{{range .MemoryContext.Skills}}- {{.Name}}: {{.Description}} (reason: {{.Why}})
|
||||
{{end}}
|
||||
Relevant knowledge from memory:
|
||||
{{range .MemoryContext.Lessons}}- [{{.Level}}] {{.Text}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
Available activities:
|
||||
{{range .Activities}}- {{.Name}}: {{.Description}} (category: {{.Category}}, timeout: {{.Timeout}}, flaky: {{.IsFlaky}})
|
||||
{{end}}
|
||||
Analyze the request and output JSON with:
|
||||
- activities: ordered list of activity names to execute
|
||||
- parameters: extracted parameters from request (repo URL, branch, etc)
|
||||
- isCron: true if user wants scheduled/recurring execution
|
||||
- cronSchedule: cron expression if scheduled (e.g., "0 2 * * *" for 2 AM daily)
|
||||
- cronTimezone: timezone (default "UTC")
|
||||
- workflowName: short descriptive name
|
||||
- errorHandling: "retry" (default), "fail-fast", or "continue"
|
||||
|
||||
Output ONLY valid JSON, no explanation.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expected Output Format
|
||||
|
||||
```json
|
||||
{
|
||||
"activities": ["CloneRepoActivity", "SecurityScanActivity", "GenerateReportActivity"],
|
||||
"parameters": {
|
||||
"repo": "https://github.com/example/repo",
|
||||
"branch": "main",
|
||||
"severity": "high"
|
||||
},
|
||||
"isCron": false,
|
||||
"cronSchedule": "",
|
||||
"cronTimezone": "UTC",
|
||||
"workflowName": "security-scan-example",
|
||||
"errorHandling": "retry"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Source File
|
||||
|
||||
`internal/routing/llm_router.go`
|
||||
+143
-3
@@ -2,9 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -13,20 +15,27 @@ import (
|
||||
"github.com/rockliang/poimen/workflows/internal/config"
|
||||
"github.com/rockliang/poimen/workflows/internal/health"
|
||||
"github.com/rockliang/poimen/workflows/internal/logging"
|
||||
"github.com/rockliang/poimen/workflows/internal/routing"
|
||||
"github.com/rockliang/poimen/workflows/statemachine"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
// Orchestrator flags
|
||||
repoPath = flag.String("repo", "", "target repo path")
|
||||
remoteURL = flag.String("remote", "", "remote URL")
|
||||
milestone = flag.String("milestone", "T0", "milestone ID")
|
||||
dryRun = flag.Bool("dry-run", false, "disable git push/merge")
|
||||
dryRun = flag.Bool("dry-run", false, "disable git push/merge (orchestrator) or skip submit (routing)")
|
||||
plannerModel = flag.String("planner-model", "reasoning", "planner model ID (local-llm)")
|
||||
judgeModel = flag.String("judge-model", "reasoning", "judge model ID (local-llm)")
|
||||
implementerModel = flag.String("implementer-model", "ornith:35b", "implementer model ID (local-llm ornith)")
|
||||
piProvider = flag.String("pi-provider", "local-llm", "pi provider name for skills (local-llm)")
|
||||
healthCheck = flag.Bool("health", false, "check health and exit")
|
||||
|
||||
// Routing workflow flags
|
||||
routeMsg = flag.String("route", "", "natural language message for LLM routing")
|
||||
specFile = flag.String("spec", "", "JSON workflow spec file (direct submit, skip LLM)")
|
||||
cronSpec = flag.Bool("cron", false, "treat spec as CronWorkflowSpec")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
@@ -66,9 +75,15 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required flags for workflow start
|
||||
// Handle routing workflow mode
|
||||
if *routeMsg != "" || *specFile != "" {
|
||||
runRoutingWorkflow(c, *routeMsg, *specFile, *cronSpec, *dryRun)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required flags for orchestrator workflow
|
||||
if *repoPath == "" || *remoteURL == "" {
|
||||
logging.Fatal("--repo and --remote flags are required")
|
||||
logging.Fatal("--repo and --remote flags are required (or use --route/--spec for routing workflow)")
|
||||
}
|
||||
|
||||
|
||||
@@ -157,3 +172,128 @@ func main() {
|
||||
fmt.Printf("\nWorkflow completed: %+v\n", result)
|
||||
}
|
||||
}
|
||||
|
||||
// runRoutingWorkflow handles --route and --spec flags
|
||||
func runRoutingWorkflow(c client.Client, routeMsg, specFile string, isCron, dryRun bool) {
|
||||
ctx := context.Background()
|
||||
|
||||
var spec *routing.WorkflowSpec
|
||||
var cronSpec *routing.CronWorkflowSpec
|
||||
|
||||
if specFile != "" {
|
||||
// Load spec from file
|
||||
data, err := os.ReadFile(specFile)
|
||||
if err != nil {
|
||||
logging.Fatal("failed to read spec file", logging.Err(err))
|
||||
}
|
||||
|
||||
validator := routing.NewValidator(nil) // nil KB = skip activity validation
|
||||
if isCron {
|
||||
cronSpec = &routing.CronWorkflowSpec{}
|
||||
if err := json.Unmarshal(data, cronSpec); err != nil {
|
||||
logging.Fatal("failed to parse cron spec", logging.Err(err))
|
||||
}
|
||||
// Validate
|
||||
result := validator.ValidateCronWorkflowSpec(cronSpec)
|
||||
if !result.Valid {
|
||||
logging.Fatal("invalid cron spec", logging.String("errors", result.String()))
|
||||
}
|
||||
} else {
|
||||
spec = &routing.WorkflowSpec{}
|
||||
if err := json.Unmarshal(data, spec); err != nil {
|
||||
logging.Fatal("failed to parse spec", logging.Err(err))
|
||||
}
|
||||
// Validate
|
||||
result := validator.ValidateWorkflowSpec(spec)
|
||||
if !result.Valid {
|
||||
logging.Fatal("invalid spec", logging.String("errors", result.String()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use LLM router
|
||||
logging.Info("routing message via LLM", logging.String("message", routeMsg))
|
||||
|
||||
kb, err := routing.LoadKnowledgeBaseFromDefaultPath()
|
||||
if err != nil {
|
||||
logging.Fatal("failed to load knowledge base", logging.Err(err))
|
||||
}
|
||||
|
||||
router, err := routing.NewLLMRouter(kb)
|
||||
if err != nil {
|
||||
logging.Fatal("failed to create LLM router", logging.Err(err))
|
||||
}
|
||||
|
||||
output, err := router.Route(ctx, routing.LLMRouterInput{Message: routeMsg})
|
||||
if err != nil {
|
||||
logging.Fatal("LLM routing failed", logging.Err(err))
|
||||
}
|
||||
|
||||
if output.IsCron {
|
||||
cronSpec = output.CronSpec
|
||||
fmt.Printf("\n=== Generated Cron Spec ===\n")
|
||||
fmt.Printf("Name: %s\n", cronSpec.Name)
|
||||
fmt.Printf("Schedule: %s\n", cronSpec.Schedule)
|
||||
fmt.Printf("States: %d\n", len(cronSpec.States))
|
||||
} else {
|
||||
spec = output.Spec
|
||||
fmt.Printf("\n=== Generated Workflow Spec ===\n")
|
||||
fmt.Printf("Name: %s\n", spec.Name)
|
||||
fmt.Printf("States: %d\n", len(spec.States))
|
||||
}
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
fmt.Printf("\n[dry-run] Spec generated but not submitted\n")
|
||||
if spec != nil {
|
||||
data, _ := json.MarshalIndent(spec, "", " ")
|
||||
fmt.Printf("%s\n", data)
|
||||
} else if cronSpec != nil {
|
||||
data, _ := json.MarshalIndent(cronSpec, "", " ")
|
||||
fmt.Printf("%s\n", data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Submit to Temporal
|
||||
if cronSpec != nil {
|
||||
// For cron, we'd use Temporal's schedule feature
|
||||
// For now, just start as regular workflow (cron scheduling TBD)
|
||||
spec = &routing.WorkflowSpec{
|
||||
Name: cronSpec.Name,
|
||||
Input: cronSpec.Input,
|
||||
States: cronSpec.States,
|
||||
}
|
||||
logging.Warn("cron scheduling not yet implemented, running as one-shot workflow")
|
||||
}
|
||||
|
||||
workflowID := "routing-" + spec.Name + "-" + time.Now().Format("20060102-150405")
|
||||
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||
|
||||
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||
ID: workflowID,
|
||||
TaskQueue: "poimen-taskqueue",
|
||||
}, statemachine.RoutingWorkflow, input)
|
||||
if err != nil {
|
||||
logging.Fatal("failed to start routing workflow", logging.Err(err))
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== Routing Workflow Started ===\n")
|
||||
fmt.Printf("Workflow ID: %s\n", workflowID)
|
||||
fmt.Printf("Run ID: %s\n", run.GetRunID())
|
||||
|
||||
// Wait briefly for result
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var result statemachine.RoutingWorkflowOutput
|
||||
if err := run.Get(waitCtx, &result); err != nil {
|
||||
fmt.Printf("\nWorkflow running (check Temporal UI for status)\n")
|
||||
} else {
|
||||
fmt.Printf("\nWorkflow completed: %s\n", result.Status)
|
||||
if len(result.StepResults) > 0 {
|
||||
for step, res := range result.StepResults {
|
||||
fmt.Printf(" %s: %v\n", step, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ func main() {
|
||||
w.RegisterWorkflow(statemachine.OrchestratorWorkflow)
|
||||
w.RegisterWorkflow(statemachine.TaskUnitWorkflow)
|
||||
w.RegisterWorkflow(statemachine.TestWorkflow)
|
||||
w.RegisterWorkflow(statemachine.RoutingWorkflow)
|
||||
|
||||
// Register all activities
|
||||
w.RegisterActivity(action.CloneRepoActivity)
|
||||
@@ -68,6 +69,25 @@ func main() {
|
||||
// w.RegisterActivity(action.UpdateLessonsActivity)
|
||||
// w.RegisterActivity(action.ReadLessonsActivity)
|
||||
|
||||
// Routing workflow activities
|
||||
w.RegisterActivity(action.LLMRouterActivity)
|
||||
w.RegisterActivity(action.ValidateWorkflowSpecActivity)
|
||||
w.RegisterActivity(action.ValidateCronWorkflowSpecActivity)
|
||||
|
||||
// Analysis activities
|
||||
w.RegisterActivity(action.AnalyzeCodeActivity)
|
||||
w.RegisterActivity(action.SecurityScanActivity)
|
||||
w.RegisterActivity(action.GenerateReportActivity)
|
||||
|
||||
// Notification and utility activities
|
||||
w.RegisterActivity(action.NotifyStatusActivity)
|
||||
w.RegisterActivity(action.ArchiveResultsActivity)
|
||||
w.RegisterActivity(action.DeploymentPreCheckActivity)
|
||||
w.RegisterActivity(action.ApproveWorkflowActivity)
|
||||
|
||||
// Memory activities
|
||||
w.RegisterActivity(action.RetrieveMemoryActivity)
|
||||
|
||||
// Initialize health checker
|
||||
healthChecker := health.NewChecker(c)
|
||||
healthHandler := health.NewHandler(healthChecker)
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
// Example: External service integrating with Poimen RoutingWorkflow
|
||||
// Shows how to submit a task and wait for completion
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"go.temporal.io/sdk/client"
|
||||
)
|
||||
|
||||
// ---- Types (mirror internal/routing/types.go) ----
|
||||
|
||||
type WorkflowSpec struct {
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input,omitempty"`
|
||||
States []State `json:"states"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // Task, Pass, Fail
|
||||
Resource string `json:"resource,omitempty"`
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty"`
|
||||
Timeout string `json:"timeout,omitempty"`
|
||||
Next string `json:"next,omitempty"`
|
||||
End bool `json:"end,omitempty"`
|
||||
}
|
||||
|
||||
type RoutingWorkflowInput struct {
|
||||
Spec *WorkflowSpec `json:"spec"`
|
||||
}
|
||||
|
||||
type RoutingWorkflowOutput struct {
|
||||
Status string `json:"status"`
|
||||
StepResults map[string]map[string]interface{} `json:"stepResults"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ---- Example Service ----
|
||||
|
||||
type TaskService struct {
|
||||
temporalClient client.Client
|
||||
taskQueue string
|
||||
}
|
||||
|
||||
func NewTaskService(temporalHost, namespace, taskQueue string) (*TaskService, error) {
|
||||
c, err := client.Dial(client.Options{
|
||||
HostPort: temporalHost,
|
||||
Namespace: namespace,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Temporal: %w", err)
|
||||
}
|
||||
|
||||
return &TaskService{
|
||||
temporalClient: c,
|
||||
taskQueue: taskQueue,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *TaskService) Close() {
|
||||
s.temporalClient.Close()
|
||||
}
|
||||
|
||||
// SubmitAndWait submits a workflow spec and waits for completion
|
||||
func (s *TaskService) SubmitAndWait(ctx context.Context, spec *WorkflowSpec, timeout time.Duration) (*RoutingWorkflowOutput, error) {
|
||||
workflowID := fmt.Sprintf("%s-%d", spec.Name, time.Now().UnixNano())
|
||||
|
||||
// Start workflow
|
||||
run, err := s.temporalClient.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||
ID: workflowID,
|
||||
TaskQueue: s.taskQueue,
|
||||
}, "RoutingWorkflow", RoutingWorkflowInput{Spec: spec})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start workflow: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Workflow started: ID=%s, RunID=%s", run.GetID(), run.GetRunID())
|
||||
|
||||
// Wait for completion with timeout
|
||||
waitCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
var result RoutingWorkflowOutput
|
||||
if err := run.Get(waitCtx, &result); err != nil {
|
||||
return nil, fmt.Errorf("workflow failed: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SubmitAsync submits workflow and returns immediately (fire-and-forget)
|
||||
func (s *TaskService) SubmitAsync(ctx context.Context, spec *WorkflowSpec) (workflowID string, runID string, err error) {
|
||||
workflowID = fmt.Sprintf("%s-%d", spec.Name, time.Now().UnixNano())
|
||||
|
||||
run, err := s.temporalClient.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||
ID: workflowID,
|
||||
TaskQueue: s.taskQueue,
|
||||
}, "RoutingWorkflow", RoutingWorkflowInput{Spec: spec})
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to start workflow: %w", err)
|
||||
}
|
||||
|
||||
return run.GetID(), run.GetRunID(), nil
|
||||
}
|
||||
|
||||
// WaitForCompletion waits for an existing workflow to complete
|
||||
func (s *TaskService) WaitForCompletion(ctx context.Context, workflowID string, timeout time.Duration) (*RoutingWorkflowOutput, error) {
|
||||
run := s.temporalClient.GetWorkflow(ctx, workflowID, "")
|
||||
|
||||
waitCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
var result RoutingWorkflowOutput
|
||||
if err := run.Get(waitCtx, &result); err != nil {
|
||||
return nil, fmt.Errorf("workflow failed: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetStatus gets current workflow status without waiting
|
||||
func (s *TaskService) GetStatus(ctx context.Context, workflowID string) (string, error) {
|
||||
desc, err := s.temporalClient.DescribeWorkflowExecution(ctx, workflowID, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return desc.WorkflowExecutionInfo.Status.String(), nil
|
||||
}
|
||||
|
||||
// ---- Example Usage ----
|
||||
|
||||
func main() {
|
||||
// Connect to Temporal
|
||||
svc, err := NewTaskService(
|
||||
"temporal-frontend.temporal:7233",
|
||||
"poimen-harness",
|
||||
"poimen-taskqueue",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer svc.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Example 1: Implement task T0.3 with code analysis
|
||||
fmt.Println("=== Example 1: Submit and Wait ===")
|
||||
spec := &WorkflowSpec{
|
||||
Name: "implement-T0.3",
|
||||
Input: map[string]interface{}{
|
||||
"taskId": "T0.3",
|
||||
"description": "Implement git worktree management",
|
||||
"repo": "https://github.com/rockliang/poimen",
|
||||
},
|
||||
States: []State{
|
||||
{
|
||||
Name: "Clone",
|
||||
Type: "Task",
|
||||
Resource: "CloneRepoActivity",
|
||||
Parameters: map[string]interface{}{
|
||||
"repo": "${workflow.input.repo}",
|
||||
"branch": "main",
|
||||
},
|
||||
Timeout: "5m",
|
||||
Next: "Analyze",
|
||||
},
|
||||
{
|
||||
Name: "Analyze",
|
||||
Type: "Task",
|
||||
Resource: "AnalyzeCodeActivity",
|
||||
Parameters: map[string]interface{}{
|
||||
"path": "${Clone.output.path}",
|
||||
"depth": 3,
|
||||
},
|
||||
Timeout: "10m",
|
||||
Next: "Report",
|
||||
},
|
||||
{
|
||||
Name: "Report",
|
||||
Type: "Task",
|
||||
Resource: "GenerateReportActivity",
|
||||
Parameters: map[string]interface{}{
|
||||
"analysisResult": "${Analyze.output}",
|
||||
"format": "markdown",
|
||||
},
|
||||
Timeout: "2m",
|
||||
End: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := svc.SubmitAndWait(ctx, spec, 30*time.Minute)
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Status: %s\n", result.Status)
|
||||
for step, output := range result.StepResults {
|
||||
fmt.Printf(" %s: %v\n", step, output)
|
||||
}
|
||||
}
|
||||
|
||||
// Example 2: Fire and forget, then poll
|
||||
fmt.Println("\n=== Example 2: Async Submit + Poll ===")
|
||||
workflowID, runID, err := svc.SubmitAsync(ctx, spec)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Submitted: workflowID=%s, runID=%s\n", workflowID, runID)
|
||||
|
||||
// Poll status
|
||||
for i := 0; i < 5; i++ {
|
||||
status, _ := svc.GetStatus(ctx, workflowID)
|
||||
fmt.Printf(" Poll %d: status=%s\n", i+1, status)
|
||||
if status == "WORKFLOW_EXECUTION_STATUS_COMPLETED" {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
// Get final result
|
||||
result, err = svc.WaitForCompletion(ctx, workflowID, 30*time.Minute)
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
} else {
|
||||
resultJSON, _ := json.MarshalIndent(result, "", " ")
|
||||
fmt.Printf("Final result:\n%s\n", resultJSON)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Example: WaitForTaskComplete pattern
|
||||
// Use case: External service submits implementation task, waits for result
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.temporal.io/sdk/client"
|
||||
)
|
||||
|
||||
// WaitForTaskComplete - the core pattern
|
||||
//
|
||||
// 1. Build workflow spec for the task
|
||||
// 2. Submit to Temporal
|
||||
// 3. Block until completion or timeout
|
||||
// 4. Return result
|
||||
func WaitForTaskComplete(
|
||||
c client.Client,
|
||||
taskID string,
|
||||
repo string,
|
||||
timeout time.Duration,
|
||||
) (map[string]interface{}, error) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Build spec for implementation task
|
||||
spec := map[string]interface{}{
|
||||
"name": fmt.Sprintf("implement-%s", taskID),
|
||||
"input": map[string]interface{}{
|
||||
"taskId": taskID,
|
||||
"repo": repo,
|
||||
},
|
||||
"states": []map[string]interface{}{
|
||||
{
|
||||
"name": "Clone",
|
||||
"type": "Task",
|
||||
"resource": "CloneRepoActivity",
|
||||
"parameters": map[string]interface{}{
|
||||
"repo": repo,
|
||||
},
|
||||
"next": "Analyze",
|
||||
},
|
||||
{
|
||||
"name": "Analyze",
|
||||
"type": "Task",
|
||||
"resource": "AnalyzeCodeActivity",
|
||||
"parameters": map[string]interface{}{
|
||||
"path": "${Clone.output.path}",
|
||||
},
|
||||
"next": "SecurityScan",
|
||||
},
|
||||
{
|
||||
"name": "SecurityScan",
|
||||
"type": "Task",
|
||||
"resource": "SecurityScanActivity",
|
||||
"parameters": map[string]interface{}{
|
||||
"path": "${Clone.output.path}",
|
||||
},
|
||||
"next": "Report",
|
||||
},
|
||||
{
|
||||
"name": "Report",
|
||||
"type": "Task",
|
||||
"resource": "GenerateReportActivity",
|
||||
"parameters": map[string]interface{}{
|
||||
"analysisResult": "${Analyze.output}",
|
||||
"securityResult": "${SecurityScan.output}",
|
||||
},
|
||||
"end": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Submit workflow
|
||||
workflowID := fmt.Sprintf("%s-%d", taskID, time.Now().UnixNano())
|
||||
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||
ID: workflowID,
|
||||
TaskQueue: "poimen-taskqueue",
|
||||
}, "RoutingWorkflow", map[string]interface{}{"spec": spec})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("submit failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Workflow started: %s\n", taskID, workflowID)
|
||||
|
||||
// Wait for completion
|
||||
waitCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := run.Get(waitCtx, &result); err != nil {
|
||||
return nil, fmt.Errorf("workflow failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Workflow completed: %s\n", taskID, result["status"])
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Example usage in another service:
|
||||
//
|
||||
// func (s *MyService) ImplementTask(taskID string) error {
|
||||
// c, _ := client.Dial(client.Options{
|
||||
// HostPort: "temporal-frontend.temporal:7233",
|
||||
// Namespace: "poimen-harness",
|
||||
// })
|
||||
// defer c.Close()
|
||||
//
|
||||
// result, err := WaitForTaskComplete(c, taskID, "https://github.com/...", 30*time.Minute)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// // Process result
|
||||
// if result["status"] == "COMPLETED" {
|
||||
// report := result["stepResults"].(map[string]interface{})["Report"]
|
||||
// // Use report...
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
@@ -19,6 +19,7 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nexus-rpc/nexus-proto-annotations v0.1.0 // indirect
|
||||
github.com/nexus-rpc/sdk-go v0.7.0 // indirect
|
||||
@@ -26,6 +27,8 @@ require (
|
||||
github.com/prometheus/common v0.70.1 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/robfig/cron v1.2.0 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
go.temporal.io/api v1.63.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
|
||||
@@ -2,6 +2,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
|
||||
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
@@ -22,6 +23,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4z
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||
@@ -50,6 +53,11 @@ github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
@@ -81,6 +89,7 @@ go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
||||
@@ -1,531 +0,0 @@
|
||||
# Poimen Memory Service Integration
|
||||
|
||||
Go client for Poimen Memory Service with **Temporal Activities**. Provides create, update, retrieve, and context operations for knowledge management with full workflow integration, retry logic, and observability.
|
||||
|
||||
## Overview
|
||||
|
||||
Memory service endpoints:
|
||||
- **POST /memory/ingest** — Create knowledge records (L1/L2/reference)
|
||||
- **POST /memory/query** — Search knowledge (hybrid semantic+lexical)
|
||||
- **POST /memory/context** — Retrieve context (three-tier: signature → vector → reference)
|
||||
- **GET /memory/vault** — Browse vault files
|
||||
- **GET /health** — Health check
|
||||
|
||||
## Temporal Activities
|
||||
|
||||
All operations are **Temporal Activities** with:
|
||||
- ✅ Automatic retries (3 attempts by default)
|
||||
- ✅ Timeout handling (per operation)
|
||||
- ✅ Heartbeat monitoring
|
||||
- ✅ Logging + observability
|
||||
- ✅ Workflow integration
|
||||
|
||||
### Activity List
|
||||
|
||||
| Activity | Purpose |
|
||||
|----------|---------|
|
||||
| `CreateKnowledgeActivity` | Create L1/L2/reference records |
|
||||
| `UpdateKnowledgeActivity` | Update existing knowledge |
|
||||
| `SearchKnowledgeActivity` | Search hybrid (semantic+lexical) |
|
||||
| `GetContextActivity` | Retrieve three-tier context |
|
||||
| `GetVaultActivity` | Browse vault files |
|
||||
| `HealthCheckActivity` | Check service health |
|
||||
| `LearnFromExecutionActivity` | Learn from task results |
|
||||
| `DiagnoseIssueActivity` | Diagnose tool/task issues |
|
||||
| `AnalyzeErrorActivity` | Analyze errors, find solutions |
|
||||
| `DocumentDecisionActivity` | Record workflow decisions |
|
||||
| `SearchAndApplyActivity` | Search and apply knowledge |
|
||||
| `RefreshMemoryActivity` | Periodic memory refresh |
|
||||
|
||||
### Register Activities
|
||||
|
||||
In worker setup:
|
||||
|
||||
```go
|
||||
service := memory.NewService(baseURL, token, project)
|
||||
memory.RegisterMemoryActivities(w, service)
|
||||
```
|
||||
|
||||
### Use in Workflows
|
||||
|
||||
```go
|
||||
// Simple activity call
|
||||
id, err := memory.ExecuteCreateKnowledge(
|
||||
ctx,
|
||||
&memory.KnowledgeRecord{
|
||||
Level: "L1",
|
||||
Content: "...",
|
||||
},
|
||||
nil, // Use default options
|
||||
)
|
||||
|
||||
// Custom retry policy
|
||||
options := &memory.ActivityOptions{
|
||||
RetryAttempts: 5,
|
||||
RetryBackoff: time.Second,
|
||||
}
|
||||
recommendations, err := memory.ExecuteDiagnoseIssue(ctx, "kubectl", "pod-crash", options)
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Import package:
|
||||
```go
|
||||
import "github.com/poimen/workflows/internal/memory"
|
||||
```
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
### Example 1: Learning Workflow
|
||||
|
||||
```go
|
||||
// Learn from task execution
|
||||
func LearningWorkflow(ctx workflow.Context, taskID string) (string, error) {
|
||||
// Execute task (placeholder)
|
||||
result := fmt.Sprintf("Task %s completed successfully", taskID)
|
||||
|
||||
// Learn from result
|
||||
knowledgeID, err := memory.ExecuteLearnFromExecution(
|
||||
ctx,
|
||||
taskID,
|
||||
result,
|
||||
[]string{"success", taskID},
|
||||
nil, // Default retry policy
|
||||
)
|
||||
return knowledgeID, err
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Diagnostic Workflow
|
||||
|
||||
```go
|
||||
// Diagnose issue using memory service
|
||||
func DiagnosticWorkflow(ctx workflow.Context, tool, issue string) ([]string, error) {
|
||||
recommendations, err := memory.ExecuteDiagnoseIssue(
|
||||
ctx,
|
||||
tool,
|
||||
issue,
|
||||
&memory.ActivityOptions{
|
||||
RetryAttempts: 3,
|
||||
RetryBackoff: time.Second,
|
||||
},
|
||||
)
|
||||
return recommendations, err
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Error Recovery
|
||||
|
||||
```go
|
||||
// Analyze error and find recovery path
|
||||
func ErrorRecoveryWorkflow(ctx workflow.Context, errorMsg string) ([]string, error) {
|
||||
// Analyze error
|
||||
records, err := memory.ExecuteAnalyzeError(ctx, errorMsg, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract recovery steps
|
||||
recovery := make([]string, 0)
|
||||
for _, record := range records {
|
||||
if record.Level == "L1" { // High confidence
|
||||
recovery = append(recovery, record.Content)
|
||||
}
|
||||
}
|
||||
return recovery, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Multi-Step Decision Workflow
|
||||
|
||||
```go
|
||||
// Get context, make decision, document it
|
||||
func ContextualDecisionWorkflow(ctx workflow.Context, tool, task, decision string) (string, error) {
|
||||
// Get context (three-tier retrieval)
|
||||
svcCtx, err := memory.ExecuteGetContext(ctx, tool, task, 8192, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Make decision based on context
|
||||
reasoning := fmt.Sprintf("Based on %d lessons (tier %d)", len(svcCtx.Lessons), svcCtx.Tier)
|
||||
|
||||
// Document decision
|
||||
docID, err := memory.ExecuteDocumentDecision(ctx, tool, decision, reasoning, nil)
|
||||
return docID, err
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Client (Low-Level)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/poimen/workflows/internal/memory"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create client
|
||||
client := memory.NewClient(
|
||||
"http://localhost:8080",
|
||||
"your-jwt-token",
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Ingest knowledge
|
||||
resp, err := client.Ingest(ctx, &memory.IngestRequest{
|
||||
Project: "poimen",
|
||||
Source: "workflow://task-123",
|
||||
Kind: "L1",
|
||||
Text: "Pod CrashLoopBackOff: check logs with kubectl logs",
|
||||
Metadata: map[string]interface{}{
|
||||
"topic": "kubernetes",
|
||||
"task_id": "debug-pod",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Created: %s (SHA256: %s)\n", resp.ID, resp.SHA256)
|
||||
|
||||
// Search knowledge
|
||||
query, err := client.Query(ctx, &memory.QueryRequest{
|
||||
Project: "poimen",
|
||||
Query: "fix pod crash loop",
|
||||
Limit: 5,
|
||||
Floor: 0.6, // minimum relevance
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, r := range query.Results {
|
||||
fmt.Printf("%s (score: %.2f): %s\n", r.Level, r.Score, r.Text)
|
||||
}
|
||||
|
||||
// Get context (three-tier retrieval)
|
||||
ctxResp, err := client.Context(ctx, &memory.ContextRequest{
|
||||
Project: "poimen",
|
||||
Tool: "kubectl",
|
||||
Task: "debug-pod",
|
||||
SignatureSource: "error_log",
|
||||
Budget: 8192,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Context tier: %d\n", ctxResp.Tier)
|
||||
for _, lesson := range ctxResp.Lessons {
|
||||
fmt.Printf("- [Tier %d] %s: %.2f\n", lesson.Tier, lesson.Level, lesson.Score)
|
||||
}
|
||||
|
||||
// Browse vault
|
||||
vault, err := client.Vault(ctx, "poimen")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Total records: %d\n", vault.TotalRecords)
|
||||
for _, f := range vault.Files {
|
||||
fmt.Printf("- %s (%s, %d records)\n", f.Path, f.Level, f.RecordCount)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service (High-Level)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/poimen/workflows/internal/memory"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create service
|
||||
svc := memory.NewService(
|
||||
"http://localhost:8080",
|
||||
"your-jwt-token",
|
||||
"poimen", // project
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create knowledge
|
||||
id, err := svc.CreateKnowledge(ctx, &memory.KnowledgeRecord{
|
||||
Level: "L1",
|
||||
Title: "Pod Debugging",
|
||||
Content: "To debug CrashLoopBackOff: kubectl logs <pod>",
|
||||
Source: "workflow://debug-task",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Created knowledge: %s\n", id)
|
||||
|
||||
// Update knowledge (re-ingest with same ID)
|
||||
id, err = svc.UpdateKnowledge(ctx, &memory.KnowledgeRecord{
|
||||
ID: id,
|
||||
Level: "L2",
|
||||
Content: "Advanced debugging: check events, describe pod, check node status",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Updated knowledge: %s\n", id)
|
||||
|
||||
// Retrieve knowledge
|
||||
records, err := svc.RetrieveKnowledge(ctx, "kubernetes pod debugging", &memory.RetrievalOptions{
|
||||
LevelFilter: []string{"L1", "L2"},
|
||||
Limit: 10,
|
||||
Floor: 0.7,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, rec := range records {
|
||||
log.Printf("- %s: %s\n", rec.ID, rec.Content)
|
||||
}
|
||||
|
||||
// Retrieve context
|
||||
svcCtx, err := svc.RetrieveContext(ctx, "kubectl", "debug-pod", 8192)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Context tier: %d (%d lessons, %d skills)\n",
|
||||
svcCtx.Tier, len(svcCtx.Lessons), len(svcCtx.Skills))
|
||||
for _, skill := range svcCtx.Skills {
|
||||
log.Printf(" - %s: %s\n", skill.Name, skill.Why)
|
||||
}
|
||||
|
||||
// Get vault
|
||||
files, err := svc.GetVault(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Vault has %d files\n", len(files))
|
||||
|
||||
// Check health
|
||||
if svc.IsHealthy(ctx) {
|
||||
log.Println("Memory service is healthy")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Client Methods
|
||||
|
||||
#### Ingest(ctx, req) → IngestResponse, error
|
||||
Create knowledge record.
|
||||
|
||||
Request:
|
||||
```go
|
||||
&IngestRequest{
|
||||
Project: "poimen",
|
||||
Source: "workflow://task-id",
|
||||
Kind: "L1", // L1|L2|reference
|
||||
Text: "knowledge content",
|
||||
Metadata: map[string]interface{}{...},
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```go
|
||||
{
|
||||
ID: "chunk-abc123",
|
||||
SHA256: "de12cd34ef56...",
|
||||
QueueStatus: "pending", // Async processing
|
||||
IdempotencyID: "sess-123:0",
|
||||
}
|
||||
```
|
||||
|
||||
#### Query(ctx, req) → QueryResponse, error
|
||||
Search knowledge (hybrid semantic + lexical).
|
||||
|
||||
Request:
|
||||
```go
|
||||
&QueryRequest{
|
||||
Project: "poimen",
|
||||
Query: "fix kubernetes pod crash",
|
||||
LevelFilter: []string{"L1", "L2"}, // Optional
|
||||
Floor: 0.6, // Minimum relevance
|
||||
Limit: 10,
|
||||
Scope: "all", // learned|reference|all
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```go
|
||||
{
|
||||
Query: "...",
|
||||
Results: []QueryResult{
|
||||
{
|
||||
ID: "chunk-abc123",
|
||||
Level: "L1",
|
||||
Score: 0.992,
|
||||
SemanticScore: 1.0,
|
||||
LexicalScore: 0.98,
|
||||
Text: "...",
|
||||
Breadcrumb: "kubernetes.md > Troubleshooting",
|
||||
Source: "transcript://session-123",
|
||||
},
|
||||
...
|
||||
},
|
||||
TotalHits: 127,
|
||||
SearchTimeMS: 145,
|
||||
}
|
||||
```
|
||||
|
||||
#### Context(ctx, req) → ContextResponse, error
|
||||
Retrieve context for tool/task (three-tier retrieval: signature → vector → reference).
|
||||
|
||||
Request:
|
||||
```go
|
||||
&ContextRequest{
|
||||
Project: "poimen",
|
||||
Tool: "kubectl",
|
||||
Task: "debug-pod",
|
||||
SignatureSource: "failure_log", // Where to find signature
|
||||
Scope: "tool_context",
|
||||
Budget: 8192, // Max response bytes
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```go
|
||||
{
|
||||
Tier: 1, // Highest tier with results
|
||||
Lessons: []ContextLesson{
|
||||
{
|
||||
Tier: 1,
|
||||
Level: "L1",
|
||||
Score: 1.0,
|
||||
Text: "Pod in CrashLoopBackOff: check logs",
|
||||
MatchedKind: "signature",
|
||||
SeenCount: 23,
|
||||
LastSeen: "2025-01-28T15:30:00Z",
|
||||
},
|
||||
...
|
||||
},
|
||||
Skills: []ContextSkill{
|
||||
{
|
||||
Name: "diagnose-pod-failure",
|
||||
Why: "Tier-1 signature matched",
|
||||
},
|
||||
},
|
||||
Budget: {
|
||||
Requested: 8192,
|
||||
Used: 4156,
|
||||
Dropped: 0,
|
||||
Degradation: nil,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Vault(ctx, project) → VaultResponse, error
|
||||
Browse vault files.
|
||||
|
||||
Response:
|
||||
```go
|
||||
{
|
||||
Project: "poimen",
|
||||
Files: []VaultFile{
|
||||
{
|
||||
Path: "kubernetes/debugging.md",
|
||||
Title: "Debugging",
|
||||
Level: "L1",
|
||||
UpdatedAt: "2025-01-28T10:00:00Z",
|
||||
RecordCount: 23,
|
||||
},
|
||||
...
|
||||
},
|
||||
TotalRecords: 542,
|
||||
}
|
||||
```
|
||||
|
||||
#### Health(ctx) → bool, error
|
||||
Check service health.
|
||||
|
||||
### Service Methods
|
||||
|
||||
Service provides higher-level operations:
|
||||
|
||||
- `CreateKnowledge(ctx, record)` → id, error
|
||||
- `UpdateKnowledge(ctx, record)` → id, error
|
||||
- `RetrieveKnowledge(ctx, query, opts)` → []KnowledgeRecord, error
|
||||
- `RetrieveContext(ctx, tool, task, budget)` → *ServiceContext, error
|
||||
- `GetVault(ctx)` → []VaultInfo, error
|
||||
- `IsHealthy(ctx)` → bool
|
||||
|
||||
## Error Handling
|
||||
|
||||
```go
|
||||
// All operations return (result, error)
|
||||
resp, err := client.Ingest(ctx, req)
|
||||
if err != nil {
|
||||
// Possible errors:
|
||||
// - Request marshal/network errors
|
||||
// - 401 Unauthorized: Missing/invalid JWT
|
||||
// - 403 Forbidden: Token lacks capability
|
||||
// - 429 Too Many Requests: Rate limit exceeded
|
||||
// - 409 Conflict: Duplicate (same idempotency key within 24h)
|
||||
// - 503 Service Unavailable: Database unreachable
|
||||
log.Fatalf("ingest failed: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
Pass JWT bearer token to NewClient/NewService:
|
||||
|
||||
```go
|
||||
// Get token from Authentik
|
||||
token := "eyJ0eXAiOiJKV1QiLCJhbGc..."
|
||||
|
||||
client := memory.NewClient(baseURL, token)
|
||||
```
|
||||
|
||||
Token must have capability:
|
||||
- `memory:read` — for Query, Context, Vault
|
||||
- `memory:write` — for Ingest
|
||||
|
||||
## Rate Limits
|
||||
|
||||
Per JWT identity:
|
||||
- Ingest: 100/hour
|
||||
- Query: 1000/hour
|
||||
- Context: 100/hour
|
||||
|
||||
Exceed limit → 429 Too Many Requests.
|
||||
|
||||
## Deployment
|
||||
|
||||
Memory service endpoints (k8s):
|
||||
- Service: `memory-service.poimen.svc.cluster.local:8080`
|
||||
- Ingress: `https://memory.riotpiao.com` (external)
|
||||
|
||||
Environment:
|
||||
```go
|
||||
baseURL := "http://memory-service.poimen.svc.cluster.local:8080"
|
||||
token := os.Getenv("MEMORY_SERVICE_TOKEN")
|
||||
svc := memory.NewService(baseURL, token, "poimen")
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
go test ./internal/memory -v
|
||||
```
|
||||
|
||||
Mock server example in `client_test.go` and `service_test.go`.
|
||||
@@ -341,10 +341,76 @@
|
||||
"dependencies": [],
|
||||
"notes": "Network-dependent. May fail on network issues or service throttling. Retry 2x."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "RetrieveMemoryActivity",
|
||||
"description": "Retrieve relevant knowledge, skills, and lessons from poimen-memory semantic search",
|
||||
"category": "memory",
|
||||
"inputs": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Semantic search query",
|
||||
"required": true
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "Memory project (default: poimen)",
|
||||
"required": false,
|
||||
"default": "poimen"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"description": "Retrieval scope: skills, lessons, references, all",
|
||||
"required": false,
|
||||
"default": "all"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max results to return",
|
||||
"required": false,
|
||||
"default": 10
|
||||
},
|
||||
"tool": {
|
||||
"type": "string",
|
||||
"description": "Tool context for skill matching",
|
||||
"required": false
|
||||
},
|
||||
"task": {
|
||||
"type": "string",
|
||||
"description": "Task description for context retrieval",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"description": "Relevant skills found"
|
||||
},
|
||||
"lessons": {
|
||||
"type": "array",
|
||||
"description": "Relevant lessons/knowledge found"
|
||||
},
|
||||
"references": {
|
||||
"type": "array",
|
||||
"description": "Reference documents found"
|
||||
},
|
||||
"totalResults": {
|
||||
"type": "integer",
|
||||
"description": "Total results found"
|
||||
}
|
||||
},
|
||||
"constraints": {
|
||||
"defaultTimeout": "30s",
|
||||
"isFlaky": true,
|
||||
"recommendedRetries": 2,
|
||||
"retryBackoff": 1.5,
|
||||
"dependencies": [],
|
||||
"notes": "Network-dependent. First activity to run for context-aware routing. Fast timeout."
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"totalActivities": 8,
|
||||
"totalActivities": 9,
|
||||
"lastUpdated": "2025-08-31T00:00:00Z",
|
||||
"categories": {
|
||||
"repository": 1,
|
||||
@@ -354,7 +420,8 @@
|
||||
"deployment": 1,
|
||||
"notification": 1,
|
||||
"approval": 1,
|
||||
"storage": 1
|
||||
"storage": 1,
|
||||
"memory": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// KnowledgeBase represents the activity knowledge base
|
||||
@@ -72,6 +73,21 @@ func LoadKnowledgeBaseFromDefaultPath() (*KnowledgeBase, error) {
|
||||
return LoadKnowledgeBase("internal/routing/activity_knowledge_base.json")
|
||||
}
|
||||
|
||||
// Try from parent directory (for tests running from tests/ dir)
|
||||
if _, err := os.Stat("../internal/routing/activity_knowledge_base.json"); err == nil {
|
||||
return LoadKnowledgeBase("../internal/routing/activity_knowledge_base.json")
|
||||
}
|
||||
|
||||
// Try using runtime to find package directory
|
||||
_, filename, _, ok := runtime.Caller(0)
|
||||
if ok {
|
||||
pkgDir := filepath.Dir(filename)
|
||||
path := filepath.Join(pkgDir, "activity_knowledge_base.json")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return LoadKnowledgeBase(path)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("activity_knowledge_base.json not found in any expected location")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
// llmBaseURL is the base URL for the LLM API
|
||||
llmBaseURL string
|
||||
)
|
||||
|
||||
func init() {
|
||||
llmBaseURL = os.Getenv("LOCAL_LLM_BASE_URL")
|
||||
if llmBaseURL == "" {
|
||||
llmBaseURL = "https://api.riotpiao.com"
|
||||
}
|
||||
}
|
||||
|
||||
// LLMClient is a simple LLM client for routing
|
||||
type LLMClient struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewLLMClient creates a new LLM client
|
||||
func NewLLMClient() *LLMClient {
|
||||
return &LLMClient{
|
||||
baseURL: llmBaseURL,
|
||||
httpClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// llmRequest is the request body for the OpenAI-compatible API
|
||||
type llmRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []llmMessage `json:"messages"`
|
||||
Stream bool `json:"stream"`
|
||||
}
|
||||
|
||||
type llmMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// llmResponse is the response from the OpenAI-compatible API
|
||||
type llmResponse struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
// Chat sends a chat completion request
|
||||
func (c *LLMClient) Chat(ctx context.Context, systemPrompt, userMessage string) (string, error) {
|
||||
req := llmRequest{
|
||||
Model: "reasoning",
|
||||
Messages: []llmMessage{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{Role: "user", Content: userMessage},
|
||||
},
|
||||
Stream: false,
|
||||
}
|
||||
|
||||
reqBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST",
|
||||
fmt.Sprintf("%s/v1/chat/completions", c.baseURL),
|
||||
bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create HTTP request: %w", err)
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to connect to LLM API at %s: %w", c.baseURL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("LLM API returned status %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
var respObj llmResponse
|
||||
if err := json.Unmarshal(respBody, &respObj); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
if len(respObj.Choices) == 0 {
|
||||
return "", fmt.Errorf("no choices in response from LLM API")
|
||||
}
|
||||
|
||||
return respObj.Choices[0].Message.Content, nil
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LLMRouterInput is input to the llm-router activity
|
||||
type LLMRouterInput struct {
|
||||
Message string `json:"message"`
|
||||
Context map[string]interface{} `json:"context,omitempty"` // Optional context (repo, branch, etc)
|
||||
MemoryContext *MemoryContext `json:"memoryContext,omitempty"` // Optional memory retrieval results
|
||||
UseMemory bool `json:"useMemory,omitempty"` // Enable memory retrieval (default: false)
|
||||
}
|
||||
|
||||
// MemoryContext holds retrieved memory for prompt injection
|
||||
type MemoryContext struct {
|
||||
Skills []MemorySkill `json:"skills"`
|
||||
Lessons []MemoryLesson `json:"lessons"`
|
||||
References []MemoryReference `json:"references"`
|
||||
}
|
||||
|
||||
// MemorySkill from memory service
|
||||
type MemorySkill struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Why string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
// MemoryLesson from memory service
|
||||
type MemoryLesson struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
// MemoryReference from memory service
|
||||
type MemoryReference struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// LLMRouterOutput is output from the llm-router activity
|
||||
type LLMRouterOutput struct {
|
||||
Spec *WorkflowSpec `json:"spec,omitempty"`
|
||||
CronSpec *CronWorkflowSpec `json:"cronSpec,omitempty"`
|
||||
IsCron bool `json:"isCron"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// LLMRouter orchestrates intent analysis and spec generation
|
||||
type LLMRouter struct {
|
||||
client *LLMClient
|
||||
knowledgeBase *KnowledgeBase
|
||||
}
|
||||
|
||||
// NewLLMRouter creates a new LLM router
|
||||
func NewLLMRouter(kb *KnowledgeBase) (*LLMRouter, error) {
|
||||
return &LLMRouter{
|
||||
client: NewLLMClient(),
|
||||
knowledgeBase: kb,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Route analyzes user message and generates appropriate workflow spec
|
||||
func (r *LLMRouter) Route(ctx context.Context, input LLMRouterInput) (*LLMRouterOutput, error) {
|
||||
// 1. Analyze intent using LLM
|
||||
intent, err := r.analyzeIntent(ctx, input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("intent analysis failed: %w", err)
|
||||
}
|
||||
|
||||
// 2. Build workflow spec based on intent
|
||||
if intent.IsCron {
|
||||
cronSpec, err := r.buildCronSpec(intent, input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cron spec build failed: %w", err)
|
||||
}
|
||||
return &LLMRouterOutput{
|
||||
CronSpec: cronSpec,
|
||||
IsCron: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
spec, err := r.buildSpec(intent, input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("spec build failed: %w", err)
|
||||
}
|
||||
|
||||
return &LLMRouterOutput{
|
||||
Spec: spec,
|
||||
IsCron: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Intent represents analyzed user intent
|
||||
type Intent struct {
|
||||
Activities []string `json:"activities"` // Selected activity names
|
||||
Parameters map[string]interface{} `json:"parameters"` // Extracted parameters
|
||||
IsCron bool `json:"isCron"` // Is scheduled workflow?
|
||||
CronSchedule string `json:"cronSchedule"` // Cron expression if scheduled
|
||||
CronTimezone string `json:"cronTimezone"` // Timezone for cron
|
||||
WorkflowName string `json:"workflowName"` // Generated workflow name
|
||||
ErrorHandling string `json:"errorHandling"` // "retry", "fail-fast", "continue"
|
||||
}
|
||||
|
||||
// analyzeIntent uses LLM to understand user request
|
||||
func (r *LLMRouter) analyzeIntent(ctx context.Context, input LLMRouterInput) (*Intent, error) {
|
||||
// Build prompt with knowledge base context
|
||||
prompt := r.buildIntentPrompt(input)
|
||||
|
||||
// Call LLM
|
||||
response, err := r.client.Chat(ctx, intentSystemPrompt, prompt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LLM call failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse LLM response
|
||||
intent, err := parseIntentResponse(response)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse intent: %w", err)
|
||||
}
|
||||
|
||||
// Validate activities exist
|
||||
for _, actName := range intent.Activities {
|
||||
if !r.knowledgeBase.HasActivity(actName) {
|
||||
return nil, fmt.Errorf("unknown activity: %s", actName)
|
||||
}
|
||||
}
|
||||
|
||||
return intent, nil
|
||||
}
|
||||
|
||||
// buildIntentPrompt creates the prompt for intent analysis
|
||||
func (r *LLMRouter) buildIntentPrompt(input LLMRouterInput) string {
|
||||
// Get activity summaries
|
||||
var activityList strings.Builder
|
||||
for _, act := range r.knowledgeBase.Activities {
|
||||
activityList.WriteString(fmt.Sprintf("- %s: %s (category: %s, timeout: %s, flaky: %v)\n",
|
||||
act.Name, act.Description, act.Category,
|
||||
act.Constraints.DefaultTimeout, act.Constraints.IsFlaky))
|
||||
}
|
||||
|
||||
// Build context string
|
||||
contextStr := ""
|
||||
if len(input.Context) > 0 {
|
||||
ctxBytes, _ := json.Marshal(input.Context)
|
||||
contextStr = fmt.Sprintf("\nProvided context: %s", string(ctxBytes))
|
||||
}
|
||||
|
||||
// Build memory context string
|
||||
memoryStr := r.formatMemoryContext(input.MemoryContext)
|
||||
|
||||
return fmt.Sprintf(`User request: %s
|
||||
%s%s
|
||||
Available activities:
|
||||
%s
|
||||
Analyze the request and output JSON with:
|
||||
- activities: ordered list of activity names to execute
|
||||
- parameters: extracted parameters from request (repo URL, branch, etc)
|
||||
- isCron: true if user wants scheduled/recurring execution
|
||||
- cronSchedule: cron expression if scheduled (e.g., "0 2 * * *" for 2 AM daily)
|
||||
- cronTimezone: timezone (default "UTC")
|
||||
- workflowName: short descriptive name
|
||||
- errorHandling: "retry" (default), "fail-fast", or "continue"
|
||||
|
||||
Output ONLY valid JSON, no explanation.`, input.Message, contextStr, memoryStr, activityList.String())
|
||||
}
|
||||
|
||||
// formatMemoryContext formats memory context for prompt injection
|
||||
func (r *LLMRouter) formatMemoryContext(mem *MemoryContext) string {
|
||||
if mem == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
if len(mem.Skills) > 0 {
|
||||
sb.WriteString("\n\nRelevant skills from memory:\n")
|
||||
for _, skill := range mem.Skills {
|
||||
if skill.Why != "" {
|
||||
sb.WriteString(fmt.Sprintf("- %s: %s (reason: %s)\n", skill.Name, skill.Description, skill.Why))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("- %s: %s\n", skill.Name, skill.Description))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(mem.Lessons) > 0 {
|
||||
sb.WriteString("\nRelevant knowledge from memory:\n")
|
||||
for _, lesson := range mem.Lessons {
|
||||
text := lesson.Text
|
||||
if len(text) > 300 {
|
||||
text = text[:300] + "..."
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("- [%s] %s\n", lesson.Level, text))
|
||||
}
|
||||
}
|
||||
|
||||
if len(mem.References) > 0 {
|
||||
sb.WriteString("\nReference documents:\n")
|
||||
for _, ref := range mem.References {
|
||||
text := ref.Text
|
||||
if len(text) > 200 {
|
||||
text = text[:200] + "..."
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("- %s\n", text))
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// parseIntentResponse extracts Intent from LLM response
|
||||
func parseIntentResponse(response string) (*Intent, error) {
|
||||
// Try to extract JSON from response
|
||||
response = strings.TrimSpace(response)
|
||||
|
||||
// Handle markdown code blocks
|
||||
if strings.HasPrefix(response, "```") {
|
||||
re := regexp.MustCompile("```(?:json)?\\s*([\\s\\S]*?)```")
|
||||
matches := re.FindStringSubmatch(response)
|
||||
if len(matches) > 1 {
|
||||
response = strings.TrimSpace(matches[1])
|
||||
}
|
||||
}
|
||||
|
||||
var intent Intent
|
||||
if err := json.Unmarshal([]byte(response), &intent); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON from LLM: %w\nResponse: %s", err, response)
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if intent.CronTimezone == "" {
|
||||
intent.CronTimezone = "UTC"
|
||||
}
|
||||
if intent.ErrorHandling == "" {
|
||||
intent.ErrorHandling = "retry"
|
||||
}
|
||||
if intent.WorkflowName == "" {
|
||||
intent.WorkflowName = "generated-workflow"
|
||||
}
|
||||
|
||||
return &intent, nil
|
||||
}
|
||||
|
||||
// buildSpec creates WorkflowSpec from intent
|
||||
func (r *LLMRouter) buildSpec(intent *Intent, input LLMRouterInput) (*WorkflowSpec, error) {
|
||||
if len(intent.Activities) == 0 {
|
||||
return nil, fmt.Errorf("no activities selected")
|
||||
}
|
||||
|
||||
states := make([]State, 0, len(intent.Activities)+1)
|
||||
|
||||
// Build states for each activity
|
||||
for i, actName := range intent.Activities {
|
||||
act := r.knowledgeBase.GetActivity(actName)
|
||||
|
||||
state := State{
|
||||
Name: actName,
|
||||
Type: StateTypeTask,
|
||||
Resource: actName,
|
||||
Parameters: r.buildParameters(act, intent, i),
|
||||
Timeout: act.Constraints.DefaultTimeout,
|
||||
Retry: r.buildRetryPolicy(act, intent),
|
||||
}
|
||||
|
||||
// Set next state or end
|
||||
if i < len(intent.Activities)-1 {
|
||||
state.Next = intent.Activities[i+1]
|
||||
} else {
|
||||
state.End = true
|
||||
}
|
||||
|
||||
// Add catch clause for flaky activities
|
||||
if act.Constraints.IsFlaky && intent.ErrorHandling != "fail-fast" {
|
||||
state.Catch = []CatchClause{
|
||||
{
|
||||
ErrorEquals: []string{"ActivityError", "TimeoutError"},
|
||||
Next: "HandleError",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
states = append(states, state)
|
||||
}
|
||||
|
||||
// Add error handler if needed
|
||||
hasFlaky := false
|
||||
for _, actName := range intent.Activities {
|
||||
act := r.knowledgeBase.GetActivity(actName)
|
||||
if act != nil && act.Constraints.IsFlaky {
|
||||
hasFlaky = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasFlaky && intent.ErrorHandling != "fail-fast" {
|
||||
states = append(states, State{
|
||||
Name: "HandleError",
|
||||
Type: StateTypeFail,
|
||||
Error: "WorkflowError",
|
||||
Cause: "Activity failed after retries",
|
||||
})
|
||||
}
|
||||
|
||||
// Build input map
|
||||
inputMap := make(map[string]interface{})
|
||||
for k, v := range intent.Parameters {
|
||||
inputMap[k] = v
|
||||
}
|
||||
for k, v := range input.Context {
|
||||
if _, exists := inputMap[k]; !exists {
|
||||
inputMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return &WorkflowSpec{
|
||||
Name: intent.WorkflowName,
|
||||
Input: inputMap,
|
||||
States: states,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildCronSpec creates CronWorkflowSpec from intent
|
||||
func (r *LLMRouter) buildCronSpec(intent *Intent, input LLMRouterInput) (*CronWorkflowSpec, error) {
|
||||
// First build regular spec
|
||||
spec, err := r.buildSpec(intent, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get schedule - check both intent and parameters (LLM sometimes puts it in parameters)
|
||||
schedule := intent.CronSchedule
|
||||
if schedule == "" {
|
||||
if sched, ok := intent.Parameters["cronSchedule"].(string); ok {
|
||||
schedule = sched
|
||||
}
|
||||
}
|
||||
if schedule == "" {
|
||||
if sched, ok := spec.Input["cronSchedule"].(string); ok {
|
||||
schedule = sched
|
||||
delete(spec.Input, "cronSchedule") // Remove from input
|
||||
}
|
||||
}
|
||||
|
||||
// Get timezone
|
||||
timezone := intent.CronTimezone
|
||||
if timezone == "" {
|
||||
if tz, ok := intent.Parameters["cronTimezone"].(string); ok {
|
||||
timezone = tz
|
||||
}
|
||||
}
|
||||
if timezone == "" {
|
||||
if tz, ok := spec.Input["cronTimezone"].(string); ok {
|
||||
timezone = tz
|
||||
delete(spec.Input, "cronTimezone") // Remove from input
|
||||
}
|
||||
}
|
||||
if timezone == "" {
|
||||
timezone = "UTC"
|
||||
}
|
||||
|
||||
return &CronWorkflowSpec{
|
||||
Name: spec.Name,
|
||||
Type: "CronWorkflow",
|
||||
Schedule: schedule,
|
||||
Timezone: timezone,
|
||||
Input: spec.Input,
|
||||
States: spec.States,
|
||||
MaxConcurrent: 1,
|
||||
Timeout: "1h",
|
||||
EnableHistory: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildParameters creates parameter map for activity
|
||||
func (r *LLMRouter) buildParameters(act *ActivityMetadata, intent *Intent, stateIndex int) map[string]interface{} {
|
||||
params := make(map[string]interface{})
|
||||
|
||||
for inputName, inputDef := range act.Inputs {
|
||||
// Check if parameter was extracted from intent
|
||||
if val, ok := intent.Parameters[inputName]; ok {
|
||||
params[inputName] = val
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for JSONPath reference from previous state
|
||||
if stateIndex > 0 {
|
||||
prevAct := intent.Activities[stateIndex-1]
|
||||
prevActDef := r.knowledgeBase.GetActivity(prevAct)
|
||||
|
||||
// Look for matching output from previous activity
|
||||
for outName := range prevActDef.Outputs {
|
||||
if outName == inputName || strings.EqualFold(outName, inputName) {
|
||||
params[inputName] = fmt.Sprintf("${%s.output.%s}", prevAct, outName)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use default if available
|
||||
if params[inputName] == nil && inputDef.Default != nil {
|
||||
params[inputName] = inputDef.Default
|
||||
}
|
||||
|
||||
// Use input reference for common fields
|
||||
if params[inputName] == nil {
|
||||
if inputName == "repo" || inputName == "path" || inputName == "branch" {
|
||||
params[inputName] = fmt.Sprintf("${input.%s}", inputName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// buildRetryPolicy creates retry policy based on activity constraints
|
||||
func (r *LLMRouter) buildRetryPolicy(act *ActivityMetadata, intent *Intent) *RetryPolicy {
|
||||
if intent.ErrorHandling == "fail-fast" {
|
||||
return &RetryPolicy{
|
||||
MaxAttempts: 1,
|
||||
BackoffRate: 1.0,
|
||||
InitialInterval: "1s",
|
||||
}
|
||||
}
|
||||
|
||||
return &RetryPolicy{
|
||||
MaxAttempts: int32(act.Constraints.RecommendedRetries),
|
||||
BackoffRate: act.Constraints.RetryBackoff,
|
||||
InitialInterval: "1s",
|
||||
MaxInterval: "30s",
|
||||
}
|
||||
}
|
||||
|
||||
const intentSystemPrompt = `You are an intelligent workflow router. Your job is to:
|
||||
1. Understand what the user wants to accomplish
|
||||
2. Select the appropriate activities from the available list
|
||||
3. Order them correctly based on dependencies
|
||||
4. Extract any parameters mentioned (URLs, branches, etc)
|
||||
5. Detect if user wants scheduled/recurring execution
|
||||
6. Use any relevant knowledge from memory to inform your decisions
|
||||
|
||||
Rules:
|
||||
- Always include CloneRepoActivity first if any analysis activity is needed
|
||||
- Order activities respecting dependencies
|
||||
- If user mentions "daily", "every hour", "weekly", etc → set isCron=true and cronSchedule
|
||||
- Common cron patterns: "0 2 * * *" (2 AM daily), "0 * * * *" (hourly), "0 0 * * 0" (weekly Sunday)
|
||||
- Extract repo URLs, branch names, severity levels from the message
|
||||
- workflowName should be short and descriptive (kebab-case)
|
||||
- If memory context includes relevant skills or lessons, incorporate that knowledge
|
||||
- Skills from memory may suggest specific activity parameters or ordering
|
||||
|
||||
Output ONLY valid JSON.`
|
||||
@@ -0,0 +1,103 @@
|
||||
// +build integration
|
||||
|
||||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestLLMRouterIntegration tests against real api.riotpiao.com
|
||||
// Run with: go test -tags=integration -v -run TestLLMRouterIntegration
|
||||
func TestLLMRouterIntegration(t *testing.T) {
|
||||
// Skip if not explicitly enabled
|
||||
if os.Getenv("RUN_INTEGRATION_TESTS") != "1" {
|
||||
t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=1 to run.")
|
||||
}
|
||||
|
||||
kb, err := LoadKnowledgeBaseFromDefaultPath()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load knowledge base: %v", err)
|
||||
}
|
||||
|
||||
router, err := NewLLMRouter(kb)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create router: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input LLMRouterInput
|
||||
validate func(*testing.T, *LLMRouterOutput)
|
||||
}{
|
||||
{
|
||||
name: "analyze repo request",
|
||||
input: LLMRouterInput{
|
||||
Message: "Analyze the GitHub repo https://github.com/rockliang/poimen for code quality and security issues",
|
||||
Context: map[string]interface{}{
|
||||
"branch": "main",
|
||||
},
|
||||
},
|
||||
validate: func(t *testing.T, output *LLMRouterOutput) {
|
||||
if output.IsCron {
|
||||
t.Error("expected one-time workflow, not cron")
|
||||
}
|
||||
if output.Spec == nil {
|
||||
t.Fatal("expected spec, got nil")
|
||||
}
|
||||
if len(output.Spec.States) < 2 {
|
||||
t.Errorf("expected at least 2 states, got %d", len(output.Spec.States))
|
||||
}
|
||||
// Should start with CloneRepoActivity
|
||||
if output.Spec.States[0].Resource != "CloneRepoActivity" {
|
||||
t.Errorf("expected first activity to be CloneRepoActivity, got %s", output.Spec.States[0].Resource)
|
||||
}
|
||||
t.Logf("Generated workflow: %s with %d states", output.Spec.Name, len(output.Spec.States))
|
||||
for i, state := range output.Spec.States {
|
||||
t.Logf(" State %d: %s (%s)", i, state.Name, state.Resource)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "daily security scan (cron)",
|
||||
input: LLMRouterInput{
|
||||
Message: "Run a security scan on https://github.com/rockliang/poimen every day at 3 AM UTC",
|
||||
},
|
||||
validate: func(t *testing.T, output *LLMRouterOutput) {
|
||||
if !output.IsCron {
|
||||
t.Error("expected cron workflow")
|
||||
}
|
||||
if output.CronSpec == nil {
|
||||
t.Fatal("expected cron spec, got nil")
|
||||
}
|
||||
if output.CronSpec.Schedule == "" {
|
||||
t.Error("expected cron schedule")
|
||||
}
|
||||
t.Logf("Generated cron workflow: %s, schedule: %s", output.CronSpec.Name, output.CronSpec.Schedule)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
output, err := router.Route(ctx, tt.input)
|
||||
if err != nil {
|
||||
t.Fatalf("Route failed: %v", err)
|
||||
}
|
||||
|
||||
// Pretty print output
|
||||
jsonOut, _ := json.MarshalIndent(output, "", " ")
|
||||
t.Logf("Output:\n%s", string(jsonOut))
|
||||
|
||||
if tt.validate != nil {
|
||||
tt.validate(t, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseIntentResponse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
response string
|
||||
wantErr bool
|
||||
validate func(*testing.T, *Intent)
|
||||
}{
|
||||
{
|
||||
name: "basic intent",
|
||||
response: `{
|
||||
"activities": ["CloneRepoActivity", "AnalyzeCodeActivity"],
|
||||
"parameters": {"repo": "https://github.com/test/repo"},
|
||||
"isCron": false,
|
||||
"workflowName": "analyze-repo"
|
||||
}`,
|
||||
wantErr: false,
|
||||
validate: func(t *testing.T, intent *Intent) {
|
||||
if len(intent.Activities) != 2 {
|
||||
t.Errorf("expected 2 activities, got %d", len(intent.Activities))
|
||||
}
|
||||
if intent.Activities[0] != "CloneRepoActivity" {
|
||||
t.Errorf("expected CloneRepoActivity first, got %s", intent.Activities[0])
|
||||
}
|
||||
if intent.IsCron {
|
||||
t.Error("expected isCron=false")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cron intent",
|
||||
response: `{
|
||||
"activities": ["CloneRepoActivity", "SecurityScanActivity"],
|
||||
"parameters": {"repo": "https://github.com/test/repo"},
|
||||
"isCron": true,
|
||||
"cronSchedule": "0 2 * * *",
|
||||
"cronTimezone": "America/New_York",
|
||||
"workflowName": "daily-security-scan"
|
||||
}`,
|
||||
wantErr: false,
|
||||
validate: func(t *testing.T, intent *Intent) {
|
||||
if !intent.IsCron {
|
||||
t.Error("expected isCron=true")
|
||||
}
|
||||
if intent.CronSchedule != "0 2 * * *" {
|
||||
t.Errorf("expected cron schedule '0 2 * * *', got %s", intent.CronSchedule)
|
||||
}
|
||||
if intent.CronTimezone != "America/New_York" {
|
||||
t.Errorf("expected timezone 'America/New_York', got %s", intent.CronTimezone)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with markdown code block",
|
||||
response: "```json\n{\"activities\": [\"CloneRepoActivity\"], \"parameters\": {}, \"isCron\": false}\n```",
|
||||
wantErr: false,
|
||||
validate: func(t *testing.T, intent *Intent) {
|
||||
if len(intent.Activities) != 1 {
|
||||
t.Errorf("expected 1 activity, got %d", len(intent.Activities))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "defaults applied",
|
||||
response: `{"activities": ["CloneRepoActivity"], "parameters": {}}`,
|
||||
wantErr: false,
|
||||
validate: func(t *testing.T, intent *Intent) {
|
||||
if intent.CronTimezone != "UTC" {
|
||||
t.Errorf("expected default timezone UTC, got %s", intent.CronTimezone)
|
||||
}
|
||||
if intent.ErrorHandling != "retry" {
|
||||
t.Errorf("expected default errorHandling 'retry', got %s", intent.ErrorHandling)
|
||||
}
|
||||
if intent.WorkflowName != "generated-workflow" {
|
||||
t.Errorf("expected default workflowName, got %s", intent.WorkflowName)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid json",
|
||||
response: "this is not json",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
intent, err := parseIntentResponse(tt.response)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
if tt.validate != nil {
|
||||
tt.validate(t, intent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSpec(t *testing.T) {
|
||||
// Load knowledge base
|
||||
kb, err := LoadKnowledgeBaseFromDefaultPath()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load knowledge base: %v", err)
|
||||
}
|
||||
|
||||
router := &LLMRouter{
|
||||
knowledgeBase: kb,
|
||||
}
|
||||
|
||||
intent := &Intent{
|
||||
Activities: []string{"CloneRepoActivity", "AnalyzeCodeActivity", "SecurityScanActivity"},
|
||||
Parameters: map[string]interface{}{"repo": "https://github.com/test/repo", "branch": "main"},
|
||||
WorkflowName: "test-workflow",
|
||||
ErrorHandling: "retry",
|
||||
}
|
||||
|
||||
input := LLMRouterInput{
|
||||
Message: "Analyze repo for security",
|
||||
Context: map[string]interface{}{},
|
||||
}
|
||||
|
||||
spec, err := router.buildSpec(intent, input)
|
||||
if err != nil {
|
||||
t.Fatalf("buildSpec failed: %v", err)
|
||||
}
|
||||
|
||||
// Validate spec
|
||||
if spec.Name != "test-workflow" {
|
||||
t.Errorf("expected name 'test-workflow', got %s", spec.Name)
|
||||
}
|
||||
|
||||
if len(spec.States) < 3 {
|
||||
t.Errorf("expected at least 3 states, got %d", len(spec.States))
|
||||
}
|
||||
|
||||
// First state should be CloneRepoActivity
|
||||
if spec.States[0].Resource != "CloneRepoActivity" {
|
||||
t.Errorf("expected first state to be CloneRepoActivity, got %s", spec.States[0].Resource)
|
||||
}
|
||||
|
||||
// Last activity state should have End=true
|
||||
lastActivityIdx := len(spec.States) - 1
|
||||
if spec.States[lastActivityIdx].Type == StateTypeFail {
|
||||
lastActivityIdx--
|
||||
}
|
||||
if !spec.States[lastActivityIdx].End {
|
||||
t.Error("expected last activity state to have End=true")
|
||||
}
|
||||
|
||||
// Check retry policy on flaky activity (AnalyzeCodeActivity)
|
||||
for _, state := range spec.States {
|
||||
if state.Resource == "AnalyzeCodeActivity" {
|
||||
if state.Retry == nil {
|
||||
t.Error("expected retry policy on flaky activity")
|
||||
} else if state.Retry.MaxAttempts != 3 {
|
||||
t.Errorf("expected 3 max attempts for flaky activity, got %d", state.Retry.MaxAttempts)
|
||||
}
|
||||
if len(state.Catch) == 0 {
|
||||
t.Error("expected catch clause on flaky activity")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCronSpec(t *testing.T) {
|
||||
kb, err := LoadKnowledgeBaseFromDefaultPath()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load knowledge base: %v", err)
|
||||
}
|
||||
|
||||
router := &LLMRouter{
|
||||
knowledgeBase: kb,
|
||||
}
|
||||
|
||||
intent := &Intent{
|
||||
Activities: []string{"CloneRepoActivity", "SecurityScanActivity"},
|
||||
Parameters: map[string]interface{}{"repo": "https://github.com/test/repo"},
|
||||
IsCron: true,
|
||||
CronSchedule: "0 2 * * *",
|
||||
CronTimezone: "UTC",
|
||||
WorkflowName: "daily-scan",
|
||||
}
|
||||
|
||||
input := LLMRouterInput{
|
||||
Message: "Run security scan daily at 2 AM",
|
||||
}
|
||||
|
||||
cronSpec, err := router.buildCronSpec(intent, input)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCronSpec failed: %v", err)
|
||||
}
|
||||
|
||||
if cronSpec.Type != "CronWorkflow" {
|
||||
t.Errorf("expected type 'CronWorkflow', got %s", cronSpec.Type)
|
||||
}
|
||||
if cronSpec.Schedule != "0 2 * * *" {
|
||||
t.Errorf("expected schedule '0 2 * * *', got %s", cronSpec.Schedule)
|
||||
}
|
||||
if cronSpec.Timezone != "UTC" {
|
||||
t.Errorf("expected timezone 'UTC', got %s", cronSpec.Timezone)
|
||||
}
|
||||
if !cronSpec.EnableHistory {
|
||||
t.Error("expected EnableHistory=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildParameters(t *testing.T) {
|
||||
kb, err := LoadKnowledgeBaseFromDefaultPath()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load knowledge base: %v", err)
|
||||
}
|
||||
|
||||
router := &LLMRouter{
|
||||
knowledgeBase: kb,
|
||||
}
|
||||
|
||||
// Test first activity (CloneRepoActivity) - should use input references
|
||||
cloneAct := kb.GetActivity("CloneRepoActivity")
|
||||
intent := &Intent{
|
||||
Activities: []string{"CloneRepoActivity", "AnalyzeCodeActivity"},
|
||||
Parameters: map[string]interface{}{"repo": "https://github.com/test/repo"},
|
||||
}
|
||||
|
||||
params := router.buildParameters(cloneAct, intent, 0)
|
||||
if params["repo"] != "https://github.com/test/repo" {
|
||||
t.Errorf("expected repo from parameters, got %v", params["repo"])
|
||||
}
|
||||
|
||||
// Test second activity (AnalyzeCodeActivity) - should reference previous output
|
||||
analyzeAct := kb.GetActivity("AnalyzeCodeActivity")
|
||||
params = router.buildParameters(analyzeAct, intent, 1)
|
||||
if params["path"] != "${CloneRepoActivity.output.path}" {
|
||||
t.Errorf("expected JSONPath reference to CloneRepoActivity.output.path, got %v", params["path"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRetryPolicy(t *testing.T) {
|
||||
kb, err := LoadKnowledgeBaseFromDefaultPath()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load knowledge base: %v", err)
|
||||
}
|
||||
|
||||
router := &LLMRouter{
|
||||
knowledgeBase: kb,
|
||||
}
|
||||
|
||||
// Flaky activity with retry error handling
|
||||
analyzeAct := kb.GetActivity("AnalyzeCodeActivity")
|
||||
intent := &Intent{ErrorHandling: "retry"}
|
||||
policy := router.buildRetryPolicy(analyzeAct, intent)
|
||||
|
||||
if policy.MaxAttempts != 3 {
|
||||
t.Errorf("expected 3 max attempts for flaky activity, got %d", policy.MaxAttempts)
|
||||
}
|
||||
if policy.BackoffRate != 2.0 {
|
||||
t.Errorf("expected backoff rate 2.0, got %f", policy.BackoffRate)
|
||||
}
|
||||
|
||||
// Fail-fast error handling
|
||||
intent = &Intent{ErrorHandling: "fail-fast"}
|
||||
policy = router.buildRetryPolicy(analyzeAct, intent)
|
||||
|
||||
if policy.MaxAttempts != 1 {
|
||||
t.Errorf("expected 1 max attempt for fail-fast, got %d", policy.MaxAttempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentJSONMarshal(t *testing.T) {
|
||||
intent := &Intent{
|
||||
Activities: []string{"CloneRepoActivity"},
|
||||
Parameters: map[string]interface{}{"repo": "https://test"},
|
||||
IsCron: true,
|
||||
CronSchedule: "0 * * * *",
|
||||
CronTimezone: "UTC",
|
||||
WorkflowName: "test",
|
||||
ErrorHandling: "retry",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(intent)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var decoded Intent
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if decoded.CronSchedule != intent.CronSchedule {
|
||||
t.Errorf("expected schedule %s, got %s", intent.CronSchedule, decoded.CronSchedule)
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -6,4 +6,5 @@ metadata:
|
||||
data:
|
||||
TEMPORAL_NAMESPACE: "poimen-harness"
|
||||
TEMPORAL_HOSTPORT: "temporal-frontend.temporal:7233"
|
||||
# ANTHROPIC_API_KEY is handled via Secret
|
||||
LOCAL_LLM_BASE_URL: "http://api-gateway.api:8080"
|
||||
POIMEN_MEMORY_URL: "http://poimen-memory.poimen.svc.cluster.local:8080"
|
||||
|
||||
+2
-2
@@ -9,6 +9,6 @@ metadata:
|
||||
app.kubernetes.io/name: poimen
|
||||
app.kubernetes.io/component: orchestrator
|
||||
data:
|
||||
GIT_COMMIT: "38dd3f8d" # Updated automatically by CI/CD
|
||||
GIT_COMMIT: "303e78f7" # Updated automatically by CI/CD
|
||||
GIT_BRANCH: "main"
|
||||
DEPLOYMENT_DATE: "2026-08-31"
|
||||
DEPLOYMENT_DATE: "2026-09-02"
|
||||
|
||||
+2
-10
@@ -4,23 +4,15 @@ kind: Kustomization
|
||||
namespace: poimen
|
||||
|
||||
resources:
|
||||
- orchestrator-job.yaml
|
||||
- worker-deployment.yaml
|
||||
- git-commit.yaml
|
||||
- configmap.yaml
|
||||
|
||||
commonLabels:
|
||||
app.kubernetes.io/name: poimen
|
||||
app.kubernetes.io/component: orchestrator
|
||||
app.kubernetes.io/component: worker
|
||||
|
||||
secretGenerator:
|
||||
- name: poimen-secrets
|
||||
envs:
|
||||
- secrets.env
|
||||
behavior: create
|
||||
|
||||
configMapGenerator:
|
||||
- name: poimen-config
|
||||
literals:
|
||||
- TEMPORAL_NAMESPACE=poimen-harness
|
||||
- TEMPORAL_HOSTPORT=temporal-frontend.temporal:7233
|
||||
behavior: create
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: poimen-orchestrator
|
||||
namespace: poimen
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: poimen-orchestrator
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: orchestrator
|
||||
image: golang:latest
|
||||
imagePullPolicy: Always # ✅ Force latest image pull
|
||||
workingDir: /app
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -e
|
||||
echo "[$(date)] Starting poimen orchestrator job..."
|
||||
apt-get update && apt-get install -y --no-install-recommends git
|
||||
echo "[$(date)] Cloning latest code from git..."
|
||||
git clone https://forgejo.riotpiao.com/rock/poimen-workflows.git /app
|
||||
cd /app
|
||||
echo "[$(date)] Latest commit: $(git rev-parse HEAD)"
|
||||
echo "[$(date)] Downloading dependencies..."
|
||||
go mod download
|
||||
echo "[$(date)] Starting orchestrator with T0-T4 complete implementation..."
|
||||
go run ./cmd/starter \
|
||||
--repo https://forgejo.riotpiao.com/rock/poimen \
|
||||
--remote file:///tmp/poimen-output \
|
||||
--milestone T0 \
|
||||
--planner-model ornith \
|
||||
--judge-model ornith \
|
||||
--implementer-model claude-sonnet-5
|
||||
env:
|
||||
- name: TEMPORAL_NAMESPACE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: poimen-config
|
||||
key: TEMPORAL_NAMESPACE
|
||||
- name: TEMPORAL_HOSTPORT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: poimen-config
|
||||
key: TEMPORAL_HOSTPORT
|
||||
- name: ANTHROPIC_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: poimen-secrets
|
||||
key: ANTHROPIC_API_KEY
|
||||
- name: LOCAL_LLM_BASE_URL
|
||||
value: "http://api-gateway.api:8080"
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "2000m"
|
||||
@@ -13,8 +13,8 @@ spec:
|
||||
labels:
|
||||
app: poimen-worker
|
||||
annotations:
|
||||
git-commit: "38dd3f8d" # ✅ Updated on each push, triggers rolling restart
|
||||
deployment-date: "2026-08-31"
|
||||
git-commit: "303e78f7" # ✅ Updated on each push, triggers rolling restart
|
||||
deployment-date: "2026-09-02"
|
||||
spec:
|
||||
containers:
|
||||
- name: worker
|
||||
@@ -52,7 +52,15 @@ spec:
|
||||
name: poimen-secrets
|
||||
key: ANTHROPIC_API_KEY
|
||||
- name: LOCAL_LLM_BASE_URL
|
||||
value: "http://api-gateway.api:8080"
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: poimen-config
|
||||
key: LOCAL_LLM_BASE_URL
|
||||
- name: POIMEN_MEMORY_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: poimen-config
|
||||
key: POIMEN_MEMORY_URL
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
package statemachine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rockliang/poimen/workflows/internal/routing"
|
||||
"go.temporal.io/sdk/log"
|
||||
"go.temporal.io/sdk/temporal"
|
||||
"go.temporal.io/sdk/workflow"
|
||||
)
|
||||
|
||||
// RoutingWorkflowInput is input for the routing workflow
|
||||
type RoutingWorkflowInput struct {
|
||||
Spec *routing.WorkflowSpec `json:"spec"`
|
||||
}
|
||||
|
||||
// RoutingWorkflowOutput is output from the routing workflow
|
||||
type RoutingWorkflowOutput struct {
|
||||
Status string `json:"status"` // "COMPLETED", "FAILED"
|
||||
FinalOutput interface{} `json:"finalOutput,omitempty"`
|
||||
StepResults map[string]interface{} `json:"stepResults"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// RoutingWorkflow executes any WorkflowSpec generated by llm-router
|
||||
func RoutingWorkflow(ctx workflow.Context, input RoutingWorkflowInput) (RoutingWorkflowOutput, error) {
|
||||
logger := workflow.GetLogger(ctx)
|
||||
|
||||
output := RoutingWorkflowOutput{
|
||||
Status: "FAILED",
|
||||
StepResults: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
if input.Spec == nil || len(input.Spec.States) == 0 {
|
||||
output.Error = "empty workflow spec"
|
||||
return output, nil
|
||||
}
|
||||
|
||||
logger.Info("RoutingWorkflow started", "name", input.Spec.Name, "stateCount", len(input.Spec.States))
|
||||
|
||||
// Build execution context
|
||||
execCtx := &routing.ExecutionContext{
|
||||
Input: input.Spec.Input,
|
||||
StepResults: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
// Build state index for fast lookup
|
||||
stateIndex := make(map[string]*routing.State)
|
||||
for i := range input.Spec.States {
|
||||
stateIndex[input.Spec.States[i].Name] = &input.Spec.States[i]
|
||||
}
|
||||
|
||||
// Find first state (first in array)
|
||||
currentStateName := input.Spec.States[0].Name
|
||||
|
||||
// State machine loop
|
||||
for {
|
||||
state, ok := stateIndex[currentStateName]
|
||||
if !ok {
|
||||
output.Error = fmt.Sprintf("state not found: %s", currentStateName)
|
||||
return output, nil
|
||||
}
|
||||
|
||||
logger.Info("executing state", "state", currentStateName, "type", state.Type)
|
||||
|
||||
switch state.Type {
|
||||
case routing.StateTypeTask:
|
||||
result, nextState, err := executeTaskState(ctx, state, execCtx, logger)
|
||||
if err != nil {
|
||||
// Check for catch clause
|
||||
if nextState != "" {
|
||||
currentStateName = nextState
|
||||
continue
|
||||
}
|
||||
output.Error = fmt.Sprintf("state %s failed: %v", currentStateName, err)
|
||||
return output, nil
|
||||
}
|
||||
// Wrap result in output key for JSONPath compatibility (e.g., ${Clone.output.path})
|
||||
wrappedResult := map[string]interface{}{"output": result}
|
||||
execCtx.StepResults[state.Name] = wrappedResult
|
||||
output.StepResults[state.Name] = result // Keep original for output
|
||||
|
||||
if state.End {
|
||||
output.Status = "COMPLETED"
|
||||
output.FinalOutput = result
|
||||
logger.Info("RoutingWorkflow completed", "name", input.Spec.Name)
|
||||
return output, nil
|
||||
}
|
||||
currentStateName = state.Next
|
||||
|
||||
case routing.StateTypePass:
|
||||
execCtx.StepResults[state.Name] = state.Result
|
||||
output.StepResults[state.Name] = state.Result
|
||||
|
||||
if state.End {
|
||||
output.Status = "COMPLETED"
|
||||
output.FinalOutput = state.Result
|
||||
return output, nil
|
||||
}
|
||||
currentStateName = state.Next
|
||||
|
||||
case routing.StateTypeFail:
|
||||
output.Error = fmt.Sprintf("%s: %s", state.Error, state.Cause)
|
||||
logger.Error("RoutingWorkflow failed at Fail state", "state", currentStateName, "error", state.Error)
|
||||
return output, nil
|
||||
|
||||
default:
|
||||
output.Error = fmt.Sprintf("unknown state type: %s", state.Type)
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// Safety check
|
||||
if currentStateName == "" {
|
||||
output.Error = "no next state and not end"
|
||||
return output, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeTaskState executes a Task state with retry policy
|
||||
func executeTaskState(ctx workflow.Context, state *routing.State, execCtx *routing.ExecutionContext, logger log.Logger) (interface{}, string, error) {
|
||||
// Parse timeout
|
||||
timeout := 5 * time.Minute
|
||||
if state.Timeout != "" {
|
||||
if parsed, err := time.ParseDuration(state.Timeout); err == nil {
|
||||
timeout = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// Build activity options
|
||||
activityOpts := workflow.ActivityOptions{
|
||||
StartToCloseTimeout: timeout,
|
||||
ScheduleToCloseTimeout: timeout + 5*time.Minute,
|
||||
}
|
||||
|
||||
// Add retry policy if specified
|
||||
if state.Retry != nil {
|
||||
initialInterval := time.Second
|
||||
if state.Retry.InitialInterval != "" {
|
||||
if parsed, err := time.ParseDuration(state.Retry.InitialInterval); err == nil {
|
||||
initialInterval = parsed
|
||||
}
|
||||
}
|
||||
maxInterval := 30 * time.Second
|
||||
if state.Retry.MaxInterval != "" {
|
||||
if parsed, err := time.ParseDuration(state.Retry.MaxInterval); err == nil {
|
||||
maxInterval = parsed
|
||||
}
|
||||
}
|
||||
|
||||
activityOpts.RetryPolicy = &temporal.RetryPolicy{
|
||||
InitialInterval: initialInterval,
|
||||
BackoffCoefficient: state.Retry.BackoffRate,
|
||||
MaximumInterval: maxInterval,
|
||||
MaximumAttempts: state.Retry.MaxAttempts,
|
||||
}
|
||||
}
|
||||
|
||||
actCtx := workflow.WithActivityOptions(ctx, activityOpts)
|
||||
|
||||
// Resolve parameters using JSONPath
|
||||
resolver := routing.NewJSONPathResolver(execCtx.Input, execCtx.StepResults)
|
||||
resolvedParams, err := resolver.ResolvePaths(state.Parameters)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to resolve parameters: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("executing activity", "activity", state.Resource, "params", resolvedParams)
|
||||
|
||||
// Execute activity
|
||||
var result interface{}
|
||||
err = workflow.ExecuteActivity(actCtx, state.Resource, resolvedParams).Get(ctx, &result)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("activity failed", "activity", state.Resource, "error", err)
|
||||
|
||||
// Check for catch clauses
|
||||
for _, catch := range state.Catch {
|
||||
if matchesError(err, catch.ErrorEquals) {
|
||||
logger.Info("error caught", "handler", catch.Next)
|
||||
return nil, catch.Next, err
|
||||
}
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
logger.Info("activity completed", "activity", state.Resource)
|
||||
return result, "", nil
|
||||
}
|
||||
|
||||
// matchesError checks if error matches any of the error types
|
||||
func matchesError(err error, errorEquals []string) bool {
|
||||
errStr := err.Error()
|
||||
for _, errType := range errorEquals {
|
||||
switch errType {
|
||||
case "ActivityError":
|
||||
return true // Match all activity errors
|
||||
case "TimeoutError":
|
||||
if temporal.IsTimeoutError(err) {
|
||||
return true
|
||||
}
|
||||
case "ApplicationError":
|
||||
if temporal.IsApplicationError(err) {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
// Match by error string contains
|
||||
if contains(errStr, errType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
|
||||
}
|
||||
|
||||
func containsHelper(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
# Handoff Prompt: Implement T0 Milestone
|
||||
|
||||
Use this prompt with Claude Haiku 4.5 to begin implementation of T0 tasks.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
You're implementing a Temporal-based multi-agent software orchestrator in Go. The system drives development work on arbitrary target repos using three LLM-backed roles:
|
||||
- **Planner** (reasoning model): reconciles state, dispatches tasks
|
||||
- **Judge** (reasoning model): reviews correctness, runs integration tests
|
||||
- **Implementer** (cheaper model): does actual work, learns from failures
|
||||
|
||||
All code lives in `/Users/rockliang/workplace/Poimen/workflows/`.
|
||||
|
||||
## What's Done
|
||||
|
||||
- ✅ Full design doc: `/Users/rockliang/.claude/plans/considered-u-are-a-curried-reef.md`
|
||||
- ✅ Project scaffold: `PLAN.md`, `tasks/INDEX.md`, `tasks/board.md`
|
||||
- ✅ Roadmap: T0 (core, 9 tasks) → T1 (hardening) → T2 (scale) → T3 (features)
|
||||
- ✅ Task breakdown: `tasks/T0.1.md` through `tasks/T0.9.md` (each with verification criteria)
|
||||
|
||||
## Your Job: Implement T0.1 → T0.9
|
||||
|
||||
Start with T0.1 (repo scaffold). Each task:
|
||||
1. Read its markdown file in `tasks/T0.x.md`
|
||||
2. Implement the code sketches provided
|
||||
3. Write unit tests per verification section
|
||||
4. Run the verification command
|
||||
5. When it passes, mark `[ ]` → `[x]` in `tasks/board.md`
|
||||
6. Move to next task
|
||||
|
||||
## Key Constraints
|
||||
|
||||
1. **No hardcoded values:** All timeouts, retry counts, model IDs come from `OrchestratorConfig.Tuning` or `PromptSpec.Model` (read at runtime).
|
||||
2. **Activities are independent:** One concern per `action/*.go` file (git, skills, planner, judge, implementer, etc). Testable in isolation.
|
||||
3. **Testing is verification:** Unit tests via `go.temporal.io/sdk/testsuite` (mocked activities). E2E test (T0.9) against real `temporal.riotpiao.com`.
|
||||
4. **Go style:** Per `golang-skills` conventions — no naked `_ =`, proper error handling, idiomatic names.
|
||||
5. **Concurrency safety:** Shared FS with git worktrees + advisory lock. Test concurrent access.
|
||||
|
||||
## Implementation Path
|
||||
|
||||
```
|
||||
1. T0.1: Scaffold directories, go.mod, empty stubs
|
||||
→ verify: go build ./... succeeds
|
||||
|
||||
2. T0.2: Shared types (ModelSpec, PromptSpec, OrchestratorConfig, etc.)
|
||||
→ verify: Unit test asserts all defaults
|
||||
|
||||
3. T0.3: Git & locking (CloneRepoActivity, worktrees, squash-merge)
|
||||
→ verify: Test against local scratch repo
|
||||
|
||||
4. T0.4: Pi & error classification (PrepareSkillsActivity, classifyPiErr)
|
||||
→ verify: Unit tests for 4xx/5xx/504 buckets
|
||||
|
||||
5. T0.5: LLM agents & prompts (Planner/Judge/Implementer, llm/client.go)
|
||||
→ verify: Unit test renders PromptSpec with system prompt
|
||||
|
||||
6. T0.6: TaskUnit workflow (retry loops, timeout escalation, lessons)
|
||||
→ verify: Testsuite: pass-first-try, fail-then-pass, timeout-escalation
|
||||
|
||||
7. T0.7: Orchestrator workflow (config, signals, fan-out/fan-in, continue-as-new, 504 learning)
|
||||
→ verify: Testsuite: fan-out/fan-in, squash-merge, signal mutations
|
||||
|
||||
8. T0.8: Worker & starter CLIs (cmd/worker, cmd/starter, internal/config)
|
||||
→ verify: go build succeeds, go run ./cmd/worker connects to temporal.riotpiao.com
|
||||
|
||||
9. T0.9: End-to-end (real cluster + disposable forgejo repo, all 7 checks)
|
||||
→ verify: Full cycle with live signals, 5xx retry/exhaust, 504 learning, continue-as-new bounded
|
||||
```
|
||||
|
||||
## Tools Available
|
||||
|
||||
- `pi` command: Clone/fetch skills from homelab API
|
||||
- Usage: `pi clone-or-fetch <skill-url>`
|
||||
- Errors: 4xx (non-retryable), 5xx (retryable), 504 (stream timeout — learn and double timeout)
|
||||
|
||||
- Temporal Web UI: `http://temporal.riotpiao.com:8080` (monitor workflows)
|
||||
- Forgejo instance: For disposable scratch repos during testing
|
||||
|
||||
## Testing Locally
|
||||
|
||||
Each task has a "Verification" section with a test command. Run it after implementing:
|
||||
|
||||
```bash
|
||||
cd /Users/rockliang/workplace/Poimen/workflows
|
||||
go test -v ./tests -run Test<TaskName>
|
||||
```
|
||||
|
||||
For T0.9 (e2e), you'll need:
|
||||
- Real Temporal cluster connection
|
||||
- Anthropic API key (`ANTHROPIC_API_KEY` env)
|
||||
- Forgejo repo access or local git repo
|
||||
|
||||
## Commit Message Style
|
||||
|
||||
```
|
||||
T0.x: Brief description
|
||||
|
||||
Detailed explanation of what was implemented.
|
||||
|
||||
Verification: <how you verified it works>
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
T0.2: Implement shared types with defaults
|
||||
|
||||
Added ModelSpec, PromptSpec, OrchestratorConfig, ActivityTuning, PiRetryPolicy.
|
||||
All defaults documented: 5m ScheduleToCloseTimeout, 2s InitialInterval, 2.0 BackoffCoefficient, etc.
|
||||
|
||||
Verification: go test ./tests -run TestTypesDefaults passes
|
||||
```
|
||||
|
||||
## When Stuck
|
||||
|
||||
1. Re-read the task markdown in `tasks/T0.x.md` — implementation sketches are concrete
|
||||
2. Check `PLAN.md` §Design sections for detailed logic (§Timeout Extension, §Pi Command Retry Policy, etc.)
|
||||
3. Look at the test case in the task markdown — it shows expected behavior
|
||||
4. If a task depends on prior work, make sure the prior task is complete first
|
||||
|
||||
## After T0 Completes
|
||||
|
||||
When all T0.1–T0.9 pass:
|
||||
```bash
|
||||
cd /Users/rockliang/workplace/Poimen/workflows
|
||||
git checkout main
|
||||
git merge --squash task/T0.1 task/T0.2 ... task/T0.9
|
||||
git commit -m "T0: Multi-agent orchestrator initial implementation"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
Then start T1 (or hand off to another assistant).
|
||||
|
||||
---
|
||||
|
||||
**Ready?** Start with `tasks/T0.1.md` and work through in order.
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
# Poimen Workflows — Development Guidelines
|
||||
|
||||
This project orchestrates multi-agent software development tasks using Temporal + Go. The repo is organized as:
|
||||
- `statemachine/` — Temporal workflow definitions (deterministic state machines)
|
||||
- `action/` — Temporal activity definitions (units of work / LLM calls)
|
||||
- `cmd/` — CLI entry points (worker registration, workflow starter)
|
||||
- `prompts/` — LLM prompt templates (Go-embedded, live-updatable)
|
||||
- `internal/` — Shared config/locking utilities
|
||||
- `tests/` — Unit tests via `go.temporal.io/sdk/testsuite`
|
||||
|
||||
## Code Standards
|
||||
|
||||
### Go Style
|
||||
|
||||
Follow `golang-skills` conventions from `~/.claude/skills/golang-skills/`:
|
||||
- Error handling mandatory; no naked `_ =` discards.
|
||||
- Idiomatic naming: `ctx` for context, `err` for error returns.
|
||||
- Interfaces kept narrow; one struct per concern.
|
||||
- No over-generalization for hypothetical use.
|
||||
- Documentation via comments only where the WHY is non-obvious.
|
||||
|
||||
### Testing
|
||||
|
||||
- Every `statemachine/*` change ships with a `testsuite`-based test in `tests/`.
|
||||
- Unit tests use `go.temporal.io/sdk/testsuite.WorkflowTestEnvironment` with mocked activities.
|
||||
- Activities are tested in isolation before integrating into workflows.
|
||||
- No literal timeout/retry values in `action/*` code — all come from `OrchestratorConfig.Tuning` at runtime.
|
||||
|
||||
### Activity Design
|
||||
|
||||
Activities stay side-effect-isolated (one concern per file) so they remain independently reusable across projects.
|
||||
- `action/git.go` — all git operations
|
||||
- `action/skills.go` — skill prep / pi command
|
||||
- `action/planner.go` — Planner LLM call
|
||||
- `action/judge.go` — Judge LLM call
|
||||
- `action/implementer.go` — Implementer LLM + tool-call loop
|
||||
- `action/lessons.go` — Lessons store read/write
|
||||
|
||||
No composite activities like `PrepareAndImplement`; that's a workflow's job, not an activity's.
|
||||
|
||||
### Config as Data
|
||||
|
||||
**No hardcoded numbers.** Every timeout, retry count, backoff coefficient, stream timeout — all come from `OrchestratorConfig.Tuning` (or role-specific `PromptSpec.Model`), read at execution time. This lets:
|
||||
- `update-tuning` signal to dynamically adjust timeouts without redeployment
|
||||
- Planner activity to recommend per-task overrides
|
||||
- Pi 504 handler to learn and persist `StreamTimeout` across `ContinueAsNew` cycles
|
||||
|
||||
**No LLM model hardcoded.** Every model ID comes from `ModelSpec.ModelID`, passed as data.
|
||||
|
||||
### Concurrency & Locking
|
||||
|
||||
Target repo sits on shared filesystem. Git concurrency is safe by construction:
|
||||
- Task units use isolated worktrees (`git worktree add -b task/T0.x ...`)
|
||||
- Commits within a worktree don't need a lock; git serializes object writes
|
||||
- Only `CloneRepoActivity`, `GitPushActivity`, `GitSquashMergeActivity` need the `orchestrator.lock`, since they mutate the main working tree / refs
|
||||
|
||||
Monitor for deadlocks: if two workflows try to push simultaneously, the lock will serialize them. Test with concurrent tasks enabled.
|
||||
|
||||
## Task Board Format
|
||||
|
||||
Each task row in the board specifies:
|
||||
- **ID**: `T0.1`, `T0.2`, etc.
|
||||
- **Description**: One-line scope
|
||||
- **Status**: `[ ]` (todo), `[x]` (done)
|
||||
- **Branch**: `task/T0.x` (created when task starts, merged to main on submilestone complete)
|
||||
- **Verification**: Specific criterion that marks it done (e.g. "Unit test passes", "E2E run completes")
|
||||
|
||||
A task is NOT marked done until its verification step passes. This mirrors the Judge role's own job: no hallucinated completion.
|
||||
|
||||
## Submilestones & Merges
|
||||
|
||||
When all tasks in `T0` (`T0.1` through `T0.9`) pass their verification:
|
||||
1. Orchestrator calls `GitSquashMergeActivity` to merge all `task/T0.*` branches into `main` as a single squashed commit
|
||||
2. All `task/T0.*` branches and worktrees are cleaned up
|
||||
3. Main branch is the canonical history; the 9 subtask commits are compacted into one
|
||||
|
||||
This demonstrates the very mechanism the system orchestrates: a working software dev pipeline with multiple agents collaborating on a shared codebase, guarded by state checks (Judge), and landing changes via deterministic git workflow.
|
||||
|
||||
## Historical Lessons
|
||||
|
||||
Lessons live at `tasks/.orchestrator/lessons/<TaskID>.jsonl` — per-task file of failed attempts. When a Judge calls a task failure, the `UpdateLessonsActivity` appends `{Attempt, Critique, FailedApproachSummary, Timestamp}`. On retry, `ReadLessonsActivity` injects the last N entries into the Implementer's prompt as "known errors — do not repeat this time".
|
||||
|
||||
Lessons flush to git only as part of the Planner's board commit, not on every retry — keeps history clean.
|
||||
|
||||
## Skills & Preparation
|
||||
|
||||
Required skill sources are listed in `OrchestratorConfig.Skills` as a list of references (e.g. `["~/.claude/skills/golang-skills", "custom-skill-repo"]`). `PrepareSkillsActivity` runs once per config change, cloning/fetching them onto the shared FS via the `pi` command. All retry/backoff/timeout is delegated to Temporal's retry machinery, with special handling for 504 (stream timeout learning).
|
||||
|
||||
## Environment & Secrets
|
||||
|
||||
All external system access is via environment variables, loaded at worker startup:
|
||||
- `TEMPORAL_NAMESPACE`, `TEMPORAL_TLS_CERT`, `TEMPORAL_TLS_KEY` — Temporal cluster connection
|
||||
- `ANTHROPIC_API_KEY` — LLM API key
|
||||
- Any target-repo-specific credentials (e.g. git SSH key) are assumed already available on the shared FS (e.g. via Kubernetes secret mount)
|
||||
|
||||
Use homelab's `vsource .env` pattern to load from a `.env` file during local development.
|
||||
|
||||
## Observability
|
||||
|
||||
- Temporal Web UI (`temporal.riotpiao.com:8080` or similar) shows workflow execution, signal delivery, activity retries
|
||||
- Workflow `current-config` query returns live `OrchestratorConfig` (useful for debugging which tuning values are in effect)
|
||||
- Activity heartbeats (`activity.RecordHeartbeat`) are sent after each tool-call iteration, visible in Temporal's activity details
|
||||
- Lessons file grows as retries happen; inspect it on failure to understand what the Implementer is learning
|
||||
|
||||
## Deployment & Homelab Integration
|
||||
|
||||
This repo is application-layer code against the homelab's Temporal cluster. The orchestrator runs as a Kubernetes pod with:
|
||||
- Persistent volume (PVC) for the shared FS where target repos are cloned
|
||||
- Network access to Temporal + Anthropic APIs
|
||||
- Git SSH key mounted for cloning target repos
|
||||
|
||||
The Kubernetes manifests + Helm charts live in the homelab repo under `k8s/` and follow homelab's GitOps workflow (commit → ArgoCD sync). This repo's CI/CD (GitHub Actions or Forgejo Actions) builds and pushes the Docker image; the homelab repo triggers a new pod deploy on image push.
|
||||
|
||||
## Questions & Debugging
|
||||
|
||||
If a task is marked done but you suspect it's wrong:
|
||||
1. Re-run its verification step manually
|
||||
2. Check the unit test against the latest code
|
||||
3. For e2e tasks, review Temporal Web UI logs + board file + lessons file on the target repo
|
||||
4. Update the board and task description if the criterion was misunderstood
|
||||
|
||||
If code doesn't compile or tests fail:
|
||||
1. Check Go version and Temporal SDK version match
|
||||
2. Run `go mod tidy` and `go mod vendor` if dependencies drift
|
||||
3. Look for hardcoded values or model IDs that should be config instead
|
||||
@@ -1,102 +0,0 @@
|
||||
# Project Roadmap
|
||||
|
||||
Multi-agent dev orchestrator for Temporal, building toward a fully autonomous software development system.
|
||||
|
||||
## Milestones
|
||||
|
||||
### T0: Core System (IN PROGRESS)
|
||||
9 tasks, foundational: Planner/Judge/Implementer orchestration, git workflow, LLM integration, e2e validation.
|
||||
|
||||
**Status:** 9 subtasks defined, testable criteria in place.
|
||||
**Timeline:** ~2-4 weeks to implementation.
|
||||
**Done:** All T0.1–T0.9 pass verification → squash-merge to main.
|
||||
|
||||
**Board:** `tasks/board.md`
|
||||
|
||||
---
|
||||
|
||||
### T1: Production Hardening (PLANNED)
|
||||
8 tasks: Error recovery, observability, metrics, audit logging, health checks.
|
||||
|
||||
**Focus:** Reliability for long-running orchestrators in homelab.
|
||||
**Key wins:**
|
||||
- Resume from crash without data loss
|
||||
- Structured logging + Grafana metrics
|
||||
- Auto-tuning based on historical failures
|
||||
- Audit trail for compliance
|
||||
|
||||
**Board:** `tasks/board-T1.md`
|
||||
|
||||
---
|
||||
|
||||
### T2: Scale & Performance (PLANNED)
|
||||
8 tasks: Caching, parallelism, batching, distributed locking.
|
||||
|
||||
**Focus:** Handle 100s of concurrent tasks, reduce API call overhead.
|
||||
**Key wins:**
|
||||
- Result caching deduplicates LLM calls
|
||||
- Parallel task dispatch (9x wall-clock speedup)
|
||||
- Git operation batching (fewer network round-trips)
|
||||
- Distributed lock for multi-pod safety
|
||||
|
||||
**Board:** `tasks/board-T2.md`
|
||||
|
||||
---
|
||||
|
||||
### T3: Feature Expansion (PLANNED)
|
||||
8 tasks: Plugins, templates, dependencies, human gates, custom judges, nested workflows.
|
||||
|
||||
**Focus:** Extensibility + domain specialization.
|
||||
**Key wins:**
|
||||
- Load custom skill plugins
|
||||
- Save/load orchestrator config as templates
|
||||
- Task dependency ordering
|
||||
- Swap Judge for domain-specific validator (security auditor, code reviewer, etc.)
|
||||
- Nest orchestrators (multi-level hierarchy)
|
||||
- Import tasks from GitHub/Linear/JIRA
|
||||
|
||||
**Board:** `tasks/board-T3.md`
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **T0 (2-4 weeks):** Core system working end-to-end.
|
||||
2. **T1 (2 weeks):** Production-harden the core.
|
||||
3. **T2 (3 weeks):** Scale & optimize.
|
||||
4. **T3 (4 weeks):** Advanced features.
|
||||
|
||||
**Total:** ~3 months to full feature parity.
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- **T0:** System runs unsupervised on 9-task milestone, all subtasks auto-complete, squash-merge to main succeeds.
|
||||
- **T1:** 1000+ tasks completed, zero data loss across pod restarts, metrics queryable.
|
||||
- **T2:** 1000 concurrent tasks complete 10x faster than T0, < 5 API calls/task (vs current ~20).
|
||||
- **T3:** Custom Judge plugin loads and validates tasks, workflow templates save/restore state.
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|-----------|
|
||||
| Temporal cluster unavailability | Implement activity heartbeat recovery + resumption logic (T1.1) |
|
||||
| Git conflicts on shared FS | Test multi-pod concurrent access (T1.6) → upgrade to distributed lock if needed (T2.8) |
|
||||
| LLM API latency | Implement result caching + batching (T2.1, T2.6) |
|
||||
| Board state corruption | Detect + auto-heal (T1.4) |
|
||||
| Audit trail tampering | Sign audit log with workflow key (T3.6) |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Each milestone is independent: T1 can start once T0 core is complete, doesn't need full T0 cleanup.
|
||||
- T0 tasks are foundational; changes will ripple into T1–T3 test suites, but core API should remain stable.
|
||||
- Future milestones (T4+) could focus on: web UI, real-time dashboard, automated deployment of orchestrator itself (meta!), multi-cluster orchestration.
|
||||
|
||||
---
|
||||
|
||||
**Next:** Start T0.1 (repo scaffold).
|
||||
@@ -1,42 +0,0 @@
|
||||
# T0.1: Repo Scaffold
|
||||
|
||||
## Scope
|
||||
Create directory structure, `go.mod`, empty stubs for all packages.
|
||||
|
||||
## Implementation Checklist
|
||||
- [ ] `go.mod`: module `github.com/rockliang/poimen/workflows`, Go 1.21+
|
||||
- [ ] `statemachine/types.go`: empty package stub (will fill in T0.2)
|
||||
- [ ] `statemachine/signals.go`: empty package stub
|
||||
- [ ] `statemachine/orchestrator.go`: empty package stub, func placeholder
|
||||
- [ ] `statemachine/taskunit.go`: empty package stub, func placeholder
|
||||
- [ ] `action/planner.go`: empty package stub
|
||||
- [ ] `action/implementer.go`: empty package stub
|
||||
- [ ] `action/judge.go`: empty package stub
|
||||
- [ ] `action/git.go`: empty package stub
|
||||
- [ ] `action/skills.go`: empty package stub
|
||||
- [ ] `action/integration_test.go`: empty package stub
|
||||
- [ ] `action/lessons.go`: empty package stub
|
||||
- [ ] `action/llm/client.go`: empty package stub
|
||||
- [ ] `prompts/registry.go`: empty package stub
|
||||
- [ ] `prompts/planner/default.tmpl`: empty text file
|
||||
- [ ] `prompts/judge/default.tmpl`: empty text file
|
||||
- [ ] `prompts/implementer/default.tmpl`: empty text file
|
||||
- [ ] `internal/config/config.go`: empty package stub
|
||||
- [ ] `internal/lock/flock.go`: empty package stub
|
||||
- [ ] `cmd/worker/main.go`: `func main()` stub
|
||||
- [ ] `cmd/starter/main.go`: `func main()` stub
|
||||
- [ ] `tests/taskunit_workflow_test.go`: empty test file
|
||||
- [ ] `tests/orchestrator_workflow_test.go`: empty test file
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
cd /Users/rockliang/workplace/Poimen/workflows
|
||||
go build ./...
|
||||
# Command should succeed with no errors
|
||||
# All directories should exist as listed above
|
||||
```
|
||||
|
||||
## Done Criteria
|
||||
- `go build ./...` succeeds with exit code 0
|
||||
- `ls -R` shows all directories match PLAN.md §Directory Structure
|
||||
- No compilation errors or warnings
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
# T0.2: Shared Types
|
||||
|
||||
## Scope
|
||||
Implement `statemachine/types.go` with all config/input/output structs and document defaults.
|
||||
|
||||
## Implementation
|
||||
File: `statemachine/types.go`
|
||||
|
||||
```go
|
||||
type ModelSpec struct {
|
||||
ModelID string // e.g. "claude-opus-5", "claude-sonnet-5"
|
||||
Thinking string // "adaptive" or ""
|
||||
Effort string // "low", "medium", "high", "xhigh", "max"
|
||||
}
|
||||
|
||||
type PromptSpec struct {
|
||||
TemplateRef string // e.g. "planner/default.tmpl"
|
||||
RawTemplate string // overrides TemplateRef if non-empty
|
||||
Variables map[string]any
|
||||
Model ModelSpec
|
||||
LessonsRef string // key into lessons store
|
||||
}
|
||||
|
||||
type PiRetryPolicy struct {
|
||||
ScheduleToCloseTimeout time.Duration // default: 5m
|
||||
InitialInterval time.Duration // default: 2s
|
||||
MaximumInterval time.Duration // default: 30s
|
||||
BackoffCoefficient float64 // default: 2.0
|
||||
StreamTimeout time.Duration // default: 30s
|
||||
StreamTimeoutMax time.Duration // default: 2m
|
||||
}
|
||||
|
||||
type ActivityTuning struct {
|
||||
ImplementerBaseTimeout time.Duration // default: 10m
|
||||
ImplementerMaxRetries int // default: 3
|
||||
JudgeTimeout time.Duration // default: 5m
|
||||
PiRetry PiRetryPolicy
|
||||
}
|
||||
|
||||
type OrchestratorConfig struct {
|
||||
SystemPrompt string // shared prompt prefix
|
||||
Skills []SkillRef // required skill sources
|
||||
RolePrompts map[string]PromptSpec // per-role: "planner", "judge", "implementer"
|
||||
Tuning ActivityTuning
|
||||
}
|
||||
|
||||
type OrchestratorInput struct {
|
||||
TargetRepoPath string
|
||||
RemoteURL string
|
||||
Milestone string // e.g. "T0"
|
||||
Config OrchestratorConfig
|
||||
DryRun bool
|
||||
CycleCount int
|
||||
MaxCyclesBeforeCAN int // default: 100
|
||||
}
|
||||
|
||||
type OrchestratorOutput struct {
|
||||
MilestoneComplete bool
|
||||
Done bool
|
||||
LastError string
|
||||
}
|
||||
|
||||
type TaskUnitInput struct {
|
||||
TaskID string
|
||||
TargetRepoPath string
|
||||
JudgeSpec PromptSpec
|
||||
ImplementerSpec PromptSpec
|
||||
BaseTimeout time.Duration
|
||||
MaxJudgeRetries int
|
||||
}
|
||||
|
||||
type TaskUnitOutput struct {
|
||||
TaskID string
|
||||
Verdict string // "pass" or "fail"
|
||||
Critique string
|
||||
Branch string
|
||||
}
|
||||
|
||||
type SkillRef struct {
|
||||
Name string // skill identifier
|
||||
URL string // source to clone
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
cd /Users/rockliang/workplace/Poimen/workflows
|
||||
go build ./statemachine/
|
||||
|
||||
# Run unit test:
|
||||
go test -v ./tests -run TestTypesDefaults
|
||||
```
|
||||
|
||||
Test file: `tests/types_test.go`
|
||||
```go
|
||||
func TestTypesDefaults(t *testing.T) {
|
||||
// Verify all defaults are correctly set
|
||||
pr := PiRetryPolicy{}
|
||||
assert.Equal(t, 5*time.Minute, pr.ScheduleToCloseTimeout)
|
||||
// ... more assertions
|
||||
}
|
||||
```
|
||||
|
||||
## Done Criteria
|
||||
- `go build ./statemachine/` succeeds
|
||||
- `go test ./tests -run TestTypesDefaults` passes
|
||||
- All struct fields documented with default values
|
||||
- No compilation errors
|
||||
@@ -1,93 +0,0 @@
|
||||
# T0.3: Git & Locking
|
||||
|
||||
## Scope
|
||||
Implement `action/git.go` + `internal/lock/flock.go` for repo cloning, worktree management, and squash-merge.
|
||||
|
||||
## Implementation
|
||||
|
||||
### File: `internal/lock/flock.go`
|
||||
```go
|
||||
package lock
|
||||
|
||||
// Acquire advisory file lock (blocking)
|
||||
func Acquire(path string) error
|
||||
|
||||
// Release advisory file lock
|
||||
func Release(path string) error
|
||||
```
|
||||
|
||||
### File: `action/git.go`
|
||||
```go
|
||||
type CloneRepoInput struct {
|
||||
RemoteURL string
|
||||
TargetRepoPath string
|
||||
}
|
||||
|
||||
func CloneRepoActivity(ctx context.Context, in CloneRepoInput) error
|
||||
// If $TargetRepoPath/.git exists: git -C $TargetRepoPath fetch origin
|
||||
// Else: git clone $RemoteURL $TargetRepoPath
|
||||
|
||||
type GitWorktreeAddInput struct {
|
||||
RepoPath string
|
||||
TaskID string
|
||||
}
|
||||
|
||||
func GitWorktreeAddActivity(ctx context.Context, in GitWorktreeAddInput) (string, error)
|
||||
// Guarded by orchestrator.lock
|
||||
// git worktree add -b task/<TaskID> ../worktrees/<id> origin/main
|
||||
// Return worktree path
|
||||
|
||||
type GitCommitInput struct {
|
||||
WorktreePath string
|
||||
Message string
|
||||
}
|
||||
|
||||
func GitCommitActivity(ctx context.Context, in GitCommitInput) error
|
||||
// No lock needed; safe within isolated worktree
|
||||
// git -C $WorktreePath add -A
|
||||
// git -C $WorktreePath commit -m "$Message"
|
||||
|
||||
type GitPushInput struct {
|
||||
RepoPath string
|
||||
}
|
||||
|
||||
func GitPushActivity(ctx context.Context, in GitPushInput) error
|
||||
// Guarded by orchestrator.lock
|
||||
// git -C $RepoPath push origin main
|
||||
|
||||
type GitSquashMergeInput struct {
|
||||
RepoPath string
|
||||
Branches []string // ["task/T0.1", "task/T0.2", ...]
|
||||
Message string
|
||||
}
|
||||
|
||||
func GitSquashMergeActivity(ctx context.Context, in GitSquashMergeInput) error
|
||||
// Guarded by orchestrator.lock
|
||||
// fetch origin main
|
||||
// checkout main && pull --ff-only origin main
|
||||
// for b in branches: merge --squash $b
|
||||
// commit -m $Message
|
||||
// push origin main
|
||||
// for b in branches: worktree remove + branch -D
|
||||
```
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
cd /Users/rockliang/workplace/Poimen/workflows
|
||||
go test -v ./tests -run TestGit
|
||||
|
||||
# Test script: tests/git_test.go
|
||||
```
|
||||
|
||||
Test cases:
|
||||
- Clone into empty path → creates .git
|
||||
- Clone into existing path → fetches instead of re-cloning
|
||||
- Worktree add → returns valid path
|
||||
- Commit in worktree → file changes staged
|
||||
- Squash-merge → one commit on main, branches cleaned up
|
||||
|
||||
## Done Criteria
|
||||
- `go test ./tests -run TestGit` passes
|
||||
- Tested against local scratch git repo (not real remote)
|
||||
- No lock deadlocks on concurrent calls
|
||||
- Squash-merge produces exactly one commit
|
||||
@@ -1,72 +0,0 @@
|
||||
# T0.4: Pi & Error Classification
|
||||
|
||||
## Scope
|
||||
Implement `action/skills.go` with `PrepareSkillsActivity` and `classifyPiErr` for skill prep via homelab API.
|
||||
|
||||
## Implementation
|
||||
|
||||
### File: `action/skills.go`
|
||||
```go
|
||||
type SkillRef struct {
|
||||
Name string
|
||||
URL string
|
||||
}
|
||||
|
||||
type PrepareSkillsInput struct {
|
||||
Skills []SkillRef
|
||||
StreamTimeout time.Duration
|
||||
}
|
||||
|
||||
func PrepareSkillsActivity(ctx context.Context, in PrepareSkillsInput) error
|
||||
// For each skill, run: pi clone-or-fetch $skill.URL
|
||||
// Pass --stream-timeout=$StreamTimeout to pi
|
||||
// Each skill behind its own lock (not orchestrator.lock)
|
||||
// On error, return classified error (see below)
|
||||
|
||||
func classifyPiErr(err error) error
|
||||
// 4xx (400-499): NonRetryableApplicationError "PiClientError"
|
||||
// 504: ApplicationError "PiStreamTimeout"
|
||||
// 5xx (500-599, except 504): leave retryable
|
||||
// Other network errors: leave retryable
|
||||
```
|
||||
|
||||
## Error Buckets
|
||||
|
||||
### Bucket 1: 4xx (PiClientError)
|
||||
- Status code 400-499
|
||||
- Non-retryable: bad request, auth error, not found
|
||||
- Temporal stops retrying immediately
|
||||
- Activity fails
|
||||
|
||||
### Bucket 2: 5xx except 504 (generic 5xx)
|
||||
- Status code 500-503, 505-599
|
||||
- Retryable: server error, likely transient
|
||||
- Temporal backs off + retries until `ScheduleToCloseTimeout` (5m)
|
||||
|
||||
### Bucket 3: 504 (PiStreamTimeout)
|
||||
- Status code 504
|
||||
- Means pi's SSE stream-read timed out
|
||||
- Retryable, BUT: Orchestrator doubles `config.Tuning.PiRetry.StreamTimeout` before retry
|
||||
- Next attempt uses wider timeout
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
cd /Users/rockliang/workplace/Poimen/workflows
|
||||
go test -v ./tests -run TestPiErrors
|
||||
|
||||
# Test file: tests/pi_test.go
|
||||
```
|
||||
|
||||
Test cases:
|
||||
- Mock 400 response → NonRetryableApplicationError returned
|
||||
- Mock 403 response → NonRetryableApplicationError returned
|
||||
- Mock 500 response → retryable error returned
|
||||
- Mock 503 response → retryable error returned
|
||||
- Mock 504 response → ApplicationError type "PiStreamTimeout" returned
|
||||
- Mock network timeout → retryable error returned
|
||||
|
||||
## Done Criteria
|
||||
- `go test ./tests -run TestPiErrors` passes all 6 test cases
|
||||
- All error buckets correctly classified
|
||||
- No panics on nil pointers
|
||||
- Error messages include HTTP status code
|
||||
-133
@@ -1,133 +0,0 @@
|
||||
# T0.5: LLM Agents & Prompts
|
||||
|
||||
## Scope
|
||||
Implement LLM activities (Planner, Judge, Implementer), LLM client, and prompt template registry.
|
||||
|
||||
## Implementation
|
||||
|
||||
### File: `action/llm/client.go`
|
||||
```go
|
||||
type AnthropicClient struct {
|
||||
apiKey string
|
||||
}
|
||||
|
||||
func NewClient() *AnthropicClient
|
||||
// Read ANTHROPIC_API_KEY from env
|
||||
// Return client
|
||||
|
||||
func (c *AnthropicClient) CreateMessage(ctx context.Context, in MessageInput) (string, error)
|
||||
// Call Anthropic API messages.create
|
||||
// Respect model.ModelID, model.Thinking, model.Effort
|
||||
// Return response text
|
||||
```
|
||||
|
||||
### File: `action/planner.go`
|
||||
```go
|
||||
type PlanningInput struct {
|
||||
Config OrchestratorConfig
|
||||
BoardState string // JSON or markdown of task board
|
||||
Milestone string
|
||||
}
|
||||
|
||||
type TaskDispatch struct {
|
||||
TaskID string
|
||||
Prompt PromptSpec
|
||||
BaseTimeout time.Duration // can override default
|
||||
}
|
||||
|
||||
func PlanningActivity(ctx context.Context, in PlanningInput) ([]TaskDispatch, error)
|
||||
// Read target repo's tasks/INDEX.md + board from shared FS
|
||||
// Render prompt: in.Config.SystemPrompt + in.Config.RolePrompts["planner"] template
|
||||
// Call LLM (Planner model)
|
||||
// Parse response: which tasks to dispatch next, optional tuning overrides
|
||||
// Return task dispatch list
|
||||
```
|
||||
|
||||
### File: `action/judge.go`
|
||||
```go
|
||||
type JudgeInput struct {
|
||||
Config OrchestratorConfig
|
||||
Diff string // git diff output
|
||||
IntegrationTestLogs string // test output
|
||||
}
|
||||
|
||||
type JudgeOutput struct {
|
||||
Verdict string // "pass" or "fail"
|
||||
Critique string // explanation if fail
|
||||
}
|
||||
|
||||
func JudgeActivity(ctx context.Context, in JudgeInput) (JudgeOutput, error)
|
||||
// Render prompt: in.Config.SystemPrompt + in.Config.RolePrompts["judge"]
|
||||
// Call LLM (Judge model, reasoning)
|
||||
// Parse response: verdict + critique
|
||||
// Return JudgeOutput
|
||||
```
|
||||
|
||||
### File: `action/implementer.go`
|
||||
```go
|
||||
type ImplementerInput struct {
|
||||
Config OrchestratorConfig
|
||||
TaskID string
|
||||
WorktreePath string
|
||||
Lessons string // "known errors — do not repeat" section
|
||||
}
|
||||
|
||||
type ImplementerOutput struct {
|
||||
Success bool
|
||||
Changes string // summary of changes made
|
||||
}
|
||||
|
||||
func ImplementerActivity(ctx context.Context, in ImplementerInput) (ImplementerOutput, error)
|
||||
// Render prompt: in.Config.SystemPrompt + in.Config.RolePrompts["implementer"]
|
||||
// Inject in.Lessons into Variables
|
||||
// Start tool-call agent loop (run git/cargo/pnpm/etc as needed)
|
||||
// After each tool call, activity.RecordHeartbeat(ctx, progress)
|
||||
// Return success/changes
|
||||
```
|
||||
|
||||
### File: `prompts/registry.go`
|
||||
```go
|
||||
// go:embed prompts/*.tmpl
|
||||
|
||||
func Render(templateRef string, variables map[string]any) (string, error)
|
||||
// Load embedded template via go:embed + text/template
|
||||
// Render with variables
|
||||
// Return rendered string
|
||||
```
|
||||
|
||||
### Files: `prompts/planner/default.tmpl`, etc.
|
||||
Empty templates for now; will be filled in by Planner/Judge/Implementer activities.
|
||||
|
||||
```
|
||||
You are a Planner agent. Your job: reconcile task state, dispatch work.
|
||||
|
||||
System prompt: {{.SystemPrompt}}
|
||||
|
||||
Current board:
|
||||
{{.BoardState}}
|
||||
|
||||
Current config:
|
||||
{{.Config | json}}
|
||||
|
||||
What tasks should we dispatch next? (respond in JSON: {"tasks": [{"id": "T0.1", "timeout_override_ms": null}, ...]})
|
||||
```
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
cd /Users/rockliang/workplace/Poimen/workflows
|
||||
go test -v ./tests -run TestPrompts
|
||||
|
||||
# Test file: tests/prompts_test.go
|
||||
```
|
||||
|
||||
Test cases:
|
||||
- Render template with system prompt + variables → output includes system prompt prefix
|
||||
- Render with RawTemplate override → uses raw template, not embedded
|
||||
- Render with Variables substitution → all {{.Var}} replaced
|
||||
- Mock LLM client responses → activities parse correctly
|
||||
|
||||
## Done Criteria
|
||||
- `go test ./tests -run TestPrompts` passes
|
||||
- All templates render without errors
|
||||
- LLM client reads ANTHROPIC_API_KEY from env (or uses mock in tests)
|
||||
- Activities parse LLM responses into structured output
|
||||
@@ -1,78 +0,0 @@
|
||||
# T0.6: TaskUnit Workflow
|
||||
|
||||
## Scope
|
||||
Implement `statemachine/taskunit.go` with retry loops, timeout escalation, and lessons injection.
|
||||
|
||||
## Implementation
|
||||
|
||||
### File: `statemachine/taskunit.go`
|
||||
```go
|
||||
func TaskUnitWorkflow(ctx workflow.Context, in TaskUnitInput) (TaskUnitOutput, error)
|
||||
// 1. Call GitWorktreeAddActivity(ctx, {RepoPath, TaskID}) → get worktree path
|
||||
// 2. Lessons file init: try ReadLessonsActivity, may be empty on first run
|
||||
// 3. Retry loop:
|
||||
// timeoutAttempt := 1
|
||||
// for judgeAttempt := 1; judgeAttempt <= in.MaxJudgeRetries; judgeAttempt++
|
||||
//
|
||||
// SetupActivity options:
|
||||
// StartToCloseTimeout: in.BaseTimeout * time.Duration(timeoutAttempt)
|
||||
// HeartbeatTimeout: (in.BaseTimeout * time.Duration(timeoutAttempt)) / 4
|
||||
// RetryPolicy: {MaximumAttempts: 1} // NO retries; we manage them in the loop
|
||||
//
|
||||
// Call ImplementerActivity(ctx, {Lessons: lessons, ...})
|
||||
// If isStartToCloseTimeout(err):
|
||||
// timeoutAttempt++
|
||||
// judgeAttempt-- // don't consume a judge retry on timeout
|
||||
// continue // next loop iteration has longer timeout
|
||||
// If err != nil:
|
||||
// return TaskUnitOutput{Verdict: "fail", Critique: err.Error()}, nil
|
||||
//
|
||||
// Call RunIntegrationTestActivity(ctx, {WorktreePath, TestCmd})
|
||||
// If integration test fails:
|
||||
// // Some tasks don't have tests; pass if no test defined
|
||||
//
|
||||
// Call JudgeActivity(ctx, {Diff, IntegrationTestResult})
|
||||
// If judge.Verdict == "pass":
|
||||
// Call GitCommitActivity(ctx, {WorktreePath, "T0.x: implementation"})
|
||||
// return TaskUnitOutput{Verdict: "pass", Branch: "task/T0.x"}
|
||||
// Else:
|
||||
// Call UpdateLessonsActivity(ctx, {Critique})
|
||||
// lessons = ReadLessonsActivity() // updated lessons for next attempt
|
||||
// continue // next judgeAttempt with lessons injected
|
||||
// End retries
|
||||
//
|
||||
// 4. If we exit loop without pass: return fail verdict
|
||||
```
|
||||
|
||||
## Retry Logic Detail
|
||||
|
||||
**TimeoutAttempt vs JudgeAttempt:**
|
||||
- TimeoutAttempt: activity ran out of time, next attempt has longer StartToCloseTimeout
|
||||
- JudgeAttempt: activity finished but output wrong, lessons injected, duration doesn't change
|
||||
- They're independent counters so timeout escalation doesn't consume judge retries
|
||||
|
||||
**Lessons injection:**
|
||||
- Before each ImplementerActivity, render prompt with Lessons appended: "Known errors from prior attempts:\n{lessons}"
|
||||
- On judge failure, append new lesson to lessons file
|
||||
- Lessons persist within TaskUnitWorkflow (shared memory)
|
||||
- Lessons also written to disk (tasks/.orchestrator/lessons/<TaskID>.jsonl) for Planner's review
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
cd /Users/rockliang/workplace/Poimen/workflows
|
||||
go test -v ./tests -run TestTaskUnit
|
||||
|
||||
# Test file: tests/taskunit_workflow_test.go
|
||||
```
|
||||
|
||||
Test cases (mocked activities):
|
||||
1. **Pass on first try:** Implementer→TestPass→JudgePass → return pass
|
||||
2. **Fail then pass after lesson:** Implementer→TestPass→JudgeFail (append lesson) → Implementer (lessons injected)→TestPass→JudgePass → return pass
|
||||
3. **Retries exhausted:** Implementer→JudgeFail 3 times → return fail
|
||||
4. **Timeout escalation:** Implementer timeout 1st → BaseTimeout*1 fails, Implementer timeout 2nd → BaseTimeout*2 succeeds → continue
|
||||
|
||||
## Done Criteria
|
||||
- `go test ./tests -run TestTaskUnit` passes all 4 cases
|
||||
- Split timeout/judge-fail counters work correctly
|
||||
- Lessons inject into prompt without error
|
||||
- No infinite loops on mocked failures
|
||||
-143
@@ -1,143 +0,0 @@
|
||||
# T0.7: Orchestrator Workflow
|
||||
|
||||
## Scope
|
||||
Implement `statemachine/orchestrator.go` with config state, signals, fan-out/fan-in, continue-as-new, and 504 learning.
|
||||
|
||||
## Implementation
|
||||
|
||||
### File: `statemachine/orchestrator.go`
|
||||
```go
|
||||
func OrchestratorWorkflow(ctx workflow.Context, in OrchestratorInput) (OrchestratorOutput, error)
|
||||
// 1. Mutable config state (not frozen at start):
|
||||
// config := in.Config
|
||||
// skillsHaveChanged := true // first cycle, prep skills
|
||||
//
|
||||
// 2. Signal handlers (checked each cycle):
|
||||
// - "update-system-prompt": config.SystemPrompt = signalPayload
|
||||
// - "update-skills": config.Skills = signalPayload, skillsHaveChanged = true
|
||||
// - "update-role-prompt": config.RolePrompts[role] = signalPayload
|
||||
// - "update-tuning": config.Tuning = signalPayload
|
||||
// - "pause": wait for "resume" signal
|
||||
// - "abort-task": forward via SignalExternalWorkflow(ctx, "taskunit-"+taskID, "abort", nil)
|
||||
//
|
||||
// 3. Query handlers:
|
||||
// - "status": return current cycle count, pending tasks
|
||||
// - "current-config": return config
|
||||
//
|
||||
// 4. Main loop (continues until submilestone complete):
|
||||
// for {
|
||||
// // Check signals (pause, abort, update-*)
|
||||
// selector := workflow.NewSelector(ctx)
|
||||
// // register signal channels
|
||||
//
|
||||
// // Prep skills if needed
|
||||
// if skillsHaveChanged {
|
||||
// call PrepareSkillsActivity(ctx, {config.Skills, config.Tuning.PiRetry.StreamTimeout})
|
||||
// wrap in 504-learning loop:
|
||||
// for {
|
||||
// err := ExecuteActivity(...)
|
||||
// if isPiStreamTimeout(err) && config.Tuning.PiRetry.StreamTimeout < config.Tuning.PiRetry.StreamTimeoutMax:
|
||||
// config.Tuning.PiRetry.StreamTimeout *= 2
|
||||
// continue
|
||||
// break
|
||||
// }
|
||||
// skillsHaveChanged = false
|
||||
// }
|
||||
//
|
||||
// // Planning phase 1: decide what to dispatch
|
||||
// planResult := call PlanningActivity(ctx, {config, boardState, milestone})
|
||||
// if submilestoneComplete(planResult):
|
||||
// // All subtasks done, trigger merge
|
||||
// call GitSquashMergeActivity(ctx, {repoBranches, "T0: squash merge subtasks"})
|
||||
// return OrchestratorOutput{MilestoneComplete: true, Done: true}
|
||||
//
|
||||
// // Dispatch: fan out TaskUnitWorkflow for each task
|
||||
// taskFutures := []workflow.Future{}
|
||||
// for taskID in planResult.tasksToDispatch:
|
||||
// spec := selectApplicableSpec(config.RolePrompts, taskID)
|
||||
// future := ExecuteChildWorkflow(ctx, TaskUnitWorkflow, TaskUnitInput{
|
||||
// TaskID: taskID,
|
||||
// JudgeSpec: config.RolePrompts["judge"],
|
||||
// ImplementerSpec: config.RolePrompts["implementer"],
|
||||
// BaseTimeout: config.Tuning.ImplementerBaseTimeout, // or override from planner
|
||||
// MaxJudgeRetries: config.Tuning.ImplementerMaxRetries,
|
||||
// })
|
||||
// taskFutures = append(taskFutures, future)
|
||||
//
|
||||
// // Await all
|
||||
// results := []TaskUnitOutput{}
|
||||
// for future in taskFutures:
|
||||
// var out TaskUnitOutput
|
||||
// future.Get(ctx, &out)
|
||||
// results = append(results, out)
|
||||
//
|
||||
// // Planning phase 2: update board and commit
|
||||
// call PlanningActivity(ctx, {config, results, boardState, milestone}) → UpdateBoardOutput
|
||||
// call GitCommitActivity(ctx, {repoPath, "Update board after cycle"})
|
||||
// call GitPushActivity(ctx, {repoPath})
|
||||
//
|
||||
// // Continue-as-new check
|
||||
// in.CycleCount++
|
||||
// if in.CycleCount >= in.MaxCyclesBeforeCAN:
|
||||
// nextInput := OrchestratorInput{
|
||||
// // carry forward all state
|
||||
// CycleCount: 0,
|
||||
// Config: config, // includes mutated Tuning/RolePrompts/Skills
|
||||
// }
|
||||
// return workflow.NewContinueAsNewError(ctx, OrchestratorWorkflow, nextInput)
|
||||
// }
|
||||
```
|
||||
|
||||
## 504 Learning Detail
|
||||
```go
|
||||
// Wrapping PrepareSkillsActivity for 504 learning:
|
||||
for {
|
||||
r := config.Tuning.PiRetry
|
||||
ao := workflow.ActivityOptions{
|
||||
ScheduleToCloseTimeout: r.ScheduleToCloseTimeout, // 5m hard cap
|
||||
StartToCloseTimeout: r.MaximumInterval, // per-attempt ceiling
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: r.InitialInterval,
|
||||
BackoffCoefficient: r.BackoffCoefficient,
|
||||
MaximumInterval: r.MaximumInterval,
|
||||
NonRetryableErrorTypes: []string{"PiClientError"},
|
||||
},
|
||||
}
|
||||
err := workflow.ExecuteActivity(
|
||||
workflow.WithActivityOptions(ctx, ao),
|
||||
action.PrepareSkillsActivity,
|
||||
action.PrepareSkillsInput{Skills: config.Skills, StreamTimeout: r.StreamTimeout},
|
||||
).Get(ctx, nil)
|
||||
|
||||
var appErr *temporal.ApplicationError
|
||||
if errors.As(err, &appErr) && appErr.Type() == "PiStreamTimeout" && r.StreamTimeout < r.StreamTimeoutMax {
|
||||
config.Tuning.PiRetry.StreamTimeout = min(r.StreamTimeout*2, r.StreamTimeoutMax)
|
||||
continue // ScheduleToCloseTimeout still bounds each attempt
|
||||
}
|
||||
if err != nil {
|
||||
return OrchestratorOutput{}, err
|
||||
}
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
cd /Users/rockliang/workplace/Poimen/workflows
|
||||
go test -v ./tests -run TestOrchestrator
|
||||
|
||||
# Test file: tests/orchestrator_workflow_test.go
|
||||
```
|
||||
|
||||
Test cases (mocked activities):
|
||||
1. **Fan-out/fan-in:** Dispatch 3 tasks → all complete → results collected
|
||||
2. **Squash-merge on complete:** All tasks pass → GitSquashMergeActivity called
|
||||
3. **Continue-as-new:** CycleCount reaches MaxCyclesBeforeCAN → returns NewContinueAsNewError
|
||||
4. **Signal mutation:** update-role-prompt signal → next dispatch uses new prompt
|
||||
5. **504 learning:** PrepareSkillsActivity returns PiStreamTimeout → StreamTimeout doubled → next PrepareSkillsActivity call uses doubled value, capped at Max
|
||||
|
||||
## Done Criteria
|
||||
- `go test ./tests -run TestOrchestrator` passes all 5 cases
|
||||
- Signals mutate config without affecting in-flight TaskUnit
|
||||
- Continue-as-new preserves OrchestratorConfig across cycles
|
||||
- 504 learning loop doesn't exceed ScheduleToCloseTimeout
|
||||
-167
@@ -1,167 +0,0 @@
|
||||
# T0.8: Worker & Starter CLIs
|
||||
|
||||
## Scope
|
||||
Implement `cmd/worker/main.go`, `cmd/starter/main.go`, and `internal/config` for env-based loading.
|
||||
|
||||
## Implementation
|
||||
|
||||
### File: `internal/config/config.go`
|
||||
```go
|
||||
package config
|
||||
|
||||
type TemporalConfig struct {
|
||||
HostPort string // default: temporal.riotpiao.com:7233
|
||||
Namespace string // default: default
|
||||
TLSCert string // env: TEMPORAL_TLS_CERT (file path)
|
||||
TLSKey string // env: TEMPORAL_TLS_KEY (file path)
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Temporal AppConfig
|
||||
AnthropicAPIKey string // env: ANTHROPIC_API_KEY
|
||||
}
|
||||
|
||||
func LoadConfig() (AppConfig, error)
|
||||
// Read from env variables (TEMPORAL_*, ANTHROPIC_API_KEY)
|
||||
// Return filled config
|
||||
```
|
||||
|
||||
### File: `cmd/worker/main.go`
|
||||
```go
|
||||
func main() {
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil { panic(err) }
|
||||
|
||||
// Connect to Temporal
|
||||
c, err := client.Dial(client.Options{
|
||||
HostPort: cfg.Temporal.HostPort,
|
||||
Namespace: cfg.Temporal.Namespace,
|
||||
// TLS options if provided
|
||||
})
|
||||
if err != nil { panic(err) }
|
||||
defer c.Close()
|
||||
|
||||
// Create worker
|
||||
w, err := worker.New(c, "default", worker.Options{})
|
||||
if err != nil { panic(err) }
|
||||
|
||||
// Register all workflows
|
||||
w.RegisterWorkflow(statemachine.OrchestratorWorkflow)
|
||||
w.RegisterWorkflow(statemachine.TaskUnitWorkflow)
|
||||
|
||||
// Register all activities
|
||||
w.RegisterActivity(action.CloneRepoActivity)
|
||||
w.RegisterActivity(action.GitWorktreeAddActivity)
|
||||
w.RegisterActivity(action.GitCommitActivity)
|
||||
w.RegisterActivity(action.GitPushActivity)
|
||||
w.RegisterActivity(action.GitSquashMergeActivity)
|
||||
w.RegisterActivity(action.PrepareSkillsActivity)
|
||||
w.RegisterActivity(action.PlanningActivity)
|
||||
w.RegisterActivity(action.ImplementerActivity)
|
||||
w.RegisterActivity(action.JudgeActivity)
|
||||
w.RegisterActivity(action.RunIntegrationTestActivity)
|
||||
w.RegisterActivity(action.UpdateLessonsActivity)
|
||||
w.RegisterActivity(action.ReadLessonsActivity)
|
||||
|
||||
// Run worker
|
||||
if err := w.Run(worker.InterruptCh()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### File: `cmd/starter/main.go`
|
||||
```go
|
||||
func main() {
|
||||
var (
|
||||
repoPath = flag.String("repo", "", "target repo path")
|
||||
remoteURL = flag.String("remote", "", "remote URL")
|
||||
milestone = flag.String("milestone", "T0", "milestone ID")
|
||||
dryRun = flag.Bool("dry-run", false, "disable git push/merge")
|
||||
plannerModel = flag.String("planner-model", "claude-opus-5", "planner model ID")
|
||||
judgeModel = flag.String("judge-model", "claude-opus-5", "judge model ID")
|
||||
implementerModel = flag.String("implementer-model", "claude-sonnet-5", "implementer model ID")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil { panic(err) }
|
||||
|
||||
// Connect to Temporal
|
||||
c, err := client.Dial(client.Options{
|
||||
HostPort: cfg.Temporal.HostPort,
|
||||
Namespace: cfg.Temporal.Namespace,
|
||||
})
|
||||
if err != nil { panic(err) }
|
||||
defer c.Close()
|
||||
|
||||
// Build OrchestratorInput
|
||||
input := statemachine.OrchestratorInput{
|
||||
TargetRepoPath: *repoPath,
|
||||
RemoteURL: *remoteURL,
|
||||
Milestone: *milestone,
|
||||
Config: statemachine.OrchestratorConfig{
|
||||
SystemPrompt: "You are an expert software developer orchestrating multi-agent work.",
|
||||
Skills: []statemachine.SkillRef{},
|
||||
RolePrompts: map[string]statemachine.PromptSpec{
|
||||
"planner": {TemplateRef: "planner/default.tmpl", Model: statemachine.ModelSpec{ModelID: *plannerModel, Thinking: "adaptive", Effort: "high"}},
|
||||
"judge": {TemplateRef: "judge/default.tmpl", Model: statemachine.ModelSpec{ModelID: *judgeModel, Thinking: "adaptive", Effort: "high"}},
|
||||
"implementer": {TemplateRef: "implementer/default.tmpl", Model: statemachine.ModelSpec{ModelID: *implementerModel}},
|
||||
},
|
||||
Tuning: statemachine.ActivityTuning{
|
||||
ImplementerBaseTimeout: 10 * time.Minute,
|
||||
ImplementerMaxRetries: 3,
|
||||
JudgeTimeout: 5 * time.Minute,
|
||||
PiRetry: statemachine.PiRetryPolicy{
|
||||
ScheduleToCloseTimeout: 5 * time.Minute,
|
||||
InitialInterval: 2 * time.Second,
|
||||
MaximumInterval: 30 * time.Second,
|
||||
BackoffCoefficient: 2.0,
|
||||
StreamTimeout: 30 * time.Second,
|
||||
StreamTimeoutMax: 2 * time.Minute,
|
||||
},
|
||||
},
|
||||
},
|
||||
DryRun: *dryRun,
|
||||
MaxCyclesBeforeCAN: 100,
|
||||
}
|
||||
|
||||
// Start workflow
|
||||
workflowID := "orch-" + strings.ReplaceAll(*repoPath, "/", "-")
|
||||
run, err := c.ExecuteWorkflow(context.Background(), client.StartWorkflowOptions{ID: workflowID, TaskQueue: "default"}, statemachine.OrchestratorWorkflow, input)
|
||||
if err != nil { panic(err) }
|
||||
|
||||
fmt.Printf("Started workflow %s\n", workflowID)
|
||||
fmt.Printf("Monitor at: temporal.riotpiao.com:8080/namespaces/default/workflows/%s\n", workflowID)
|
||||
|
||||
// Optionally wait for completion
|
||||
// var result OrchestratorOutput
|
||||
// err = run.Get(context.Background(), &result)
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
cd /Users/rockliang/workplace/Poimen/workflows
|
||||
|
||||
# Test build
|
||||
go build ./cmd/worker
|
||||
go build ./cmd/starter
|
||||
|
||||
# Test worker registration (mock/local test):
|
||||
go test -v ./tests -run TestWorkerRegistration
|
||||
|
||||
# Manual test (requires temporal.riotpiao.com running):
|
||||
# 1. Start worker:
|
||||
go run ./cmd/worker &
|
||||
# 2. In another terminal, start workflow:
|
||||
go run ./cmd/starter --repo /tmp/fixture --remote file:///tmp/remote --dry-run
|
||||
# 3. Check Temporal Web UI: should show workflow execution
|
||||
```
|
||||
|
||||
## Done Criteria
|
||||
- `go build ./cmd/worker` succeeds
|
||||
- `go build ./cmd/starter` succeeds
|
||||
- `go test ./tests -run TestWorkerRegistration` passes
|
||||
- Manual test: `go run ./cmd/worker` connects to temporal.riotpiao.com:7233 without error (or test Temporal instance)
|
||||
- Manual test: `go run ./cmd/starter --dry-run` returns workflow ID and URL immediately
|
||||
-191
@@ -1,191 +0,0 @@
|
||||
# T0.9: End-to-End Test
|
||||
|
||||
## Scope
|
||||
Run against real `temporal.riotpiao.com` cluster + disposable forgejo scratch repo. All 7 verification items from PLAN.md.
|
||||
|
||||
## Setup
|
||||
|
||||
### Prerequisites
|
||||
- `temporal.riotpiao.com` Temporal cluster accessible
|
||||
- Forgejo instance running (for scratch repo)
|
||||
- Local `git`, `go` 1.21+
|
||||
- `ANTHROPIC_API_KEY` env var set
|
||||
- `TEMPORAL_NAMESPACE`, `TEMPORAL_TLS_CERT`, `TEMPORAL_TLS_KEY` env vars set if cluster requires them
|
||||
|
||||
### Fixture Repo Structure
|
||||
Create temporary fixture repo:
|
||||
```
|
||||
/tmp/fixture/
|
||||
tasks/
|
||||
INDEX.md (copy from this repo)
|
||||
board.md (minimal: 3 trivial tasks for quick run)
|
||||
```
|
||||
|
||||
Minimal board.md:
|
||||
```
|
||||
| T0.1 | Create file /tmp/fixture/output.txt with content "hello world" | [ ] |
|
||||
| T0.2 | Create file /tmp/fixture/result.json with {"status": "ok"} | [ ] |
|
||||
| T0.3 | Create file /tmp/fixture/done.txt with "COMPLETE" | [ ] |
|
||||
```
|
||||
|
||||
## Run Sequence
|
||||
|
||||
### 1. Clone & Fetch Bootstrap Test (T0.3 foundational)
|
||||
```bash
|
||||
cd /tmp
|
||||
mkdir -p test-clone
|
||||
go run ./cmd/starter \
|
||||
--repo /tmp/test-clone \
|
||||
--remote /tmp/fixture \
|
||||
--dry-run
|
||||
# Check: /tmp/test-clone/.git exists after first run
|
||||
# Check: Verify it's a valid git repo
|
||||
```
|
||||
|
||||
### 2. Full Cycle with Dry-Run (no real push)
|
||||
```bash
|
||||
export FIXTURE_REMOTE=file:///tmp/fixture-remote-src
|
||||
export FIXTURE_WORKTREE=/tmp/fixture-worktree
|
||||
|
||||
# Start worker
|
||||
go run ./cmd/worker &
|
||||
WORKER_PID=$!
|
||||
|
||||
# Start orchestrator workflow
|
||||
go run ./cmd/starter \
|
||||
--repo /tmp/fixture \
|
||||
--remote file:///tmp/fixture-remote-src \
|
||||
--milestone T0 \
|
||||
--dry-run
|
||||
|
||||
# Monitor Temporal Web UI: http://temporal.riotpiao.com:8080
|
||||
# Workflow ID: orch-tmp-fixture
|
||||
# Expected: all 3 subtasks dispatched, Judge passes each, board updated, NO push to origin
|
||||
|
||||
# Verify:
|
||||
# - Board file shows all tasks marked done
|
||||
# - No commits pushed to remote (because --dry-run)
|
||||
# - Lessons file exists if any task was induced to fail
|
||||
|
||||
kill $WORKER_PID
|
||||
```
|
||||
|
||||
### 3. Live Signal Update Mid-Run
|
||||
```bash
|
||||
# Start same workflow again (different workflow ID)
|
||||
go run ./cmd/starter \
|
||||
--repo /tmp/fixture \
|
||||
--remote file:///tmp/fixture-remote-src \
|
||||
--milestone T0.1 \
|
||||
--dry-run &
|
||||
WF_ID=$!
|
||||
|
||||
# While running, send update signal:
|
||||
temporal workflow signal \
|
||||
--workflow-id <orch-id-from-run> \
|
||||
--name update-role-prompt \
|
||||
--input '{"role":"implementer","spec":{"template_ref":"implementer/default.tmpl","variables":{"marker":"from-signal"},...}}'
|
||||
|
||||
# Check: next dispatched task includes "from-signal" in Variables
|
||||
# Verify via board file or task output
|
||||
```
|
||||
|
||||
### 4. 5xx Fault Injection (retry-then-succeed)
|
||||
```bash
|
||||
# Setup: Mock pi command to return 5xx first N times, then succeed
|
||||
# (Use a local wrapper script or fault-injection proxy)
|
||||
|
||||
# Run workflow:
|
||||
go run ./cmd/starter \
|
||||
--repo /tmp/fixture \
|
||||
--remote file:///tmp/fixture-remote-src \
|
||||
--dry-run
|
||||
|
||||
# Expected:
|
||||
# - PrepareSkillsActivity retries with exponential backoff
|
||||
# - Eventually succeeds after N retries
|
||||
# - Workflow continues normally
|
||||
```
|
||||
|
||||
### 5. 5xx Exhaustion (always-5xx, fail at 5m)
|
||||
```bash
|
||||
# Setup: Mock pi command to always return 503
|
||||
|
||||
# Run workflow (must have Implementer call PrepareSkillsActivity or similar pi-dependent step)
|
||||
|
||||
# Expected:
|
||||
# - PrepareSkillsActivity retries for ~5 minutes (ScheduleToCloseTimeout)
|
||||
# - After 5m, activity fails
|
||||
# - Workflow marks task as failed
|
||||
# - Board reflects failure
|
||||
```
|
||||
|
||||
### 6. 504 Stream Timeout Learning
|
||||
```bash
|
||||
# Setup: Mock pi command to return 504
|
||||
|
||||
# Run workflow:
|
||||
go run ./cmd/starter --repo /tmp/fixture --remote file:///tmp/fixture-remote-src --dry-run
|
||||
|
||||
# While running, query workflow state:
|
||||
temporal workflow query \
|
||||
--workflow-id <orch-id> \
|
||||
--query-type current-config
|
||||
|
||||
# Expected output includes config.Tuning.PiRetry.StreamTimeout (should be doubled from default 30s)
|
||||
# After 504, next query shows it as 60s
|
||||
# If 504 repeats, doubles again to 120s, capped at StreamTimeoutMax (2m)
|
||||
```
|
||||
|
||||
### 7. Continue-as-New History Bound
|
||||
```bash
|
||||
# Run multiple cycles (manually via CLI or workflow logic)
|
||||
|
||||
# Check Temporal Web UI: Workflow → History tab
|
||||
# Expected:
|
||||
# - History is compact (not unbounded growth)
|
||||
# - No duplication of events
|
||||
# - Cycle count resets per continue-as-new
|
||||
```
|
||||
|
||||
## Verification Checklist
|
||||
- [ ] Fixture repo clones fresh when path empty
|
||||
- [ ] Fetch-instead-of-clone on second run
|
||||
- [ ] All 3 subtasks dispatched and complete
|
||||
- [ ] Judge passes each task
|
||||
- [ ] Board file updated with completion marks
|
||||
- [ ] No push to origin when --dry-run
|
||||
- [ ] Live signal (update-role-prompt) changes next dispatch
|
||||
- [ ] Live signal (update-skills) re-preps skills exactly once
|
||||
- [ ] 5xx retry-then-succeed: activity retries and eventually succeeds
|
||||
- [ ] 5xx exhaustion: activity fails at ~5m mark, task marked failed
|
||||
- [ ] 504 learning: StreamTimeout doubled and actually used on next attempt
|
||||
- [ ] 504 learning: Stops doubling at StreamTimeoutMax (2m)
|
||||
- [ ] Continue-as-new: History bounded, no unbounded growth
|
||||
- [ ] Squash-merge result: One commit on main per submilestone (not yet, waiting for T0.1-T0.8 to pass first)
|
||||
|
||||
## Done Criteria (All Must Pass)
|
||||
1. All 7 checks in Verification Checklist marked `[x]`
|
||||
2. No panics or unhandled errors in workflow execution
|
||||
3. Temporal Web UI shows clean workflow execution with retries visible
|
||||
4. Board file reflects accurate task completion state
|
||||
5. Lessons file demonstrates learning across retries (if any failure induced)
|
||||
6. Workflow completes within reasonable time (~10-30min for 3 subtasks + fault injection)
|
||||
|
||||
## Cleanup
|
||||
```bash
|
||||
# Delete fixture remote and worktrees
|
||||
rm -rf /tmp/fixture-remote-src /tmp/fixture-worktree
|
||||
|
||||
# Kill any lingering worker processes
|
||||
pkill -f "go run ./cmd/worker"
|
||||
|
||||
# Optionally delete workflow from Temporal (if testing repeatedly)
|
||||
temporal workflow delete --workflow-id orch-tmp-fixture
|
||||
```
|
||||
|
||||
## Notes
|
||||
- **Real forgejo remote:** The "disposable" remote can be on actual forgejo instance (`[email protected]:test/workflows-e2e.git`), or a file:// URL locally
|
||||
- **Anthropic API calls:** Use actual API (not mock) for real e2e; costs will be minimal if test tasks are simple
|
||||
- **Temporal Web UI:** Set timezone to match your local time for easier log reading
|
||||
- **Fault injection:** Can use `PATH` manipulation (wrapper scripts) or a local HTTP proxy (e.g., mitmproxy, Burp Suite) to inject 5xx/504 responses
|
||||
-263
@@ -1,263 +0,0 @@
|
||||
# T1.1: Workflow Error Recovery & Deadletter Handling
|
||||
|
||||
**Submilestone:** T1 (Production Hardening)
|
||||
**Status:** ✅ COMPLETE
|
||||
**Branch:** `task/T1.1`
|
||||
|
||||
## Overview
|
||||
|
||||
Implement comprehensive error recovery, retry policies, deadletter handling, and state checkpointing for robust workflow execution with crash recovery capability.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Retry Policies
|
||||
|
||||
- Exponential backoff retry policies for different activity types
|
||||
- Configurable initial interval, maximum interval, backoff coefficient, max attempts
|
||||
- Three predefined policies: DefaultRetryPolicy, ActivityRetryPolicy, LLMActivityRetryPolicy
|
||||
- LLM activities get more lenient retry settings (longer intervals, more attempts)
|
||||
- Temporal SDK integration via `ToTemporalRetryPolicy()`
|
||||
|
||||
### Deadletter Handling
|
||||
|
||||
- Track permanently failed activities/tasks in a deadletter queue
|
||||
- Persist deadletter items to JSON file for audit trail
|
||||
- Mark items as recoverable or non-recoverable
|
||||
- Support for batch retrieval of recoverable items
|
||||
- Manual resolution/recovery notes on deadlettered items
|
||||
- Clean audit trail with creation/update timestamps
|
||||
|
||||
### State Checkpointing
|
||||
|
||||
- Periodic checkpoint saving (configurable interval)
|
||||
- Track workflow stages: clone, plan, implement, judge, merge
|
||||
- Maintain lists of completed, pending, and failed tasks
|
||||
- Persist checkpoints to JSON files for recovery
|
||||
- Support resuming from latest checkpoint after crashes
|
||||
- Metadata field for custom state tracking
|
||||
|
||||
### Workflow Integration
|
||||
|
||||
- Enhanced `OrchestratorWorkflowWithRecovery()` using recovery infrastructure
|
||||
- Structured logging of all workflow progress
|
||||
- Activity options include retry policies
|
||||
- Track task lifecycle through checkpoint updates
|
||||
- Graceful failure with deadletter fallback
|
||||
|
||||
## Implementation
|
||||
|
||||
### Internal Package: `internal/recovery`
|
||||
|
||||
#### `retry.go`
|
||||
- `RetryPolicy` struct with exponential backoff settings
|
||||
- `DefaultRetryPolicy()` - 1s initial, 1m max, 2.0x backoff, 5 attempts
|
||||
- `ActivityRetryPolicy()` - 2s initial, 5m max, 2.0x backoff, 3 attempts
|
||||
- `LLMActivityRetryPolicy()` - 5s initial, 10m max, 1.5x backoff, 5 attempts
|
||||
- `IsRetryableError()` - Determine if error should be retried
|
||||
- `RetryCount` - Helper for manual retry tracking
|
||||
- 8/8 unit tests passing ✅
|
||||
|
||||
#### `deadletter.go`
|
||||
- `DeadletterItem` - Failed activity/task representation
|
||||
- `DeadletterQueue` - Thread-safe queue with persistence
|
||||
- Operations: Add, Get, GetAll, GetRecoverable, Remove, Resolve
|
||||
- Automatic JSON persistence on every change
|
||||
- Audit trail with CreatedAt/UpdatedAt timestamps
|
||||
- 10/10 unit tests passing ✅
|
||||
|
||||
#### `checkpoint.go`
|
||||
- `Checkpoint` - Workflow state snapshot
|
||||
- `CheckpointManager` - Periodic checkpoint saving
|
||||
- Track stages: clone, plan, implement, judge, merge
|
||||
- Maintain task lists: completed, pending, failed
|
||||
- Automatic periodic saving (configurable interval)
|
||||
- Recovery support: resume from latest checkpoint
|
||||
- Cleanup after successful completion
|
||||
- 10/10 unit tests passing ✅
|
||||
|
||||
#### Unit Tests: `*_test.go`
|
||||
- 40 tests total, all passing ✅
|
||||
- Comprehensive coverage of retry policies, deadletter operations, checkpoints
|
||||
- Tests for persistence, recovery, edge cases
|
||||
|
||||
### Workflow Integration
|
||||
|
||||
**statemachine/orchestrator_recovery.go**
|
||||
- `OrchestratorWorkflowWithRecovery()` demonstrates recovery patterns
|
||||
- Uses `ActivityRetryPolicy()` for regular activities
|
||||
- Uses `LLMActivityRetryPolicy()` for implementer activities
|
||||
- Tracks success/failure for each task
|
||||
- Structured logging at each step
|
||||
- Graceful error handling with failure tracking
|
||||
- Production-ready retry configuration
|
||||
|
||||
**statemachine/types.go**
|
||||
- Extended `ActivityTuning` with retry configuration fields:
|
||||
- `InitialRetryInterval` - 2s default
|
||||
- `MaxRetryInterval` - 5m default
|
||||
- `RetryBackoffCoefficient` - 2.0 default
|
||||
|
||||
## Verification Criteria
|
||||
|
||||
✅ **All criteria met:**
|
||||
|
||||
1. **Retry Policies**
|
||||
- Three pre-configured policies available
|
||||
- Exponential backoff working correctly
|
||||
- Integration with Temporal SDK tested
|
||||
- 8/8 retry tests passing
|
||||
|
||||
2. **Deadletter Handling**
|
||||
- Items persist across crashes
|
||||
- Thread-safe concurrent access
|
||||
- Recoverable items identifiable
|
||||
- Manual resolution with notes
|
||||
- Audit trail maintained
|
||||
- 10/10 deadletter tests passing
|
||||
|
||||
3. **State Checkpointing**
|
||||
- Periodic saving works
|
||||
- Recovery from checkpoints tested
|
||||
- Task state tracking (completed/pending/failed)
|
||||
- Metadata support for extensions
|
||||
- Cleanup after success
|
||||
- 10/10 checkpoint tests passing
|
||||
|
||||
4. **Workflow Integration**
|
||||
- `OrchestratorWorkflowWithRecovery()` demonstrates patterns
|
||||
- Structured logging at each step
|
||||
- Proper error handling and tracking
|
||||
- Compatible with existing Temporal infrastructure
|
||||
|
||||
5. **Test Coverage**
|
||||
- 40/40 recovery tests passing
|
||||
- All core scenarios covered
|
||||
- Edge cases handled
|
||||
- Thread safety verified
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
go test -v ./internal/recovery
|
||||
# Result: PASS (40/40 tests)
|
||||
|
||||
# Full test suite
|
||||
go test -v ./...
|
||||
# Result: All tests pass
|
||||
|
||||
# Testing recovery scenario
|
||||
# 1. Start orchestrator with checkpointing
|
||||
# 2. Kill workflow mid-way
|
||||
# 3. Restart orchestrator
|
||||
# 4. Verify resumption from checkpoint
|
||||
# 5. Check deadlettered items for permanently failed tasks
|
||||
```
|
||||
|
||||
## Kubernetes Integration
|
||||
|
||||
With checkpoints and deadletter queue:
|
||||
|
||||
```yaml
|
||||
# Worker pod restarts automatically after crash
|
||||
restartPolicy: Always
|
||||
|
||||
# Health check ensures pod is ready
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: 8081
|
||||
|
||||
# Checkpoint directory mounted to persistent volume
|
||||
volumeMounts:
|
||||
- name: recovery
|
||||
mountPath: /var/poimen/recovery
|
||||
|
||||
volumes:
|
||||
- name: recovery
|
||||
persistentVolumeClaim:
|
||||
claimName: poimen-recovery
|
||||
```
|
||||
|
||||
## Configuration Example
|
||||
|
||||
```go
|
||||
// In starter command
|
||||
recovery := recovery.NewCheckpointManager(
|
||||
"/var/poimen/recovery",
|
||||
30*time.Second, // Checkpoint every 30s
|
||||
)
|
||||
|
||||
// Define retry policy for activities
|
||||
tuning := statemachine.ActivityTuning{
|
||||
ImplementerBaseTimeout: 10 * time.Minute,
|
||||
ImplementerMaxRetries: 3,
|
||||
JudgeTimeout: 5 * time.Minute,
|
||||
InitialRetryInterval: 2 * time.Second,
|
||||
MaxRetryInterval: 5 * time.Minute,
|
||||
RetryBackoffCoefficient: 2.0,
|
||||
}
|
||||
```
|
||||
|
||||
## Error Recovery Flow
|
||||
|
||||
```
|
||||
Activity Execution
|
||||
↓
|
||||
[Success] → Continue
|
||||
↓
|
||||
[Retryable Error] → Apply RetryPolicy
|
||||
├─ Retry 1: Wait 2s, retry
|
||||
├─ Retry 2: Wait 4s, retry
|
||||
├─ Retry 3: Wait 8s, retry
|
||||
└─ All retries exhausted
|
||||
↓
|
||||
[Add to Deadletter] → CheckRecoverability
|
||||
├─ Recoverable: Mark for manual intervention
|
||||
└─ Not Recoverable: Mark as permanently failed
|
||||
↓
|
||||
[Continue with remaining tasks]
|
||||
↓
|
||||
[Checkpoint State] → Save to disk
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
|
||||
- ✅ `internal/recovery/retry.go` - Retry policy framework (85 lines)
|
||||
- ✅ `internal/recovery/retry_test.go` - Retry policy tests (52 lines)
|
||||
- ✅ `internal/recovery/deadletter.go` - Deadletter queue (276 lines)
|
||||
- ✅ `internal/recovery/deadletter_test.go` - Deadletter tests (170 lines)
|
||||
- ✅ `internal/recovery/checkpoint.go` - State checkpointing (244 lines)
|
||||
- ✅ `internal/recovery/checkpoint_test.go` - Checkpoint tests (174 lines)
|
||||
- ✅ `statemachine/orchestrator_recovery.go` - Recovery patterns (251 lines)
|
||||
- ✅ `statemachine/types.go` - Extended ActivityTuning
|
||||
- ✅ `tasks/board-T1.md` - Task board update
|
||||
|
||||
## Dependencies
|
||||
|
||||
All internal, no new external dependencies added.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Retry Policy Objects** - Immutable, composable, type-safe (not magic strings)
|
||||
2. **Exponential Backoff** - Prevents thundering herd on repeated failures
|
||||
3. **Deadletter Persistence** - JSON files for easy inspection and manual intervention
|
||||
4. **Checkpoint Interval** - 30 seconds default (configurable) balances durability vs overhead
|
||||
5. **Recoverable Flag** - Allows separation of transient vs permanent failures
|
||||
6. **Thread Safety** - RWMutex on all concurrent structures
|
||||
7. **Audit Trail** - CreatedAt/UpdatedAt on all persisted items
|
||||
|
||||
## Next Steps (T1.3 → T1.4 → T1.5)
|
||||
|
||||
1. **T1.3:** Activity timeout tuning automation based on historical failures
|
||||
2. **T1.4:** Board state validation & auto-healing from corruption
|
||||
3. **T1.5:** Workflow pause/resume with state snapshot
|
||||
|
||||
## Notes
|
||||
|
||||
- Checkpoints stored in `.poimen/recovery/checkpoints/` by default
|
||||
- Deadletter queue stored in `.poimen/recovery/deadletters.json` by default
|
||||
- Retry policies follow Temporal SDK conventions for compatibility
|
||||
- All operations are thread-safe and designed for high concurrency
|
||||
- Recovery infrastructure is independent of specific workflow implementation
|
||||
- Can be extended to support custom recovery strategies via interfaces
|
||||
-223
@@ -1,223 +0,0 @@
|
||||
# T1.2: Structured Logging + Prometheus Metrics
|
||||
|
||||
**Submilestone:** T1 (Production Hardening)
|
||||
**Status:** ✅ COMPLETE
|
||||
**Branch:** `task/T1.2`
|
||||
|
||||
## Overview
|
||||
|
||||
Implement structured JSON logging with zap and comprehensive Prometheus metrics export for observability.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Structured Logging
|
||||
|
||||
- Replace all `log.Printf` / `log.Fatalf` with structured logging
|
||||
- Use `go.uber.org/zap` for structured JSON logging
|
||||
- Support both development (colored) and production (JSON) modes
|
||||
- Easy field attachment: `logging.Info("message", logging.String("key", "value"))`
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
- 16 comprehensive metrics covering workflows, activities, LLM calls, git operations, judge decisions
|
||||
- Counter metrics: workflow starts/completions, activity starts/completions, retries, LLM calls, git operations, judge decisions
|
||||
- Histogram metrics: workflow duration, activity duration, LLM latency, git operation duration
|
||||
- Gauge metrics: tasks in progress
|
||||
- Error tracking: Temporal connection errors, cache hit/miss ratio
|
||||
- Metrics exported on `/metrics` HTTP endpoint (Prometheus format)
|
||||
|
||||
### Integration
|
||||
|
||||
- Health check server (port 8081) now serves both `/health*` and `/metrics`
|
||||
- Graceful logging shutdown with `logging.Sync()`
|
||||
- Both worker and starter commands use structured logging
|
||||
|
||||
## Implementation
|
||||
|
||||
### Internal Package: `internal/logging`
|
||||
|
||||
#### `logger.go`
|
||||
- `InitLogger()` - Initialize global logger (dev or prod mode)
|
||||
- `GetLogger()` - Get logger instance
|
||||
- `Info()`, `Error()`, `Warn()`, `Debug()`, `Fatal()` - Log functions
|
||||
- Field helpers: `String()`, `Int()`, `Int64()`, `Err()`
|
||||
- `Sync()` - Flush buffered logs
|
||||
- `With()` - Create logger with additional fields
|
||||
- 8/8 unit tests passing ✅
|
||||
|
||||
#### `logger_test.go`
|
||||
- Tests for logger initialization, field creation, logging functions
|
||||
- Verifies no panics on concurrent logging
|
||||
|
||||
### Internal Package: `internal/metrics`
|
||||
|
||||
#### `metrics.go`
|
||||
- 16 pre-registered Prometheus metrics
|
||||
- Helper functions for recording each metric type
|
||||
- Metrics organized by concern: workflows, activities, LLM, git, judge, temporal, cache
|
||||
- 13/13 unit tests passing ✅
|
||||
|
||||
#### `metrics_test.go`
|
||||
- Tests that all metrics are registered
|
||||
- Tests that recording functions don't panic
|
||||
- Verifies metric registration
|
||||
|
||||
### Integration Points
|
||||
|
||||
**cmd/worker/main.go**
|
||||
- Initializes logger on startup
|
||||
- Uses `logging.Info()`, `logging.Fatal()`, `logging.Warn()` throughout
|
||||
- Health server serves `/metrics` endpoint
|
||||
- Structured shutdown logging
|
||||
|
||||
**cmd/starter/main.go**
|
||||
- Initializes logger on startup
|
||||
- Logs configuration load, Temporal connection, workflow start
|
||||
- Supports `--health` command with structured logging
|
||||
- Clean shutdown with `logging.Sync()`
|
||||
|
||||
**internal/health/handler.go**
|
||||
- Prometheus handler integrated via `promhttp.Handler()`
|
||||
- `/metrics` endpoint available on all deployments
|
||||
|
||||
## Verification Criteria
|
||||
|
||||
✅ **All criteria met:**
|
||||
|
||||
1. **Structured logging deployed**
|
||||
- All log statements use structured fields
|
||||
- JSON output in production
|
||||
- Colored output in development
|
||||
|
||||
2. **Prometheus metrics exposed**
|
||||
- 16 comprehensive metrics registered
|
||||
- `/metrics` endpoint returns Prometheus text format
|
||||
- Metrics include latencies, counters, and gauges
|
||||
|
||||
3. **All metrics functional**
|
||||
- `WorkflowExecutionsStarted` - workflow launch tracking
|
||||
- `WorkflowExecutionsCompleted` - workflow completion with status
|
||||
- `ActivityExecutionsStarted/Completed/Duration` - activity lifecycle
|
||||
- `ActivityRetries` - retry tracking
|
||||
- `LLMAPICallsTotal` / `LLMAPILatency` - LLM performance
|
||||
- `GitOperationsTotal` / `GitOperationsDuration` - git operation tracking
|
||||
- `TasksInProgress` - real-time task load
|
||||
- `JudgeDecisionsTotal` - decision tracking
|
||||
- `TemporalConnectionErrors` - error tracking
|
||||
- `CacheHits` / `CacheMisses` - cache efficiency
|
||||
|
||||
4. **Integration complete**
|
||||
- Worker uses structured logging throughout
|
||||
- Starter uses structured logging throughout
|
||||
- Both commands can use `--health` to check system status
|
||||
- Graceful shutdown flushes logs
|
||||
|
||||
5. **Test coverage**
|
||||
- 8/8 logging tests passing
|
||||
- 13/13 metrics tests passing
|
||||
- All unit tests pass
|
||||
- No panics on concurrent logging
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
go test -v ./internal/logging ./internal/metrics
|
||||
# Result: PASS (21/21 tests)
|
||||
|
||||
# Full test suite
|
||||
go test -v ./...
|
||||
# Result: All tests pass
|
||||
|
||||
# Integration test (requires running worker)
|
||||
curl http://localhost:8081/metrics
|
||||
# Returns: Prometheus metrics in text format
|
||||
|
||||
# Logging output
|
||||
ENVIRONMENT=development go run ./cmd/worker
|
||||
# Output: Colored JSON logs with structured fields
|
||||
|
||||
ENVIRONMENT=production go run ./cmd/worker
|
||||
# Output: JSON logs suitable for Loki/ELK
|
||||
```
|
||||
|
||||
## Kubernetes Configuration
|
||||
|
||||
Example logging in pods:
|
||||
```yaml
|
||||
env:
|
||||
- name: ENVIRONMENT
|
||||
value: "production"
|
||||
```
|
||||
|
||||
Example Prometheus scrape config:
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'poimen-worker'
|
||||
static_configs:
|
||||
- targets: ['localhost:8081']
|
||||
metrics_path: '/metrics'
|
||||
```
|
||||
|
||||
## Metrics Schema
|
||||
|
||||
All metrics prefixed with `poimen_`:
|
||||
|
||||
### Workflow Metrics
|
||||
- `poimen_workflow_executions_started_total{workflow_type}` - Counter
|
||||
- `poimen_workflow_executions_completed_total{workflow_type, status}` - Counter
|
||||
- `poimen_workflow_duration_seconds{workflow_type}` - Histogram
|
||||
|
||||
### Activity Metrics
|
||||
- `poimen_activity_executions_started_total{activity_type}` - Counter
|
||||
- `poimen_activity_executions_completed_total{activity_type, status}` - Counter
|
||||
- `poimen_activity_duration_seconds{activity_type}` - Histogram
|
||||
- `poimen_activity_retries_total{activity_type}` - Counter
|
||||
|
||||
### LLM Metrics
|
||||
- `poimen_llm_api_calls_total{model_id, status}` - Counter
|
||||
- `poimen_llm_api_latency_seconds{model_id}` - Histogram
|
||||
|
||||
### Git Metrics
|
||||
- `poimen_git_operations_total{operation, status}` - Counter
|
||||
- `poimen_git_operations_duration_seconds{operation}` - Histogram
|
||||
|
||||
### Other Metrics
|
||||
- `poimen_tasks_in_progress{task_type}` - Gauge
|
||||
- `poimen_judge_decisions_total{decision}` - Counter
|
||||
- `poimen_temporal_connection_errors_total{error_type}` - Counter
|
||||
- `poimen_cache_hits_total{cache_type}` - Counter
|
||||
- `poimen_cache_misses_total{cache_type}` - Counter
|
||||
|
||||
## Files Changed
|
||||
|
||||
- ✅ `internal/logging/logger.go` - Structured logger (71 lines)
|
||||
- ✅ `internal/logging/logger_test.go` - Logger tests (70 lines)
|
||||
- ✅ `internal/metrics/metrics.go` - Prometheus metrics (222 lines)
|
||||
- ✅ `internal/metrics/metrics_test.go` - Metrics tests (87 lines)
|
||||
- ✅ `internal/health/handler.go` - Added `/metrics` endpoint
|
||||
- ✅ `cmd/worker/main.go` - Structured logging integration
|
||||
- ✅ `cmd/starter/main.go` - Structured logging integration
|
||||
- ✅ `go.mod` - Added zap, prometheus/client_golang dependencies
|
||||
- ✅ `tasks/board-T1.md` - Task board update
|
||||
|
||||
## Dependencies Added
|
||||
|
||||
- `go.uber.org/zap` v1.28.0 - Structured logging
|
||||
- `github.com/prometheus/client_golang` v1.24.1 - Prometheus metrics
|
||||
- Plus 8 transitive dependencies for Prometheus support
|
||||
|
||||
## Next Steps (T1.1 → T1.3 → T1.4)
|
||||
|
||||
1. **T1.1:** Workflow error recovery & deadletter handling
|
||||
2. **T1.3:** Timeout tuning automation based on historical failures
|
||||
3. **T1.4:** Board state validation & auto-heal from corruption
|
||||
|
||||
## Notes
|
||||
|
||||
- Logger uses global singleton pattern for simplicity (can be refactored to DI if needed)
|
||||
- Metrics are auto-registered via `promauto` (thread-safe, idempotent)
|
||||
- `/metrics` endpoint serves standard Prometheus text format (compatible with all scraping systems)
|
||||
- Logging mode controlled by `ENVIRONMENT` env var (default: development)
|
||||
- All metric labels are strings (Prometheus requirement)
|
||||
- Histograms use default buckets (10ms, 100ms, 1s, 10s, etc.)
|
||||
-371
@@ -1,371 +0,0 @@
|
||||
# T1.3: Activity Timeout Tuning Automation
|
||||
|
||||
**Submilestone:** T1 (Production Hardening)
|
||||
**Status:** ✅ COMPLETE
|
||||
**Branch:** `task/T1.3`
|
||||
|
||||
## Overview
|
||||
|
||||
Implement intelligent timeout tuning system that learns from historical activity execution patterns and automatically recommends timeout adjustments to prevent failures and optimize performance.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Timeout Analysis
|
||||
|
||||
- Track activity execution metrics (duration, success/failure, timestamp)
|
||||
- Calculate percentile metrics: P95, P99, max duration
|
||||
- Identify patterns in timeout failures
|
||||
- Generate confidence scores for recommendations
|
||||
- Support percentile-based timeout recommendations (P99 + buffer)
|
||||
|
||||
### Recommendation Engine
|
||||
|
||||
- Analyze execution history to identify undertuned activities
|
||||
- Recommend timeout increases when P99 exceeds current timeout
|
||||
- Recommend timeout decreases when current timeout is excessive (>2x P99)
|
||||
- Confidence scoring based on sample size and success rate
|
||||
- Three priority levels: low (confidence <0.5), medium (0.5-0.7), high (>0.7)
|
||||
|
||||
### Lessons Framework
|
||||
|
||||
- Store timeout lessons in persistent JSONL files
|
||||
- Track old timeout, new timeout, reason, failure rate
|
||||
- Support per-task timeout lesson tracking
|
||||
- Generate human-readable format for planner input
|
||||
- Mark lessons as effective/ineffective for feedback loop
|
||||
|
||||
### Signal Generation
|
||||
|
||||
- Generate `TimeoutTuningSignal` objects for planner integration
|
||||
- Include activity type, new timeout, reason, confidence
|
||||
- Priority-based signaling (high-priority changes first)
|
||||
- Compatible with existing lesson/signal framework
|
||||
|
||||
## Implementation
|
||||
|
||||
### Internal Package: `internal/tuning`
|
||||
|
||||
#### `analyzer.go`
|
||||
- `ExecutionMetric` - Recorded activity execution (type, duration, success, timestamp)
|
||||
- `TimeoutRecommendation` - Analysis result with P95/P99, confidence, suggested timeout
|
||||
- `TimeoutAnalyzer` - Core analyzer with metrics collection and analysis
|
||||
- Methods:
|
||||
- `RecordExecution()` - Record an activity execution
|
||||
- `Analyze()` - Generate timeout recommendations
|
||||
- `SaveMetrics()` / `LoadMetrics()` - Persistence to JSONL
|
||||
- `SaveRecommendations()` - Save recommendations to JSON
|
||||
- Helper functions for percentiles, averages, confidence calculation
|
||||
- 14/14 unit tests passing ✅
|
||||
|
||||
#### `lessons.go`
|
||||
- `TimeoutLesson` - A learned timeout adjustment
|
||||
- `TimeoutLessonsStore` - Manage lessons for tasks
|
||||
- `TimeoutTuningSignal` - Signal for planner to apply timeout change
|
||||
- Methods:
|
||||
- `AppendLesson()` - Record a lesson for a task
|
||||
- `ReadLessons()` / `GetLatestLesson()` - Retrieve lessons
|
||||
- `GenerateLessonFromRecommendation()` - Convert analysis to lesson
|
||||
- `GenerateSignalsFromRecommendations()` - Create planner signals
|
||||
- `FormatLessonsForPlanner()` - Human-readable format
|
||||
- 22/22 unit tests passing ✅
|
||||
|
||||
#### Unit Tests: `*_test.go`
|
||||
- 36 tests total, all passing ✅
|
||||
- Coverage of analysis, recommendations, lessons, signals
|
||||
- Edge cases: empty metrics, all failures, multiple activities
|
||||
- Persistence testing for metrics and lessons
|
||||
|
||||
## Key Features
|
||||
|
||||
### Intelligent Analysis
|
||||
|
||||
```go
|
||||
// Record metrics over time
|
||||
analyzer.RecordExecution("implementer", 8*time.Second, true, nil)
|
||||
analyzer.RecordExecution("implementer", 12*time.Second, true, nil)
|
||||
analyzer.RecordExecution("implementer", 15*time.Second, false, err)
|
||||
|
||||
// Analyze and get recommendations
|
||||
currentTimeouts := map[string]time.Duration{"implementer": 5*time.Second}
|
||||
recs, _ := analyzer.Analyze(currentTimeouts)
|
||||
// Recommends: 5s → ~20s (P99 + buffer) with 85% confidence
|
||||
```
|
||||
|
||||
### Confidence Scoring
|
||||
|
||||
- Sample confidence: More data = higher confidence (capped at 100 samples)
|
||||
- Reliability confidence: 1.0 - failure_rate
|
||||
- Weighted average: 40% sample + 60% reliability
|
||||
- Example: 50 samples, 5% failure rate = 0.93 confidence
|
||||
|
||||
### Lesson Tracking
|
||||
|
||||
```go
|
||||
// Persist lessons for task
|
||||
lesson := &TimeoutLesson{
|
||||
ActivityType: "implementer",
|
||||
OldTimeout: 5 * time.Second,
|
||||
NewTimeout: 20 * time.Second,
|
||||
Reason: "P99 duration 18s exceeded old timeout",
|
||||
ConfidenceScore: 0.95,
|
||||
}
|
||||
store.AppendLesson("task-001", lesson)
|
||||
|
||||
// Format for planner
|
||||
formatted := FormatLessonsForPlanner(lessons)
|
||||
// "Recent timeout lessons learned:
|
||||
// [Lesson 1] implementer:
|
||||
// Old Timeout: 5s → New Timeout: 20s
|
||||
// Reason: P99 duration 18s exceeded...
|
||||
// Confidence: 95.0%"
|
||||
```
|
||||
|
||||
### Signal Generation
|
||||
|
||||
```go
|
||||
// Generate signals from recommendations
|
||||
signals := GenerateSignalsFromRecommendations(recommendations)
|
||||
// Each signal includes:
|
||||
// - ActivityType: "implementer"
|
||||
// - NewTimeout: 20 * time.Second
|
||||
// - Reason: "P99 exceeded"
|
||||
// - Confidence: 0.95
|
||||
// - Priority: "high" (confidence > 0.7)
|
||||
```
|
||||
|
||||
## Verification Criteria
|
||||
|
||||
✅ **All criteria met:**
|
||||
|
||||
1. **Metrics Tracking**
|
||||
- Recording works with success/failure
|
||||
- Timestamps captured
|
||||
- Error information stored
|
||||
- 4 tests passing
|
||||
|
||||
2. **Analysis Engine**
|
||||
- P95/P99 calculation correct
|
||||
- Confidence scoring reasonable
|
||||
- Multiple activities handled
|
||||
- Failure detection working
|
||||
- 10 tests passing
|
||||
|
||||
3. **Recommendation Generation**
|
||||
- Undertuned timeouts identified
|
||||
- Overtuned timeouts detected
|
||||
- Confidence scores calculated
|
||||
- Priority levels assigned
|
||||
- 6 tests passing
|
||||
|
||||
4. **Lesson Storage**
|
||||
- JSONL persistence working
|
||||
- Per-task lesson files
|
||||
- Retrieval and formatting correct
|
||||
- 16 tests passing
|
||||
|
||||
5. **Integration Ready**
|
||||
- Planner can read lessons
|
||||
- Signals generated with correct structure
|
||||
- Human-readable format
|
||||
- File organization clear
|
||||
|
||||
6. **Test Coverage**
|
||||
- 36/36 tuning tests passing ✅
|
||||
- Edge cases covered
|
||||
- Persistence tested
|
||||
- Thread safety verified
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
go test -v ./internal/tuning
|
||||
# Result: PASS (36/36 tests)
|
||||
|
||||
# Full test suite
|
||||
go test -v ./...
|
||||
# Result: All tests pass
|
||||
|
||||
# Integration test scenario
|
||||
ta := NewTimeoutAnalyzer("/var/poimen")
|
||||
|
||||
// Record metric data from past runs
|
||||
for _, metric := range historicalMetrics {
|
||||
ta.RecordExecution(metric.Activity, metric.Duration, metric.Success, metric.Error)
|
||||
}
|
||||
|
||||
// Get recommendations
|
||||
recs, _ := ta.Analyze(currentTimeouts)
|
||||
ta.SaveRecommendations(recs)
|
||||
|
||||
// Generate lessons for planner
|
||||
for _, rec := range recs {
|
||||
lesson := GenerateLessonFromRecommendation(&rec)
|
||||
store.AppendLesson("current-task", lesson)
|
||||
}
|
||||
|
||||
// Get signals for planner
|
||||
signals := GenerateSignalsFromRecommendations(recs)
|
||||
// Planner reads and applies: update-tuning signals
|
||||
```
|
||||
|
||||
## Kubernetes Integration
|
||||
|
||||
With timeout tuning:
|
||||
|
||||
```yaml
|
||||
# Activity metrics persisted in shared volume
|
||||
volumeMounts:
|
||||
- name: tuning
|
||||
mountPath: /var/poimen/tuning
|
||||
|
||||
# Recommendations available across pod restarts
|
||||
volumes:
|
||||
- name: tuning
|
||||
persistentVolumeClaim:
|
||||
claimName: poimen-tuning
|
||||
```
|
||||
|
||||
## Configuration Example
|
||||
|
||||
```go
|
||||
// Initialize timeout analyzer
|
||||
analyzer := tuning.NewTimeoutAnalyzer(
|
||||
"/var/poimen/tuning",
|
||||
)
|
||||
|
||||
// Initialize lessons store
|
||||
store := tuning.NewTimeoutLessonsStore(
|
||||
"/var/poimen/tuning",
|
||||
)
|
||||
|
||||
// During workflow execution
|
||||
for _, activity := range activities {
|
||||
start := time.Now()
|
||||
err := executeActivity(activity)
|
||||
duration := time.Since(start)
|
||||
|
||||
analyzer.RecordExecution(
|
||||
activity.Type,
|
||||
duration,
|
||||
err == nil,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
// After milestone completion
|
||||
recommendations, _ := analyzer.Analyze(currentActivityTimeouts)
|
||||
|
||||
// Generate lessons for planner
|
||||
for _, rec := range recommendations {
|
||||
if rec.Confidence > 0.7 { // High confidence only
|
||||
lesson := GenerateLessonFromRecommendation(&rec)
|
||||
store.AppendLesson(taskID, lesson)
|
||||
}
|
||||
}
|
||||
|
||||
// Save recommendations to disk
|
||||
analyzer.SaveRecommendations(recommendations)
|
||||
|
||||
// Planner can read and suggest timeout updates
|
||||
lessons, _ := store.ReadLessons(taskID)
|
||||
formatted := FormatLessonsForPlanner(lessons)
|
||||
// Pass to planner as context for decision-making
|
||||
```
|
||||
|
||||
## Timeout Tuning Algorithm
|
||||
|
||||
```
|
||||
Analysis Pipeline
|
||||
↓
|
||||
[Collect Execution Metrics]
|
||||
├─ Duration (success and failure)
|
||||
├─ Success/failure count
|
||||
└─ Timestamps
|
||||
↓
|
||||
[Calculate Statistics]
|
||||
├─ P95, P99 percentiles
|
||||
├─ Max duration
|
||||
└─ Failure rate
|
||||
↓
|
||||
[Generate Recommendations]
|
||||
├─ Compare P99 + 20% buffer vs current timeout
|
||||
├─ Calculate confidence
|
||||
│ ├─ Sample confidence (n/100, capped at 1.0)
|
||||
│ ├─ Reliability confidence (1.0 - failure_rate)
|
||||
│ └─ Weighted: 0.4*sample + 0.6*reliability
|
||||
└─ Assign priority (high/medium/low)
|
||||
↓
|
||||
[Store Lessons]
|
||||
├─ Save as JSONL per task
|
||||
├─ Track effectiveness
|
||||
└─ Enable feedback loop
|
||||
↓
|
||||
[Generate Signals]
|
||||
├─ Create TimeoutTuningSignal objects
|
||||
├─ Include reason and confidence
|
||||
└─ Ready for planner integration
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
|
||||
- ✅ `internal/tuning/analyzer.go` - Timeout analysis engine (295 lines)
|
||||
- ✅ `internal/tuning/analyzer_test.go` - Analyzer tests (220 lines)
|
||||
- ✅ `internal/tuning/lessons.go` - Lesson storage and signals (175 lines)
|
||||
- ✅ `internal/tuning/lessons_test.go` - Lesson tests (224 lines)
|
||||
- ✅ `tasks/board-T1.md` - Task board update
|
||||
|
||||
## Dependencies
|
||||
|
||||
All internal, no new external dependencies added.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Percentile-Based Timeout** - Uses P99 + 20% buffer (industry standard)
|
||||
2. **Confidence Scoring** - Weighted combination of data quantity and reliability
|
||||
3. **JSONL Persistence** - Human-readable, easy to debug, append-only
|
||||
4. **Per-Task Lessons** - Enables targeted tuning for specific tasks
|
||||
5. **Priority Signaling** - High-confidence changes promoted for planner attention
|
||||
6. **Separation of Concerns** - Analyzer (metrics), Lessons (storage), Signals (integration)
|
||||
|
||||
## Integration with Planner
|
||||
|
||||
The planner can leverage timeout tuning:
|
||||
|
||||
```go
|
||||
// Planner initialization
|
||||
lessons, _ := store.ReadLessons(taskID)
|
||||
formattedLessons := FormatLessonsForPlanner(lessons)
|
||||
|
||||
// Include in planner prompt context
|
||||
systemPrompt := fmt.Sprintf(
|
||||
"You are an expert planner. Previous lessons:\n%s\n...",
|
||||
formattedLessons,
|
||||
)
|
||||
|
||||
// After planner suggests implementer, planner can suggest:
|
||||
// "Signal: update-tuning(activity='implementer', newTimeout='20s')"
|
||||
```
|
||||
|
||||
## Future Extensions
|
||||
|
||||
- Activity dependency-aware timeouts
|
||||
- Seasonal/periodic timeout adjustments
|
||||
- ML-based timeout prediction
|
||||
- SLO-aware timeout optimization
|
||||
- Automatic circuit breaker thresholds
|
||||
|
||||
## Next Steps (T1.4 → T1.5 → T1.6)
|
||||
|
||||
1. **T1.4:** Board state validation & auto-healing
|
||||
2. **T1.5:** Workflow pause/resume with state snapshots
|
||||
3. **T1.6:** Comprehensive integration tests for concurrency
|
||||
|
||||
## Notes
|
||||
|
||||
- All metrics stored as JSONL (one per line)
|
||||
- Recommendations stored as pretty JSON (easy to read)
|
||||
- Lessons support feedback (can mark as effective/ineffective)
|
||||
- Confidence range: 0.0-1.0 (0% to 100%)
|
||||
- P99 + 20% buffer is conservative (safe overestimate)
|
||||
- Works with any activity type (implementer, judge, git, etc.)
|
||||
-443
@@ -1,443 +0,0 @@
|
||||
# T1.4: Board State Validation & Auto-Healing
|
||||
|
||||
**Submilestone:** T1 (Production Hardening)
|
||||
**Status:** ✅ COMPLETE
|
||||
**Branch:** `task/T1.4`
|
||||
|
||||
## Overview
|
||||
|
||||
Implement comprehensive board file validation and automatic corruption recovery to detect and fix inconsistencies between board file state and actual workflow state, preventing manual intervention and ensuring data integrity.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Board Validation
|
||||
|
||||
- Validate markdown structure (headers, table format)
|
||||
- Check task ID format (T1.1, T1.2, etc.)
|
||||
- Validate status fields ([x] or [ ])
|
||||
- Detect malformed rows and missing columns
|
||||
- Generate detailed error and warning reports
|
||||
- Parse task information from valid boards
|
||||
|
||||
### Corruption Detection
|
||||
|
||||
- Detect divergence between board file and actual task states
|
||||
- Track state mismatches (expected vs actual)
|
||||
- Support timestamp-based divergence tracking
|
||||
- Identify missing or invalid task entries
|
||||
|
||||
### Auto-Healing
|
||||
|
||||
- Repair missing markdown headers
|
||||
- Fix malformed status values
|
||||
- Add missing table separators
|
||||
- Correct invalid task IDs
|
||||
- Heal divergences by syncing board with actual states
|
||||
- Preserve task information during repairs
|
||||
|
||||
### State Tracking
|
||||
|
||||
- Persist actual task states to JSON
|
||||
- Track task progression (pending → in_progress → completed/failed)
|
||||
- Store task metrics alongside state
|
||||
- Support multi-task concurrent state updates
|
||||
- Generate statistics and completion reports
|
||||
|
||||
## Implementation
|
||||
|
||||
### Internal Package: `internal/board`
|
||||
|
||||
#### `validator.go`
|
||||
- `BoardValidationError` - Validation error with type, message, line number
|
||||
- `BoardValidator` - Core validation and healing engine
|
||||
- `TaskRow` - Parsed task from board file
|
||||
- Methods:
|
||||
- `ValidateBoard()` - Full board structure validation
|
||||
- `ParseTasks()` - Extract tasks from valid boards
|
||||
- `DetectDivergence()` - Find state mismatches
|
||||
- `HealDivergence()` - Auto-fix state mismatches
|
||||
- `RepairBoard()` - Fix structural issues
|
||||
- Error/warning tracking and reporting
|
||||
- 13/13 unit tests passing ✅
|
||||
|
||||
#### `state.go`
|
||||
- `TaskState` - Actual task state (status, completion time, metrics)
|
||||
- `StateTracker` - Manage actual task states
|
||||
- Methods:
|
||||
- `UpdateTaskState()` - Record task status change
|
||||
- `GetTaskState()` / `GetAllStates()` - Retrieve states
|
||||
- `GetCompletedTasks()` / `GetFailedTasks()` / `GetPendingTasks()` - Filter by status
|
||||
- `AddMetric()` - Attach metrics to tasks
|
||||
- `GetAsCompletionMap()` - Boolean map for comparison
|
||||
- `GetStats()` / `GetLastUpdate()` - Analytics
|
||||
- `Load()` - Persistence from JSON
|
||||
- `Reset()` - Clear all state
|
||||
- 16/16 unit tests passing ✅
|
||||
|
||||
#### Unit Tests: `*_test.go`
|
||||
- 29 tests total, all passing ✅
|
||||
- Validator: parsing, validation, repair, divergence detection/healing
|
||||
- State: tracking, filtering, persistence, metrics
|
||||
- Integration: multi-task scenarios, state transitions
|
||||
|
||||
## Key Features
|
||||
|
||||
### Validation Pipeline
|
||||
|
||||
```
|
||||
Board File Content
|
||||
↓
|
||||
[Check Structure]
|
||||
├─ Has title header
|
||||
├─ Has table separator
|
||||
└─ Has task rows
|
||||
↓
|
||||
[Validate Each Task]
|
||||
├─ Valid task ID format (T#.# or T#)
|
||||
├─ Valid status ([x] or [ ])
|
||||
├─ No missing columns
|
||||
└─ Reasonable description
|
||||
↓
|
||||
[Report Results]
|
||||
├─ Errors (validation failed)
|
||||
└─ Warnings (suspicious but valid)
|
||||
```
|
||||
|
||||
### Corruption Healing
|
||||
|
||||
```go
|
||||
// Board has T1.1, T1.2, T1.3, T1.4
|
||||
// Actual states: T1.1=done, T1.2=done, T1.3=pending, T1.4=done
|
||||
// Board shows: T1.1=done, T1.2=pending, T1.3=pending, T1.4=pending
|
||||
|
||||
actualStates := map[string]bool{
|
||||
"T1.1": true, "T1.2": true,
|
||||
"T1.3": false, "T1.4": true,
|
||||
}
|
||||
|
||||
divergences := validator.DetectDivergence(boardContent, actualStates)
|
||||
// Finds: T1.2 (expected false, actual true), T1.4 (expected false, actual true)
|
||||
|
||||
healed, changes := validator.HealDivergence(boardContent, actualStates)
|
||||
// Fixes: Updates T1.2 and T1.4 status in board file
|
||||
// Changes: ["Fixed T1.2: [ ] → [x]", "Fixed T1.4: [ ] → [x]"]
|
||||
```
|
||||
|
||||
### State Tracking
|
||||
|
||||
```go
|
||||
// Initialize state tracker
|
||||
tracker := NewStateTracker("/var/poimen")
|
||||
|
||||
// Record task progress
|
||||
tracker.UpdateTaskState("T1.1", "in_progress", "task/T1.1", nil)
|
||||
tracker.AddMetric("T1.1", "lines_changed", 1247)
|
||||
tracker.AddMetric("T1.1", "files_modified", 15)
|
||||
|
||||
// Later, task completes
|
||||
tracker.UpdateTaskState("T1.1", "completed", "task/T1.1", nil)
|
||||
|
||||
// Query states
|
||||
completed := tracker.GetCompletedTasks() // ["T1.1", ...]
|
||||
stats := tracker.GetStats()
|
||||
// {"total": 4, "counts": {"completed": 1, "pending": 3}}
|
||||
|
||||
// Persist and recover
|
||||
tracker.Load() // From disk
|
||||
```
|
||||
|
||||
### Board Repair Examples
|
||||
|
||||
```
|
||||
❌ BEFORE: Missing header
|
||||
| T1.1 | Task | [x] | branch | verify |
|
||||
|
||||
✅ AFTER: Header added
|
||||
# Task Board — Milestone T1: Production Hardening
|
||||
| T1.1 | Task | [x] | branch | verify |
|
||||
|
||||
---
|
||||
|
||||
❌ BEFORE: Invalid status
|
||||
| T1.1 | Task | [?] | branch | verify |
|
||||
|
||||
✅ AFTER: Normalized
|
||||
| T1.1 | Task | [ ] | branch | verify |
|
||||
|
||||
---
|
||||
|
||||
❌ BEFORE: Missing separator
|
||||
| ID | Scope | Status | Branch |
|
||||
| T1.1 | Task | [x] | branch |
|
||||
|
||||
✅ AFTER: Separator added
|
||||
| ID | Scope | Status | Branch |
|
||||
|----|-------|--------|--------|
|
||||
| T1.1 | Task | [x] | branch |
|
||||
```
|
||||
|
||||
## Verification Criteria
|
||||
|
||||
✅ **All criteria met:**
|
||||
|
||||
1. **Validation Engine**
|
||||
- Detects missing headers
|
||||
- Detects malformed tables
|
||||
- Validates task IDs
|
||||
- Validates status values
|
||||
- Reports errors and warnings
|
||||
- 13 tests passing
|
||||
|
||||
2. **Corruption Detection**
|
||||
- Identifies task divergences
|
||||
- Tracks expected vs actual states
|
||||
- Timestamps divergences
|
||||
- Handles missing tasks
|
||||
- 4 tests passing
|
||||
|
||||
3. **Auto-Healing**
|
||||
- Adds missing headers
|
||||
- Fixes invalid status values
|
||||
- Adds table separators
|
||||
- Repairs divergent states
|
||||
- Preserves data integrity
|
||||
- 3 tests passing
|
||||
|
||||
4. **State Management**
|
||||
- Tracks task progression
|
||||
- Stores completion timestamps
|
||||
- Records failure information
|
||||
- Supports metrics attachment
|
||||
- Persists state to disk
|
||||
- 16 tests passing
|
||||
|
||||
5. **Integration**
|
||||
- Works with actual board.md format
|
||||
- Compatible with validation/tracking
|
||||
- Supports concurrent updates
|
||||
- Thread-safe operations
|
||||
- 3 tests passing
|
||||
|
||||
6. **Test Coverage**
|
||||
- 29/29 board tests passing ✅
|
||||
- Edge cases covered
|
||||
- Persistence tested
|
||||
- Multi-task scenarios validated
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
go test -v ./internal/board
|
||||
# Result: PASS (29/29 tests)
|
||||
|
||||
# Full test suite
|
||||
go test -v ./...
|
||||
# Result: All tests pass
|
||||
|
||||
# Integration scenario
|
||||
validator := NewBoardValidator("repo/tasks")
|
||||
|
||||
// Validate board
|
||||
if !validator.ValidateBoard(boardContent) {
|
||||
errors := validator.GetErrors()
|
||||
// Fix: validator.RepairBoard(boardContent)
|
||||
}
|
||||
|
||||
// Parse tasks
|
||||
tasks, _ := validator.ParseTasks(boardContent)
|
||||
for _, task := range tasks {
|
||||
// Track actual state
|
||||
tracker.UpdateTaskState(task.ID, "completed", task.Branch, nil)
|
||||
}
|
||||
|
||||
// Detect divergence
|
||||
tracker.Load()
|
||||
actualStates := tracker.GetAsCompletionMap()
|
||||
divergences := validator.DetectDivergence(boardContent, actualStates)
|
||||
|
||||
// Heal if needed
|
||||
if len(divergences) > 0 {
|
||||
healed, changes := validator.HealDivergence(boardContent, actualStates)
|
||||
// Save healed board
|
||||
ioutil.WriteFile("tasks/board.md", []byte(healed), 0644)
|
||||
}
|
||||
```
|
||||
|
||||
## Kubernetes Integration
|
||||
|
||||
With board healing:
|
||||
|
||||
```yaml
|
||||
# Board state persisted in shared volume
|
||||
volumeMounts:
|
||||
- name: board
|
||||
mountPath: /var/poimen/board
|
||||
|
||||
# State accessible across pod restarts
|
||||
volumes:
|
||||
- name: board
|
||||
persistentVolumeClaim:
|
||||
claimName: poimen-board
|
||||
|
||||
# Liveness check includes board validation
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
validator validate /var/poimen/board/board.md || exit 1
|
||||
```
|
||||
|
||||
## Configuration Example
|
||||
|
||||
```go
|
||||
// Initialize validator and tracker
|
||||
validator := NewBoardValidator("/var/poimen/board")
|
||||
tracker := NewStateTracker("/var/poimen")
|
||||
|
||||
// Load existing state from previous run
|
||||
if err := tracker.Load(); err != nil {
|
||||
log.Printf("Warning: could not load previous state: %v", err)
|
||||
}
|
||||
|
||||
// During workflow execution
|
||||
boardContent, _ := ioutil.ReadFile("/var/poimen/board/board.md")
|
||||
|
||||
// Validate board
|
||||
if !validator.ValidateBoard(string(boardContent)) {
|
||||
log.Printf("Board validation errors: %s", validator.ErrorSummary())
|
||||
|
||||
// Attempt repair
|
||||
repaired, _ := validator.RepairBoard(string(boardContent))
|
||||
ioutil.WriteFile("/var/poimen/board/board.md", []byte(repaired), 0644)
|
||||
}
|
||||
|
||||
// Track task progress
|
||||
for _, taskID := range tasksToRun {
|
||||
tracker.UpdateTaskState(taskID, "in_progress", fmt.Sprintf("task/%s", taskID), nil)
|
||||
|
||||
// ... execute task ...
|
||||
|
||||
if taskSuccess {
|
||||
tracker.UpdateTaskState(taskID, "completed", fmt.Sprintf("task/%s", taskID), nil)
|
||||
} else {
|
||||
tracker.UpdateTaskState(taskID, "failed", fmt.Sprintf("task/%s", taskID), taskErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect and heal divergence
|
||||
actualStates := tracker.GetAsCompletionMap()
|
||||
divergences := validator.DetectDivergence(string(boardContent), actualStates)
|
||||
|
||||
if len(divergences) > 0 {
|
||||
log.Printf("Detected %d divergences, healing...", len(divergences))
|
||||
healed, changes := validator.HealDivergence(string(boardContent), actualStates)
|
||||
|
||||
for _, change := range changes {
|
||||
log.Printf("Fixed: %s", change)
|
||||
}
|
||||
|
||||
ioutil.WriteFile("/var/poimen/board/board.md", []byte(healed), 0644)
|
||||
}
|
||||
|
||||
// Persist state for next run
|
||||
_ = tracker.Load()
|
||||
```
|
||||
|
||||
## Validation Algorithm
|
||||
|
||||
```
|
||||
Board Validation
|
||||
↓
|
||||
[1] Check Presence
|
||||
├─ Has markdown header ("#")
|
||||
└─ Has table separator ("---")
|
||||
↓
|
||||
[2] Find Task Table
|
||||
├─ Locate header row (| ID | ... |)
|
||||
├─ Skip separator
|
||||
└─ Find first data row
|
||||
↓
|
||||
[3] Validate Each Row
|
||||
├─ Check column count
|
||||
├─ Validate task ID (T#.# format)
|
||||
├─ Validate status ([x] or [ ])
|
||||
└─ Warn on missing/empty fields
|
||||
↓
|
||||
[4] Generate Report
|
||||
├─ Collect all errors
|
||||
├─ Collect all warnings
|
||||
└─ Return validation result (pass/fail)
|
||||
```
|
||||
|
||||
## Healing Algorithm
|
||||
|
||||
```
|
||||
Divergence Healing
|
||||
↓
|
||||
[1] Compare States
|
||||
├─ Board expected: [x] or [ ]
|
||||
└─ Actual state: true or false
|
||||
↓
|
||||
[2] Find Mismatches
|
||||
├─ Board ≠ Actual: need fix
|
||||
└─ Board = Actual: OK
|
||||
↓
|
||||
[3] Update Board
|
||||
├─ Replace [x] with [ ] or vice versa
|
||||
├─ Track changes made
|
||||
└─ Preserve all other fields
|
||||
↓
|
||||
[4] Report Changes
|
||||
├─ List updated tasks
|
||||
├─ Show old → new status
|
||||
└─ Ready to write to disk
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
|
||||
- ✅ `internal/board/validator.go` - Board validation and healing (378 lines)
|
||||
- ✅ `internal/board/validator_test.go` - Validator tests (224 lines)
|
||||
- ✅ `internal/board/state.go` - State tracking (195 lines)
|
||||
- ✅ `internal/board/state_test.go` - State tests (229 lines)
|
||||
- ✅ `tasks/board-T1.md` - Task board update
|
||||
|
||||
## Dependencies
|
||||
|
||||
All internal, no new external dependencies added.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Separate Validator & Tracker** - Validation (format) vs State (semantics)
|
||||
2. **JSON Persistence** - Human-readable, easy to inspect/debug
|
||||
3. **Non-destructive Repairs** - Try to fix, report changes, allow rollback
|
||||
4. **Detailed Error Reporting** - Line numbers, context, suggestions
|
||||
5. **Thread-Safe State** - RWMutex for concurrent access
|
||||
6. **Status Normalization** - [X] → [x] for consistency
|
||||
|
||||
## Future Extensions
|
||||
|
||||
- Git integration: auto-commit healed boards
|
||||
- Webhook notifications on divergence
|
||||
- Historical divergence tracking
|
||||
- Predictive healing (forecast issues)
|
||||
- Multi-branch board tracking
|
||||
- Board diffs and change logs
|
||||
|
||||
## Next Steps (T1.5 → T1.6 → T1.7)
|
||||
|
||||
1. **T1.5:** Workflow pause/resume with state snapshots
|
||||
2. **T1.6:** Comprehensive integration tests for concurrency
|
||||
3. **T1.7:** Audit logging (immutable decision log)
|
||||
|
||||
## Notes
|
||||
|
||||
- Board must have at least header and one task row
|
||||
- Task IDs must match format: T# or T#.#
|
||||
- Status values are case-insensitive during repair ([X] becomes [x])
|
||||
- Validation reports are detailed and actionable
|
||||
- State tracking is optional (validator works standalone)
|
||||
- Both validator and tracker are thread-safe
|
||||
- Perfect for container/K8s environments with restart policies
|
||||
-434
@@ -1,434 +0,0 @@
|
||||
# T1.5: Workflow Pause/Resume with State Snapshots
|
||||
|
||||
**Submilestone:** T1 (Production Hardening)
|
||||
**Status:** ✅ COMPLETE
|
||||
**Branch:** `task/T1.5`
|
||||
|
||||
## Overview
|
||||
|
||||
Implement workflow pause/resume capability with complete state serialization and recovery, enabling graceful pod restarts and mid-cycle workflow preservation without data loss.
|
||||
|
||||
## Requirements
|
||||
|
||||
### State Snapshots
|
||||
|
||||
- Capture complete workflow state at any point in time
|
||||
- Serialize all task metadata, metrics, configuration
|
||||
- Persist snapshots to disk for recovery
|
||||
- Track paused and resumed timestamps
|
||||
- Support snapshot cleanup (after successful completion)
|
||||
|
||||
### Pause Handling
|
||||
|
||||
- Accept pause signals (manual or automatic)
|
||||
- Save current workflow state before pausing
|
||||
- Block workflow execution gracefully
|
||||
- Prevent new activity starts while paused
|
||||
|
||||
### Resume Handling
|
||||
|
||||
- Accept resume signals after pod restart
|
||||
- Restore workflow state from snapshots
|
||||
- Continue execution from exact pause point
|
||||
- Track resume attempts and success
|
||||
|
||||
### Signal Management
|
||||
|
||||
- PauseSignal with reason and grace period
|
||||
- ResumeSignal with reason
|
||||
- Channel-based signal reception (compatible with Temporal)
|
||||
- Configurable timeout for pause/resume operations
|
||||
|
||||
## Implementation
|
||||
|
||||
### Internal Package: `internal/pause`
|
||||
|
||||
#### `snapshot.go`
|
||||
- `WorkflowSnapshot` - Complete workflow state capture
|
||||
- `SnapshotManager` - Manage snapshots with persistence
|
||||
- Methods:
|
||||
- `CreateSnapshot()` - Capture current state
|
||||
- `GetLatestSnapshot()` / `GetAllSnapshots()` - Retrieve snapshots
|
||||
- `RestoreFromSnapshot()` - Load state for resumption
|
||||
- `MarkResumed()` - Update snapshot after resumption
|
||||
- `DeleteSnapshot()` - Cleanup after completion
|
||||
- `ClearOldSnapshots()` - Batch cleanup by age
|
||||
- `Load()` - Restore from disk
|
||||
- `GetSnapshotStats()` - Analytics
|
||||
- 16/16 unit tests passing ✅
|
||||
|
||||
#### `handler.go`
|
||||
- `PauseSignal` - Pause request with reason and grace period
|
||||
- `ResumeSignal` - Resume request with reason
|
||||
- `PauseState` - Current pause/resume state
|
||||
- `PauseHandler` - Orchestrate pause/resume operations
|
||||
- Methods:
|
||||
- `RequestPause()` / `RequestResume()` - Signal handling
|
||||
- `IsPaused()` / `GetPauseState()` - State queries
|
||||
- `WaitForPauseOrResume()` - Blocking wait with timeout
|
||||
- `SaveSnapshot()` - Save state during pause
|
||||
- `RestoreSnapshot()` - Load state during resume
|
||||
- `ResetPauseState()` - Cleanup after completion
|
||||
- `GetAllPauseStates()` / `GetPauseStats()` - Analytics
|
||||
- 18/18 unit tests passing ✅
|
||||
|
||||
#### Unit Tests: `*_test.go`
|
||||
- 34 tests total, all passing ✅
|
||||
- Snapshots: creation, persistence, recovery, cleanup
|
||||
- Signals: pause/resume, state transitions, error handling
|
||||
- Integration: concurrent workflows, multi-state transitions
|
||||
|
||||
## Key Features
|
||||
|
||||
### State Snapshot Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"workflow_id": "orch-repo-path",
|
||||
"timestamp": "2025-01-23T12:34:56Z",
|
||||
"stage": "implement",
|
||||
"completed_tasks": ["T1.1", "T1.2"],
|
||||
"pending_tasks": ["T1.3", "T1.4"],
|
||||
"failed_tasks": [],
|
||||
"current_task_id": "T1.3",
|
||||
"current_activity_id": "implementer-activity-123",
|
||||
"task_metrics": {
|
||||
"duration": 42.5,
|
||||
"lines_modified": 1247
|
||||
},
|
||||
"workflow_metrics": {
|
||||
"total_time": 300
|
||||
},
|
||||
"configuration": {
|
||||
"timeout": 600,
|
||||
"max_retries": 3
|
||||
},
|
||||
"paused_at": "2025-01-23T12:34:56Z",
|
||||
"resumed_at": "2025-01-23T12:35:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Pause/Resume Flow
|
||||
|
||||
```
|
||||
Running Workflow
|
||||
↓
|
||||
[Pause Signal Received]
|
||||
├─ Save snapshot to disk
|
||||
├─ Block activity execution
|
||||
└─ Wait for pause acknowledgment
|
||||
↓
|
||||
[Pod Restarts]
|
||||
↓
|
||||
[Resume Signal Sent]
|
||||
├─ Load snapshot from disk
|
||||
├─ Restore all state
|
||||
└─ Continue from exact point
|
||||
↓
|
||||
Workflow Resumes
|
||||
```
|
||||
|
||||
### Usage Example
|
||||
|
||||
```go
|
||||
// Initialize pause infrastructure
|
||||
snapshotMgr := pause.NewSnapshotManager("/var/poimen")
|
||||
pauseHandler := pause.NewPauseHandler(snapshotMgr)
|
||||
|
||||
// During workflow execution
|
||||
// ... tasks executing ...
|
||||
if isPauseRequested {
|
||||
// Save state before pausing
|
||||
snapshot, _ := pauseHandler.SaveSnapshot(
|
||||
"orch-task-1",
|
||||
"implement",
|
||||
[]string{"T1.1", "T1.2"}, // completed
|
||||
[]string{"T1.3", "T1.4"}, // pending
|
||||
[]string{}, // failed
|
||||
"T1.3", // current
|
||||
"activity-123",
|
||||
taskMetrics,
|
||||
workflowMetrics,
|
||||
configuration,
|
||||
)
|
||||
|
||||
// Handle pause signal
|
||||
pauseHandler.RequestPause(&pause.PauseSignal{
|
||||
WorkflowID: "orch-task-1",
|
||||
Reason: "pod restart",
|
||||
RequestedAt: time.Now(),
|
||||
})
|
||||
|
||||
// Wait for actual pause (with timeout)
|
||||
_ = pauseHandler.WaitForPauseOrResume("orch-task-1", 5*time.Second)
|
||||
// Pod restarts here
|
||||
}
|
||||
|
||||
// On resume
|
||||
if pauseHandler.HasSnapshot("orch-task-1") {
|
||||
// Restore state
|
||||
snapshot, _ := pauseHandler.RestoreSnapshot("orch-task-1")
|
||||
|
||||
// Resume signal
|
||||
pauseHandler.RequestResume(&pause.ResumeSignal{
|
||||
WorkflowID: "orch-task-1",
|
||||
Reason: "pod restarted",
|
||||
RequestedAt: time.Now(),
|
||||
})
|
||||
|
||||
// Continue execution from restored state
|
||||
restoreTasks(snapshot.PendingTasks)
|
||||
executeFrom(snapshot.CurrentTaskID)
|
||||
}
|
||||
|
||||
// After workflow completes
|
||||
pauseHandler.ResetPauseState("orch-task-1")
|
||||
```
|
||||
|
||||
## Verification Criteria
|
||||
|
||||
✅ **All criteria met:**
|
||||
|
||||
1. **State Snapshots**
|
||||
- Complete state captured (tasks, metrics, configuration)
|
||||
- Persisted to disk (JSON format)
|
||||
- Retrieved correctly
|
||||
- Timestamps tracked (paused_at, resumed_at)
|
||||
- 16 tests passing
|
||||
|
||||
2. **Pause Handling**
|
||||
- Pause signal accepted
|
||||
- State saved before pausing
|
||||
- Workflow blocks during pause
|
||||
- Multiple workflows can be paused
|
||||
- 10 tests passing
|
||||
|
||||
3. **Resume Handling**
|
||||
- Resume signal accepted
|
||||
- State restored correctly
|
||||
- Workflow continues from exact point
|
||||
- Timestamps updated
|
||||
- 8 tests passing
|
||||
|
||||
4. **Signal Management**
|
||||
- PauseSignal with reason/grace period
|
||||
- ResumeSignal with reason
|
||||
- Channel-based signal reception
|
||||
- Configurable timeouts
|
||||
- Error handling
|
||||
- 10 tests passing
|
||||
|
||||
5. **Snapshot Recovery**
|
||||
- Snapshots load from disk
|
||||
- Old snapshots can be cleaned up
|
||||
- Multiple snapshots managed
|
||||
- Stats available
|
||||
- 16 tests passing
|
||||
|
||||
6. **Test Coverage**
|
||||
- 34/34 pause/resume tests passing ✅
|
||||
- Edge cases covered (resume without pause, nil signals, timeouts)
|
||||
- Concurrent workflows tested
|
||||
- State transitions verified
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
go test -v ./internal/pause
|
||||
# Result: PASS (34/34 tests)
|
||||
|
||||
# Full test suite
|
||||
go test -v ./...
|
||||
# Result: All tests pass
|
||||
|
||||
# Integration scenario
|
||||
// Simulate pause/resume cycle
|
||||
sm := pause.NewSnapshotManager("/var/poimen")
|
||||
ph := pause.NewPauseHandler(sm)
|
||||
|
||||
// Save snapshot before pause
|
||||
ph.SaveSnapshot(
|
||||
"wf-1", "implement",
|
||||
[]string{"T1.1"}, []string{"T1.2"}, nil,
|
||||
"T1.2", "activity-1",
|
||||
nil, nil, nil,
|
||||
)
|
||||
|
||||
// Pause
|
||||
ph.RequestPause(&pause.PauseSignal{WorkflowID: "wf-1"})
|
||||
|
||||
// Verify paused
|
||||
assert.True(t, ph.IsPaused("wf-1"))
|
||||
|
||||
// Resume
|
||||
ph.RequestResume(&pause.ResumeSignal{WorkflowID: "wf-1"})
|
||||
assert.False(t, ph.IsPaused("wf-1"))
|
||||
|
||||
// Restore
|
||||
snapshot, _ := ph.RestoreSnapshot("wf-1")
|
||||
assert.Equal(t, "implement", snapshot.Stage)
|
||||
```
|
||||
|
||||
## Kubernetes Integration
|
||||
|
||||
With pause/resume:
|
||||
|
||||
```yaml
|
||||
# Workflow pod restarts gracefully
|
||||
terminationGracePeriodSeconds: 30
|
||||
|
||||
# Pre-stop hook saves state and signals pause
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command: ["/bin/sh", "-c", "pkill -SIGTERM orchestrator"]
|
||||
|
||||
# State persisted in shared volume
|
||||
volumeMounts:
|
||||
- name: pause-state
|
||||
mountPath: /var/poimen/snapshots
|
||||
|
||||
volumes:
|
||||
- name: pause-state
|
||||
persistentVolumeClaim:
|
||||
claimName: poimen-pause-state
|
||||
|
||||
# Startup hook detects and restores from snapshot
|
||||
postStart:
|
||||
exec:
|
||||
command: ["/bin/sh", "-c", "if [ -f /var/poimen/snapshots/$(WORKFLOW_ID).snapshot.json ]; then /app/orchestrator --resume; fi"]
|
||||
```
|
||||
|
||||
## Configuration Example
|
||||
|
||||
```go
|
||||
// Initialize with custom base path
|
||||
snapshotMgr := pause.NewSnapshotManager("/data/poimen/pause")
|
||||
|
||||
// Create pause handler
|
||||
pauseHandler := pause.NewPauseHandler(snapshotMgr)
|
||||
|
||||
// Load existing snapshots from disk
|
||||
_ = snapshotMgr.Load()
|
||||
|
||||
// Handle pause request
|
||||
pauseHandler.RequestPause(&pause.PauseSignal{
|
||||
WorkflowID: workflowID,
|
||||
Reason: "graceful shutdown",
|
||||
RequestedAt: time.Now(),
|
||||
GracePeriod: 30 * time.Second,
|
||||
})
|
||||
|
||||
// Wait for pause to complete
|
||||
isPaused, err := pauseHandler.WaitForPauseOrResume(workflowID, 60*time.Second)
|
||||
|
||||
// Handle resume after restart
|
||||
if pauseHandler.HasSnapshot(workflowID) {
|
||||
snapshot, _ := pauseHandler.RestoreSnapshot(workflowID)
|
||||
|
||||
// Resume workflow from exact point
|
||||
executeWorkflow(snapshot)
|
||||
}
|
||||
```
|
||||
|
||||
## Storage Layout
|
||||
|
||||
```
|
||||
/var/poimen/
|
||||
├── snapshots/
|
||||
│ ├── orch-task-1.snapshot.json
|
||||
│ ├── orch-task-2.snapshot.json
|
||||
│ └── orch-task-3.snapshot.json
|
||||
└── pause-state/
|
||||
└── (managed by PauseHandler)
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
|
||||
- ✅ `internal/pause/snapshot.go` - Snapshot management (251 lines)
|
||||
- ✅ `internal/pause/snapshot_test.go` - Snapshot tests (227 lines)
|
||||
- ✅ `internal/pause/handler.go` - Pause/resume handler (224 lines)
|
||||
- ✅ `internal/pause/handler_test.go` - Handler tests (274 lines)
|
||||
- ✅ `tasks/board-T1.md` - Task board update
|
||||
|
||||
## Dependencies
|
||||
|
||||
All internal, no new external dependencies added.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Separate Manager & Handler** - Snapshots (storage) vs Signals (orchestration)
|
||||
2. **JSON Persistence** - Human-readable, debuggable snapshots
|
||||
3. **Channel-Based Signaling** - Compatible with Temporal SDK patterns
|
||||
4. **Complete State Capture** - Tasks, metrics, configuration all included
|
||||
5. **Non-Destructive Pause** - Snapshot saved before pause, can be cleaned up later
|
||||
6. **Configurable Timeout** - Flexible pause duration handling
|
||||
7. **Thread-Safe Operations** - RWMutex for concurrent access
|
||||
|
||||
## Pause/Resume Algorithm
|
||||
|
||||
```
|
||||
Pause Flow
|
||||
↓
|
||||
[1] Receive Pause Signal
|
||||
├─ Record workflow ID and reason
|
||||
└─ Set grace period
|
||||
↓
|
||||
[2] Save Snapshot
|
||||
├─ Capture all task state
|
||||
├─ Record metrics/config
|
||||
└─ Persist to JSON file
|
||||
↓
|
||||
[3] Block Execution
|
||||
├─ Set IsPaused flag
|
||||
├─ Notify channels
|
||||
└─ Wait for acknowledgment
|
||||
↓
|
||||
[4] Pod Restart
|
||||
└─ Snapshot persists on disk
|
||||
|
||||
Resume Flow
|
||||
↓
|
||||
[1] Pod Restarted
|
||||
├─ Load snapshots from disk
|
||||
└─ Check for paused workflows
|
||||
↓
|
||||
[2] Receive Resume Signal
|
||||
├─ Record workflow ID and reason
|
||||
└─ Mark ResumedAt timestamp
|
||||
↓
|
||||
[3] Restore Snapshot
|
||||
├─ Load from disk
|
||||
├─ Restore all state
|
||||
└─ Return to caller
|
||||
↓
|
||||
[4] Continue Execution
|
||||
├─ Execute remaining tasks
|
||||
└─ Update metrics as normal
|
||||
```
|
||||
|
||||
## Future Extensions
|
||||
|
||||
- Snapshot compression for large workflows
|
||||
- Incremental snapshots (only changed state)
|
||||
- Cross-pod snapshot sharing
|
||||
- Snapshot encryption for sensitive data
|
||||
- Snapshot versioning and rollback
|
||||
- Activity-level state checkpoints
|
||||
- Automatic pause on resource limits
|
||||
|
||||
## Next Steps (T1.6 → T1.7)
|
||||
|
||||
1. **T1.6:** Comprehensive integration tests for concurrency
|
||||
2. **T1.7:** Audit logging (immutable decision log)
|
||||
|
||||
## Notes
|
||||
|
||||
- Snapshots identified by workflow ID
|
||||
- Paused workflows can be resumed from any pod
|
||||
- Snapshot cleanup is manual (via DeleteSnapshot or ClearOldSnapshots)
|
||||
- Multiple workflows can be paused concurrently
|
||||
- Pause handler is thread-safe for concurrent signal handling
|
||||
- Compatible with Temporal workflow signals pattern
|
||||
- Perfect for Kubernetes rolling updates and graceful shutdowns
|
||||
-174
@@ -1,174 +0,0 @@
|
||||
# T1.8: Health Checks for Kubernetes
|
||||
|
||||
**Submilestone:** T1 (Production Hardening)
|
||||
**Status:** ✅ COMPLETE
|
||||
**Branch:** `task/T1.8`
|
||||
|
||||
## Overview
|
||||
|
||||
Implement comprehensive health checks for Kubernetes deployments with liveness and readiness probes.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Endpoints
|
||||
|
||||
- **GET /health** - Full health report (JSON)
|
||||
- Returns 200 if healthy, 503 if unhealthy
|
||||
- Includes all component statuses, latencies, timestamps
|
||||
|
||||
- **GET /health/live** - Kubernetes liveness probe
|
||||
- Returns 200 if service is running
|
||||
- Returns 503 if not initialized
|
||||
|
||||
- **GET /health/ready** - Kubernetes readiness probe
|
||||
- Returns 200 if service is ready to accept traffic
|
||||
- Returns 503 if any component unhealthy
|
||||
|
||||
### Components
|
||||
|
||||
1. **Temporal** - Cluster connectivity check
|
||||
- Attempts to get a workflow execution
|
||||
- Returns healthy if Temporal responds (even with NotFound)
|
||||
- Returns unhealthy if unreachable
|
||||
|
||||
### Features
|
||||
|
||||
- Periodic health check caching (30s interval) to avoid excessive checks
|
||||
- JSON health reports with component status, latency, timestamp
|
||||
- Separate liveness and readiness checks for K8s probes
|
||||
- Graceful shutdown with health server cleanup
|
||||
|
||||
## Implementation
|
||||
|
||||
### Internal Package: `internal/health`
|
||||
|
||||
#### `health.go`
|
||||
- `Status` type with constants: `StatusHealthy`, `StatusUnhealthy`, `StatusUnknown`
|
||||
- `ComponentHealth` struct for individual component status
|
||||
- `HealthReport` struct for complete health status
|
||||
- `Checker` interface for health checking
|
||||
- `Check()` method that performs comprehensive health check
|
||||
- `IsHealthy()` for quick boolean check
|
||||
- Caching mechanism to avoid repeated checks within interval
|
||||
|
||||
#### `handler.go`
|
||||
- HTTP handler implementation
|
||||
- `RegisterRoutes()` to set up endpoints on a mux
|
||||
- Handlers for `/health`, `/health/live`, `/health/ready`
|
||||
- Proper HTTP status codes (200 for healthy, 503 for unhealthy)
|
||||
|
||||
#### `health_test.go`
|
||||
- Unit tests for health checker
|
||||
- Tests for nil client, caching, JSON serialization
|
||||
- Tests for timestamp validation
|
||||
- 10/10 tests passing ✅
|
||||
|
||||
### Integration
|
||||
|
||||
**cmd/worker/main.go**
|
||||
- Health check server runs on port 8081
|
||||
- Runs in separate goroutine alongside worker
|
||||
- Graceful shutdown on SIGINT/SIGTERM
|
||||
- Waits for health server to shutdown before exiting
|
||||
|
||||
**cmd/starter/main.go**
|
||||
- `--health` flag to run health check and exit
|
||||
- Outputs JSON health report
|
||||
- Returns non-zero exit code if unhealthy
|
||||
|
||||
## Verification Criteria
|
||||
|
||||
✅ **All criteria met:**
|
||||
|
||||
1. **Health endpoints responsive**
|
||||
- GET /health returns 200 with JSON report
|
||||
- GET /health/live returns 200 if running
|
||||
- GET /health/ready returns 503 if Temporal unavailable
|
||||
|
||||
2. **Kubernetes integration**
|
||||
- Can be used as livenessProbe target
|
||||
- Can be used as readinessProbe target
|
||||
- Port 8081 exposed for probes
|
||||
|
||||
3. **Component checks**
|
||||
- Temporal connectivity verified via GetWorkflow call
|
||||
- Caching prevents excessive health checks
|
||||
- Latency measured and reported
|
||||
|
||||
4. **Graceful shutdown**
|
||||
- Health server stops on SIGINT/SIGTERM
|
||||
- Worker stops cleanly
|
||||
- No hanging goroutines
|
||||
|
||||
5. **CLI integration**
|
||||
- `starter --health` command works
|
||||
- Outputs JSON report
|
||||
- Exits with appropriate code
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
go test -v ./internal/health
|
||||
# Result: PASS (10/10 tests)
|
||||
|
||||
# Integration test (requires Temporal)
|
||||
# When Temporal unavailable:
|
||||
curl http://localhost:8081/health
|
||||
# Returns: 503 with status="unhealthy", components.temporal.error set
|
||||
|
||||
# When Temporal available:
|
||||
curl http://localhost:8081/health
|
||||
# Returns: 200 with status="healthy"
|
||||
```
|
||||
|
||||
## Kubernetes Configuration
|
||||
|
||||
Example liveness probe:
|
||||
```yaml
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health/live
|
||||
port: 8081
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
```
|
||||
|
||||
Example readiness probe:
|
||||
```yaml
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: 8081
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
|
||||
- ✅ `internal/health/health.go` - Core health checker (106 lines)
|
||||
- ✅ `internal/health/handler.go` - HTTP endpoints (68 lines)
|
||||
- ✅ `internal/health/health_test.go` - Unit tests (119 lines)
|
||||
- ✅ `cmd/worker/main.go` - Worker integration
|
||||
- ✅ `cmd/starter/main.go` - Starter health check command
|
||||
- ✅ `tasks/board-T1.md` - Task board update
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `go.temporal.io/sdk/client` - Already in go.mod
|
||||
- `net/http` - Standard library
|
||||
- `encoding/json` - Standard library
|
||||
- `github.com/stretchr/testify/assert` - Already in go.mod
|
||||
|
||||
## Notes
|
||||
|
||||
- Health check server runs on `:8081` (separate from main application)
|
||||
- Caching interval set to 30 seconds (configurable)
|
||||
- Temporal check uses GetWorkflow with timeout for quick response
|
||||
- Handler is reusable across different services
|
||||
|
||||
## Next Steps (T1.7 → T1.1 → T1.2)
|
||||
|
||||
1. **T1.7:** Immutable audit logging (track all decisions)
|
||||
2. **T1.2:** Structured logging + Prometheus metrics
|
||||
3. **T1.1:** Workflow error recovery & deadletter handling
|
||||
@@ -1,19 +0,0 @@
|
||||
# Task Board — Milestone T1: Production Hardening
|
||||
|
||||
**Submilestone:** T1 (Error recovery, observability, metrics, reliability)
|
||||
|
||||
| ID | Scope | Status | Branch | Verification |
|
||||
|----|-------|--------|--------|--------------|
|
||||
| T1.1 | Workflow error recovery: retry policies, deadletter handling, graceful shutdown | [x] | `task/T1.1` | Simulate orchestrator crash mid-cycle, resume without data loss |
|
||||
| T1.2 | Structured logging + metrics export (Prometheus/OpenTelemetry integration) | [x] | `task/T1.2` | Metrics visible in homelab Grafana, logs queryable in Loki |
|
||||
| T1.3 | Activity timeout tuning automation: learn from historical failures, recommend overrides | [x] | `task/T1.3` | Planner reads lessons file, suggests `update-tuning` signal based on patterns |
|
||||
| T1.4 | Board state validation: detect corruption, auto-heal from board divergence | [x] | `task/T1.4` | Corrupt board file recovered without manual intervention |
|
||||
| T1.5 | Workflow pause/resume with state snapshot: serialize mid-cycle state to persistent store | [x] | `task/T1.5` | Pause signal, restart pod, resume signal → workflow continues from exact point |
|
||||
| T1.6 | Comprehensive integration tests: multi-pod concurrency, network flakiness simulation | [x] | `task/T1.6` | Concurrent orchestrator instances on shared repo pass e2e without conflicts |
|
||||
| T1.7 | Audit logging: all planner decisions, judge verdicts, implementer changes logged immutably | [x] | `task/T1.7` | Audit log persists across workflow restarts, queryable by task/timestamp |
|
||||
| T1.8 | Health checks: Temporal connectivity, git repo accessibility, LLM API availability | [x] | `task/T1.8` | Periodic health probes, liveness/readiness endpoints for K8s |
|
||||
|
||||
---
|
||||
|
||||
## Submission Criteria
|
||||
All T1.1–T1.8 marked `[x]` → submilestone complete → squash-merge `task/T1.*` to main.
|
||||
@@ -1,19 +0,0 @@
|
||||
# Task Board — Milestone T2: Scale & Performance
|
||||
|
||||
**Submilestone:** T2 (Distributed execution, caching, performance optimization)
|
||||
|
||||
| ID | Scope | Status | Branch | Verification |
|
||||
|----|-------|--------|--------|--------------|
|
||||
| T2.1 | Activity result caching: deduplicate repeated LLM calls for same task state | [x] | `task/T2.1` | Implementer called 2x on same code → second call returns cached Implementer output |
|
||||
| T2.2 | Parallel task dispatch: multiple T0.x tasks execute truly concurrently (not sequential) | [x] | `task/T2.2` | 9 tasks complete in ~1/9 total time (wall-clock speedup measured) |
|
||||
| T2.3 | Prompt template caching: pre-compile Go templates on worker startup | [x] | `task/T2.3` | Template render latency < 100ms (vs parse+render each time) |
|
||||
| T2.4 | Lessons file indexing: fast lookup of past failures without full file scan | [x] | `task/T2.4` | Query lessons by task type → return in < 10ms for 1000s of entries |
|
||||
| T2.5 | Git operation batching: combine multiple worktree commits into single push/merge | [x] | `task/T2.5` | N tasks → 1 push (vs N pushes), measured via git ref-log |
|
||||
| T2.6 | LLM request batching: group similar Implementer calls into one API request | [x] | `task/T2.6` | 3 implementer tasks → 1 Anthropic API call with batch input (vs 3 separate calls) |
|
||||
| T2.7 | Workflow history pruning: trim old task unit outputs from orchestrator history | [x] | `task/T2.7` | Continue-as-new cycle history size constant despite 1000s of task units completed |
|
||||
| T2.8 | Distributed lock optimization: replace flock with Redis/etcd for multi-pod scenarios | [x] | `task/T2.8` | 5 concurrent orchestrators on different pods share FS safely via distributed lock |
|
||||
|
||||
---
|
||||
|
||||
## Submission Criteria
|
||||
All T2.1–T2.8 marked `[x]` → submilestone complete → squash-merge `task/T2.*` to main.
|
||||
@@ -1,19 +0,0 @@
|
||||
# Task Board — Milestone T3: Feature Expansion
|
||||
|
||||
**Submilestone:** T3 (Custom plugins, workflow templates, audit, advanced features)
|
||||
|
||||
| ID | Scope | Status | Branch | Verification |
|
||||
|----|-------|--------|--------|--------------|
|
||||
| T3.1 | Custom skill plugins: load user-defined skills from plugin registry (not just pi clone) | [x] | `task/T3.1` | Custom skill plugin loads, PrepareSkillsActivity calls plugin:// URLs |
|
||||
| T3.2 | Workflow templates: save/load orchestrator config as YAML templates (not CLI flags only) | [x] | `task/T3.2` | Load template `templates/golang-project.yaml` → workflow configures Planner/Judge/Implementer for Go projects |
|
||||
| T3.3 | Task dependency graph: specify task order (T0.2 must complete before T0.3 can start) | [x] | `task/T3.3` | Board supports `depends_on: [T0.1]` field, orchestrator respects ordering |
|
||||
| T3.4 | Human-in-the-loop gates: pause workflow, require approval before proceeding to next task | [x] | `task/T3.4` | Workflow waits for `approve-task` signal, Judge verdict is final (can't auto-retry after user approval) |
|
||||
| T3.5 | Custom Judge implementations: swap default Judge for domain-specific validator | [x] | `task/T3.5` | Register custom JudgeActivity, orchestrator uses it instead of default |
|
||||
| T3.6 | Immutable audit trail: all Planner/Judge/Implementer decisions written to tamper-proof log | [x] | `task/T3.6` | Audit log signed with per-workflow key, verification prevents tampering |
|
||||
| T3.7 | Workflow composition: nest OrchestratorWorkflows (one orchestrator dispatches child orchestrators) | [x] | `task/T3.7` | Multi-level task hierarchy: T0 milestone → T0.a/T0.b sub-milestones, each with own orchestrator |
|
||||
| T3.8 | Integration with external task systems: import tasks from Linear, GitHub Issues, JIRA | [x] | `task/T3.8` | Load board from GitHub Issues API, update issues with task completion status |
|
||||
|
||||
---
|
||||
|
||||
## Submission Criteria
|
||||
All T3.1–T3.8 marked `[x]` → submilestone complete → squash-merge `task/T3.*` to main.
|
||||
@@ -1,128 +0,0 @@
|
||||
# T4: Advanced Operations & Analytics
|
||||
|
||||
## Overview
|
||||
Advanced operational capabilities for monitoring, visualization, cost optimization, and multi-cluster orchestration. Builds on T1-T3 foundation to enable enterprise-scale deployment.
|
||||
|
||||
## Tasks
|
||||
|
||||
| Task | Description | Tests | Status |
|
||||
|------|-------------|-------|--------|
|
||||
| T4.1 | Real-time metrics dashboard: queryable Prometheus metrics with aggregation | [ ] | 🔜 TODO |
|
||||
| T4.2 | Workflow visualization & DAG rendering: browser-based workflow inspector | [ ] | 🔜 TODO |
|
||||
| T4.3 | Advanced search & filtering: Elasticsearch-like task/workflow search | [ ] | 🔜 TODO |
|
||||
| T4.4 | Cost tracking & optimization: LLM API, git push, compute resource costs | [ ] | 🔜 TODO |
|
||||
| T4.5 | Automated alerting & anomaly detection: threshold rules, ML-based anomalies | [ ] | 🔜 TODO |
|
||||
| T4.6 | Workflow profiling & bottleneck analysis: identify slowest tasks | [ ] | 🔜 TODO |
|
||||
| T4.7 | Multi-cluster orchestration: deploy orchestrators across K8s clusters | [ ] | 🔜 TODO |
|
||||
| T4.8 | Self-deployment: orchestrator deploys itself (meta!) | [ ] | 🔜 TODO |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### T4.1: Real-time Metrics Dashboard
|
||||
- `internal/dashboard/metrics_aggregator.go` - Query Prometheus for metrics
|
||||
- `internal/dashboard/metrics_aggregator_test.go` - 15 tests
|
||||
- Features:
|
||||
- Aggregate gauge/counter/histogram metrics
|
||||
- Time-range queries
|
||||
- Percentile calculations (p50, p95, p99)
|
||||
- Error rate aggregation
|
||||
- Throughput calculations
|
||||
|
||||
### T4.2: Workflow Visualization
|
||||
- `internal/visualization/dag_renderer.go` - DAG graph generation
|
||||
- `internal/visualization/dag_renderer_test.go` - 12 tests
|
||||
- Features:
|
||||
- Convert dependency graph to DOT format
|
||||
- SVG/PNG rendering capability
|
||||
- Task status coloring
|
||||
- Critical path highlighting
|
||||
- Parallel task grouping
|
||||
|
||||
### T4.3: Advanced Search & Filtering
|
||||
- `internal/search/workflow_search.go` - Full-text search
|
||||
- `internal/search/workflow_search_test.go` - 18 tests
|
||||
- Features:
|
||||
- Index workflows by content
|
||||
- Filter by status, date, assignee
|
||||
- Full-text search on task descriptions
|
||||
- Regex pattern matching
|
||||
- Saved filters
|
||||
|
||||
### T4.4: Cost Tracking & Optimization
|
||||
- `internal/cost/cost_tracker.go` - Track compute/API costs
|
||||
- `internal/cost/cost_tracker_test.go` - 16 tests
|
||||
- Features:
|
||||
- LLM API call costs (tokens × price)
|
||||
- Git push operation costs
|
||||
- K8s compute resource costs
|
||||
- Cost per workflow
|
||||
- Cost optimization recommendations
|
||||
|
||||
### T4.5: Automated Alerting & Anomaly Detection
|
||||
- `internal/alerting/alert_manager.go` - Rule-based alerts
|
||||
- `internal/alerting/alert_manager_test.go` - 20 tests
|
||||
- Features:
|
||||
- Threshold-based alerts
|
||||
- Pattern-based anomaly detection
|
||||
- Alert routing (email, Slack, PagerDuty)
|
||||
- Alert history
|
||||
- Deduplication
|
||||
|
||||
### T4.6: Workflow Profiling & Bottleneck Analysis
|
||||
- `internal/profiling/workflow_profiler.go` - Identify slow tasks
|
||||
- `internal/profiling/workflow_profiler_test.go` - 17 tests
|
||||
- Features:
|
||||
- Per-task execution time breakdown
|
||||
- Critical path identification
|
||||
- Parallel vs sequential timing
|
||||
- Resource utilization per task
|
||||
- Optimization suggestions
|
||||
|
||||
### T4.7: Multi-cluster Orchestration
|
||||
- `internal/clusters/cluster_manager.go` - Manage multiple K8s clusters
|
||||
- `internal/clusters/cluster_manager_test.go` - 19 tests
|
||||
- Features:
|
||||
- Register/discover clusters
|
||||
- Route workflows to clusters
|
||||
- Cross-cluster task coordination
|
||||
- Cluster health monitoring
|
||||
- Failover support
|
||||
|
||||
### T4.8: Self-Deployment
|
||||
- `internal/deployment/self_deployer.go` - Orchestrator deploys itself
|
||||
- `internal/deployment/self_deployer_test.go` - 14 tests
|
||||
- Features:
|
||||
- Build orchestrator container
|
||||
- Generate K8s manifests
|
||||
- Deploy new version
|
||||
- Health check & rollback
|
||||
- Version management
|
||||
|
||||
---
|
||||
|
||||
## Test Coverage Target
|
||||
- T4 Total: **131+ tests** (similar to T3)
|
||||
- All packages: 100% test pass rate
|
||||
- Performance benchmarks included
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
✅ All 8 T4 tasks complete
|
||||
✅ 131+ tests passing
|
||||
✅ Dashboard queryable in real-time
|
||||
✅ DAG visualization renders workflow dependencies
|
||||
✅ Cost tracking shows savings from T2 optimizations
|
||||
✅ Anomaly detection catches performance regressions
|
||||
✅ Multi-cluster deployment supported
|
||||
✅ Orchestrator can self-deploy
|
||||
|
||||
---
|
||||
|
||||
## Timeline
|
||||
- T4.1-T4.4: Week 1 (implementation + tests)
|
||||
- T4.5-T4.8: Week 2 (implementation + tests)
|
||||
- Integration testing: Week 3
|
||||
- Production deployment: Week 4
|
||||
@@ -1,38 +0,0 @@
|
||||
# Task Board — Milestone T0
|
||||
|
||||
**Submilestone:** T0 (Multi-Agent Dev Orchestrator Temporal system)
|
||||
|
||||
| ID | Scope | Status | Branch | Verification | Notes |
|
||||
|----|-------|--------|--------|--------------|-------|
|
||||
| T0.1 | Repo scaffold: go.mod, statemachine/, action/, cmd/, prompts/, internal/, tests/ | [x] | `task/T0.1` | `go build ./...` succeeds; layout matches PLAN.md | Foundation |
|
||||
| T0.2 | Shared types: ModelSpec, PromptSpec, OrchestratorConfig, ActivityTuning, PiRetryPolicy | [x] | `task/T0.2` | Unit test asserts all defaults (5m/2s/30s/2.0/30s stream/2m stream-max) | Config data model |
|
||||
| T0.3 | Git & locking: CloneRepoActivity, worktrees, squash-merge, orchestrator.lock | [x] | `task/T0.3` | Test vs local scratch repo: clone-if-empty vs fetch, worktree lifecycle, squash-merge produces 1 commit | Concurrency safety |
|
||||
| T0.4 | PrepareSkillsActivity, classifyPiErr (4xx/5xx/504), stream timeout learning | [x] | `task/T0.4` | Unit tests: all 3 error buckets against mocked pi HTTP client | Pi integration |
|
||||
| T0.5 | Planner/Judge/Implementer activities, LLM client, prompt templates | [x] | `task/T0.5` | Unit test: PromptSpec renders with system prompt + template override + raw template | LLM orchestration |
|
||||
| T0.6 | TaskUnitWorkflow: retry loops (timeout/judge-fail split), lessons injection, escalation | [x] | `task/T0.6` | Implemented: retry loop, lessons injection, judge/implementer orchestration, timeout escalation | Task execution core |
|
||||
| T0.7 | OrchestratorWorkflow: config state, signals, fan-out/fan-in, continue-as-new, 504 learning | [x] | `task/T0.7` | Implemented: planning cycle, fan-out/fan-in, 504 learning, continue-as-new, board updates | Orchestration core |
|
||||
| T0.8 | cmd/worker, cmd/starter, internal/config (env/vsource loading) | [x] | `task/T0.8` | `go run ./cmd/worker` connects to temporal.riotpiao.com; `go run ./cmd/starter --dry-run` visible in Web UI | CLI integration |
|
||||
| T0.9 | End-to-end: real temporal.riotpiao.com + disposable forgejo scratch repo, all 7 verification items | [x] | `task/T0.9` | Workflows implemented; fixture setup ready; E2E test successful against temporal.riotpiao.com | System validation complete |
|
||||
|
||||
## Submission Criteria
|
||||
|
||||
All T0.1–T0.9 marked `[x]` → submilestone complete.
|
||||
|
||||
At that point:
|
||||
1. `git -C /workspace/Poimen/workflows checkout main && git pull`
|
||||
2. `git merge --squash task/T0.1 task/T0.2 ... task/T0.9`
|
||||
3. `git commit -m "T0: multi-agent orchestrator initial implementation"`
|
||||
4. `git push origin main`
|
||||
5. Delete all `task/T0.*` branches and worktrees
|
||||
|
||||
This merge is the first real dogfood of the system's own git workflow: squashing 9 subtask branches into main as a single milestone commit.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Lessons file location:** `tasks/.orchestrator/lessons/<TaskID>.jsonl` (created on first failure, not committed until Planner's board commit)
|
||||
- **Branch naming:** Strict `task/T0.x` format; Orchestrator expects this pattern
|
||||
- **Dry-run vs real:** T0.8 tests with `--dry-run` (no real push); T0.9 removes flag (real remote operations)
|
||||
- **Temporal Web UI:** Monitor at `http://temporal.riotpiao.com:8080` (adjust port/host as needed)
|
||||
- **E2E fixture:** Disposable forgejo repo (deleted post-run); confirm it's not a production repo before starting T0.9
|
||||
@@ -0,0 +1,145 @@
|
||||
// +build integration
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rockliang/poimen/workflows/internal/routing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestRoutingE2E_GenerateAndValidate tests full flow against real api.riotpiao.com
|
||||
// Run with: go test -tags=integration -v -run TestRoutingE2E ./tests/...
|
||||
func TestRoutingE2E_GenerateAndValidate(t *testing.T) {
|
||||
if os.Getenv("RUN_INTEGRATION_TESTS") != "1" {
|
||||
t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=1 to run.")
|
||||
}
|
||||
|
||||
// Load knowledge base
|
||||
kb, err := routing.LoadKnowledgeBaseFromDefaultPath()
|
||||
require.NoError(t, err, "failed to load knowledge base")
|
||||
|
||||
// Create router
|
||||
router, err := routing.NewLLMRouter(kb)
|
||||
require.NoError(t, err, "failed to create router")
|
||||
|
||||
// Create validator
|
||||
validator := routing.NewValidator(kb)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
message string
|
||||
context map[string]interface{}
|
||||
isCron bool
|
||||
}{
|
||||
{
|
||||
name: "one-time repo analysis",
|
||||
message: "Analyze https://github.com/rockliang/poimen for code quality and security issues",
|
||||
context: map[string]interface{}{"branch": "main"},
|
||||
isCron: false,
|
||||
},
|
||||
{
|
||||
name: "scheduled security scan",
|
||||
message: "Run daily security scan at 3 AM on https://github.com/rockliang/poimen",
|
||||
isCron: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate workflow spec
|
||||
input := routing.LLMRouterInput{
|
||||
Message: tt.message,
|
||||
Context: tt.context,
|
||||
}
|
||||
|
||||
output, err := router.Route(ctx, input)
|
||||
require.NoError(t, err, "LLM router failed")
|
||||
|
||||
// Log generated spec
|
||||
specJSON, _ := json.MarshalIndent(output, "", " ")
|
||||
t.Logf("Generated spec:\n%s", string(specJSON))
|
||||
|
||||
// Validate based on type
|
||||
if tt.isCron {
|
||||
require.True(t, output.IsCron, "expected cron workflow")
|
||||
require.NotNil(t, output.CronSpec, "cron spec is nil")
|
||||
require.NotEmpty(t, output.CronSpec.Schedule, "cron schedule is empty")
|
||||
|
||||
result := validator.ValidateCronWorkflowSpec(output.CronSpec)
|
||||
require.True(t, result.Valid, "validation failed: %v", result.Errors)
|
||||
|
||||
t.Logf("Cron workflow validated: %s (schedule: %s)",
|
||||
output.CronSpec.Name, output.CronSpec.Schedule)
|
||||
} else {
|
||||
require.False(t, output.IsCron, "expected one-time workflow")
|
||||
require.NotNil(t, output.Spec, "spec is nil")
|
||||
|
||||
result := validator.ValidateWorkflowSpec(output.Spec)
|
||||
require.True(t, result.Valid, "validation failed: %v", result.Errors)
|
||||
|
||||
t.Logf("One-time workflow validated: %s (%d states)",
|
||||
output.Spec.Name, len(output.Spec.States))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRoutingE2E_FullPipeline tests LLM router -> validation -> (simulated) execution
|
||||
func TestRoutingE2E_FullPipeline(t *testing.T) {
|
||||
if os.Getenv("RUN_INTEGRATION_TESTS") != "1" {
|
||||
t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=1 to run.")
|
||||
}
|
||||
|
||||
kb, err := routing.LoadKnowledgeBaseFromDefaultPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
router, err := routing.NewLLMRouter(kb)
|
||||
require.NoError(t, err)
|
||||
|
||||
validator := routing.NewValidator(kb)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate workflow
|
||||
output, err := router.Route(ctx, routing.LLMRouterInput{
|
||||
Message: "Clone and analyze https://github.com/rockliang/poimen for security vulnerabilities",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, output.IsCron)
|
||||
require.NotNil(t, output.Spec)
|
||||
|
||||
// Validate
|
||||
result := validator.ValidateWorkflowSpec(output.Spec)
|
||||
require.True(t, result.Valid, "validation failed: %v", result.Errors)
|
||||
|
||||
// Verify structure
|
||||
require.NotEmpty(t, output.Spec.Name)
|
||||
require.NotEmpty(t, output.Spec.States)
|
||||
|
||||
// First state should be CloneRepoActivity
|
||||
require.Equal(t, "CloneRepoActivity", output.Spec.States[0].Resource,
|
||||
"expected first activity to be CloneRepoActivity")
|
||||
|
||||
// Check that flaky activities have retry policies
|
||||
for _, state := range output.Spec.States {
|
||||
if state.Type == routing.StateTypeTask {
|
||||
if kb.IsFlaky(state.Resource) {
|
||||
require.NotNil(t, state.Retry, "flaky activity %s should have retry policy", state.Resource)
|
||||
require.GreaterOrEqual(t, state.Retry.MaxAttempts, int32(2),
|
||||
"flaky activity %s should have at least 2 retries", state.Resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Full pipeline test passed: %s with %d states", output.Spec.Name, len(output.Spec.States))
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/rockliang/poimen/workflows/internal/routing"
|
||||
"github.com/rockliang/poimen/workflows/statemachine"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.temporal.io/sdk/testsuite"
|
||||
)
|
||||
|
||||
func TestRoutingWorkflow_SimpleWorkflow(t *testing.T) {
|
||||
testSuite := &testsuite.WorkflowTestSuite{}
|
||||
env := testSuite.NewTestWorkflowEnvironment()
|
||||
|
||||
// Register mock activity
|
||||
env.RegisterActivity(mockCloneRepoActivity)
|
||||
|
||||
// Create simple workflow spec
|
||||
spec := &routing.WorkflowSpec{
|
||||
Name: "test-workflow",
|
||||
Input: map[string]interface{}{
|
||||
"repo": "https://github.com/test/repo",
|
||||
"branch": "main",
|
||||
},
|
||||
States: []routing.State{
|
||||
{
|
||||
Name: "Clone",
|
||||
Type: routing.StateTypeTask,
|
||||
Resource: "mockCloneRepoActivity",
|
||||
Parameters: map[string]interface{}{
|
||||
"repo": "${input.repo}",
|
||||
"branch": "${input.branch}",
|
||||
},
|
||||
Timeout: "5m",
|
||||
Retry: &routing.RetryPolicy{
|
||||
MaxAttempts: 2,
|
||||
BackoffRate: 1.5,
|
||||
InitialInterval: "1s",
|
||||
},
|
||||
End: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||
|
||||
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||
|
||||
require.True(t, env.IsWorkflowCompleted())
|
||||
require.NoError(t, env.GetWorkflowError())
|
||||
|
||||
var output statemachine.RoutingWorkflowOutput
|
||||
require.NoError(t, env.GetWorkflowResult(&output))
|
||||
require.Equal(t, "COMPLETED", output.Status)
|
||||
require.NotNil(t, output.FinalOutput)
|
||||
}
|
||||
|
||||
func TestRoutingWorkflow_MultiStepWorkflow(t *testing.T) {
|
||||
testSuite := &testsuite.WorkflowTestSuite{}
|
||||
env := testSuite.NewTestWorkflowEnvironment()
|
||||
|
||||
// Register mock activities
|
||||
env.RegisterActivity(mockCloneRepoActivity)
|
||||
env.RegisterActivity(mockAnalyzeActivity)
|
||||
|
||||
// Create multi-step workflow spec
|
||||
spec := &routing.WorkflowSpec{
|
||||
Name: "multi-step-workflow",
|
||||
Input: map[string]interface{}{
|
||||
"repo": "https://github.com/test/repo",
|
||||
},
|
||||
States: []routing.State{
|
||||
{
|
||||
Name: "Clone",
|
||||
Type: routing.StateTypeTask,
|
||||
Resource: "mockCloneRepoActivity",
|
||||
Parameters: map[string]interface{}{
|
||||
"repo": "${input.repo}",
|
||||
},
|
||||
Timeout: "5m",
|
||||
Next: "Analyze",
|
||||
},
|
||||
{
|
||||
Name: "Analyze",
|
||||
Type: routing.StateTypeTask,
|
||||
Resource: "mockAnalyzeActivity",
|
||||
Parameters: map[string]interface{}{
|
||||
"path": "${Clone.output.path}",
|
||||
},
|
||||
Timeout: "10m",
|
||||
End: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||
|
||||
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||
|
||||
require.True(t, env.IsWorkflowCompleted())
|
||||
require.NoError(t, env.GetWorkflowError())
|
||||
|
||||
var output statemachine.RoutingWorkflowOutput
|
||||
require.NoError(t, env.GetWorkflowResult(&output))
|
||||
t.Logf("Output: %+v", output)
|
||||
t.Logf("Error: %s", output.Error)
|
||||
require.Equal(t, "COMPLETED", output.Status)
|
||||
require.Contains(t, output.StepResults, "Clone")
|
||||
require.Contains(t, output.StepResults, "Analyze")
|
||||
}
|
||||
|
||||
func TestRoutingWorkflow_PassState(t *testing.T) {
|
||||
testSuite := &testsuite.WorkflowTestSuite{}
|
||||
env := testSuite.NewTestWorkflowEnvironment()
|
||||
|
||||
// Create workflow with Pass state
|
||||
spec := &routing.WorkflowSpec{
|
||||
Name: "pass-state-workflow",
|
||||
Input: map[string]interface{}{},
|
||||
States: []routing.State{
|
||||
{
|
||||
Name: "StaticResult",
|
||||
Type: routing.StateTypePass,
|
||||
Result: map[string]interface{}{"status": "ok", "message": "static result"},
|
||||
End: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||
|
||||
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||
|
||||
require.True(t, env.IsWorkflowCompleted())
|
||||
require.NoError(t, env.GetWorkflowError())
|
||||
|
||||
var output statemachine.RoutingWorkflowOutput
|
||||
require.NoError(t, env.GetWorkflowResult(&output))
|
||||
require.Equal(t, "COMPLETED", output.Status)
|
||||
}
|
||||
|
||||
func TestRoutingWorkflow_FailState(t *testing.T) {
|
||||
testSuite := &testsuite.WorkflowTestSuite{}
|
||||
env := testSuite.NewTestWorkflowEnvironment()
|
||||
|
||||
// Create workflow with Fail state
|
||||
spec := &routing.WorkflowSpec{
|
||||
Name: "fail-state-workflow",
|
||||
Input: map[string]interface{}{},
|
||||
States: []routing.State{
|
||||
{
|
||||
Name: "HandleError",
|
||||
Type: routing.StateTypeFail,
|
||||
Error: "WorkflowError",
|
||||
Cause: "Something went wrong",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||
|
||||
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||
|
||||
require.True(t, env.IsWorkflowCompleted())
|
||||
require.NoError(t, env.GetWorkflowError())
|
||||
|
||||
var output statemachine.RoutingWorkflowOutput
|
||||
require.NoError(t, env.GetWorkflowResult(&output))
|
||||
require.Equal(t, "FAILED", output.Status)
|
||||
require.Contains(t, output.Error, "WorkflowError")
|
||||
}
|
||||
|
||||
func TestRoutingWorkflow_ErrorCatch(t *testing.T) {
|
||||
testSuite := &testsuite.WorkflowTestSuite{}
|
||||
env := testSuite.NewTestWorkflowEnvironment()
|
||||
|
||||
// Register mock activities
|
||||
env.RegisterActivity(mockFailingActivity)
|
||||
|
||||
// Create workflow with error handling
|
||||
spec := &routing.WorkflowSpec{
|
||||
Name: "error-catch-workflow",
|
||||
Input: map[string]interface{}{},
|
||||
States: []routing.State{
|
||||
{
|
||||
Name: "FlakyStep",
|
||||
Type: routing.StateTypeTask,
|
||||
Resource: "mockFailingActivity",
|
||||
Parameters: map[string]interface{}{},
|
||||
Timeout: "1m",
|
||||
Retry: &routing.RetryPolicy{
|
||||
MaxAttempts: 1,
|
||||
BackoffRate: 1.0,
|
||||
InitialInterval: "1s",
|
||||
},
|
||||
Catch: []routing.CatchClause{
|
||||
{
|
||||
ErrorEquals: []string{"ActivityError"},
|
||||
Next: "HandleError",
|
||||
},
|
||||
},
|
||||
Next: "Success",
|
||||
},
|
||||
{
|
||||
Name: "Success",
|
||||
Type: routing.StateTypePass,
|
||||
Result: "success",
|
||||
End: true,
|
||||
},
|
||||
{
|
||||
Name: "HandleError",
|
||||
Type: routing.StateTypeFail,
|
||||
Error: "CaughtError",
|
||||
Cause: "Activity failed and was caught",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||
|
||||
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||
|
||||
require.True(t, env.IsWorkflowCompleted())
|
||||
require.NoError(t, env.GetWorkflowError())
|
||||
|
||||
var output statemachine.RoutingWorkflowOutput
|
||||
require.NoError(t, env.GetWorkflowResult(&output))
|
||||
require.Equal(t, "FAILED", output.Status)
|
||||
require.Contains(t, output.Error, "CaughtError")
|
||||
}
|
||||
|
||||
func TestRoutingWorkflow_EmptySpec(t *testing.T) {
|
||||
testSuite := &testsuite.WorkflowTestSuite{}
|
||||
env := testSuite.NewTestWorkflowEnvironment()
|
||||
|
||||
// Empty spec
|
||||
input := statemachine.RoutingWorkflowInput{Spec: nil}
|
||||
|
||||
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||
|
||||
require.True(t, env.IsWorkflowCompleted())
|
||||
require.NoError(t, env.GetWorkflowError())
|
||||
|
||||
var output statemachine.RoutingWorkflowOutput
|
||||
require.NoError(t, env.GetWorkflowResult(&output))
|
||||
require.Equal(t, "FAILED", output.Status)
|
||||
require.Contains(t, output.Error, "empty")
|
||||
}
|
||||
|
||||
// Mock activities
|
||||
func mockCloneRepoActivity(ctx context.Context, params map[string]interface{}) (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"path": "/tmp/cloned-repo",
|
||||
"commit": "abc123",
|
||||
"branch": "main",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mockAnalyzeActivity(ctx context.Context, params map[string]interface{}) (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"quality": 0.85,
|
||||
"issues": []string{},
|
||||
"summary": "Code analysis complete",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mockFailingActivity(ctx context.Context, params map[string]interface{}) (map[string]interface{}, error) {
|
||||
return nil, fmt.Errorf("mock activity failure")
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// +build integration
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rockliang/poimen/workflows/internal/routing"
|
||||
"github.com/rockliang/poimen/workflows/statemachine"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.temporal.io/sdk/client"
|
||||
)
|
||||
|
||||
// TestTemporalRoutingWorkflow tests full flow against real Temporal cluster
|
||||
// Run with: TEMPORAL_HOSTPORT=temporal.riotpiao.com:7233 RUN_INTEGRATION_TESTS=1 go test -tags=integration -v -run TestTemporalRoutingWorkflow ./tests/...
|
||||
func TestTemporalRoutingWorkflow(t *testing.T) {
|
||||
if os.Getenv("RUN_INTEGRATION_TESTS") != "1" {
|
||||
t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=1 to run.")
|
||||
}
|
||||
|
||||
hostPort := os.Getenv("TEMPORAL_HOSTPORT")
|
||||
if hostPort == "" {
|
||||
hostPort = "temporal.riotpiao.com:7233"
|
||||
}
|
||||
|
||||
namespace := os.Getenv("TEMPORAL_NAMESPACE")
|
||||
if namespace == "" {
|
||||
namespace = "poimen-harness"
|
||||
}
|
||||
|
||||
t.Logf("Connecting to Temporal at %s (namespace: %s)", hostPort, namespace)
|
||||
|
||||
// Connect to Temporal
|
||||
c, err := client.Dial(client.Options{
|
||||
HostPort: hostPort,
|
||||
Namespace: namespace,
|
||||
})
|
||||
if err != nil {
|
||||
t.Skipf("Skipping - cannot connect to Temporal: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
t.Log("Connected to Temporal successfully")
|
||||
|
||||
// Test 1: Generate spec via LLM and submit
|
||||
t.Run("LLM_Route_And_Submit", func(t *testing.T) {
|
||||
// Load KB and create router
|
||||
kb, err := routing.LoadKnowledgeBaseFromDefaultPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
router, err := routing.NewLLMRouter(kb)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate workflow spec
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
output, err := router.Route(ctx, routing.LLMRouterInput{
|
||||
Message: "Clone and analyze https://github.com/rockliang/poimen",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, output.IsCron)
|
||||
require.NotNil(t, output.Spec)
|
||||
|
||||
t.Logf("Generated spec: %s with %d states", output.Spec.Name, len(output.Spec.States))
|
||||
|
||||
// Submit to Temporal
|
||||
workflowID := "test-routing-" + time.Now().Format("20060102-150405")
|
||||
input := statemachine.RoutingWorkflowInput{Spec: output.Spec}
|
||||
|
||||
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||
ID: workflowID,
|
||||
TaskQueue: "poimen-taskqueue",
|
||||
}, statemachine.RoutingWorkflow, input)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("Workflow submitted: ID=%s, RunID=%s", run.GetID(), run.GetRunID())
|
||||
|
||||
// Check workflow started (don't wait for completion - activities may not be registered)
|
||||
desc, err := c.DescribeWorkflowExecution(ctx, workflowID, "")
|
||||
require.NoError(t, err)
|
||||
t.Logf("Workflow status: %s", desc.WorkflowExecutionInfo.Status.String())
|
||||
|
||||
// Cancel the workflow (since activities may not be running)
|
||||
err = c.CancelWorkflow(ctx, workflowID, "")
|
||||
if err != nil {
|
||||
t.Logf("Cancel failed (may already be done): %v", err)
|
||||
} else {
|
||||
t.Log("Workflow cancelled")
|
||||
}
|
||||
})
|
||||
|
||||
// Test 2: Submit simple Pass-only workflow (no activities needed)
|
||||
t.Run("PassOnly_Workflow", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
spec := &routing.WorkflowSpec{
|
||||
Name: "pass-only-test",
|
||||
Input: map[string]interface{}{"test": true},
|
||||
States: []routing.State{
|
||||
{
|
||||
Name: "Step1",
|
||||
Type: routing.StateTypePass,
|
||||
Result: map[string]interface{}{"status": "step1-done"},
|
||||
Next: "Step2",
|
||||
},
|
||||
{
|
||||
Name: "Step2",
|
||||
Type: routing.StateTypePass,
|
||||
Result: map[string]interface{}{"status": "step2-done", "final": true},
|
||||
End: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
workflowID := "test-pass-only-" + time.Now().Format("20060102-150405")
|
||||
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||
|
||||
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||
ID: workflowID,
|
||||
TaskQueue: "poimen-taskqueue",
|
||||
}, statemachine.RoutingWorkflow, input)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("Pass-only workflow submitted: ID=%s", run.GetID())
|
||||
|
||||
// Wait for result (Pass states don't need workers)
|
||||
var result statemachine.RoutingWorkflowOutput
|
||||
err = run.Get(ctx, &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("Workflow result: status=%s", result.Status)
|
||||
require.Equal(t, "COMPLETED", result.Status)
|
||||
require.Contains(t, result.StepResults, "Step1")
|
||||
require.Contains(t, result.StepResults, "Step2")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user