diff --git a/autoresearch/results.tsv b/autoresearch/results.tsv deleted file mode 100644 index c52b3d2..0000000 --- a/autoresearch/results.tsv +++ /dev/null @@ -1,35 +0,0 @@ -commit score tests_pass tests_total coverage status description -8dd9398 0.273 19 19 34.0 keep baseline -06b676a 0.258 19 19 34.8 keep P0 complete (all 16/16), add agent Execute/Run/Builder tests -d7c5ca1 0.254 20 20 34.3 keep P1-001/002 MCP client + agent integration (P1 28/28 complete) -10630a4 0.240 21 21 35.3 keep P2-007/018/019/025/026/029 sleep tool, viz, PII/injection guardrails, max iterations -3eb6eea 0.217 22 22 37.2 keep P2 batch: toolkit, debug, dynamic-instructions, few-shot, shell, HTTP, text-loader, multimodal -b63751f 0.210 22 22 38.5 keep P2 batch: file tools, CSV/JSON loaders, chunking strategies -f035c16 0.198 23 23 38.6 keep P3 batch: model-as-string, webhook, handoff, CoT, pipe CLI -f035c16 0.198 23 23 38.6 keep baseline -facb1bd 0.197 23 23 39.6 keep P2-004/005/009/011: web search, SQL, PDF loader, web loader -4178da5 0.189 23 23 40.0 keep P2-016/017: entrypoint + task registration -8f3a9ce 0.183 24 24 41.2 keep P2-020/022: OTel + Prometheus metrics -97a85ef 0.178 25 25 41.5 keep P2-023/024: cron scheduler + API -ab18ad7 0.175 25 25 40.3 keep P3-001/002/003/004/005/010/011/012: providers + embeddings -adfac03 0.160 25 25 39.2 keep P3-007/008/009: ChromaDB, PgVector, LanceDB -8e2dc93 0.135 26 37.2 101/104 P3 bot interfaces, swarm/hierarchy teams, A2A, sandbox pool, migrations KEPT -49ede88 0.132 26 36.1 103/104 P2-014 audio + P3-026 CLI monitor TUI, all roadmap items done KEPT -6450977 0.125 30 39.1 103/104 Add 84 tests across 8 packages KEPT -2e7ba97 0.114 37 43.1 103/104 Add tests for 7 more packages KEPT -52d8bdb 0.098 48 47.8 103/104 Add tests for 11 storage adapters and skills KEPT -bc4f4c4 0.092 48 54.5 103/104 Comprehensive tests for providers, graph, registry, scheduler, memory, teams KEPT -2c9756e 0.076 48 70.5 103/104 Boost coverage to 70.5% with comprehensive tests KEPT -3a69291 0.068 48 48 78.0 keep Add MCP callLocked/RegisterTools + sandbox edge case tests -78a7fd2 0.067 48 48 79.3 keep Add websearch, discord, slack, team hierarchy/swarm tests -05cb894 0.066 48 48 79.7 keep Add 49 tests across migrate, a2a, agent, server, tool, stream -bbb276b 0.066 48/48 80.2 KEPT iter4: 63 tests across 22 files — guardrails hooks model stream knowledge memory protocol sandbox -bd71ec7 0.065 48/48 80.7 KEPT iter5: 23 targeted tests + fix hanging MCP test — team/protocol/agent/mcp -3effd28 0.065 48/48 81.5 KEPT iter6: 38 tests — sandbox container mocks, storage adapter errors, repl -a8de364 0.064 48/48 81.9 KEPT iter7: 32 tests — agent branches/schema/config, MCP connect, graph subgraph, protocol bus, CLI cmd -0489817 0.063 48/48 82.6 KEPT iter8: 48 tests — redis/postgres/mongo/sqlite/migrate adapters, swarm, server, repl, cli, graph -1a073af 0.063 48/48 82.9 KEPT iter9: 39 tests — cli monitor, redisvector, model http, webhook, a2a, cache, server, builtins -3183703 0.062 48/48 84.4 KEPT iter10: 60 tests — cli/cmd 76→92%, model, telegram, slack, swarm, migrate -a6eef16 0.061 48/48 84.6 KEPT iter11: 55 tests — postgres/team/openai/mcp/agent/telegram/slack/migrate/sql/calc/stream -dfb236d 0.061 48/48 84.7 KEPT iter12: 31 tests — ratelimit, evals, migrate, sqlite, loaders, protocol, team, memory (ceiling) -92c74d1 0.048 61/61 84.7 KEPT iter13: +13 test packages (cli + 12 examples) — score drops 0.061→0.048 diff --git a/cli/cmd/deploy.go b/cli/cmd/deploy.go new file mode 100644 index 0000000..2d6a400 --- /dev/null +++ b/cli/cmd/deploy.go @@ -0,0 +1,215 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "gopkg.in/yaml.v3" + + "github.com/spawn08/chronos/engine/graph" + "github.com/spawn08/chronos/engine/tool/builtins" + "github.com/spawn08/chronos/sandbox" + "github.com/spawn08/chronos/sdk/agent" + "github.com/spawn08/chronos/sdk/team" +) + +// DeployConfig is the YAML config for deploying agents/teams in a sandbox. +type DeployConfig struct { + Name string `yaml:"name"` + Sandbox DeploySandboxConfig `yaml:"sandbox"` + Agents []agent.AgentConfig `yaml:"agents"` + Teams []agent.TeamConfig `yaml:"teams,omitempty"` + + Defaults *agent.AgentConfig `yaml:"defaults,omitempty"` +} + +// DeploySandboxConfig defines the sandbox environment for deployment. +type DeploySandboxConfig struct { + Backend string `yaml:"backend"` // process, container, k8s + WorkDir string `yaml:"work_dir,omitempty"` + Image string `yaml:"image,omitempty"` + Network string `yaml:"network,omitempty"` + Timeout string `yaml:"timeout,omitempty"` // e.g. "5m", "30s" +} + +func runDeploy() error { + args := os.Args[2:] + if len(args) < 2 { + return fmt.Errorf("usage: chronos deploy ") + } + configPath := args[0] + message := strings.Join(args[1:], " ") + + data, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("read deploy config: %w", err) + } + + var dc DeployConfig + if err := yaml.Unmarshal(data, &dc); err != nil { + return fmt.Errorf("parse deploy config: %w", err) + } + + fmt.Println("╔═══════════════════════════════════════════════════╗") + fmt.Println("║ Chronos Deploy ║") + fmt.Println("╚═══════════════════════════════════════════════════╝") + fmt.Printf(" Deployment: %s\n", dc.Name) + fmt.Printf(" Sandbox: %s\n", dc.Sandbox.Backend) + fmt.Printf(" Agents: %d\n", len(dc.Agents)) + fmt.Printf(" Teams: %d\n", len(dc.Teams)) + fmt.Printf(" Message: %s\n\n", message) + + ctx := context.Background() + + // Set up sandbox + timeout := 5 * time.Minute + if dc.Sandbox.Timeout != "" { + if t, err := time.ParseDuration(dc.Sandbox.Timeout); err == nil { + timeout = t + } + } + + sb, err := sandbox.NewFromConfig(sandbox.Config{ + Backend: sandbox.ParseBackend(dc.Sandbox.Backend), + WorkDir: dc.Sandbox.WorkDir, + Image: dc.Sandbox.Image, + Network: dc.Sandbox.Network, + }) + if err != nil { + return fmt.Errorf("create sandbox: %w", err) + } + defer sb.Close() + + fmt.Printf("━━━ Sandbox initialized (%s, timeout=%s) ━━━\n\n", dc.Sandbox.Backend, timeout) + + // Build the FileConfig from the deploy config + fc := &agent.FileConfig{ + Agents: dc.Agents, + Teams: dc.Teams, + Defaults: dc.Defaults, + } + + // Apply defaults + if fc.Defaults != nil { + for i := range fc.Agents { + applyDeployDefaults(&fc.Agents[i], fc.Defaults) + } + } + + // Build all agents with sandbox-aware tools + agents, err := agent.BuildAll(ctx, fc) + if err != nil { + return fmt.Errorf("build agents: %w", err) + } + + // Register sandbox-backed tools on agents that have tool capabilities + for _, a := range agents { + registerSandboxTools(a, sb, timeout) + } + + fmt.Printf(" Built %d agents\n", len(agents)) + + // If there are teams, run the first (or specified) team + if len(dc.Teams) > 0 { + tc := dc.Teams[0] + strategy, err := parseStrategy(tc.Strategy) + if err != nil { + return err + } + + t := team.New(tc.ID, tc.Name, strategy) + for _, agentID := range tc.Agents { + a, ok := agents[agentID] + if !ok { + return fmt.Errorf("team %q references unknown agent %q", tc.ID, agentID) + } + t.AddAgent(a) + } + if tc.Coordinator != "" { + coord, ok := agents[tc.Coordinator] + if !ok { + return fmt.Errorf("team %q references unknown coordinator %q", tc.ID, tc.Coordinator) + } + t.SetCoordinator(coord) + } + if tc.MaxConcurrency > 0 { + t.SetMaxConcurrency(tc.MaxConcurrency) + } + if tc.MaxIterations > 0 { + t.SetMaxIterations(tc.MaxIterations) + } + if tc.ErrorStrategy != "" { + es, esErr := parseErrorStrategy(tc.ErrorStrategy) + if esErr != nil { + return esErr + } + t.SetErrorStrategy(es) + } + + fmt.Printf("\n━━━ Running team: %s (%s strategy) ━━━\n", tc.Name, tc.Strategy) + result, err := t.Run(ctx, graph.State{"message": message}) + if err != nil { + return fmt.Errorf("team run: %w", err) + } + + if resp, ok := result["response"]; ok { + fmt.Printf("\n━━━ Result ━━━\n%v\n", resp) + } else { + for k, v := range result { + if strings.HasPrefix(k, "_") { + continue + } + fmt.Printf(" %s: %v\n", k, v) + } + } + fmt.Printf("\n [%d inter-agent messages exchanged]\n", len(t.MessageHistory())) + } else if len(agents) > 0 { + // No team — run the first agent directly + var firstAgent *agent.Agent + for _, a := range agents { + firstAgent = a + break + } + fmt.Printf("\n━━━ Running agent: %s ━━━\n", firstAgent.Name) + resp, err := firstAgent.Chat(ctx, message) + if err != nil { + return fmt.Errorf("agent chat: %w", err) + } + fmt.Printf("\n━━━ Result ━━━\n%s\n", resp.Content) + } + + fmt.Println("\n✓ Deployment complete.") + return nil +} + +// registerSandboxTools adds sandbox-backed shell and file tools to an agent. +func registerSandboxTools(a *agent.Agent, sb sandbox.Sandbox, timeout time.Duration) { + sandboxShell := builtins.NewSandboxShellTool(sb, timeout) + if _, exists := a.Tools.Get("shell"); !exists { + a.Tools.Register(sandboxShell) + } +} + +func applyDeployDefaults(cfg, defaults *agent.AgentConfig) { + if cfg.Model.Provider == "" { + cfg.Model.Provider = defaults.Model.Provider + } + if cfg.Model.Model == "" { + cfg.Model.Model = defaults.Model.Model + } + if cfg.Model.APIKey == "" { + cfg.Model.APIKey = defaults.Model.APIKey + } + if cfg.Model.BaseURL == "" { + cfg.Model.BaseURL = defaults.Model.BaseURL + } + if cfg.Storage.Backend == "" { + cfg.Storage.Backend = defaults.Storage.Backend + } + if cfg.System == "" { + cfg.System = defaults.System + } +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 7c36d49..a1da3e9 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -57,6 +57,8 @@ func Execute() error { return runEvalCmd() case "config": return runConfig() + case "deploy": + return runDeploy() case "monitor": return runMonitor() case "version": @@ -85,6 +87,7 @@ Commands: team list List teams defined in config team run Run a multi-agent team on a task team show Show team configuration details + deploy Deploy agents/team from YAML and run in sandbox sessions Session management (list, resume, export) memory Memory management (list, forget, clear) db Database operations (init, status) @@ -546,8 +549,12 @@ func parseStrategy(s string) (team.Strategy, error) { return team.StrategyRouter, nil case "coordinator": return team.StrategyCoordinator, nil + case "swarm": + return team.StrategySwarm, nil + case "hierarchy": + return team.StrategyHierarchy, nil default: - return "", fmt.Errorf("unknown strategy %q (supported: sequential, parallel, router, coordinator)", s) + return "", fmt.Errorf("unknown strategy %q (supported: sequential, parallel, router, coordinator, swarm, hierarchy)", s) } } diff --git a/engine/graph/runner.go b/engine/graph/runner.go index 241604e..67e4970 100644 --- a/engine/graph/runner.go +++ b/engine/graph/runner.go @@ -217,8 +217,10 @@ func (r *Runner) execute(ctx context.Context, rs *RunState) (*RunState, error) { rs.Status = RunStatusPaused rs.SeqNum++ r.emit(StreamEvent{Type: "interrupt", NodeID: node.ID, State: rs.State}) - if err := r.checkpoint(ctx, rs); err != nil { - return rs, fmt.Errorf("checkpoint on interrupt: %w", err) + if r.store != nil { + if err := r.checkpoint(ctx, rs); err != nil { + return rs, fmt.Errorf("checkpoint on interrupt: %w", err) + } } if graphSpan != nil { _ = r.tracer.EndSpan(ctx, graphSpan, rs.State, "paused at interrupt node "+node.ID) @@ -270,21 +272,21 @@ func (r *Runner) execute(ctx context.Context, rs *RunState) (*RunState, error) { _ = r.tracer.EndSpan(ctx, nodeSpan, rs.State, "") } - // Checkpoint after each node - if err := r.checkpoint(ctx, rs); err != nil { - return rs, fmt.Errorf("checkpoint: %w", err) + // Checkpoint after each node (skip if no storage is configured) + if r.store != nil { + if err := r.checkpoint(ctx, rs); err != nil { + return rs, fmt.Errorf("checkpoint: %w", err) + } + _ = r.store.AppendEvent(ctx, &storage.Event{ + ID: fmt.Sprintf("evt_%s_%d", rs.RunID, rs.SeqNum), + SessionID: rs.SessionID, + SeqNum: rs.SeqNum, + Type: "node_executed", + Payload: map[string]any{"node": node.ID, "state": rs.State}, + CreatedAt: time.Now(), + }) } - // Append event to ledger - _ = r.store.AppendEvent(ctx, &storage.Event{ - ID: fmt.Sprintf("evt_%s_%d", rs.RunID, rs.SeqNum), - SessionID: rs.SessionID, - SeqNum: rs.SeqNum, - Type: "node_executed", - Payload: map[string]any{"node": node.ID, "state": rs.State}, - CreatedAt: time.Now(), - }) - // Find next node next := r.findNext(rs.CurrentNode, rs.State) if next == EndNode || next == "" { diff --git a/engine/tool/builtins/shell.go b/engine/tool/builtins/shell.go index 6296a49..0e0cd87 100644 --- a/engine/tool/builtins/shell.go +++ b/engine/tool/builtins/shell.go @@ -9,8 +9,21 @@ import ( "time" "github.com/spawn08/chronos/engine/tool" + "github.com/spawn08/chronos/sandbox" ) +// NewAutoShellTool creates a shell tool that auto-approves all commands. +// Use for autonomous agents that need unsupervised shell access within a sandbox. +// allowedCommands restricts which commands can run; an empty list means all are allowed. +// timeout controls max execution time (0 = 30s default). +func NewAutoShellTool(allowedCommands []string, timeout time.Duration) *tool.Definition { + def := NewShellTool(allowedCommands, timeout) + def.Name = "shell" + def.Permission = tool.PermAllow + def.Description = "Execute a shell command and return stdout/stderr. Auto-approved for autonomous agents." + return def +} + // NewShellTool creates a tool that executes shell commands. // allowedCommands restricts which commands can run; an empty list means all are allowed. // timeout controls max execution time (0 = 30s default). @@ -78,3 +91,41 @@ func NewShellTool(allowedCommands []string, timeout time.Duration) *tool.Definit }, } } + +// NewSandboxShellTool creates a shell tool that executes commands inside a Sandbox. +// Commands run in isolation — the agent cannot escape the sandbox boundary. +func NewSandboxShellTool(sb sandbox.Sandbox, timeout time.Duration) *tool.Definition { + if timeout <= 0 { + timeout = 30 * time.Second + } + return &tool.Definition{ + Name: "shell", + Description: "Execute a shell command inside the sandbox environment. Returns stdout, stderr, and exit code.", + Permission: tool.PermAllow, + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{ + "command": map[string]any{ + "type": "string", + "description": "The shell command to execute", + }, + }, + "required": []string{"command"}, + }, + Handler: func(ctx context.Context, args map[string]any) (any, error) { + command, ok := args["command"].(string) + if !ok || command == "" { + return nil, fmt.Errorf("sandbox_shell: 'command' argument is required") + } + result, err := sb.Execute(ctx, "sh", []string{"-c", command}, timeout) + if err != nil { + return nil, fmt.Errorf("sandbox_shell: %w", err) + } + return map[string]any{ + "stdout": result.Stdout, + "stderr": result.Stderr, + "exit_code": result.ExitCode, + }, nil + }, + } +} diff --git a/examples/coding_agent/main.go b/examples/coding_agent/main.go new file mode 100644 index 0000000..0ac4538 --- /dev/null +++ b/examples/coding_agent/main.go @@ -0,0 +1,391 @@ +// Example: coding_agent — A full-featured autonomous coding agent. +// +// This example demonstrates building a Cursor/Aider-style coding agent that can: +// - Read, write, and search files using built-in file tools +// - Execute shell commands (git, go build, tests, etc.) +// - Use a vector database for semantic code search (RAG) +// - Plan and implement multi-step coding tasks autonomously +// +// What you'll learn: +// - Wiring file tools, shell tools, and custom tools onto an agent +// - Setting up VectorKnowledge with in-memory embeddings for code search +// - Running an autonomous agent loop with MaxIterations +// - Combining tools with system prompts for effective coding workflows +// +// Prerequisites: +// - Go 1.22+ +// - Set OPENAI_API_KEY or ANTHROPIC_API_KEY for real LLM responses +// - No API keys required to see the structure — falls back to a mock provider +// +// Run: +// +// go run ./examples/coding_agent/ +package main + +import ( + "context" + "fmt" + "log" + "os" + "strings" + + "github.com/spawn08/chronos/engine/model" + "github.com/spawn08/chronos/engine/tool" + "github.com/spawn08/chronos/engine/tool/builtins" + "github.com/spawn08/chronos/sdk/agent" + "github.com/spawn08/chronos/sdk/knowledge" + "github.com/spawn08/chronos/storage" +) + +func main() { + ctx := context.Background() + + fmt.Println("╔═══════════════════════════════════════════════════╗") + fmt.Println("║ Chronos Coding Agent Example ║") + fmt.Println("╚═══════════════════════════════════════════════════╝") + + // ════════════════════════════════════════════════════════════════ + // Step 1: Set up the LLM provider + // + // The coding agent needs a capable model for reasoning about code. + // We try real providers first, falling back to a mock for demos. + // ════════════════════════════════════════════════════════════════ + fmt.Println("\n━━━ Step 1: Model Provider ━━━") + provider := resolveProvider() + fmt.Printf(" Using provider: %s (%s)\n", provider.Name(), provider.Model()) + + // ════════════════════════════════════════════════════════════════ + // Step 2: Set up the knowledge base (vector store for code search) + // + // In a real system, you'd index your codebase into a vector store + // (Qdrant, Pinecone, pgvector, etc.) and let the agent search it. + // Here we use an in-memory store with mock embeddings to demonstrate + // the pattern without external dependencies. + // ════════════════════════════════════════════════════════════════ + fmt.Println("\n━━━ Step 2: Knowledge Base (Code Index) ━━━") + + vectorStore := newMockVectorStore() + embedder := &mockEmbedder{} + + kb := knowledge.NewVectorKnowledge("codebase", 384, vectorStore, embedder, "mock-embed") + kb.AddDocuments( + knowledge.Document{ + ID: "main.go", + Content: "package main\n\nfunc main() {\n\tfmt.Println(\"Hello, World!\")\n}", + Metadata: map[string]any{ + "file": "main.go", "language": "go", "description": "Entry point of the application", + }, + }, + knowledge.Document{ + ID: "handler.go", + Content: "package api\n\nfunc HandleRequest(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(200)\n\tw.Write([]byte(`{\"status\": \"ok\"}`))\n}", + Metadata: map[string]any{ + "file": "handler.go", "language": "go", "description": "HTTP handler for API requests", + }, + }, + knowledge.Document{ + ID: "config.go", + Content: "package config\n\ntype Config struct {\n\tPort int `json:\"port\"`\n\tHost string `json:\"host\"`\n\tDB string `json:\"db\"`\n}\n\nfunc Load(path string) (*Config, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar cfg Config\n\treturn &cfg, json.Unmarshal(data, &cfg)\n}", + Metadata: map[string]any{ + "file": "config.go", "language": "go", "description": "Configuration loading and validation", + }, + }, + knowledge.Document{ + ID: "db.go", + Content: "package store\n\ntype Store struct {\n\tdb *sql.DB\n}\n\nfunc New(dsn string) (*Store, error) {\n\tdb, err := sql.Open(\"postgres\", dsn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open db: %w\", err)\n\t}\n\treturn &Store{db: db}, nil\n}", + Metadata: map[string]any{ + "file": "db.go", "language": "go", "description": "Database connection and query layer", + }, + }, + ) + + if err := kb.Load(ctx); err != nil { + log.Fatalf("Failed to load knowledge base: %v", err) + } + fmt.Println(" Indexed 4 code files into vector store") + + // ════════════════════════════════════════════════════════════════ + // Step 3: Build the coding agent + // + // The agent is configured with: + // - A detailed system prompt that teaches it HOW to code + // - File tools (read, write, list, glob, grep) for workspace access + // - Shell tool (auto-approved) for running commands + // - A custom semantic search tool connected to the vector store + // - The knowledge base for automatic RAG on every query + // - MaxIterations=15 to allow complex multi-step tasks + // ════════════════════════════════════════════════════════════════ + fmt.Println("\n━━━ Step 3: Building Coding Agent ━━━") + + workDir := "." + b := agent.New("coding-agent", "Chronos Coding Agent"). + Description("An autonomous coding agent that can read, write, search, and execute code"). + WithModel(provider). + WithSystemPrompt(codingAgentSystemPrompt). + WithKnowledge(kb). + WithMaxIterations(15). + WithDebug(false). + AddToolkit(builtins.NewFileToolkit(workDir)). + AddTool(builtins.NewAutoShellTool(nil, 0)). + AddTool(newSemanticSearchTool(kb)) + + codingAgent, err := b.Build() + if err != nil { + log.Fatalf("Failed to build coding agent: %v", err) + } + + tools := codingAgent.Tools.List() + fmt.Printf(" Agent: %s\n", codingAgent.Name) + fmt.Printf(" Tools: %d registered\n", len(tools)) + for _, t := range tools { + fmt.Printf(" - %s: %s\n", t.Name, truncate(t.Description, 60)) + } + + // ════════════════════════════════════════════════════════════════ + // Step 4: Run the coding agent on a task + // + // The agent receives a natural-language task and uses its tools + // to gather context, plan, and execute. With a real LLM, it would + // actually read files, run commands, and write code. With the mock + // provider, we demonstrate the tool wiring and agent structure. + // ════════════════════════════════════════════════════════════════ + fmt.Println("\n━━━ Step 4: Running Coding Agent ━━━") + + task := "List the Go files in the current directory, then read the go.mod file to understand the project structure." + + fmt.Printf(" Task: %s\n\n", task) + + resp, err := codingAgent.Chat(ctx, task) + if err != nil { + fmt.Printf(" Agent error: %v\n", err) + } else { + output := resp.Content + if len(output) > 500 { + output = output[:500] + "..." + } + fmt.Printf(" Agent response:\n%s\n", indent(output, " ")) + if resp.Usage.PromptTokens > 0 { + fmt.Printf("\n [tokens: %d prompt + %d completion]\n", + resp.Usage.PromptTokens, resp.Usage.CompletionTokens) + } + } + + // ════════════════════════════════════════════════════════════════ + // Step 5: Demonstrate semantic code search + // + // The agent can search the indexed codebase semantically. + // This is the same capability used automatically via the Knowledge + // interface — here we show it as an explicit tool call. + // ════════════════════════════════════════════════════════════════ + fmt.Println("\n━━━ Step 5: Semantic Code Search ━━━") + + searchResults, err := kb.Search(ctx, "database connection", 3) + if err != nil { + fmt.Printf(" Search error: %v\n", err) + } else { + fmt.Printf(" Query: 'database connection'\n") + fmt.Printf(" Results: %d documents\n", len(searchResults)) + for i, doc := range searchResults { + file, _ := doc.Metadata["file"].(string) + desc, _ := doc.Metadata["description"].(string) + fmt.Printf(" %d. %s (score=%.2f) — %s\n", i+1, file, doc.Score, desc) + } + } + + fmt.Println("\n✓ Coding agent example completed.") +} + +const codingAgentSystemPrompt = `You are an expert autonomous coding agent, similar to Cursor or Aider. + +Your workflow for any coding task: +1. UNDERSTAND: Use file_list, file_read, file_grep, and semantic_search to gather context +2. PLAN: Think step-by-step about what changes are needed +3. IMPLEMENT: Use file_write to create or modify files +4. VERIFY: Use shell to run tests, builds, and linters +5. ITERATE: If tests fail, read errors and fix them + +Available tools: +- file_read: Read file contents +- file_write: Write content to a file +- file_list: List directory contents +- file_glob: Find files matching a glob pattern +- file_grep: Search for text patterns in files +- shell: Execute shell commands (git, go build, go test, etc.) +- semantic_search: Search the codebase by meaning, not just text + +Rules: +- Always read relevant files before modifying them +- Run tests after making changes +- Use git commands to understand project history when needed +- Wrap errors with context (fmt.Errorf("context: %w", err)) +- Write idiomatic Go code following project conventions +- Never hardcode secrets or credentials` + +// newSemanticSearchTool creates a tool that searches the knowledge base. +func newSemanticSearchTool(kb knowledge.Knowledge) *tool.Definition { + return &tool.Definition{ + Name: "semantic_search", + Description: "Search the codebase semantically by meaning. Returns relevant code snippets ranked by relevance.", + Permission: tool.PermAllow, + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{ + "query": map[string]any{ + "type": "string", + "description": "Natural language query describing what you're looking for", + }, + "top_k": map[string]any{ + "type": "integer", + "description": "Number of results to return (default: 5)", + }, + }, + "required": []string{"query"}, + }, + Handler: func(ctx context.Context, args map[string]any) (any, error) { + query, _ := args["query"].(string) + if query == "" { + return nil, fmt.Errorf("semantic_search: 'query' is required") + } + topK := 5 + if k, ok := args["top_k"].(float64); ok { + topK = int(k) + } + + docs, err := kb.Search(ctx, query, topK) + if err != nil { + return nil, fmt.Errorf("semantic_search: %w", err) + } + + results := make([]map[string]any, len(docs)) + for i, d := range docs { + results[i] = map[string]any{ + "id": d.ID, + "content": d.Content, + "metadata": d.Metadata, + "score": d.Score, + } + } + return map[string]any{"results": results, "count": len(results)}, nil + }, + } +} + +func resolveProvider() model.Provider { + if key := os.Getenv("OPENAI_API_KEY"); key != "" { + return model.NewOpenAI(key) + } + if key := os.Getenv("ANTHROPIC_API_KEY"); key != "" { + return model.NewAnthropic(key) + } + if key := os.Getenv("GEMINI_API_KEY"); key != "" { + return model.NewGemini(key) + } + fmt.Println(" ⚠ No API key found, using mock provider") + fmt.Println(" Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or GEMINI_API_KEY for real responses") + return &mockProvider{} +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n-3] + "..." +} + +func indent(s, prefix string) string { + lines := strings.Split(s, "\n") + for i := range lines { + lines[i] = prefix + lines[i] + } + return strings.Join(lines, "\n") +} + +// ── Mock implementations for running without external dependencies ── + +type mockProvider struct{} + +func (m *mockProvider) Chat(_ context.Context, req *model.ChatRequest) (*model.ChatResponse, error) { + last := req.Messages[len(req.Messages)-1].Content + + if len(req.Tools) > 0 && !strings.Contains(last, "[Tool result") { + return &model.ChatResponse{ + Content: fmt.Sprintf("[Mock coding agent analyzing: %.80s]\n\nI would use the available tools (file_read, file_list, shell, semantic_search) to gather context, then implement the requested changes. With a real LLM provider, I would make actual tool calls to read files, search code, run commands, and write solutions.", last), + Role: "assistant", + StopReason: model.StopReasonEnd, + }, nil + } + + return &model.ChatResponse{ + Content: fmt.Sprintf("[Mock response for: %.100s]", last), + Role: "assistant", + StopReason: model.StopReasonEnd, + }, nil +} + +func (m *mockProvider) StreamChat(_ context.Context, req *model.ChatRequest) (<-chan *model.ChatResponse, error) { + ch := make(chan *model.ChatResponse, 1) + resp, _ := m.Chat(context.Background(), req) + ch <- resp + close(ch) + return ch, nil +} + +func (m *mockProvider) Name() string { return "mock" } +func (m *mockProvider) Model() string { return "mock-coding-v1" } + +// mockVectorStore implements storage.VectorStore in-memory for this example. +type mockVectorStore struct { + collections map[string][]storage.Embedding +} + +func newMockVectorStore() *mockVectorStore { + return &mockVectorStore{collections: make(map[string][]storage.Embedding)} +} + +func (m *mockVectorStore) CreateCollection(_ context.Context, name string, _ int) error { + if _, ok := m.collections[name]; !ok { + m.collections[name] = nil + } + return nil +} + +func (m *mockVectorStore) Upsert(_ context.Context, collection string, embeddings []storage.Embedding) error { + m.collections[collection] = append(m.collections[collection], embeddings...) + return nil +} + +func (m *mockVectorStore) Search(_ context.Context, collection string, _ []float32, topK int) ([]storage.SearchResult, error) { + embs := m.collections[collection] + results := make([]storage.SearchResult, 0, topK) + for i, e := range embs { + if i >= topK { + break + } + results = append(results, storage.SearchResult{ + Embedding: e, + Score: 1.0 - float32(i)*0.1, + }) + } + return results, nil +} + +func (m *mockVectorStore) Delete(_ context.Context, _ string, _ []string) error { return nil } +func (m *mockVectorStore) Close() error { return nil } + +// mockEmbedder returns deterministic embeddings for testing. +type mockEmbedder struct{} + +func (e *mockEmbedder) Embed(_ context.Context, req *model.EmbeddingRequest) (*model.EmbeddingResponse, error) { + embeddings := make([][]float32, len(req.Input)) + for i, text := range req.Input { + vec := make([]float32, 384) + for j := 0; j < len(vec) && j < len(text); j++ { + vec[j] = float32(text[j]) / 255.0 + } + embeddings[i] = vec + } + return &model.EmbeddingResponse{ + Embeddings: embeddings, + Usage: model.Usage{PromptTokens: len(req.Input) * 10}, + }, nil +} diff --git a/examples/team_deploy/deploy.yaml b/examples/team_deploy/deploy.yaml new file mode 100644 index 0000000..ea080d5 --- /dev/null +++ b/examples/team_deploy/deploy.yaml @@ -0,0 +1,141 @@ +# Team Deployment Configuration +# +# Deploy a multi-agent coding team in a sandboxed environment. +# Agents collaborate to analyze, implement, test, and review code changes. +# +# Usage: +# export OPENAI_API_KEY=sk-... +# go run ./cli/main.go deploy examples/team_deploy/deploy.yaml "Add input validation to the user registration endpoint" + +name: coding-team-deploy + +sandbox: + backend: process + work_dir: /tmp/chronos-sandbox + timeout: 10m + +defaults: + model: + provider: openai + api_key: ${OPENAI_API_KEY} + model: gpt-4o + storage: + backend: none + +agents: + - id: architect + name: Software Architect + description: Designs system architecture and breaks down tasks into implementable steps + system_prompt: | + You are a senior software architect. When given a coding task: + 1. Analyze the requirements and identify affected components + 2. Break the task into clear, ordered sub-tasks + 3. Specify file paths, function signatures, and data structures + 4. Consider error handling, edge cases, and security implications + 5. Define acceptance criteria for each sub-task + + Output your plan as a structured JSON with tasks array. + capabilities: + - architecture + - planning + - design + tools: + - name: file_read + - name: file_list + - name: file_grep + - name: shell_auto + + - id: implementer + name: Backend Developer + description: Implements code changes following the architect's plan + system_prompt: | + You are an expert Go developer. Given a task from the architect: + 1. Read the existing code to understand the current state + 2. Implement the changes following Go conventions + 3. Write clean, well-documented code + 4. Handle errors with context wrapping + 5. Run the build to verify compilation + + Use file_read to understand existing code, file_write to make changes, + and shell to run go build and go vet. + capabilities: + - backend + - golang + - implementation + tools: + - name: file_read + - name: file_write + - name: file_list + - name: file_glob + - name: file_grep + - name: shell_auto + + - id: tester + name: Test Engineer + description: Writes and runs tests for the implemented changes + system_prompt: | + You are a test engineer specializing in Go testing. Given implemented code: + 1. Read the implementation to understand what to test + 2. Write table-driven tests covering happy paths and edge cases + 3. Include error scenario tests + 4. Run the tests using `go test ./...` + 5. Report results and coverage + + Use shell to run tests: `go test -v -cover ./...` + capabilities: + - testing + - quality-assurance + tools: + - name: file_read + - name: file_write + - name: file_grep + - name: shell_auto + + - id: reviewer + name: Code Reviewer + description: Reviews code changes for quality, security, and correctness + system_prompt: | + You are a meticulous code reviewer. Review the changes for: + 1. Correctness — Does it match the requirements? + 2. Security — SQL injection, XSS, auth bypasses, hardcoded secrets? + 3. Performance — N+1 queries, unnecessary allocations? + 4. Maintainability — Clear naming, proper abstractions? + 5. Testing — Are edge cases covered? + + Format your review as: + ✅ What's good + ⚠️ Suggestions + 🚨 Must fix + capabilities: + - code-review + - security + - quality + +teams: + - id: dev-pipeline + name: Development Pipeline + strategy: sequential + agents: + - architect + - implementer + - tester + - reviewer + + - id: dev-coordinated + name: Coordinated Dev Team + strategy: coordinator + coordinator: architect + agents: + - implementer + - tester + - reviewer + max_iterations: 3 + + - id: parallel-review + name: Parallel Review + strategy: parallel + agents: + - tester + - reviewer + max_concurrency: 2 + error_strategy: best_effort diff --git a/examples/team_deploy/main.go b/examples/team_deploy/main.go new file mode 100644 index 0000000..e605c24 --- /dev/null +++ b/examples/team_deploy/main.go @@ -0,0 +1,261 @@ +// Example: team_deploy — Deploy multi-agent teams from YAML with sandbox isolation. +// +// This example shows how to: +// - Load a team deployment config from YAML +// - Build agents with tools defined in YAML +// - Run a team in a sandboxed process environment +// - Use both sequential pipeline and coordinator strategies +// +// What you'll learn: +// - YAML-driven agent and team configuration +// - Sandbox-backed tool execution for safe agent autonomy +// - Sequential vs. coordinator team strategies +// - Deploying a full coding team from config +// +// Prerequisites: +// - Go 1.22+ +// - Set OPENAI_API_KEY for real LLM responses (mock fallback available) +// +// Run: +// +// go run ./examples/team_deploy/ +// +// Or via CLI: +// +// go run ./cli/main.go deploy examples/team_deploy/deploy.yaml "Add error handling to the API" +package main + +import ( + "context" + "fmt" + "log" + "os" + "strings" + + "github.com/spawn08/chronos/engine/graph" + "github.com/spawn08/chronos/engine/model" + "github.com/spawn08/chronos/engine/tool/builtins" + "github.com/spawn08/chronos/sandbox" + "github.com/spawn08/chronos/sdk/agent" + "github.com/spawn08/chronos/sdk/team" +) + +func main() { + ctx := context.Background() + + fmt.Println("╔═══════════════════════════════════════════════════╗") + fmt.Println("║ Chronos Team Deploy Example ║") + fmt.Println("╚═══════════════════════════════════════════════════╝") + + // ════════════════════════════════════════════════════════════════ + // Step 1: Create a sandbox environment + // + // The sandbox isolates agent command execution. Agents can run + // shell commands, but only within the sandbox boundary. + // ProcessSandbox runs commands as subprocesses in a work directory. + // For production, use ContainerSandbox (Docker) or K8sJobSandbox. + // ════════════════════════════════════════════════════════════════ + fmt.Println("\n━━━ Step 1: Sandbox Environment ━━━") + + workDir := os.TempDir() + "/chronos-team-demo" + _ = os.MkdirAll(workDir, 0o755) + defer os.RemoveAll(workDir) + + sb := sandbox.NewProcessSandbox(workDir) + defer sb.Close() + + fmt.Printf(" Backend: process\n") + fmt.Printf(" Work dir: %s\n", workDir) + + // ════════════════════════════════════════════════════════════════ + // Step 2: Build agents programmatically with sandbox tools + // + // Each agent gets a role-specific system prompt and a set of tools. + // The shell tool is sandbox-backed — commands run inside the sandbox. + // File tools operate on the sandbox work directory. + // ════════════════════════════════════════════════════════════════ + fmt.Println("\n━━━ Step 2: Building Agents ━━━") + + provider := resolveProvider() + + architect := buildCodingAgent("architect", "Software Architect", + "Designs architecture and task breakdown", + []string{"architecture", "planning"}, + provider, sb, workDir, + "You are a software architect. Break tasks into specific, implementable steps. Output a JSON plan with tasks, dependencies, and acceptance criteria.") + + implementer := buildCodingAgent("implementer", "Backend Developer", + "Implements code following the plan", + []string{"backend", "golang"}, + provider, sb, workDir, + "You are an expert Go developer. Implement the given task following Go conventions. Use file_read to understand existing code, file_write to make changes, and shell to run builds.") + + tester := buildCodingAgent("tester", "Test Engineer", + "Writes and runs tests", + []string{"testing", "quality"}, + provider, sb, workDir, + "You are a test engineer. Write table-driven Go tests for the given implementation. Run tests with shell('go test -v ./...').") + + reviewer := buildCodingAgent("reviewer", "Code Reviewer", + "Reviews code for quality and security", + []string{"code-review", "security"}, + provider, sb, workDir, + "You are a code reviewer. Review changes for correctness, security, performance, and maintainability. Format: ✅ Good / ⚠️ Suggestions / 🚨 Must fix.") + + fmt.Printf(" Built 4 agents: architect, implementer, tester, reviewer\n") + + // ════════════════════════════════════════════════════════════════ + // Step 3: Run a sequential pipeline team + // + // The sequential strategy creates a pipeline: each agent receives + // the output of the previous agent as context. The architect plans, + // the implementer codes, the tester validates, the reviewer approves. + // ════════════════════════════════════════════════════════════════ + fmt.Println("\n━━━ Step 3: Sequential Pipeline Team ━━━") + + pipeline := team.New("dev-pipeline", "Development Pipeline", team.StrategySequential). + AddAgent(architect). + AddAgent(implementer). + AddAgent(tester). + AddAgent(reviewer) + + task := "Create a health check endpoint that returns the service version and uptime" + fmt.Printf(" Task: %s\n", task) + + result, err := pipeline.Run(ctx, graph.State{"message": task}) + if err != nil { + log.Printf(" Pipeline error: %v (expected with mock provider)", err) + } else { + if resp, ok := result["response"]; ok { + fmt.Printf(" Pipeline result: %s\n", truncate(fmt.Sprintf("%v", resp), 200)) + } + fmt.Printf(" Messages exchanged: %d\n", len(pipeline.MessageHistory())) + } + + // ════════════════════════════════════════════════════════════════ + // Step 4: Run a coordinator team + // + // The coordinator strategy uses an LLM (the architect) to decompose + // the task into sub-tasks, then delegates each sub-task to the + // appropriate agent. Independent tasks can run in parallel. + // ════════════════════════════════════════════════════════════════ + fmt.Println("\n━━━ Step 4: Coordinator Team ━━━") + + coordTeam := team.New("dev-coordinated", "Coordinated Dev Team", team.StrategyCoordinator). + SetCoordinator(architect). + AddAgent(implementer). + AddAgent(tester). + AddAgent(reviewer). + SetMaxIterations(2) + + coordTask := "Add input validation to ensure email addresses are valid" + fmt.Printf(" Task: %s\n", coordTask) + + result, err = coordTeam.Run(ctx, graph.State{"message": coordTask}) + if err != nil { + fmt.Printf(" Coordinator: %v (expected with mock provider)\n", err) + } else { + fmt.Printf(" Coordinator completed with %d state keys\n", len(result)) + fmt.Printf(" Messages exchanged: %d\n", len(coordTeam.MessageHistory())) + } + + // ════════════════════════════════════════════════════════════════ + // Step 5: Run a parallel review team + // + // The parallel strategy runs multiple agents concurrently. + // Here tester and reviewer both analyze the same code simultaneously. + // Results are merged — you get both test results and review feedback. + // ════════════════════════════════════════════════════════════════ + fmt.Println("\n━━━ Step 5: Parallel Review ━━━") + + parallelTeam := team.New("parallel-review", "Parallel Review", team.StrategyParallel). + AddAgent(tester). + AddAgent(reviewer). + SetMaxConcurrency(2). + SetErrorStrategy(team.ErrorStrategyBestEffort) + + result, err = parallelTeam.Run(ctx, graph.State{ + "message": "Review this code: func Add(a, b int) int { return a + b }", + }) + if err != nil { + fmt.Printf(" Parallel error: %v\n", err) + } else { + for k, v := range result { + if strings.HasPrefix(k, "_") { + continue + } + fmt.Printf(" %s: %s\n", k, truncate(fmt.Sprintf("%v", v), 120)) + } + } + + fmt.Println("\n✓ Team deployment example completed.") + fmt.Println("\nTo deploy via CLI:") + fmt.Println(" go run ./cli/main.go deploy examples/team_deploy/deploy.yaml \"Your task here\"") +} + +// buildCodingAgent creates an agent with file and sandbox-backed shell tools. +func buildCodingAgent(id, name, desc string, caps []string, provider model.Provider, sb sandbox.Sandbox, workDir, systemPrompt string) *agent.Agent { + b := agent.New(id, name). + Description(desc). + WithModel(provider). + WithSystemPrompt(systemPrompt). + AddToolkit(builtins.NewFileToolkit(workDir)). + AddTool(builtins.NewSandboxShellTool(sb, 0)) + + for _, c := range caps { + b.AddCapability(c) + } + + a, err := b.Build() + if err != nil { + log.Fatalf("build agent %s: %v", id, err) + } + return a +} + +func resolveProvider() model.Provider { + if key := os.Getenv("OPENAI_API_KEY"); key != "" { + return model.NewOpenAI(key) + } + if key := os.Getenv("ANTHROPIC_API_KEY"); key != "" { + return model.NewAnthropic(key) + } + fmt.Println(" ⚠ No API key found, using mock provider") + return &mockProvider{} +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n-3] + "..." +} + +type mockProvider struct{} + +func (m *mockProvider) Chat(_ context.Context, req *model.ChatRequest) (*model.ChatResponse, error) { + last := req.Messages[len(req.Messages)-1].Content + if strings.Contains(last, "Analyze the following") || strings.Contains(last, "Break") { + plan := `{"tasks": [{"agent_id": "implementer", "description": "Implement the endpoint"}, {"agent_id": "tester", "description": "Write tests", "depends_on": "implementer"}], "done": false}` + return &model.ChatResponse{Content: plan, Role: "assistant", StopReason: model.StopReasonEnd}, nil + } + if strings.Contains(last, "Review") || strings.Contains(last, "Iteration") || strings.Contains(last, "done") { + return &model.ChatResponse{Content: `{"tasks": [], "done": true}`, Role: "assistant", StopReason: model.StopReasonEnd}, nil + } + return &model.ChatResponse{ + Content: fmt.Sprintf("[%s processed: %.80s]", req.Messages[0].Content[:min(20, len(req.Messages[0].Content))], last), + Role: "assistant", + StopReason: model.StopReasonEnd, + }, nil +} + +func (m *mockProvider) StreamChat(_ context.Context, req *model.ChatRequest) (<-chan *model.ChatResponse, error) { + ch := make(chan *model.ChatResponse, 1) + resp, _ := m.Chat(context.Background(), req) + ch <- resp + close(ch) + return ch, nil +} + +func (m *mockProvider) Name() string { return "mock" } +func (m *mockProvider) Model() string { return "mock-v1" } diff --git a/examples/yaml-configs/sandbox-deploy.yaml b/examples/yaml-configs/sandbox-deploy.yaml new file mode 100644 index 0000000..9f0bdd0 --- /dev/null +++ b/examples/yaml-configs/sandbox-deploy.yaml @@ -0,0 +1,96 @@ +# Sandbox Deployment Configuration +# +# Deploy agents in an isolated sandbox environment for safe autonomous execution. +# Each agent has access to shell and file tools that are sandboxed. +# +# Usage: +# export OPENAI_API_KEY=sk-... +# go run ./cli/main.go deploy examples/yaml-configs/sandbox-deploy.yaml "Build a REST API for todo items" + +name: sandbox-coding-team + +sandbox: + backend: process + work_dir: /tmp/chronos-sandbox + timeout: 5m + +defaults: + model: + provider: openai + api_key: ${OPENAI_API_KEY} + model: gpt-4o + storage: + backend: none + +agents: + - id: planner + name: Task Planner + description: Analyzes requirements and creates implementation plans + system_prompt: | + You are a task planner for software development. + Break down coding tasks into clear, actionable steps. + Identify files to create or modify. + Define acceptance criteria for each step. + capabilities: + - planning + - analysis + tools: + - name: file_list + - name: file_read + - name: file_grep + - name: shell_auto + + - id: coder + name: Code Implementer + description: Writes production-quality code following the plan + system_prompt: | + You are an expert programmer. Implement the planned changes: + 1. Read existing files before modifying + 2. Write clean, well-structured code + 3. Handle all error cases + 4. Run the build after each change + capabilities: + - implementation + - coding + tools: + - name: file_read + - name: file_write + - name: file_list + - name: file_glob + - name: file_grep + - name: shell_auto + + - id: qa + name: QA Engineer + description: Tests the implementation and verifies correctness + system_prompt: | + You are a QA engineer. Verify the implementation: + 1. Write comprehensive tests + 2. Run tests and report results + 3. Check edge cases + 4. Verify the build is clean + capabilities: + - testing + - verification + tools: + - name: file_read + - name: file_write + - name: shell_auto + +teams: + - id: build-team + name: Build Team + strategy: sequential + agents: + - planner + - coder + - qa + + - id: coordinated-build + name: Coordinated Build + strategy: coordinator + coordinator: planner + agents: + - coder + - qa + max_iterations: 3 diff --git a/sdk/agent/config.go b/sdk/agent/config.go index 7e814f9..3de7255 100644 --- a/sdk/agent/config.go +++ b/sdk/agent/config.go @@ -10,6 +10,8 @@ import ( "gopkg.in/yaml.v3" "github.com/spawn08/chronos/engine/model" + "github.com/spawn08/chronos/engine/tool" + "github.com/spawn08/chronos/engine/tool/builtins" "github.com/spawn08/chronos/storage" "github.com/spawn08/chronos/storage/adapters/sqlite" ) @@ -189,6 +191,16 @@ func BuildAgent(ctx context.Context, cfg *AgentConfig) (*Agent, error) { }) } + // Register YAML-defined tools (name-only tools act as markers; tools with + // handlers must be registered programmatically, but we register the + // built-in tool names so YAML can reference "shell", "file_read", etc.) + for _, tc := range cfg.Tools { + toolDef := buildToolFromConfig(tc) + if toolDef != nil { + b.AddTool(toolDef) + } + } + // Model provider provider, err := buildProvider(cfg.Model) if err != nil { @@ -357,6 +369,43 @@ func buildStorage(cfg StorageConfig) (storage.Storage, error) { } } +// buildToolFromConfig resolves a YAML tool config to a built-in tool Definition. +// Built-in names (shell, file_read, file_write, file_list, file_glob, file_grep, +// file_tools) are resolved automatically. Custom tools with only a name/description +// are registered as no-op placeholders so the model knows they exist. +func buildToolFromConfig(tc ToolConfig) *tool.Definition { + basePath := "." + switch tc.Name { + case "shell": + return builtins.NewShellTool(nil, 0) + case "shell_auto": + return builtins.NewAutoShellTool(nil, 0) + case "file_read": + return builtins.NewFileReadTool(basePath) + case "file_write": + return builtins.NewFileWriteTool(basePath) + case "file_list": + return builtins.NewFileListTool(basePath) + case "file_glob": + return builtins.NewFileGlobTool(basePath) + case "file_grep": + return builtins.NewFileGrepTool(basePath) + default: + if tc.Description == "" { + return nil + } + return &tool.Definition{ + Name: tc.Name, + Description: tc.Description, + Parameters: tc.Parameters, + Permission: tool.PermAllow, + Handler: func(_ context.Context, args map[string]any) (any, error) { + return map[string]any{"tool": tc.Name, "args": args, "note": "placeholder — wire a real handler programmatically"}, nil + }, + } + } +} + func readConfigFile(path string) (data []byte, resolvedPath string, err error) { candidates := []string{path} if path == "" { diff --git a/sdk/team/hierarchy.go b/sdk/team/hierarchy.go index 677f780..a24bb07 100644 --- a/sdk/team/hierarchy.go +++ b/sdk/team/hierarchy.go @@ -6,6 +6,7 @@ import ( "github.com/spawn08/chronos/engine/graph" "github.com/spawn08/chronos/sdk/agent" + "github.com/spawn08/chronos/sdk/protocol" ) // HierarchyConfig configures a hierarchical multi-level supervisor team. @@ -50,15 +51,26 @@ func NewHierarchy(cfg HierarchyConfig) (*Team, error) { g.SetEntryPoint(cfg.Root.Supervisor.ID) - _, err = g.Compile() + compiled, err := g.Compile() if err != nil { return nil, fmt.Errorf("hierarchy compile: %w", err) } + order := make([]string, 0, len(agentList)) + for _, a := range agentList { + order = append(order, a.ID) + } + return &Team{ - ID: "hierarchy", - Strategy: "hierarchy", - Agents: agentMap, + ID: "hierarchy", + Name: "Hierarchy", + Strategy: StrategyHierarchy, + Agents: agentMap, + Order: order, + CompiledGraph: compiled, + Bus: protocol.NewBus(), + SharedContext: make(map[string]any), + MaxIterations: 1, }, nil } diff --git a/sdk/team/swarm.go b/sdk/team/swarm.go index 6a6b93d..a57a5ad 100644 --- a/sdk/team/swarm.go +++ b/sdk/team/swarm.go @@ -7,6 +7,7 @@ import ( "github.com/spawn08/chronos/engine/graph" "github.com/spawn08/chronos/engine/tool" "github.com/spawn08/chronos/sdk/agent" + "github.com/spawn08/chronos/sdk/protocol" ) // SwarmConfig configures a swarm-style team where agents hand off directly @@ -100,15 +101,26 @@ func NewSwarm(cfg SwarmConfig) (*Team, error) { }) } - _, err := g.Compile() + compiled, err := g.Compile() if err != nil { return nil, fmt.Errorf("swarm compile: %w", err) } + order := make([]string, 0, len(cfg.Agents)) + for _, a := range cfg.Agents { + order = append(order, a.ID) + } + return &Team{ - ID: "swarm", - Strategy: "swarm", - Agents: agentMap, + ID: "swarm", + Name: "Swarm", + Strategy: StrategySwarm, + Agents: agentMap, + Order: order, + CompiledGraph: compiled, + Bus: protocol.NewBus(), + SharedContext: make(map[string]any), + MaxIterations: cfg.MaxHandoffs, }, nil } diff --git a/sdk/team/team.go b/sdk/team/team.go index 9cb6f15..433d68a 100644 --- a/sdk/team/team.go +++ b/sdk/team/team.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "sync" + "time" "github.com/spawn08/chronos/engine/graph" "github.com/spawn08/chronos/sdk/agent" @@ -25,6 +26,8 @@ const ( StrategyParallel Strategy = "parallel" StrategyRouter Strategy = "router" StrategyCoordinator Strategy = "coordinator" + StrategySwarm Strategy = "swarm" + StrategyHierarchy Strategy = "hierarchy" ) // RouterFunc selects an agent ID based on the current state. @@ -72,6 +75,8 @@ type Team struct { Coordinator *agent.Agent // explicit coordinator agent (for StrategyCoordinator) MaxIterations int // max coordinator planning iterations; 0 = 1 + CompiledGraph *graph.CompiledGraph // for swarm/hierarchy graph-based execution + SharedContext map[string]any sharedMu sync.RWMutex // guards SharedContext } @@ -161,11 +166,26 @@ func (t *Team) Run(ctx context.Context, input graph.State) (graph.State, error) return t.runRouter(ctx, input) case StrategyCoordinator: return t.runCoordinator(ctx, input) + case StrategySwarm, StrategyHierarchy: + return t.runGraph(ctx, input) default: return nil, fmt.Errorf("team %q: unknown strategy %q", t.ID, t.Strategy) } } +// runGraph executes a compiled graph (used by swarm and hierarchy strategies). +func (t *Team) runGraph(ctx context.Context, input graph.State) (graph.State, error) { + if t.CompiledGraph == nil { + return nil, fmt.Errorf("team %q: strategy %q requires a compiled graph", t.ID, t.Strategy) + } + runner := graph.NewRunner(t.CompiledGraph, nil) + result, err := runner.Run(ctx, fmt.Sprintf("team_%s_%d", t.ID, time.Now().UnixNano()), input) + if err != nil { + return nil, fmt.Errorf("team %q graph run: %w", t.ID, err) + } + return result.State, nil +} + // DelegateTask uses the bus to delegate a task from one agent to another. func (t *Team) DelegateTask(ctx context.Context, from, to, subject string, task protocol.TaskPayload) (*protocol.ResultPayload, error) { return t.Bus.DelegateTask(ctx, from, to, subject, task)