feat: complete v0.3.1 agent workflow
This commit is contained in:
+32
-11
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"agentu/internal/agent"
|
||||
"agentu/internal/session"
|
||||
"agentu/internal/skills"
|
||||
"agentu/internal/tools"
|
||||
"agentu/internal/tui"
|
||||
"agentu/pkg/config"
|
||||
@@ -40,8 +41,20 @@ func Run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
globalSkills, err := skills.LoadGlobal()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load global skills: %w", err)
|
||||
}
|
||||
rollbackStore := tools.NewRollbackStore()
|
||||
var approvalBroker *tui.ApprovalBroker
|
||||
var authorizer agent.ToolAuthorizer
|
||||
if !opts.yolo && !opts.plain {
|
||||
approvalBroker = tui.NewApprovalBroker()
|
||||
authorizer = approvalBroker
|
||||
}
|
||||
|
||||
providerName, providerConfig, provider := defaultProvider(cfg, http.DefaultClient)
|
||||
assistant := newAssistant(cfg, providerName, providerConfig, provider, opts.yolo, http.DefaultClient)
|
||||
assistant := newAssistant(cfg, providerName, providerConfig, provider, opts, globalSkills, authorizer, rollbackStore, http.DefaultClient)
|
||||
modelManager, err := newSessionManager(cfg, assistant, http.DefaultClient)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -54,7 +67,7 @@ func Run() error {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
return runInterface(ctx, assistant, modelManager, providerConfig, cfg.Agent.WorkingDir, opts, themeMode)
|
||||
return runInterface(ctx, assistant, modelManager, providerConfig, cfg.Agent.WorkingDir, opts, themeMode, approvalBroker, rollbackStore)
|
||||
}
|
||||
|
||||
func parseCLIOptions() cliOptions {
|
||||
@@ -102,7 +115,8 @@ func newProviderClient(providerConfig config.ProviderConfig, httpClient *http.Cl
|
||||
return llm.NewOpenAICompatibleClient(providerConfig.BaseURL, providerConfig.APIKey, httpClient)
|
||||
}
|
||||
|
||||
func newAssistant(cfg *config.Config, providerName string, providerConfig config.ProviderConfig, provider llm.Provider, yolo bool, httpClient *http.Client) *agent.Agent {
|
||||
func newAssistant(cfg *config.Config, providerName string, providerConfig config.ProviderConfig, provider llm.Provider, opts cliOptions, globalSkills []skills.Skill, authorizer agent.ToolAuthorizer, rollbackStore *tools.RollbackStore, httpClient *http.Client) *agent.Agent {
|
||||
writesEnabled := opts.yolo || !opts.plain
|
||||
return agent.New(agent.Options{
|
||||
Provider: provider,
|
||||
ProviderName: providerName,
|
||||
@@ -110,16 +124,21 @@ func newAssistant(cfg *config.Config, providerName string, providerConfig config
|
||||
Thinking: providerConfig.Thinking,
|
||||
ThinkingParam: providerConfig.ThinkingParam,
|
||||
ThinkingEnabled: providerConfig.ThinkingConfigured,
|
||||
SystemPrompt: systemPrompt(cfg, yolo),
|
||||
ToolRegistry: newToolRegistry(cfg.Agent.WorkingDir, yolo, httpClient),
|
||||
SystemPrompt: systemPrompt(cfg, writesEnabled, opts.yolo, globalSkills),
|
||||
ToolRegistry: newToolRegistry(cfg.Agent.WorkingDir, writesEnabled, httpClient),
|
||||
Authorizer: authorizer,
|
||||
RollbackStore: rollbackStore,
|
||||
ToolTimeout: cfg.Agent.ToolTimeout,
|
||||
MaxContextTokens: maxContextTokens(cfg, providerConfig),
|
||||
})
|
||||
}
|
||||
|
||||
func systemPrompt(cfg *config.Config, yolo bool) string {
|
||||
instructions := tools.AgentInstructions(cfg.Agent.WorkingDir, yolo)
|
||||
return strings.TrimSpace(cfg.Agent.SystemPrompt + "\n\n" + instructions)
|
||||
func systemPrompt(cfg *config.Config, writesEnabled bool, yolo bool, globalSkills []skills.Skill) string {
|
||||
parts := []string{cfg.Agent.SystemPrompt, tools.AgentInstructionsForMode(cfg.Agent.WorkingDir, writesEnabled, yolo)}
|
||||
if formatted := skills.FormatForSystemPrompt(globalSkills); formatted != "" {
|
||||
parts = append(parts, formatted)
|
||||
}
|
||||
return strings.TrimSpace(strings.Join(parts, "\n\n"))
|
||||
}
|
||||
|
||||
func maxContextTokens(cfg *config.Config, providerConfig config.ProviderConfig) int {
|
||||
@@ -129,9 +148,9 @@ func maxContextTokens(cfg *config.Config, providerConfig config.ProviderConfig)
|
||||
return providerConfig.ContextTokens
|
||||
}
|
||||
|
||||
func newToolRegistry(workingDir string, yolo bool, httpClient *http.Client) *tools.Registry {
|
||||
func newToolRegistry(workingDir string, writesEnabled bool, httpClient *http.Client) *tools.Registry {
|
||||
var registry *tools.Registry
|
||||
if yolo {
|
||||
if writesEnabled {
|
||||
registry = tools.Builtins(workingDir)
|
||||
} else {
|
||||
registry = tools.ReadOnlyBuiltins(workingDir)
|
||||
@@ -154,7 +173,7 @@ func newSessionManager(cfg *config.Config, assistant *agent.Agent, httpClient *h
|
||||
return session.NewManager(cfg, assistant, httpClient, store)
|
||||
}
|
||||
|
||||
func runInterface(ctx context.Context, assistant *agent.Agent, modelManager *session.Manager, providerConfig config.ProviderConfig, workingDir string, opts cliOptions, themeMode tui.ThemeMode) error {
|
||||
func runInterface(ctx context.Context, assistant *agent.Agent, modelManager *session.Manager, providerConfig config.ProviderConfig, workingDir string, opts cliOptions, themeMode tui.ThemeMode, approvalBroker *tui.ApprovalBroker, rollbackStore *tools.RollbackStore) error {
|
||||
if opts.plain {
|
||||
return repl(ctx, assistant, modelManager)
|
||||
}
|
||||
@@ -165,6 +184,8 @@ func runInterface(ctx context.Context, assistant *agent.Agent, modelManager *ses
|
||||
ThemeMode: themeMode,
|
||||
ModelManager: modelManager,
|
||||
WorkingDir: workingDir,
|
||||
Approval: approvalBroker,
|
||||
Rollback: rollbackStore,
|
||||
})
|
||||
printExitSession(modelManager)
|
||||
return err
|
||||
|
||||
@@ -3,10 +3,12 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"agentu/internal/agent"
|
||||
"agentu/internal/session"
|
||||
"agentu/internal/skills"
|
||||
"agentu/pkg/config"
|
||||
"agentu/pkg/llm"
|
||||
)
|
||||
@@ -99,3 +101,13 @@ func TestInitializeSessionResumesExplicitID(t *testing.T) {
|
||||
t.Fatal("explicit resume did not restore saved context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemPromptIncludesGlobalSkillsAndApprovalMode(t *testing.T) {
|
||||
cfg := &config.Config{Agent: config.AgentConfig{SystemPrompt: "base", WorkingDir: "/tmp/project"}}
|
||||
prompt := systemPrompt(cfg, true, false, []skills.Skill{{Name: "review", Path: "/tmp/review/SKILL.md", Content: "Review carefully."}})
|
||||
for _, want := range []string{"base", "interactive user approval", "### review", "Review carefully."} {
|
||||
if !strings.Contains(prompt, want) {
|
||||
t.Fatalf("prompt missing %q:\n%s", want, prompt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-1
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"agentu/internal/agent"
|
||||
"agentu/internal/session"
|
||||
"agentu/internal/skills"
|
||||
)
|
||||
|
||||
func repl(ctx context.Context, assistant *agent.Agent, modelManager *session.Manager) error {
|
||||
@@ -16,7 +17,7 @@ func repl(ctx context.Context, assistant *agent.Agent, modelManager *session.Man
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
|
||||
fmt.Println("agentu")
|
||||
fmt.Println("Type /exit to quit, /clear to reset context, /compact to compress context.")
|
||||
fmt.Println("Type /exit to quit, /clear to reset context, /compact to compress context, /btw <question> for a side question.")
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
if !scanner.Scan() {
|
||||
@@ -29,6 +30,7 @@ func repl(ctx context.Context, assistant *agent.Agent, modelManager *session.Man
|
||||
}
|
||||
|
||||
input := strings.TrimSpace(scanner.Text())
|
||||
fields := strings.Fields(input)
|
||||
switch input {
|
||||
case "":
|
||||
continue
|
||||
@@ -50,6 +52,39 @@ func repl(ctx context.Context, assistant *agent.Agent, modelManager *session.Man
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(fields) > 0 && fields[0] == "/btw" {
|
||||
question := strings.TrimSpace(strings.TrimPrefix(input, "/btw"))
|
||||
if question == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: /btw <question>")
|
||||
continue
|
||||
}
|
||||
fmt.Print("[btw] ")
|
||||
if err := assistant.BTW(ctx, question, os.Stdout); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "\nbtw error:", err)
|
||||
} else {
|
||||
fmt.Println()
|
||||
}
|
||||
continue
|
||||
}
|
||||
if input == "/skills" || input == "/skills list" {
|
||||
items, err := skills.LoadGlobal()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "skills error:", err)
|
||||
} else {
|
||||
fmt.Println(skills.FormatList(items))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(fields) >= 3 && fields[0] == "/skills" && fields[1] == "install" {
|
||||
result, err := skills.InstallGlobal(ctx, strings.Join(fields[2:], " "))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "skills error:", err)
|
||||
} else {
|
||||
assistant.AddSystemInstructions(skills.FormatForSystemPrompt([]skills.Skill{result.Skill}))
|
||||
fmt.Printf("installed skill %q; active in this conversation\n", result.Skill.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := assistant.RunTurn(ctx, input, os.Stdout, os.Stderr); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "\nerror:", err)
|
||||
|
||||
Reference in New Issue
Block a user