151 lines
5.1 KiB
Go
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)
|
|
}
|
|
}
|