Implement Beat 1: Sequential Thinking Age-Encrypted Wrapper (Skeleton)
This commit completes Beat 1 of the SequentialThinkingForCHORUS implementation, providing a functional plaintext skeleton for the age-encrypted wrapper. ## Deliverables ### 1. Main Wrapper Entry Point - `cmd/seqthink-wrapper/main.go`: HTTP server on :8443 - Configuration loading from environment variables - Graceful shutdown handling - MCP server readiness checking with timeout ### 2. MCP Client Package - `pkg/seqthink/mcpclient/client.go`: HTTP client for MCP server - Communicates with MCP server on localhost:8000 - Health check endpoint - Tool call endpoint with 120s timeout ### 3. Proxy Server Package - `pkg/seqthink/proxy/server.go`: HTTP handlers for wrapper - Health and readiness endpoints - Tool call proxy (plaintext for Beat 1) - SSE endpoint placeholder - Metrics endpoint integration ### 4. Observability Package - `pkg/seqthink/observability/logger.go`: Structured logging with zerolog - `pkg/seqthink/observability/metrics.go`: Prometheus metrics - Counters for requests, errors, decrypt/encrypt failures, policy denials - Request duration histogram ### 5. Docker Infrastructure - `deploy/seqthink/Dockerfile`: Multi-stage build - `deploy/seqthink/entrypoint.sh`: Startup orchestration - `deploy/seqthink/mcp_stub.py`: Minimal MCP server for testing ### 6. Build System Integration - Updated `Makefile` with `build-seqthink` target - Uses GOWORK=off and -mod=mod for clean builds - `docker-seqthink` target for container builds ## Testing Successfully builds with: ``` make build-seqthink ``` Binary successfully starts and waits for MCP server connection. ## Next Steps Beat 2 will add: - Age encryption/decryption (pkg/seqthink/ageio) - Content-Type: application/age enforcement - SSE streaming with encrypted frames - Golden tests for crypto round-trips 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
32
Makefile
32
Makefile
@@ -1,11 +1,12 @@
|
||||
# CHORUS Multi-Binary Makefile
|
||||
# Builds both chorus-agent and chorus-hap binaries
|
||||
# Builds chorus-agent, chorus-hap, and seqthink-wrapper binaries
|
||||
|
||||
# Build configuration
|
||||
BINARY_NAME_AGENT = chorus-agent
|
||||
BINARY_NAME_HAP = chorus-hap
|
||||
BINARY_NAME_COMPAT = chorus
|
||||
VERSION ?= 0.5.5
|
||||
BINARY_NAME_SEQTHINK = seqthink-wrapper
|
||||
VERSION ?= 0.5.28
|
||||
COMMIT_HASH ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
BUILD_DATE ?= $(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
|
||||
@@ -49,6 +50,14 @@ build-compat:
|
||||
go build $(BUILD_FLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_COMPAT) ./$(CMD_DIR)/chorus
|
||||
@echo "✅ Compatibility wrapper built: $(BUILD_DIR)/$(BINARY_NAME_COMPAT)"
|
||||
|
||||
# Build Sequential Thinking age-encrypted wrapper
|
||||
.PHONY: build-seqthink
|
||||
build-seqthink:
|
||||
@echo "🔐 Building Sequential Thinking wrapper..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
GOWORK=off go build -mod=mod $(BUILD_FLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_SEQTHINK) ./$(CMD_DIR)/seqthink-wrapper
|
||||
@echo "✅ SeqThink wrapper built: $(BUILD_DIR)/$(BINARY_NAME_SEQTHINK)"
|
||||
|
||||
# Test compilation without building
|
||||
.PHONY: test-compile
|
||||
test-compile:
|
||||
@@ -103,8 +112,13 @@ docker-hap:
|
||||
@echo "🐳 Building Docker image for CHORUS HAP..."
|
||||
docker build -f docker/Dockerfile.hap -t chorus-hap:$(VERSION) .
|
||||
|
||||
.PHONY: docker-seqthink
|
||||
docker-seqthink:
|
||||
@echo "🔐 Building Docker image for Sequential Thinking wrapper..."
|
||||
docker build -f deploy/seqthink/Dockerfile -t seqthink-wrapper:$(VERSION) .
|
||||
|
||||
.PHONY: docker
|
||||
docker: docker-agent docker-hap
|
||||
docker: docker-agent docker-hap docker-seqthink
|
||||
|
||||
# Help
|
||||
.PHONY: help
|
||||
@@ -112,22 +126,24 @@ help:
|
||||
@echo "CHORUS Multi-Binary Build System"
|
||||
@echo ""
|
||||
@echo "Targets:"
|
||||
@echo " all - Clean and build both binaries (default)"
|
||||
@echo " build - Build both binaries"
|
||||
@echo " all - Clean and build all binaries (default)"
|
||||
@echo " build - Build all binaries"
|
||||
@echo " build-agent - Build autonomous agent binary only"
|
||||
@echo " build-hap - Build human agent portal binary only"
|
||||
@echo " test-compile - Test that both binaries compile"
|
||||
@echo " build-seqthink - Build Sequential Thinking wrapper only"
|
||||
@echo " test-compile - Test that binaries compile"
|
||||
@echo " test - Run tests"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo " install - Install binaries to GOPATH/bin"
|
||||
@echo " run-agent - Build and run agent"
|
||||
@echo " run-hap - Build and run HAP"
|
||||
@echo " docker - Build Docker images for both binaries"
|
||||
@echo " docker - Build Docker images for all binaries"
|
||||
@echo " docker-agent - Build Docker image for agent only"
|
||||
@echo " docker-hap - Build Docker image for HAP only"
|
||||
@echo " docker-seqthink - Build Docker image for SeqThink wrapper only"
|
||||
@echo " help - Show this help"
|
||||
@echo ""
|
||||
@echo "Environment Variables:"
|
||||
@echo " VERSION - Version string (default: 0.1.0-dev)"
|
||||
@echo " VERSION - Version string (default: 0.5.28)"
|
||||
@echo " COMMIT_HASH - Git commit hash (auto-detected)"
|
||||
@echo " BUILD_DATE - Build timestamp (auto-generated)"
|
||||
173
cmd/seqthink-wrapper/main.go
Normal file
173
cmd/seqthink-wrapper/main.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"chorus/pkg/seqthink/mcpclient"
|
||||
"chorus/pkg/seqthink/observability"
|
||||
"chorus/pkg/seqthink/proxy"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Config holds the wrapper configuration
|
||||
type Config struct {
|
||||
Port string
|
||||
MCPLocalURL string
|
||||
LogLevel string
|
||||
MaxBodyMB int
|
||||
HealthTimeout time.Duration
|
||||
ShutdownTimeout time.Duration
|
||||
AgeIdentPath string
|
||||
AgeRecipsPath string
|
||||
KachingJWKSURL string
|
||||
RequiredScope string
|
||||
}
|
||||
|
||||
func loadConfig() *Config {
|
||||
return &Config{
|
||||
Port: getEnv("PORT", "8443"),
|
||||
MCPLocalURL: getEnv("MCP_LOCAL", "http://127.0.0.1:8000"),
|
||||
LogLevel: getEnv("LOG_LEVEL", "info"),
|
||||
MaxBodyMB: getEnvInt("MAX_BODY_MB", 4),
|
||||
HealthTimeout: 5 * time.Second,
|
||||
ShutdownTimeout: 30 * time.Second,
|
||||
AgeIdentPath: getEnv("AGE_IDENT_PATH", ""),
|
||||
AgeRecipsPath: getEnv("AGE_RECIPS_PATH", ""),
|
||||
KachingJWKSURL: getEnv("KACHING_JWKS_URL", ""),
|
||||
RequiredScope: getEnv("REQUIRED_SCOPE", "sequentialthinking.run"),
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
// Initialize observability
|
||||
observability.InitLogger(cfg.LogLevel)
|
||||
metrics := observability.InitMetrics()
|
||||
|
||||
log.Info().
|
||||
Str("port", cfg.Port).
|
||||
Str("mcp_url", cfg.MCPLocalURL).
|
||||
Str("version", "0.1.0-beta1").
|
||||
Msg("🚀 Starting Sequential Thinking Age Wrapper")
|
||||
|
||||
// Create MCP client
|
||||
mcpClient := mcpclient.New(cfg.MCPLocalURL)
|
||||
|
||||
// Wait for MCP server to be ready
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
log.Info().Msg("⏳ Waiting for MCP server...")
|
||||
if err := waitForMCP(ctx, mcpClient); err != nil {
|
||||
log.Fatal().Err(err).Msg("❌ MCP server not ready")
|
||||
}
|
||||
|
||||
log.Info().Msg("✅ MCP server ready")
|
||||
|
||||
// Create proxy server
|
||||
proxyServer, err := proxy.NewServer(proxy.ServerConfig{
|
||||
MCPClient: mcpClient,
|
||||
Metrics: metrics,
|
||||
MaxBodyMB: cfg.MaxBodyMB,
|
||||
AgeIdentPath: cfg.AgeIdentPath,
|
||||
AgeRecipsPath: cfg.AgeRecipsPath,
|
||||
KachingJWKSURL: cfg.KachingJWKSURL,
|
||||
RequiredScope: cfg.RequiredScope,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("❌ Failed to create proxy server")
|
||||
}
|
||||
|
||||
// Setup HTTP server
|
||||
srv := &http.Server{
|
||||
Addr: ":" + cfg.Port,
|
||||
Handler: proxyServer.Handler(),
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 90 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
// Start server in goroutine
|
||||
go func() {
|
||||
log.Info().
|
||||
Str("addr", srv.Addr).
|
||||
Bool("encryption_enabled", cfg.AgeIdentPath != "").
|
||||
Bool("policy_enabled", cfg.KachingJWKSURL != "").
|
||||
Msg("🔐 Wrapper listening")
|
||||
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatal().Err(err).Msg("❌ HTTP server failed")
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for shutdown signal
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
|
||||
log.Info().Msg("🛑 Shutting down gracefully...")
|
||||
|
||||
// Graceful shutdown
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
|
||||
defer shutdownCancel()
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Error().Err(err).Msg("⚠️ Shutdown error")
|
||||
}
|
||||
|
||||
log.Info().Msg("✅ Shutdown complete")
|
||||
}
|
||||
|
||||
// waitForMCP waits for MCP server to be ready
|
||||
func waitForMCP(ctx context.Context, client *mcpclient.Client) error {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("timeout waiting for MCP server")
|
||||
case <-ticker.C:
|
||||
if err := client.Health(ctx); err == nil {
|
||||
return nil
|
||||
}
|
||||
log.Debug().Msg("Waiting for MCP server...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getEnv gets environment variable with default
|
||||
func getEnv(key, defaultVal string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
// getEnvInt gets environment variable as int with default
|
||||
func getEnvInt(key string, defaultVal int) int {
|
||||
val := os.Getenv(key)
|
||||
if val == "" {
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
var result int
|
||||
if _, err := fmt.Sscanf(val, "%d", &result); err != nil {
|
||||
log.Warn().
|
||||
Str("key", key).
|
||||
Str("value", val).
|
||||
Int("default", defaultVal).
|
||||
Msg("Invalid integer env var, using default")
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
86
deploy/seqthink/Dockerfile
Normal file
86
deploy/seqthink/Dockerfile
Normal file
@@ -0,0 +1,86 @@
|
||||
# Sequential Thinking Age-Encrypted Wrapper
|
||||
# Beat 1: Plaintext skeleton - encryption added in Beat 2
|
||||
|
||||
# Stage 1: Build Go wrapper
|
||||
FROM golang:1.23-alpine AS go-builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache git make
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the wrapper binary
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo \
|
||||
-ldflags '-w -s -extldflags "-static"' \
|
||||
-o seqthink-wrapper \
|
||||
./cmd/seqthink-wrapper
|
||||
|
||||
# Stage 2: Build Python MCP server
|
||||
FROM python:3.11-slim AS python-builder
|
||||
|
||||
WORKDIR /mcp
|
||||
|
||||
# Install Sequential Thinking MCP server dependencies
|
||||
# Note: For Beat 1, we'll use a minimal Python HTTP server
|
||||
# Full MCP server integration happens in later beats
|
||||
RUN pip install --no-cache-dir \
|
||||
fastapi==0.109.0 \
|
||||
uvicorn[standard]==0.27.0 \
|
||||
pydantic==2.5.3
|
||||
|
||||
# Copy MCP server stub (to be replaced with real implementation)
|
||||
COPY deploy/seqthink/mcp_stub.py /mcp/server.py
|
||||
|
||||
# Stage 3: Runtime
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
python3 \
|
||||
python3-pip && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python packages in runtime
|
||||
RUN pip3 install --no-cache-dir --break-system-packages \
|
||||
fastapi==0.109.0 \
|
||||
uvicorn[standard]==0.27.0 \
|
||||
pydantic==2.5.3
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -r -u 1000 -m -s /bin/bash seqthink
|
||||
|
||||
# Copy binaries
|
||||
COPY --from=go-builder /build/seqthink-wrapper /usr/local/bin/
|
||||
COPY --from=python-builder /mcp/server.py /opt/mcp/server.py
|
||||
|
||||
# Copy entrypoint
|
||||
COPY deploy/seqthink/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Setup directories
|
||||
RUN mkdir -p /etc/seqthink /var/log/seqthink && \
|
||||
chown -R seqthink:seqthink /etc/seqthink /var/log/seqthink
|
||||
|
||||
# Switch to non-root user
|
||||
USER seqthink
|
||||
WORKDIR /home/seqthink
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:8443/health || exit 1
|
||||
|
||||
# Expose wrapper port (MCP server on 127.0.0.1:8000 is internal only)
|
||||
EXPOSE 8443
|
||||
|
||||
# Run entrypoint
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
27
deploy/seqthink/entrypoint.sh
Normal file
27
deploy/seqthink/entrypoint.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting Sequential Thinking Age Wrapper (Beat 1)"
|
||||
|
||||
# Start MCP server on loopback
|
||||
echo "📡 Starting MCP server on 127.0.0.1:8000..."
|
||||
python3 /opt/mcp/server.py &
|
||||
MCP_PID=$!
|
||||
|
||||
# Wait for MCP server to be ready
|
||||
echo "⏳ Waiting for MCP server to be ready..."
|
||||
for i in {1..30}; do
|
||||
if curl -sf http://127.0.0.1:8000/health > /dev/null 2>&1; then
|
||||
echo "✅ MCP server ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo "❌ MCP server failed to start"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Start wrapper
|
||||
echo "🔐 Starting wrapper on :8443..."
|
||||
exec seqthink-wrapper
|
||||
70
deploy/seqthink/mcp_stub.py
Normal file
70
deploy/seqthink/mcp_stub.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sequential Thinking MCP Server Stub (Beat 1)
|
||||
|
||||
This is a minimal implementation for testing the wrapper infrastructure.
|
||||
In later beats, this will be replaced with the full Sequential Thinking MCP server.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict, Any, Optional
|
||||
import uvicorn
|
||||
|
||||
app = FastAPI(title="Sequential Thinking MCP Server Stub")
|
||||
|
||||
|
||||
class ToolRequest(BaseModel):
|
||||
tool: str
|
||||
payload: Dict[str, Any]
|
||||
|
||||
|
||||
class ToolResponse(BaseModel):
|
||||
result: Optional[Any] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/mcp/tool")
|
||||
async def call_tool(request: ToolRequest) -> ToolResponse:
|
||||
"""
|
||||
Tool call endpoint - stub implementation
|
||||
|
||||
In Beat 1, this just echoes back the request to verify the wrapper works.
|
||||
Later beats will implement the actual Sequential Thinking logic.
|
||||
"""
|
||||
if request.tool != "mcp__sequential-thinking__sequentialthinking":
|
||||
return ToolResponse(
|
||||
error=f"Unknown tool: {request.tool}"
|
||||
)
|
||||
|
||||
# Stub response for Sequential Thinking tool
|
||||
payload = request.payload
|
||||
thought_number = payload.get("thoughtNumber", 1)
|
||||
total_thoughts = payload.get("totalThoughts", 5)
|
||||
thought = payload.get("thought", "")
|
||||
next_thought_needed = payload.get("nextThoughtNeeded", True)
|
||||
|
||||
return ToolResponse(
|
||||
result={
|
||||
"thoughtNumber": thought_number,
|
||||
"totalThoughts": total_thoughts,
|
||||
"thought": thought,
|
||||
"nextThoughtNeeded": next_thought_needed,
|
||||
"message": "Beat 1 stub - Sequential Thinking not yet implemented"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
app,
|
||||
host="127.0.0.1",
|
||||
port=8000,
|
||||
log_level="info"
|
||||
)
|
||||
1090
docs/SEQTHINK-AGE-WRAPPER-IMPLEMENTATION.md
Normal file
1090
docs/SEQTHINK-AGE-WRAPPER-IMPLEMENTATION.md
Normal file
File diff suppressed because it is too large
Load Diff
579
docs/SEQUENTIAL-THINKING-INTEGRATION-PLAN.md
Normal file
579
docs/SEQUENTIAL-THINKING-INTEGRATION-PLAN.md
Normal file
@@ -0,0 +1,579 @@
|
||||
# Sequential Thinking Integration Plan for CHORUS Agents
|
||||
|
||||
**Date**: 2025-10-13
|
||||
**Status**: Design Phase
|
||||
**Priority**: High - Blocking further intelligence improvements
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines the integration of the Sequential Thinking MCP server into CHORUS agents to enable **structured, multi-step reasoning** before task execution. This addresses the limitation in the SequentialThinkingForCHORUS repository issue and unlocks advanced agent decision-making capabilities.
|
||||
|
||||
**Problem Statement**: CHORUS agents currently use simple prompt-response cycles without structured reasoning, limiting their ability to handle complex tasks requiring multi-step analysis, hypothesis generation, and iterative refinement.
|
||||
|
||||
**Solution**: Integrate the `mcp__sequential-thinking__sequentialthinking` MCP tool into the AI provider layer to enable chain-of-thought reasoning for complex tasks.
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture Analysis
|
||||
|
||||
### 1. Existing AI Provider Flow
|
||||
|
||||
```
|
||||
TaskRequest → ModelProvider.ExecuteTask() → TaskResponse
|
||||
↓
|
||||
[Single LLM Call]
|
||||
↓
|
||||
Response String
|
||||
```
|
||||
|
||||
**Current Providers**:
|
||||
- **OllamaProvider**: Local model execution
|
||||
- **ResetDataProvider**: ResetData LaaS API
|
||||
- **OpenAIProvider**: OpenAI API
|
||||
|
||||
**Current Limitations**:
|
||||
- ✗ No structured reasoning process
|
||||
- ✗ No ability to revise initial thoughts
|
||||
- ✗ No hypothesis generation and verification
|
||||
- ✗ No branching for alternative approaches
|
||||
- ✗ Simple string reasoning field (not structured)
|
||||
|
||||
### 2. TaskResponse Structure
|
||||
|
||||
**Location**: `/home/tony/chorus/project-queues/active/CHORUS/pkg/ai/provider.go:53-78`
|
||||
|
||||
```go
|
||||
type TaskResponse struct {
|
||||
Success bool `json:"success"`
|
||||
TaskID string `json:"task_id"`
|
||||
Response string `json:"response"`
|
||||
Reasoning string `json:"reasoning,omitempty"` // ← Simple string
|
||||
Actions []TaskAction `json:"actions,omitempty"`
|
||||
Artifacts []Artifact `json:"artifacts,omitempty"`
|
||||
TokensUsed TokenUsage `json:"tokens_used,omitempty"`
|
||||
// ... other fields
|
||||
}
|
||||
```
|
||||
|
||||
**Opportunity**: The `Reasoning` field is perfect for storing structured thinking output!
|
||||
|
||||
---
|
||||
|
||||
## Sequential Thinking MCP Tool
|
||||
|
||||
### Tool Signature
|
||||
|
||||
```go
|
||||
mcp__sequential-thinking__sequentialthinking(
|
||||
thought: string,
|
||||
nextThoughtNeeded: bool,
|
||||
thoughtNumber: int,
|
||||
totalThoughts: int,
|
||||
isRevision: bool = false,
|
||||
revisesThought: int = null,
|
||||
branchFromThought: int = null,
|
||||
branchId: string = null,
|
||||
needsMoreThoughts: bool = false
|
||||
)
|
||||
```
|
||||
|
||||
### Capabilities
|
||||
|
||||
1. **Adaptive Thinking**: Adjust `totalThoughts` up or down as understanding deepens
|
||||
2. **Revision Support**: Question and revise previous thoughts (`isRevision`, `revisesThought`)
|
||||
3. **Branching**: Explore alternative approaches (`branchFromThought`, `branchId`)
|
||||
4. **Hypothesis Testing**: Generate and verify hypotheses in chain-of-thought
|
||||
5. **Uncertainty Expression**: Express and work through unclear aspects
|
||||
6. **Context Maintenance**: Keep track of all previous thoughts
|
||||
|
||||
### When to Use
|
||||
|
||||
- **Complex problem decomposition**
|
||||
- **Multi-step solution planning**
|
||||
- **Problems requiring course correction**
|
||||
- **Unclear scope requiring exploration**
|
||||
- **Tasks needing context over multiple steps**
|
||||
- **Filtering irrelevant information**
|
||||
|
||||
---
|
||||
|
||||
## Proposed Integration Architecture
|
||||
|
||||
### Phase 1: Enhanced TaskResponse Structure
|
||||
|
||||
**File**: `pkg/ai/provider.go`
|
||||
|
||||
```go
|
||||
// StructuredReasoning represents chain-of-thought reasoning process
|
||||
type StructuredReasoning struct {
|
||||
Thoughts []ThoughtStep `json:"thoughts"`
|
||||
FinalHypothesis string `json:"final_hypothesis,omitempty"`
|
||||
VerificationSteps []string `json:"verification_steps,omitempty"`
|
||||
Confidence float32 `json:"confidence"` // 0.0-1.0
|
||||
TotalRevisions int `json:"total_revisions"`
|
||||
BranchesExplored int `json:"branches_explored"`
|
||||
}
|
||||
|
||||
// ThoughtStep represents a single step in the reasoning process
|
||||
type ThoughtStep struct {
|
||||
Number int `json:"number"`
|
||||
Content string `json:"content"`
|
||||
IsRevision bool `json:"is_revision"`
|
||||
RevisesThought int `json:"revises_thought,omitempty"`
|
||||
BranchID string `json:"branch_id,omitempty"`
|
||||
BranchFrom int `json:"branch_from,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// TaskResponse update
|
||||
type TaskResponse struct {
|
||||
// ... existing fields ...
|
||||
Reasoning string `json:"reasoning,omitempty"` // Legacy simple string
|
||||
StructuredReasoning *StructuredReasoning `json:"structured_reasoning,omitempty"` // NEW
|
||||
// ... rest of fields ...
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Sequential Thinking Wrapper
|
||||
|
||||
**New File**: `pkg/ai/sequential_thinking.go`
|
||||
|
||||
```go
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SequentialThinkingEngine wraps MCP sequential thinking tool
|
||||
type SequentialThinkingEngine struct {
|
||||
mcpClient MCPClient // Interface to MCP tool
|
||||
}
|
||||
|
||||
// ThinkingRequest represents input for sequential thinking
|
||||
type ThinkingRequest struct {
|
||||
Problem string
|
||||
Context map[string]interface{}
|
||||
MaxThoughts int
|
||||
AllowRevisions bool
|
||||
AllowBranching bool
|
||||
}
|
||||
|
||||
// ThinkingResult represents output from sequential thinking
|
||||
type ThinkingResult struct {
|
||||
Thoughts []ThoughtStep
|
||||
FinalConclusion string
|
||||
Confidence float32
|
||||
ReasoningPath string // Markdown summary of thinking process
|
||||
}
|
||||
|
||||
// Think executes sequential thinking process
|
||||
func (e *SequentialThinkingEngine) Think(ctx context.Context, req *ThinkingRequest) (*ThinkingResult, error) {
|
||||
// Implementation:
|
||||
// 1. Initialize thinking with problem statement
|
||||
// 2. Iteratively call MCP tool until nextThoughtNeeded = false
|
||||
// 3. Track all thoughts, revisions, branches
|
||||
// 4. Generate final conclusion and reasoning summary
|
||||
// 5. Return structured result
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Provider Integration
|
||||
|
||||
**Modified File**: `pkg/ai/resetdata.go`
|
||||
|
||||
```go
|
||||
// ExecuteTask with sequential thinking
|
||||
func (p *ResetDataProvider) ExecuteTask(ctx context.Context, request *TaskRequest) (*TaskResponse, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Determine if task requires sequential thinking
|
||||
useSequentialThinking := p.shouldUseSequentialThinking(request)
|
||||
|
||||
var structuredReasoning *StructuredReasoning
|
||||
var enhancedPrompt string
|
||||
|
||||
if useSequentialThinking {
|
||||
// Use sequential thinking engine to analyze task first
|
||||
thinkingEngine := NewSequentialThinkingEngine(p.mcpClient)
|
||||
|
||||
thinkingResult, err := thinkingEngine.Think(ctx, &ThinkingRequest{
|
||||
Problem: p.formatTaskAsProblem(request),
|
||||
Context: request.Context,
|
||||
MaxThoughts: 10,
|
||||
AllowRevisions: true,
|
||||
AllowBranching: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// Fall back to direct execution if thinking fails
|
||||
log.Warn().Err(err).Msg("Sequential thinking failed, falling back to direct execution")
|
||||
} else {
|
||||
// Use thinking result to enhance prompt
|
||||
enhancedPrompt = p.buildPromptWithThinking(request, thinkingResult)
|
||||
structuredReasoning = convertToStructuredReasoning(thinkingResult)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute with enhanced prompt (if available) or standard prompt
|
||||
messages, _ := p.buildChatMessages(request, enhancedPrompt)
|
||||
|
||||
// ... rest of execution ...
|
||||
|
||||
return &TaskResponse{
|
||||
Success: true,
|
||||
Response: responseText,
|
||||
Reasoning: legacyReasoningString,
|
||||
StructuredReasoning: structuredReasoning, // NEW
|
||||
// ... rest of response ...
|
||||
}
|
||||
}
|
||||
|
||||
// shouldUseSequentialThinking determines if task warrants sequential thinking
|
||||
func (p *ResetDataProvider) shouldUseSequentialThinking(request *TaskRequest) bool {
|
||||
// Use sequential thinking for:
|
||||
// - High complexity tasks (complexity >= 7)
|
||||
// - Architect role (requires system design)
|
||||
// - Tasks with "design" or "architecture" in title/labels
|
||||
// - Tasks requiring multi-step planning
|
||||
|
||||
if request.Complexity >= 7 {
|
||||
return true
|
||||
}
|
||||
|
||||
role := strings.ToLower(request.AgentRole)
|
||||
if role == "architect" || role == "senior-developer" {
|
||||
return true
|
||||
}
|
||||
|
||||
keywords := []string{"design", "architecture", "refactor", "plan", "strategy"}
|
||||
taskText := strings.ToLower(request.TaskTitle + " " + request.TaskDescription)
|
||||
for _, keyword := range keywords {
|
||||
if strings.Contains(taskText, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Foundation (Days 1-2)
|
||||
|
||||
**Tasks**:
|
||||
1. ✅ Define `StructuredReasoning` and `ThoughtStep` types
|
||||
2. ✅ Add `StructuredReasoning` field to `TaskResponse`
|
||||
3. ✅ Create `SequentialThinkingEngine` skeleton
|
||||
4. ✅ Add MCP client interface for sequential-thinking tool
|
||||
|
||||
**Files to Create/Modify**:
|
||||
- `pkg/ai/provider.go` - Add new types
|
||||
- `pkg/ai/sequential_thinking.go` - New file
|
||||
- `pkg/ai/mcp_client.go` - New file for MCP integration
|
||||
|
||||
**Success Criteria**:
|
||||
- Code compiles without errors
|
||||
- Types are properly defined
|
||||
- MCP client interface is clear
|
||||
|
||||
### Phase 2: Sequential Thinking Engine (Days 3-5)
|
||||
|
||||
**Tasks**:
|
||||
1. Implement `SequentialThinkingEngine.Think()` method
|
||||
2. Implement MCP tool call wrapper
|
||||
3. Add thought tracking and revision detection
|
||||
4. Implement branch management
|
||||
5. Generate reasoning summaries
|
||||
6. Write unit tests
|
||||
|
||||
**Files**:
|
||||
- `pkg/ai/sequential_thinking.go` - Full implementation
|
||||
- `pkg/ai/sequential_thinking_test.go` - Unit tests
|
||||
|
||||
**Success Criteria**:
|
||||
- Can execute complete thinking cycles
|
||||
- Properly tracks revisions and branches
|
||||
- Generates clear reasoning summaries
|
||||
- All unit tests pass
|
||||
|
||||
### Phase 3: Provider Integration (Days 6-8)
|
||||
|
||||
**Tasks**:
|
||||
1. Modify `ResetDataProvider.ExecuteTask()` for sequential thinking
|
||||
2. Implement `shouldUseSequentialThinking()` heuristics
|
||||
3. Add prompt enhancement with thinking results
|
||||
4. Implement fallback for thinking failures
|
||||
5. Add configuration options
|
||||
6. Write integration tests
|
||||
|
||||
**Files**:
|
||||
- `pkg/ai/resetdata.go` - Modify ExecuteTask
|
||||
- `pkg/ai/ollama.go` - Same modifications
|
||||
- `config/agent.yaml` - Add sequential thinking config
|
||||
|
||||
**Success Criteria**:
|
||||
- Complex tasks trigger sequential thinking
|
||||
- Thinking results enhance task execution
|
||||
- Graceful fallback on failures
|
||||
- Integration tests pass
|
||||
|
||||
### Phase 4: Testing & Validation (Days 9-10)
|
||||
|
||||
**Tasks**:
|
||||
1. End-to-end testing with real councils
|
||||
2. Test with various complexity levels
|
||||
3. Validate reasoning quality improvements
|
||||
4. Performance benchmarking
|
||||
5. Documentation updates
|
||||
|
||||
**Test Cases**:
|
||||
- Simple task (complexity=3) → No sequential thinking
|
||||
- Complex task (complexity=8) → Sequential thinking enabled
|
||||
- Architect role → Always uses sequential thinking
|
||||
- Design task → Sequential thinking with branching
|
||||
- Fallback scenario → Graceful degradation
|
||||
|
||||
**Success Criteria**:
|
||||
- Demonstrable improvement in task quality
|
||||
- Acceptable performance overhead (<30% increase in latency)
|
||||
- Clear reasoning traces in artifacts
|
||||
- Documentation complete
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Agent Configuration
|
||||
|
||||
**File**: `config/agent.yaml`
|
||||
|
||||
```yaml
|
||||
ai_providers:
|
||||
resetdata:
|
||||
type: "resetdata"
|
||||
endpoint: "${RESETDATA_API_ENDPOINT}"
|
||||
api_key: "${RESETDATA_API_KEY}"
|
||||
default_model: "llama3.1:70b"
|
||||
|
||||
# Sequential thinking configuration
|
||||
enable_sequential_thinking: true
|
||||
sequential_thinking:
|
||||
min_complexity: 7 # Minimum complexity to trigger
|
||||
force_for_roles: # Always use for these roles
|
||||
- architect
|
||||
- senior-developer
|
||||
max_thoughts: 15 # Maximum thinking iterations
|
||||
enable_revisions: true # Allow thought revisions
|
||||
enable_branching: true # Allow exploring alternatives
|
||||
confidence_threshold: 0.7 # Minimum confidence for final answer
|
||||
```
|
||||
|
||||
### Runtime Toggle
|
||||
|
||||
Allow runtime control via council brief:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-123",
|
||||
"complexity": 8,
|
||||
"use_sequential_thinking": true, // Explicit override
|
||||
"thinking_config": {
|
||||
"max_thoughts": 20,
|
||||
"allow_branching": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits & Expected Improvements
|
||||
|
||||
### 1. Better Problem Decomposition
|
||||
|
||||
**Before**:
|
||||
```
|
||||
Agent: Here's my solution [immediately provides implementation]
|
||||
```
|
||||
|
||||
**After**:
|
||||
```
|
||||
Thought 1: Breaking down the task into 3 main components...
|
||||
Thought 2: Component A requires database schema changes...
|
||||
Thought 3: Wait, revising thought 2 - migration strategy needs consideration...
|
||||
Thought 4: Exploring alternative: event sourcing vs direct updates...
|
||||
Thought 5: Event sourcing better for audit trail requirements...
|
||||
Final: Implementation plan with 5 concrete steps...
|
||||
```
|
||||
|
||||
### 2. Improved Architecture Decisions
|
||||
|
||||
Architect agents can:
|
||||
- Explore multiple design alternatives
|
||||
- Revise decisions based on discovered constraints
|
||||
- Build and verify hypotheses about scalability
|
||||
- Document reasoning trail for future reference
|
||||
|
||||
### 3. Higher Quality Code
|
||||
|
||||
Developer agents can:
|
||||
- Think through edge cases before coding
|
||||
- Consider multiple implementation approaches
|
||||
- Revise initial assumptions
|
||||
- Plan testing strategy upfront
|
||||
|
||||
### 4. Debugging Enhancement
|
||||
|
||||
When tasks fail:
|
||||
- Reasoning traces show where agent went wrong
|
||||
- Can identify flawed assumptions
|
||||
- Easier to improve prompts and heuristics
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### 1. Latency Impact
|
||||
|
||||
**Estimated Overhead**:
|
||||
- Sequential thinking: 5-15 LLM calls (vs 1 direct call)
|
||||
- Expected latency increase: 10-30 seconds for complex tasks
|
||||
- **Mitigation**: Only use for high-complexity tasks (complexity >= 7)
|
||||
|
||||
### 2. Token Usage
|
||||
|
||||
**Estimated Increase**:
|
||||
- Each thought: ~200-500 tokens
|
||||
- 10 thoughts: ~3000-5000 additional tokens
|
||||
- **Mitigation**: Set reasonable `max_thoughts` limits
|
||||
|
||||
### 3. Resource Requirements
|
||||
|
||||
**MCP Server**:
|
||||
- Sequential thinking MCP server must be available
|
||||
- Requires proper error handling and fallback
|
||||
|
||||
---
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| MCP server unavailable | High | Graceful fallback to direct execution |
|
||||
| Increased latency unacceptable | Medium | Make sequential thinking opt-in per task |
|
||||
| Token cost explosion | Medium | Set hard limits on max_thoughts |
|
||||
| Reasoning doesn't improve quality | High | A/B testing with metrics |
|
||||
| Complex implementation | Medium | Phased rollout with testing |
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Quantitative
|
||||
|
||||
1. **Task Success Rate**: Compare before/after for complexity >= 7 tasks
|
||||
- Target: +15% improvement
|
||||
2. **Code Quality**: Static analysis scores for generated code
|
||||
- Target: +20% improvement in complexity score
|
||||
3. **PR Acceptance Rate**: How many agent PRs get merged
|
||||
- Target: +25% improvement
|
||||
4. **Latency**: Task execution time
|
||||
- Acceptable: <30% increase for complex tasks
|
||||
|
||||
### Qualitative
|
||||
|
||||
1. **Reasoning Quality**: Human review of reasoning traces
|
||||
2. **Decision Clarity**: Can humans understand agent's thought process?
|
||||
3. **Developer Feedback**: Easier to debug failed tasks?
|
||||
|
||||
---
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
### Stage 1: Internal Testing (Week 1)
|
||||
|
||||
- Deploy to development environment
|
||||
- Test with synthetic tasks
|
||||
- Gather performance metrics
|
||||
- Refine heuristics
|
||||
|
||||
### Stage 2: Limited Production (Week 2)
|
||||
|
||||
- Enable for architect role only
|
||||
- Enable for complexity >= 9 only
|
||||
- Monitor closely
|
||||
- Collect feedback
|
||||
|
||||
### Stage 3: Expanded Rollout (Week 3-4)
|
||||
|
||||
- Enable for all roles with complexity >= 7
|
||||
- Add complexity-based opt-in
|
||||
- Full production deployment
|
||||
- Continuous monitoring
|
||||
|
||||
### Stage 4: Optimization (Week 5+)
|
||||
|
||||
- Fine-tune heuristics based on data
|
||||
- Optimize thought limits
|
||||
- Improve reasoning summaries
|
||||
- Add advanced features (e.g., multi-agent reasoning)
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### 1. Multi-Agent Reasoning
|
||||
|
||||
Multiple agents can contribute thoughts to same reasoning chain:
|
||||
- Architect proposes design
|
||||
- Security agent reviews security implications
|
||||
- Performance agent analyzes scalability
|
||||
|
||||
### 2. Reasoning Templates
|
||||
|
||||
Pre-defined thinking patterns for common scenarios:
|
||||
- API design checklist
|
||||
- Security review framework
|
||||
- Performance optimization workflow
|
||||
|
||||
### 3. Learning from Reasoning
|
||||
|
||||
Store successful reasoning patterns:
|
||||
- Build knowledge base of good reasoning traces
|
||||
- Use as examples in future tasks
|
||||
- Identify common pitfalls
|
||||
|
||||
### 4. Visualization
|
||||
|
||||
Dashboard showing reasoning graphs:
|
||||
- Thought flow diagrams
|
||||
- Revision history
|
||||
- Branch exploration trees
|
||||
- Confidence evolution
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **SequentialThinkingForCHORUS Issue**: (Repository in GITEA)
|
||||
- **MCP Sequential Thinking Tool**: Available in Claude Code MCP servers
|
||||
- **CHORUS Task Execution**: `/home/tony/chorus/project-queues/active/CHORUS/pkg/execution/engine.go`
|
||||
- **AI Provider Interface**: `/home/tony/chorus/project-queues/active/CHORUS/pkg/ai/provider.go`
|
||||
- **ResetData Provider**: `/home/tony/chorus/project-queues/active/CHORUS/pkg/ai/resetdata.go`
|
||||
|
||||
---
|
||||
|
||||
## Document Info
|
||||
|
||||
- **Created**: 2025-10-13
|
||||
- **Author**: Claude Code
|
||||
- **Status**: Design Complete - Ready for Implementation
|
||||
- **Next Steps**: Begin Phase 1 implementation
|
||||
|
||||
100
pkg/seqthink/mcpclient/client.go
Normal file
100
pkg/seqthink/mcpclient/client.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package mcpclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client is a client for the Sequential Thinking MCP server
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// ToolRequest represents a request to call an MCP tool
|
||||
type ToolRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
}
|
||||
|
||||
// ToolResponse represents the response from an MCP tool call
|
||||
type ToolResponse struct {
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// New creates a new MCP client
|
||||
func New(baseURL string) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 120 * time.Second, // Longer timeout for thinking operations
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Health checks if the MCP server is healthy
|
||||
func (c *Client) Health(ctx context.Context) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/health", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("health check failed: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CallTool calls an MCP tool
|
||||
func (c *Client) CallTool(ctx context.Context, req *ToolRequest) (*ToolResponse, error) {
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/mcp/tool", bytes.NewReader(jsonData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("tool call failed: status %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var toolResp ToolResponse
|
||||
if err := json.Unmarshal(body, &toolResp); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
if toolResp.Error != "" {
|
||||
return nil, fmt.Errorf("tool error: %s", toolResp.Error)
|
||||
}
|
||||
|
||||
return &toolResp, nil
|
||||
}
|
||||
39
pkg/seqthink/observability/logger.go
Normal file
39
pkg/seqthink/observability/logger.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package observability
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// InitLogger initializes the global logger
|
||||
func InitLogger(level string) {
|
||||
// Set up zerolog with human-friendly console output
|
||||
output := zerolog.ConsoleWriter{
|
||||
Out: os.Stdout,
|
||||
TimeFormat: time.RFC3339,
|
||||
}
|
||||
|
||||
log.Logger = zerolog.New(output).
|
||||
With().
|
||||
Timestamp().
|
||||
Caller().
|
||||
Logger()
|
||||
|
||||
// Set log level
|
||||
switch strings.ToLower(level) {
|
||||
case "debug":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "info":
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
case "warn":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "error":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
default:
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
}
|
||||
}
|
||||
85
pkg/seqthink/observability/metrics.go
Normal file
85
pkg/seqthink/observability/metrics.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package observability
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// Metrics holds Prometheus metrics for the wrapper
|
||||
type Metrics struct {
|
||||
requestsTotal prometheus.Counter
|
||||
errorsTotal prometheus.Counter
|
||||
decryptFails prometheus.Counter
|
||||
encryptFails prometheus.Counter
|
||||
policyDenials prometheus.Counter
|
||||
requestDuration prometheus.Histogram
|
||||
}
|
||||
|
||||
// InitMetrics initializes Prometheus metrics
|
||||
func InitMetrics() *Metrics {
|
||||
return &Metrics{
|
||||
requestsTotal: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "seqthink_requests_total",
|
||||
Help: "Total number of requests received",
|
||||
}),
|
||||
errorsTotal: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "seqthink_errors_total",
|
||||
Help: "Total number of errors",
|
||||
}),
|
||||
decryptFails: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "seqthink_decrypt_failures_total",
|
||||
Help: "Total number of decryption failures",
|
||||
}),
|
||||
encryptFails: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "seqthink_encrypt_failures_total",
|
||||
Help: "Total number of encryption failures",
|
||||
}),
|
||||
policyDenials: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "seqthink_policy_denials_total",
|
||||
Help: "Total number of policy denials",
|
||||
}),
|
||||
requestDuration: promauto.NewHistogram(prometheus.HistogramOpts{
|
||||
Name: "seqthink_request_duration_seconds",
|
||||
Help: "Request duration in seconds",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// IncrementRequests increments the request counter
|
||||
func (m *Metrics) IncrementRequests() {
|
||||
m.requestsTotal.Inc()
|
||||
}
|
||||
|
||||
// IncrementErrors increments the error counter
|
||||
func (m *Metrics) IncrementErrors() {
|
||||
m.errorsTotal.Inc()
|
||||
}
|
||||
|
||||
// IncrementDecryptFails increments the decrypt failure counter
|
||||
func (m *Metrics) IncrementDecryptFails() {
|
||||
m.decryptFails.Inc()
|
||||
}
|
||||
|
||||
// IncrementEncryptFails increments the encrypt failure counter
|
||||
func (m *Metrics) IncrementEncryptFails() {
|
||||
m.encryptFails.Inc()
|
||||
}
|
||||
|
||||
// IncrementPolicyDenials increments the policy denial counter
|
||||
func (m *Metrics) IncrementPolicyDenials() {
|
||||
m.policyDenials.Inc()
|
||||
}
|
||||
|
||||
// ObserveRequestDuration records request duration
|
||||
func (m *Metrics) ObserveRequestDuration(seconds float64) {
|
||||
m.requestDuration.Observe(seconds)
|
||||
}
|
||||
|
||||
// Handler returns the Prometheus metrics HTTP handler
|
||||
func (m *Metrics) Handler() http.Handler {
|
||||
return promhttp.Handler()
|
||||
}
|
||||
150
pkg/seqthink/proxy/server.go
Normal file
150
pkg/seqthink/proxy/server.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"chorus/pkg/seqthink/mcpclient"
|
||||
"chorus/pkg/seqthink/observability"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// ServerConfig holds the proxy server configuration
|
||||
type ServerConfig struct {
|
||||
MCPClient *mcpclient.Client
|
||||
Metrics *observability.Metrics
|
||||
MaxBodyMB int
|
||||
AgeIdentPath string
|
||||
AgeRecipsPath string
|
||||
KachingJWKSURL string
|
||||
RequiredScope string
|
||||
}
|
||||
|
||||
// Server is the proxy server handling requests
|
||||
type Server struct {
|
||||
config ServerConfig
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
// NewServer creates a new proxy server
|
||||
func NewServer(cfg ServerConfig) (*Server, error) {
|
||||
s := &Server{
|
||||
config: cfg,
|
||||
router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
// Setup routes
|
||||
s.setupRoutes()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Handler returns the HTTP handler
|
||||
func (s *Server) Handler() http.Handler {
|
||||
return s.router
|
||||
}
|
||||
|
||||
// setupRoutes configures the HTTP routes
|
||||
func (s *Server) setupRoutes() {
|
||||
// Health checks
|
||||
s.router.HandleFunc("/health", s.handleHealth).Methods("GET")
|
||||
s.router.HandleFunc("/ready", s.handleReady).Methods("GET")
|
||||
|
||||
// MCP tool endpoint (plaintext for Beat 1)
|
||||
s.router.HandleFunc("/mcp/tool", s.handleToolCall).Methods("POST")
|
||||
|
||||
// SSE endpoint (placeholder for Beat 1)
|
||||
s.router.HandleFunc("/mcp/sse", s.handleSSE).Methods("GET")
|
||||
|
||||
// Metrics endpoint
|
||||
s.router.Handle("/metrics", s.config.Metrics.Handler())
|
||||
}
|
||||
|
||||
// handleHealth returns 200 OK if wrapper is running
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
// handleReady checks if MCP server is ready
|
||||
func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := s.config.MCPClient.Health(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("MCP server not ready")
|
||||
http.Error(w, "MCP server not ready", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("READY"))
|
||||
}
|
||||
|
||||
// handleToolCall proxies tool calls to MCP server (plaintext for Beat 1)
|
||||
func (s *Server) handleToolCall(w http.ResponseWriter, r *http.Request) {
|
||||
s.config.Metrics.IncrementRequests()
|
||||
startTime := time.Now()
|
||||
|
||||
// Limit request body size
|
||||
r.Body = http.MaxBytesReader(w, r.Body, int64(s.config.MaxBodyMB)*1024*1024)
|
||||
|
||||
// Read request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to read request body")
|
||||
s.config.Metrics.IncrementErrors()
|
||||
http.Error(w, "Failed to read request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse tool request
|
||||
var toolReq mcpclient.ToolRequest
|
||||
if err := json.Unmarshal(body, &toolReq); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to parse tool request")
|
||||
s.config.Metrics.IncrementErrors()
|
||||
http.Error(w, "Invalid request format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("tool", toolReq.Tool).
|
||||
Msg("Proxying tool call to MCP server")
|
||||
|
||||
// Call MCP server
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
toolResp, err := s.config.MCPClient.CallTool(ctx, &toolReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("MCP tool call failed")
|
||||
s.config.Metrics.IncrementErrors()
|
||||
http.Error(w, fmt.Sprintf("Tool call failed: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(toolResp); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to encode response")
|
||||
s.config.Metrics.IncrementErrors()
|
||||
return
|
||||
}
|
||||
|
||||
duration := time.Since(startTime)
|
||||
log.Info().
|
||||
Str("tool", toolReq.Tool).
|
||||
Dur("duration", duration).
|
||||
Msg("Tool call completed")
|
||||
}
|
||||
|
||||
// handleSSE is a placeholder for Server-Sent Events streaming (Beat 1)
|
||||
func (s *Server) handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||
log.Warn().Msg("SSE endpoint not yet implemented")
|
||||
http.Error(w, "SSE endpoint not implemented in Beat 1", http.StatusNotImplemented)
|
||||
}
|
||||
Reference in New Issue
Block a user