- Agent roles and coordination features - Chat API integration testing - New configuration and workspace management 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
292 lines
9.2 KiB
Go
292 lines
9.2 KiB
Go
package workspace
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/anthonyrawlins/bzzz/pkg/config"
|
|
)
|
|
|
|
// HCFSWorkspaceManager manages agent workspaces using HCFS as the backing store
|
|
type HCFSWorkspaceManager struct {
|
|
hcfsAPIURL string
|
|
mountPath string
|
|
client *http.Client
|
|
activeSpaces map[string]*HCFSWorkspace
|
|
}
|
|
|
|
// HCFSWorkspace represents an HCFS-backed workspace for an agent
|
|
type HCFSWorkspace struct {
|
|
ID string `json:"id"`
|
|
AgentID string `json:"agent_id"`
|
|
TaskID string `json:"task_id"`
|
|
HCFSPath string `json:"hcfs_path"`
|
|
LocalMount string `json:"local_mount"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
LastUsed time.Time `json:"last_used"`
|
|
}
|
|
|
|
// HCFSContextRequest represents a request to create context in HCFS
|
|
type HCFSContextRequest struct {
|
|
Path string `json:"path"`
|
|
Content string `json:"content"`
|
|
Summary string `json:"summary"`
|
|
Metadata map[string]interface{} `json:"metadata"`
|
|
}
|
|
|
|
// HCFSContextResponse represents the response from HCFS context creation
|
|
type HCFSContextResponse struct {
|
|
ID string `json:"id"`
|
|
Path string `json:"path"`
|
|
CreatedAt string `json:"created_at"`
|
|
Metadata map[string]interface{} `json:"metadata"`
|
|
}
|
|
|
|
// NewHCFSWorkspaceManager creates a new HCFS workspace manager
|
|
func NewHCFSWorkspaceManager(hcfsAPIURL string, mountPath string) *HCFSWorkspaceManager {
|
|
return &HCFSWorkspaceManager{
|
|
hcfsAPIURL: hcfsAPIURL,
|
|
mountPath: mountPath,
|
|
client: &http.Client{Timeout: 30 * time.Second},
|
|
activeSpaces: make(map[string]*HCFSWorkspace),
|
|
}
|
|
}
|
|
|
|
// CreateWorkspace creates a new HCFS-backed workspace for an agent
|
|
func (m *HCFSWorkspaceManager) CreateWorkspace(ctx context.Context, agentID, taskID string, agentConfig *config.AgentConfig) (*HCFSWorkspace, error) {
|
|
// Generate unique workspace ID
|
|
workspaceID := fmt.Sprintf("bzzz-workspace-%s-%d", agentID, time.Now().Unix())
|
|
|
|
// Create HCFS path for the workspace
|
|
hcfsPath := fmt.Sprintf("/agents/%s/workspaces/%s", agentID, workspaceID)
|
|
|
|
// Create workspace context in HCFS
|
|
contextReq := HCFSContextRequest{
|
|
Path: hcfsPath,
|
|
Content: fmt.Sprintf("Workspace for agent %s, task %s", agentID, taskID),
|
|
Summary: fmt.Sprintf("Agent workspace: %s", workspaceID),
|
|
Metadata: map[string]interface{}{
|
|
"agent_id": agentID,
|
|
"task_id": taskID,
|
|
"workspace_id": workspaceID,
|
|
"created_by": "bzzz_sandbox",
|
|
"workspace_type": "agent_sandbox",
|
|
},
|
|
}
|
|
|
|
contextID, err := m.createHCFSContext(ctx, contextReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create HCFS context: %w", err)
|
|
}
|
|
|
|
// Create local mount point using HCFS FUSE
|
|
localMount := filepath.Join(m.mountPath, workspaceID)
|
|
if err := m.createHCFSMount(ctx, hcfsPath, localMount); err != nil {
|
|
return nil, fmt.Errorf("failed to create HCFS mount: %w", err)
|
|
}
|
|
|
|
workspace := &HCFSWorkspace{
|
|
ID: contextID,
|
|
AgentID: agentID,
|
|
TaskID: taskID,
|
|
HCFSPath: hcfsPath,
|
|
LocalMount: localMount,
|
|
CreatedAt: time.Now(),
|
|
LastUsed: time.Now(),
|
|
}
|
|
|
|
// Track active workspace
|
|
m.activeSpaces[workspaceID] = workspace
|
|
|
|
fmt.Printf("✅ HCFS workspace created: %s -> %s\n", hcfsPath, localMount)
|
|
return workspace, nil
|
|
}
|
|
|
|
// DestroyWorkspace cleans up an HCFS workspace
|
|
func (m *HCFSWorkspaceManager) DestroyWorkspace(ctx context.Context, workspace *HCFSWorkspace) error {
|
|
if workspace == nil {
|
|
return nil
|
|
}
|
|
|
|
// Unmount HCFS FUSE mount
|
|
if err := m.unmountHCFS(workspace.LocalMount); err != nil {
|
|
fmt.Printf("⚠️ Warning: failed to unmount HCFS: %v\n", err)
|
|
}
|
|
|
|
// Update HCFS context with final state
|
|
if err := m.finalizeWorkspaceContext(ctx, workspace); err != nil {
|
|
fmt.Printf("⚠️ Warning: failed to finalize workspace context: %v\n", err)
|
|
}
|
|
|
|
// Remove from active workspaces
|
|
delete(m.activeSpaces, workspace.ID)
|
|
|
|
fmt.Printf("✅ HCFS workspace destroyed: %s\n", workspace.HCFSPath)
|
|
return nil
|
|
}
|
|
|
|
// GetWorkspacePath returns the local path for container mounting
|
|
func (m *HCFSWorkspaceManager) GetWorkspacePath(workspace *HCFSWorkspace) string {
|
|
if workspace == nil {
|
|
return ""
|
|
}
|
|
return workspace.LocalMount
|
|
}
|
|
|
|
// UpdateWorkspaceUsage updates the last used timestamp
|
|
func (m *HCFSWorkspaceManager) UpdateWorkspaceUsage(workspace *HCFSWorkspace) {
|
|
if workspace != nil {
|
|
workspace.LastUsed = time.Now()
|
|
}
|
|
}
|
|
|
|
// StoreWorkspaceArtifacts stores final workspace artifacts in HCFS
|
|
func (m *HCFSWorkspaceManager) StoreWorkspaceArtifacts(ctx context.Context, workspace *HCFSWorkspace, artifacts map[string]string) error {
|
|
if workspace == nil {
|
|
return fmt.Errorf("workspace is nil")
|
|
}
|
|
|
|
// Store each artifact as a separate context in HCFS
|
|
for artifactName, content := range artifacts {
|
|
artifactPath := fmt.Sprintf("%s/artifacts/%s", workspace.HCFSPath, artifactName)
|
|
|
|
artifactReq := HCFSContextRequest{
|
|
Path: artifactPath,
|
|
Content: content,
|
|
Summary: fmt.Sprintf("Artifact from workspace %s: %s", workspace.ID, artifactName),
|
|
Metadata: map[string]interface{}{
|
|
"workspace_id": workspace.ID,
|
|
"agent_id": workspace.AgentID,
|
|
"task_id": workspace.TaskID,
|
|
"artifact_name": artifactName,
|
|
"artifact_type": "workspace_output",
|
|
"created_by": "bzzz_sandbox",
|
|
},
|
|
}
|
|
|
|
_, err := m.createHCFSContext(ctx, artifactReq)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store artifact %s: %w", artifactName, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createHCFSContext creates a new context in HCFS
|
|
func (m *HCFSWorkspaceManager) createHCFSContext(ctx context.Context, req HCFSContextRequest) (string, error) {
|
|
reqBody, err := json.Marshal(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", m.hcfsAPIURL+"/contexts", strings.NewReader(string(reqBody)))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := m.client.Do(httpReq)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to make request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return "", fmt.Errorf("HCFS API returned status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var contextResp HCFSContextResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&contextResp); err != nil {
|
|
return "", fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
return contextResp.ID, nil
|
|
}
|
|
|
|
// createHCFSMount creates a FUSE mount for the HCFS path
|
|
func (m *HCFSWorkspaceManager) createHCFSMount(ctx context.Context, hcfsPath, localPath string) error {
|
|
// Ensure the mount directory exists
|
|
if err := os.MkdirAll(localPath, 0755); err != nil {
|
|
return fmt.Errorf("failed to create mount directory: %w", err)
|
|
}
|
|
|
|
// TODO: Implement HCFS FUSE mount
|
|
// For now, we'll create a placeholder directory structure
|
|
// In a full implementation, this would mount the HCFS path using FUSE
|
|
fmt.Printf("🔗 Creating HCFS FUSE mount: %s -> %s\n", hcfsPath, localPath)
|
|
|
|
// Create basic workspace structure
|
|
subdirs := []string{"src", "build", "output", "logs"}
|
|
for _, subdir := range subdirs {
|
|
if err := os.MkdirAll(filepath.Join(localPath, subdir), 0755); err != nil {
|
|
return fmt.Errorf("failed to create workspace subdir %s: %w", subdir, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// unmountHCFS unmounts the HCFS FUSE mount
|
|
func (m *HCFSWorkspaceManager) unmountHCFS(localPath string) error {
|
|
// TODO: Implement HCFS FUSE unmount
|
|
// For now, we'll just remove the directory
|
|
fmt.Printf("🔌 Unmounting HCFS: %s\n", localPath)
|
|
|
|
return os.RemoveAll(localPath)
|
|
}
|
|
|
|
// finalizeWorkspaceContext updates the workspace context with final metadata
|
|
func (m *HCFSWorkspaceManager) finalizeWorkspaceContext(ctx context.Context, workspace *HCFSWorkspace) error {
|
|
// Update workspace context with completion metadata
|
|
finalReq := HCFSContextRequest{
|
|
Path: workspace.HCFSPath + "/completion",
|
|
Content: fmt.Sprintf("Workspace %s completed at %s", workspace.ID, time.Now().Format(time.RFC3339)),
|
|
Summary: fmt.Sprintf("Completion record for workspace %s", workspace.ID),
|
|
Metadata: map[string]interface{}{
|
|
"workspace_id": workspace.ID,
|
|
"agent_id": workspace.AgentID,
|
|
"task_id": workspace.TaskID,
|
|
"completed_at": time.Now().Format(time.RFC3339),
|
|
"total_duration": time.Since(workspace.CreatedAt).Seconds(),
|
|
"record_type": "workspace_completion",
|
|
},
|
|
}
|
|
|
|
_, err := m.createHCFSContext(ctx, finalReq)
|
|
return err
|
|
}
|
|
|
|
// GetActiveWorkspaces returns all currently active workspaces
|
|
func (m *HCFSWorkspaceManager) GetActiveWorkspaces() map[string]*HCFSWorkspace {
|
|
return m.activeSpaces
|
|
}
|
|
|
|
// CleanupIdleWorkspaces removes workspaces that haven't been used recently
|
|
func (m *HCFSWorkspaceManager) CleanupIdleWorkspaces(ctx context.Context, maxIdleTime time.Duration) error {
|
|
now := time.Now()
|
|
var toCleanup []*HCFSWorkspace
|
|
|
|
for _, workspace := range m.activeSpaces {
|
|
if now.Sub(workspace.LastUsed) > maxIdleTime {
|
|
toCleanup = append(toCleanup, workspace)
|
|
}
|
|
}
|
|
|
|
for _, workspace := range toCleanup {
|
|
if err := m.DestroyWorkspace(ctx, workspace); err != nil {
|
|
fmt.Printf("⚠️ Failed to cleanup idle workspace %s: %v\n", workspace.ID, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
} |