Files
coding-agent/orchestrator.go
2026-04-17 15:24:21 +03:00

151 lines
5.1 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"strings"
)
const managerSystemPrompt = `You are the Manager agent. You coordinate between the user, a Programmer agent, and a QA agent.
You have two orchestration tools:
- assign_task(instructions): delegate coding work to the Programmer. Returns the Programmer's report.
- request_qa_review(focus): ask QA to validate specific files/changes. Returns QA's report.
Workflow:
1. Read the user's request. If it is purely conversational or meta (a greeting, a question about the agent itself), answer directly with no tool calls.
2. Otherwise, plan the work, then call assign_task with clear, self-contained instructions for the Programmer.
3. When the Programmer reports back, call request_qa_review naming the files touched and what QA should verify.
4. If QA reports issues, call assign_task again with specific fix instructions that reference QA's findings.
5. Iterate until QA approves, then reply to the user with a concise summary of what changed and any caveats.
Important:
- You have NO file tools. Do not try to read or edit files yourself.
- Every call to assign_task/request_qa_review spawns a fresh sub-agent with no memory of prior calls — put all needed context into the brief.
- Keep the final user-facing reply brief: what was done, where, and anything flagged by QA.`
func managerTools() []Tool {
return []Tool{
{
Type: "function",
Function: ToolFunction{
Name: "assign_task",
Description: "Delegate a coding task to the Programmer agent. The Programmer has read/create/edit/delete/list file tools scoped to the project root. Returns the Programmer's written report.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"instructions": map[string]any{
"type": "string",
"description": "Clear, self-contained instructions for the Programmer. Include all context needed (files, goals, constraints).",
},
},
"required": []string{"instructions"},
},
},
},
{
Type: "function",
Function: ToolFunction{
Name: "request_qa_review",
Description: "Ask the QA agent to review code. QA has read-only file tools (read_file, list_directory) plus a shell command runner (run_command) that can execute any test suite, linter, type-checker, or build command. Returns QA's written report with a verdict.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"focus": map[string]any{
"type": "string",
"description": "What QA should review: specific files, what aspect of the change, what to look for. Be explicit.",
},
},
"required": []string{"focus"},
},
},
},
}
}
type Orchestrator struct {
client *Client
root string
manager *Agent
history []Message
}
func NewOrchestrator(client *Client, root string) *Orchestrator {
o := &Orchestrator{client: client, root: root}
o.manager = &Agent{
Name: "manager",
Client: client,
SystemPrompt: managerSystemPrompt,
Tools: managerTools(),
ToolExec: o.execManagerTool,
OnToolCall: logToolCall(""),
}
o.history = []Message{{Role: "system", Content: managerSystemPrompt}}
return o
}
// Handle advances the Manager conversation with one user turn.
func (o *Orchestrator) Handle(ctx context.Context, userInput string) (string, error) {
o.history = append(o.history, Message{Role: "user", Content: userInput})
reply, updated, err := o.manager.Run(ctx, o.history)
o.history = updated
return reply, err
}
func (o *Orchestrator) execManagerTool(ctx context.Context, name, argsJSON string) string {
switch name {
case "assign_task":
var a struct {
Instructions string `json:"instructions"`
}
if err := json.Unmarshal([]byte(argsJSON), &a); err != nil {
return "error: invalid arguments: " + err.Error()
}
if strings.TrimSpace(a.Instructions) == "" {
return "error: instructions is required"
}
prog := newProgrammerAgent(o.client, o.root, logToolCall(" "))
report, err := prog.Do(ctx, a.Instructions)
if err != nil {
return "error: programmer failed: " + err.Error()
}
printReport("programmer", report)
return report
case "request_qa_review":
var a struct {
Focus string `json:"focus"`
}
if err := json.Unmarshal([]byte(argsJSON), &a); err != nil {
return "error: invalid arguments: " + err.Error()
}
if strings.TrimSpace(a.Focus) == "" {
return "error: focus is required"
}
qa := newQAAgent(o.client, o.root, logToolCall(" "))
report, err := qa.Do(ctx, a.Focus)
if err != nil {
return "error: qa failed: " + err.Error()
}
printReport("qa", report)
return report
default:
return "error: unknown tool " + name
}
}
func logToolCall(indent string) func(string, ToolCall, string) {
return func(agent string, tc ToolCall, _ string) {
fmt.Printf("%s[%s] %s %s\n", indent, agent, tc.Function.Name, tc.Function.Arguments)
}
}
func printReport(from, report string) {
fmt.Printf(" [%s → manager]\n", from)
for _, line := range strings.Split(strings.TrimRight(report, "\n"), "\n") {
fmt.Printf(" %s\n", line)
}
}