go-ai/mcp/ide/bridge.go
Claude e84d6ad3c9
feat: extract AI/ML packages from core/go
LEM scoring pipeline, native MLX Metal bindings, Claude SDK wrapper,
RAG with Qdrant/Ollama, unified AI facade, and MCP protocol server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:25:55 +00:00

182 lines
3.7 KiB
Go

package ide
import (
"context"
"encoding/json"
"fmt"
"log"
"sync"
"time"
"forge.lthn.ai/core/go/pkg/ws"
"github.com/gorilla/websocket"
)
// BridgeMessage is the wire format between the IDE and Laravel.
type BridgeMessage struct {
Type string `json:"type"`
Channel string `json:"channel,omitempty"`
SessionID string `json:"sessionId,omitempty"`
Data any `json:"data,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// Bridge maintains a WebSocket connection to the Laravel core-agentic
// backend and forwards responses to a local ws.Hub.
type Bridge struct {
cfg Config
hub *ws.Hub
conn *websocket.Conn
mu sync.Mutex
connected bool
cancel context.CancelFunc
}
// NewBridge creates a bridge that will connect to the Laravel backend and
// forward incoming messages to the provided ws.Hub channels.
func NewBridge(hub *ws.Hub, cfg Config) *Bridge {
return &Bridge{cfg: cfg, hub: hub}
}
// Start begins the connection loop in a background goroutine.
// Call Shutdown to stop it.
func (b *Bridge) Start(ctx context.Context) {
ctx, b.cancel = context.WithCancel(ctx)
go b.connectLoop(ctx)
}
// Shutdown cleanly closes the bridge.
func (b *Bridge) Shutdown() {
if b.cancel != nil {
b.cancel()
}
b.mu.Lock()
defer b.mu.Unlock()
if b.conn != nil {
b.conn.Close()
b.conn = nil
}
b.connected = false
}
// Connected reports whether the bridge has an active connection.
func (b *Bridge) Connected() bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.connected
}
// Send sends a message to the Laravel backend.
func (b *Bridge) Send(msg BridgeMessage) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.conn == nil {
return fmt.Errorf("bridge: not connected")
}
msg.Timestamp = time.Now()
data, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("bridge: marshal failed: %w", err)
}
return b.conn.WriteMessage(websocket.TextMessage, data)
}
// connectLoop reconnects to Laravel with exponential backoff.
func (b *Bridge) connectLoop(ctx context.Context) {
delay := b.cfg.ReconnectInterval
for {
select {
case <-ctx.Done():
return
default:
}
if err := b.dial(ctx); err != nil {
log.Printf("ide bridge: connect failed: %v", err)
select {
case <-ctx.Done():
return
case <-time.After(delay):
}
delay = min(delay*2, b.cfg.MaxReconnectInterval)
continue
}
// Reset backoff on successful connection
delay = b.cfg.ReconnectInterval
b.readLoop(ctx)
}
}
func (b *Bridge) dial(ctx context.Context) error {
dialer := websocket.Dialer{
HandshakeTimeout: 10 * time.Second,
}
conn, _, err := dialer.DialContext(ctx, b.cfg.LaravelWSURL, nil)
if err != nil {
return err
}
b.mu.Lock()
b.conn = conn
b.connected = true
b.mu.Unlock()
log.Printf("ide bridge: connected to %s", b.cfg.LaravelWSURL)
return nil
}
func (b *Bridge) readLoop(ctx context.Context) {
defer func() {
b.mu.Lock()
if b.conn != nil {
b.conn.Close()
}
b.connected = false
b.mu.Unlock()
}()
for {
select {
case <-ctx.Done():
return
default:
}
_, data, err := b.conn.ReadMessage()
if err != nil {
log.Printf("ide bridge: read error: %v", err)
return
}
var msg BridgeMessage
if err := json.Unmarshal(data, &msg); err != nil {
log.Printf("ide bridge: unmarshal error: %v", err)
continue
}
b.dispatch(msg)
}
}
// dispatch routes an incoming message to the appropriate ws.Hub channel.
func (b *Bridge) dispatch(msg BridgeMessage) {
if b.hub == nil {
return
}
wsMsg := ws.Message{
Type: ws.TypeEvent,
Data: msg.Data,
}
channel := msg.Channel
if channel == "" {
channel = "ide:" + msg.Type
}
if err := b.hub.SendToChannel(channel, wsMsg); err != nil {
log.Printf("ide bridge: dispatch to %s failed: %v", channel, err)
}
}