v0.0.1: agentu mvp

Add OpenAI-compatible providers, TUI, slash commands, and model switching.
Add local file tools plus optional yolo shell execution.
Document config, usage, and development workflow.
This commit is contained in:
loveuer
2026-06-22 17:28:22 +08:00
commit a431d1ec95
23 changed files with 3526 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
package session
import (
"fmt"
"net/http"
"sort"
"strings"
"agentu/internal/agent"
"agentu/internal/config"
"agentu/internal/llm"
)
type Manager struct {
cfg *config.Config
agent *agent.Agent
httpClient *http.Client
providerName string
provider config.ProviderConfig
}
func NewManager(cfg *config.Config, assistant *agent.Agent, httpClient *http.Client) (*Manager, error) {
if httpClient == nil {
httpClient = http.DefaultClient
}
providerName := cfg.DefaultProviderName()
provider, ok := cfg.Providers[providerName]
if !ok {
return nil, fmt.Errorf("no provider is configured")
}
return &Manager{
cfg: cfg,
agent: assistant,
httpClient: httpClient,
providerName: providerName,
provider: provider,
}, nil
}
func (m *Manager) CurrentProvider() string {
return m.providerName
}
func (m *Manager) CurrentModel() string {
return m.provider.Model
}
func (m *Manager) CurrentThinking() string {
if !m.provider.ThinkingConfigured {
return "unset"
}
return m.provider.Thinking
}
func (m *Manager) Info() string {
var b strings.Builder
fmt.Fprintf(&b, "provider: %s\n", m.CurrentProvider())
fmt.Fprintf(&b, "model: %s\n", m.CurrentModel())
fmt.Fprintf(&b, "thinking: %s\n", m.CurrentThinking())
fmt.Fprintf(&b, "providers: %s\n", strings.Join(m.providerNames(), ", "))
if len(m.provider.Models) > 0 {
fmt.Fprintf(&b, "models: %s\n", strings.Join(m.provider.Models, ", "))
} else {
b.WriteString("models: any model name is accepted for this provider\n")
}
fmt.Fprintf(&b, "thinking levels: %s\n", strings.Join(config.ThinkingLevels(), ", "))
b.WriteString("usage: /model provider <name> | /model model <name> | /model thinking <level>")
return b.String()
}
func (m *Manager) SetProvider(name string) (string, error) {
provider, ok := m.cfg.Providers[name]
if !ok {
return "", fmt.Errorf("unknown provider %q; available: %s", name, strings.Join(m.providerNames(), ", "))
}
m.providerName = name
m.provider = provider
m.syncAgent(llm.NewOpenAICompatibleClient(provider.BaseURL, provider.APIKey, m.httpClient))
return "Switched provider.\n" + m.summary(), nil
}
func (m *Manager) SetModel(model string) (string, error) {
model = strings.TrimSpace(model)
if model == "" {
return "", fmt.Errorf("model is required")
}
if len(m.provider.Models) > 0 && !contains(m.provider.Models, model) {
return "", fmt.Errorf("model %q is not configured for provider %q; available: %s", model, m.providerName, strings.Join(m.provider.Models, ", "))
}
m.provider.Model = model
m.cfg.Providers[m.providerName] = m.provider
m.syncAgent(nil)
return "Switched model.\n" + m.summary(), nil
}
func (m *Manager) SetThinking(level string) (string, error) {
level = strings.ToLower(strings.TrimSpace(level))
if !config.IsThinkingLevel(level) {
return "", fmt.Errorf("thinking must be one of: %s", strings.Join(config.ThinkingLevels(), ", "))
}
m.provider.Thinking = level
m.provider.ThinkingConfigured = true
if m.provider.ThinkingParam == "" {
m.provider.ThinkingParam = "thinking"
}
m.cfg.Providers[m.providerName] = m.provider
m.syncAgent(nil)
return "Updated thinking.\n" + m.summary(), nil
}
func (m *Manager) summary() string {
return fmt.Sprintf("provider: %s\nmodel: %s\nthinking: %s", m.CurrentProvider(), m.CurrentModel(), m.CurrentThinking())
}
func (m *Manager) syncAgent(provider llm.Provider) {
m.agent.SetRuntime(agent.RuntimeOptions{
Provider: provider,
ProviderName: m.providerName,
Model: m.provider.Model,
Thinking: m.provider.Thinking,
ThinkingParam: m.provider.ThinkingParam,
ThinkingEnabled: m.provider.ThinkingConfigured,
})
}
func (m *Manager) providerNames() []string {
names := make([]string, 0, len(m.cfg.Providers))
for name := range m.cfg.Providers {
names = append(names, name)
}
sort.Strings(names)
return names
}
func contains(values []string, value string) bool {
for _, item := range values {
if item == value {
return true
}
}
return false
}
+94
View File
@@ -0,0 +1,94 @@
package session
import (
"context"
"strings"
"testing"
"agentu/internal/agent"
"agentu/internal/config"
"agentu/internal/llm"
)
type recordingProvider struct {
req llm.ChatRequest
}
func (p *recordingProvider) ChatStream(ctx context.Context, req llm.ChatRequest, emit func(llm.StreamEvent) error) error {
p.req = req
return emit(llm.StreamEvent{Content: "ok"})
}
func TestManagerSwitchesModelAndThinking(t *testing.T) {
cfg := &config.Config{
Providers: map[string]config.ProviderConfig{
"one": {
Name: "one",
BaseURL: "https://one.example.com",
APIKey: "sk-test",
Model: "model-a",
Models: []string{"model-a", "model-b"},
Thinking: "none",
ThinkingParam: "thinking",
ThinkingConfigured: true,
},
},
}
provider := &recordingProvider{}
assistant := agent.New(agent.Options{
Provider: provider,
ProviderName: "one",
Model: "model-a",
Thinking: "none",
ThinkingParam: "thinking",
ThinkingEnabled: true,
})
manager, err := NewManager(cfg, assistant, nil)
if err != nil {
t.Fatal(err)
}
if _, err := manager.SetModel("model-b"); err != nil {
t.Fatal(err)
}
if _, err := manager.SetThinking("high"); err != nil {
t.Fatal(err)
}
var out strings.Builder
if err := assistant.RunTurn(context.Background(), "hi", &out, nil); err != nil {
t.Fatal(err)
}
if provider.req.Model != "model-b" {
t.Fatalf("model = %q", provider.req.Model)
}
if provider.req.Extra["thinking"] != "high" {
t.Fatalf("thinking = %#v", provider.req.Extra["thinking"])
}
}
func TestManagerRejectsUnknownModelAndThinking(t *testing.T) {
cfg := &config.Config{
Providers: map[string]config.ProviderConfig{
"one": {
Name: "one",
BaseURL: "https://one.example.com",
APIKey: "sk-test",
Model: "model-a",
Models: []string{"model-a"},
},
},
}
assistant := agent.New(agent.Options{Provider: &recordingProvider{}, Model: "model-a"})
manager, err := NewManager(cfg, assistant, nil)
if err != nil {
t.Fatal(err)
}
if _, err := manager.SetModel("model-b"); err == nil {
t.Fatal("expected unknown model error")
}
if _, err := manager.SetThinking("huge"); err == nil {
t.Fatal("expected invalid thinking error")
}
}