- 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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,20 +15,27 @@ import (
|
|||||||
"github.com/rockliang/poimen/workflows/internal/config"
|
"github.com/rockliang/poimen/workflows/internal/config"
|
||||||
"github.com/rockliang/poimen/workflows/internal/health"
|
"github.com/rockliang/poimen/workflows/internal/health"
|
||||||
"github.com/rockliang/poimen/workflows/internal/logging"
|
"github.com/rockliang/poimen/workflows/internal/logging"
|
||||||
|
"github.com/rockliang/poimen/workflows/internal/routing"
|
||||||
"github.com/rockliang/poimen/workflows/statemachine"
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
|
// Orchestrator flags
|
||||||
repoPath = flag.String("repo", "", "target repo path")
|
repoPath = flag.String("repo", "", "target repo path")
|
||||||
remoteURL = flag.String("remote", "", "remote URL")
|
remoteURL = flag.String("remote", "", "remote URL")
|
||||||
milestone = flag.String("milestone", "T0", "milestone ID")
|
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)")
|
plannerModel = flag.String("planner-model", "reasoning", "planner model ID (local-llm)")
|
||||||
judgeModel = flag.String("judge-model", "reasoning", "judge 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)")
|
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)")
|
piProvider = flag.String("pi-provider", "local-llm", "pi provider name for skills (local-llm)")
|
||||||
healthCheck = flag.Bool("health", false, "check health and exit")
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -66,9 +75,15 @@ func main() {
|
|||||||
return
|
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 == "" {
|
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)
|
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.OrchestratorWorkflow)
|
||||||
w.RegisterWorkflow(statemachine.TaskUnitWorkflow)
|
w.RegisterWorkflow(statemachine.TaskUnitWorkflow)
|
||||||
w.RegisterWorkflow(statemachine.TestWorkflow)
|
w.RegisterWorkflow(statemachine.TestWorkflow)
|
||||||
|
w.RegisterWorkflow(statemachine.RoutingWorkflow)
|
||||||
|
|
||||||
// Register all activities
|
// Register all activities
|
||||||
w.RegisterActivity(action.CloneRepoActivity)
|
w.RegisterActivity(action.CloneRepoActivity)
|
||||||
@@ -68,6 +69,25 @@ func main() {
|
|||||||
// w.RegisterActivity(action.UpdateLessonsActivity)
|
// w.RegisterActivity(action.UpdateLessonsActivity)
|
||||||
// w.RegisterActivity(action.ReadLessonsActivity)
|
// 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
|
// Initialize health checker
|
||||||
healthChecker := health.NewChecker(c)
|
healthChecker := health.NewChecker(c)
|
||||||
healthHandler := health.NewHandler(healthChecker)
|
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/google/uuid v1.6.0 // indirect
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // 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/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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nexus-rpc/nexus-proto-annotations v0.1.0 // indirect
|
github.com/nexus-rpc/nexus-proto-annotations v0.1.0 // indirect
|
||||||
github.com/nexus-rpc/sdk-go v0.7.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/common v0.70.1 // indirect
|
||||||
github.com/prometheus/procfs v0.21.1 // indirect
|
github.com/prometheus/procfs v0.21.1 // indirect
|
||||||
github.com/robfig/cron v1.2.0 // 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
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
go.temporal.io/api v1.63.4 // indirect
|
go.temporal.io/api v1.63.4 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // 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/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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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 h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
|
||||||
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
|
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
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/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 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
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/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 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
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 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
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.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 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
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 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
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=
|
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": [],
|
"dependencies": [],
|
||||||
"notes": "Network-dependent. May fail on network issues or service throttling. Retry 2x."
|
"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": {
|
"metadata": {
|
||||||
"totalActivities": 8,
|
"totalActivities": 9,
|
||||||
"lastUpdated": "2025-08-31T00:00:00Z",
|
"lastUpdated": "2025-08-31T00:00:00Z",
|
||||||
"categories": {
|
"categories": {
|
||||||
"repository": 1,
|
"repository": 1,
|
||||||
@@ -354,7 +420,8 @@
|
|||||||
"deployment": 1,
|
"deployment": 1,
|
||||||
"notification": 1,
|
"notification": 1,
|
||||||
"approval": 1,
|
"approval": 1,
|
||||||
"storage": 1
|
"storage": 1,
|
||||||
|
"memory": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KnowledgeBase represents the activity knowledge base
|
// KnowledgeBase represents the activity knowledge base
|
||||||
@@ -72,6 +73,21 @@ func LoadKnowledgeBaseFromDefaultPath() (*KnowledgeBase, error) {
|
|||||||
return LoadKnowledgeBase("internal/routing/activity_knowledge_base.json")
|
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")
|
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:
|
data:
|
||||||
TEMPORAL_NAMESPACE: "poimen-harness"
|
TEMPORAL_NAMESPACE: "poimen-harness"
|
||||||
TEMPORAL_HOSTPORT: "temporal-frontend.temporal:7233"
|
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/name: poimen
|
||||||
app.kubernetes.io/component: orchestrator
|
app.kubernetes.io/component: orchestrator
|
||||||
data:
|
data:
|
||||||
GIT_COMMIT: "38dd3f8d" # Updated automatically by CI/CD
|
GIT_COMMIT: "303e78f7" # Updated automatically by CI/CD
|
||||||
GIT_BRANCH: "main"
|
GIT_BRANCH: "main"
|
||||||
DEPLOYMENT_DATE: "2026-08-31"
|
DEPLOYMENT_DATE: "2026-09-02"
|
||||||
|
|||||||
+2
-10
@@ -4,23 +4,15 @@ kind: Kustomization
|
|||||||
namespace: poimen
|
namespace: poimen
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- orchestrator-job.yaml
|
|
||||||
- worker-deployment.yaml
|
- worker-deployment.yaml
|
||||||
- git-commit.yaml
|
- configmap.yaml
|
||||||
|
|
||||||
commonLabels:
|
commonLabels:
|
||||||
app.kubernetes.io/name: poimen
|
app.kubernetes.io/name: poimen
|
||||||
app.kubernetes.io/component: orchestrator
|
app.kubernetes.io/component: worker
|
||||||
|
|
||||||
secretGenerator:
|
secretGenerator:
|
||||||
- name: poimen-secrets
|
- name: poimen-secrets
|
||||||
envs:
|
envs:
|
||||||
- secrets.env
|
- secrets.env
|
||||||
behavior: create
|
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:
|
labels:
|
||||||
app: poimen-worker
|
app: poimen-worker
|
||||||
annotations:
|
annotations:
|
||||||
git-commit: "38dd3f8d" # ✅ Updated on each push, triggers rolling restart
|
git-commit: "303e78f7" # ✅ Updated on each push, triggers rolling restart
|
||||||
deployment-date: "2026-08-31"
|
deployment-date: "2026-09-02"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: worker
|
- name: worker
|
||||||
@@ -52,7 +52,15 @@ spec:
|
|||||||
name: poimen-secrets
|
name: poimen-secrets
|
||||||
key: ANTHROPIC_API_KEY
|
key: ANTHROPIC_API_KEY
|
||||||
- name: LOCAL_LLM_BASE_URL
|
- 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:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: "512Mi"
|
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