Make pkg/agent internals private; move app-level utils to cmd/ollie
Everything not part of the agent<->frontend contract is now unexported: - contextConfig/Builder/Stats, loopConfig, toolExecutor, confirmFn, run, state, toolResult — internal loop/session machinery - buildFirstPrompt, systemPrompt, builtinTools — impl details - agentCore struct (NewAgentCore returns Core) - Session methods (saveTo, history, rollback, compact, etc.) - AgentEnv internal fields (mcpExec, exec, confirm, tools, etc.) App-level utilities moved from pkg/agent to cmd/ollie: - resolveBackendName, defaultModelForBackend, newSessionID, agentConfigPath RestoreSession now takes int (ctxOverhead) directly, eliminating the need for contextConfig to be public. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c8d2a7a907
commit
f280ef17db
|
|
@ -2,18 +2,19 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ollie/internal/agent"
|
||||
|
||||
"ollie/internal/backend"
|
||||
"ollie/internal/config"
|
||||
execpkg "ollie/internal/exec"
|
||||
"ollie/internal/tui"
|
||||
"ollie/pkg/core"
|
||||
"ollie/pkg/agent"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -36,10 +37,10 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
backendName := agent.ResolveBackendName()
|
||||
backendName := resolveBackendName()
|
||||
modelName := os.Getenv("OLLIE_MODEL")
|
||||
if modelName == "" {
|
||||
modelName = agent.DefaultModelForBackend(backendName)
|
||||
modelName = defaultModelForBackend(backendName)
|
||||
}
|
||||
|
||||
builtinExec := execpkg.New(
|
||||
|
|
@ -52,7 +53,7 @@ func main() {
|
|||
agentName = "default"
|
||||
}
|
||||
|
||||
sessionID := agent.NewSessionID()
|
||||
sessionID := newSessionID()
|
||||
var resumeMessages []backend.Message
|
||||
if *sessionFlag != "" {
|
||||
sessionPath := sessionsDir + "/" + *sessionFlag + ".json"
|
||||
|
|
@ -76,15 +77,13 @@ func main() {
|
|||
agentName = extraArgs[0]
|
||||
}
|
||||
|
||||
cfgPath := agent.AgentConfigPath(agentsDir, agentName)
|
||||
cfgPath := agentConfigPath(agentsDir, agentName)
|
||||
cfg, cfgErr := config.Load(cfgPath)
|
||||
env := agent.BuildAgentEnv(cfg, builtinExec)
|
||||
|
||||
var initialSession *agent.Session
|
||||
if len(resumeMessages) > 0 {
|
||||
initialSession = agent.RestoreSession(resumeMessages, agent.ContextConfig{
|
||||
FixedOverheadChars: env.CtxOverhead,
|
||||
})
|
||||
initialSession = agent.RestoreSession(resumeMessages, env.CtxOverhead)
|
||||
}
|
||||
|
||||
agentCore := agent.NewAgentCore(agent.AgentCoreConfig{
|
||||
|
|
@ -112,7 +111,7 @@ func main() {
|
|||
fmt.Fprintf(os.Stderr, "session: %s\n", sessionID)
|
||||
}
|
||||
|
||||
env.Hooks.Run(core.HookAgentSpawn)
|
||||
env.Hooks.Run(agent.HookAgentSpawn)
|
||||
|
||||
if *promptFlag != "" {
|
||||
agentCore.Submit(context.Background(), *promptFlag, tui.MakeOutputFn(os.Stdout))
|
||||
|
|
@ -121,3 +120,61 @@ func main() {
|
|||
|
||||
tui.New(agentCore).Run(context.Background())
|
||||
}
|
||||
|
||||
func resolveBackendName() string {
|
||||
which := os.Getenv("OLLIE_BACKEND")
|
||||
if which == "" {
|
||||
which = "ollama"
|
||||
}
|
||||
if which != "openai" {
|
||||
return which
|
||||
}
|
||||
url := strings.ToLower(os.Getenv("OLLIE_OPENAI_URL"))
|
||||
switch {
|
||||
case strings.Contains(url, "openrouter"):
|
||||
return "openrouter"
|
||||
case strings.Contains(url, "together"):
|
||||
return "together"
|
||||
case strings.Contains(url, "groq"):
|
||||
return "groq"
|
||||
case strings.Contains(url, "mistral"):
|
||||
return "mistral"
|
||||
case strings.Contains(url, "anthropic"):
|
||||
return "anthropic"
|
||||
case strings.Contains(url, "localhost") || strings.Contains(url, "127.0.0.1"):
|
||||
return "local"
|
||||
default:
|
||||
return "openai"
|
||||
}
|
||||
}
|
||||
|
||||
func defaultModelForBackend(name string) string {
|
||||
switch name {
|
||||
case "anthropic":
|
||||
return "claude-sonnet-4-5"
|
||||
case "openrouter":
|
||||
return "deepseek/deepseek-v3.2"
|
||||
case "kiro", "codewhisperer":
|
||||
return "auto"
|
||||
default:
|
||||
return "qwen3.5:9b"
|
||||
}
|
||||
}
|
||||
|
||||
func newSessionID() string {
|
||||
b := make([]byte, 3)
|
||||
rand.Read(b) //nolint:errcheck
|
||||
return time.Now().Format("20060102-150405") + "-" + fmt.Sprintf("%06x", b)
|
||||
}
|
||||
|
||||
func agentConfigPath(agentsDir, name string) string {
|
||||
p := agentsDir + "/" + name + ".json"
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
if name == "default" {
|
||||
home, _ := os.UserHomeDir()
|
||||
return home + "/.config/ollie/config.json"
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import (
|
|||
gotty "github.com/mattn/go-tty"
|
||||
readline "github.com/nyaosorg/go-readline-ny"
|
||||
|
||||
"ollie/pkg/core"
|
||||
"ollie/pkg/agent"
|
||||
)
|
||||
|
||||
// recentHistory implements readline.IHistory for the multiline editor.
|
||||
|
|
@ -33,13 +33,13 @@ func (h *recentHistory) At(i int) string {
|
|||
|
||||
// TUI is the terminal frontend. It owns all TUI state and drives a Core.
|
||||
type TUI struct {
|
||||
core core.Core
|
||||
core agent.Core
|
||||
split *splitInput
|
||||
history recentHistory
|
||||
}
|
||||
|
||||
// New creates a TUI backed by the given Core.
|
||||
func New(c core.Core) *TUI {
|
||||
func New(c agent.Core) *TUI {
|
||||
return &TUI{core: c}
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ func (t *TUI) Run(ctx context.Context) {
|
|||
t.split = newSplitInput(tt, tt.Output(), t.core.Prompt(), nil)
|
||||
|
||||
appCtx, appCancel := context.WithCancelCause(ctx)
|
||||
core.WatchSignals(appCancel, t.core, os.Stderr)
|
||||
agent.WatchSignals(appCancel, t.core, os.Stderr)
|
||||
|
||||
var lastCtrlC time.Time
|
||||
firstRead := true
|
||||
|
|
@ -101,7 +101,7 @@ func (t *TUI) Run(ctx context.Context) {
|
|||
errs := err.Error()
|
||||
if errs == "interrupted" || errs == "^C" {
|
||||
now := time.Now()
|
||||
if !lastCtrlC.IsZero() && now.Sub(lastCtrlC) <= core.CtrlCExitWindow {
|
||||
if !lastCtrlC.IsZero() && now.Sub(lastCtrlC) <= agent.CtrlCExitWindow {
|
||||
break
|
||||
}
|
||||
lastCtrlC = now
|
||||
|
|
@ -183,8 +183,8 @@ func (t *TUI) processInputWithSplit(ctx context.Context, input string, ed *multi
|
|||
|
||||
// MakeOutputFn returns an EventHandler that renders events as text to out.
|
||||
// This is the TUI's bridge from typed events to terminal output.
|
||||
func MakeOutputFn(out io.Writer) core.EventHandler {
|
||||
return func(em core.Event) {
|
||||
func MakeOutputFn(out io.Writer) agent.EventHandler {
|
||||
return func(em agent.Event) {
|
||||
switch em.Role {
|
||||
case "assistant":
|
||||
fmt.Fprint(out, em.Content)
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import (
|
|||
"ollie/internal/backend"
|
||||
)
|
||||
|
||||
// ContextConfig controls the bounded context window behaviour.
|
||||
// contextConfig controls the bounded context window behaviour.
|
||||
// All sizes are in characters (a rough proxy for tokens; ~4 chars per token).
|
||||
type ContextConfig struct {
|
||||
type contextConfig struct {
|
||||
// SoftLimit: if assembled history exceeds this, begin evicting old messages.
|
||||
// Defaults to 24000 (~6k tokens).
|
||||
SoftLimit int
|
||||
|
|
@ -27,21 +27,21 @@ type ContextConfig struct {
|
|||
TailMessages int
|
||||
|
||||
// FixedOverheadChars is the estimated character count of fixed per-request
|
||||
// overhead (system prompt, tool schemas) sent outside the ContextBuilder.
|
||||
// overhead (system prompt, tool schemas) sent outside the contextBuilder.
|
||||
// Subtracted from the budget before greedy inclusion.
|
||||
// Defaults to 0.
|
||||
FixedOverheadChars int
|
||||
}
|
||||
|
||||
func defaultContextConfig() ContextConfig {
|
||||
return ContextConfig{
|
||||
func defaultContextConfig() contextConfig {
|
||||
return contextConfig{
|
||||
SoftLimit: 120_000,
|
||||
HardLimit: 400_000,
|
||||
TailMessages: 10,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ContextConfig) setDefaults() {
|
||||
func (c *contextConfig) setDefaults() {
|
||||
if c.SoftLimit <= 0 {
|
||||
c.SoftLimit = 120_000
|
||||
}
|
||||
|
|
@ -53,38 +53,38 @@ func (c *ContextConfig) setDefaults() {
|
|||
}
|
||||
}
|
||||
|
||||
// ContextBuilder manages a rolling bounded history window.
|
||||
type ContextBuilder struct {
|
||||
cfg ContextConfig
|
||||
// contextBuilder manages a rolling bounded history window.
|
||||
type contextBuilder struct {
|
||||
cfg contextConfig
|
||||
messages []backend.Message // full unbounded log
|
||||
}
|
||||
|
||||
// NewContextBuilder creates a ContextBuilder with the given config.
|
||||
// Pass a zero-value ContextConfig to use all defaults.
|
||||
func NewContextBuilder(cfg ContextConfig) *ContextBuilder {
|
||||
// newContextBuilder creates a contextBuilder with the given config.
|
||||
// Pass a zero-value contextConfig to use all defaults.
|
||||
func newContextBuilder(cfg contextConfig) *contextBuilder {
|
||||
cfg.setDefaults()
|
||||
return &ContextBuilder{cfg: cfg}
|
||||
return &contextBuilder{cfg: cfg}
|
||||
}
|
||||
|
||||
// Append adds a message to the full history.
|
||||
func (cb *ContextBuilder) Append(m backend.Message) {
|
||||
func (cb *contextBuilder) Append(m backend.Message) {
|
||||
cb.messages = append(cb.messages, m)
|
||||
}
|
||||
|
||||
// Messages returns the full stored message log (unbounded).
|
||||
func (cb *ContextBuilder) Messages() []backend.Message {
|
||||
func (cb *contextBuilder) Messages() []backend.Message {
|
||||
return cb.messages
|
||||
}
|
||||
|
||||
// Truncate discards all messages after index i.
|
||||
func (cb *ContextBuilder) Truncate(i int) {
|
||||
func (cb *contextBuilder) Truncate(i int) {
|
||||
if i < len(cb.messages) {
|
||||
cb.messages = cb.messages[:i]
|
||||
}
|
||||
}
|
||||
|
||||
// EvictedMessages returns the messages that would be dropped by buildBounded.
|
||||
func (cb *ContextBuilder) EvictedMessages() []backend.Message {
|
||||
func (cb *contextBuilder) EvictedMessages() []backend.Message {
|
||||
var rest []backend.Message
|
||||
for _, m := range cb.messages {
|
||||
if m.Role != "system" {
|
||||
|
|
@ -114,13 +114,13 @@ func (cb *ContextBuilder) EvictedMessages() []backend.Message {
|
|||
}
|
||||
|
||||
// BoundedHistory returns a context-window-safe slice of messages.
|
||||
func (cb *ContextBuilder) BoundedHistory() []backend.Message {
|
||||
func (cb *contextBuilder) BoundedHistory() []backend.Message {
|
||||
return cb.buildBounded(false)
|
||||
}
|
||||
|
||||
// BoundedHistoryWithNotice is like BoundedHistory but injects a compaction
|
||||
// notice message when older messages were dropped, so the model is aware.
|
||||
func (cb *ContextBuilder) BoundedHistoryWithNotice() []backend.Message {
|
||||
func (cb *contextBuilder) BoundedHistoryWithNotice() []backend.Message {
|
||||
return cb.buildBounded(true)
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ func (cb *ContextBuilder) BoundedHistoryWithNotice() []backend.Message {
|
|||
// accounting for FixedOverheadChars (system prompt, tool schemas).
|
||||
// 4. If total still exceeds HardLimit, oldest non-system/non-tail messages
|
||||
// are dropped atomically (assistant[tool_calls]+tool pairs together).
|
||||
func (cb *ContextBuilder) buildBounded(injectNotice bool) []backend.Message {
|
||||
func (cb *contextBuilder) buildBounded(injectNotice bool) []backend.Message {
|
||||
var system []backend.Message
|
||||
var rest []backend.Message
|
||||
|
||||
|
|
@ -243,10 +243,10 @@ func computeTailStart(rest []backend.Message, tailCount int) int {
|
|||
}
|
||||
|
||||
// Len returns the number of stored messages.
|
||||
func (cb *ContextBuilder) Len() int { return len(cb.messages) }
|
||||
func (cb *contextBuilder) Len() int { return len(cb.messages) }
|
||||
|
||||
// SystemMessages returns all system-role messages.
|
||||
func (cb *ContextBuilder) SystemMessages() []backend.Message {
|
||||
func (cb *contextBuilder) SystemMessages() []backend.Message {
|
||||
var out []backend.Message
|
||||
for _, m := range cb.messages {
|
||||
if m.Role == "system" {
|
||||
|
|
@ -258,7 +258,7 @@ func (cb *ContextBuilder) SystemMessages() []backend.Message {
|
|||
|
||||
// OlderMessages returns non-system messages that fall before the tail window.
|
||||
// These are the candidates for compaction.
|
||||
func (cb *ContextBuilder) OlderMessages() []backend.Message {
|
||||
func (cb *contextBuilder) OlderMessages() []backend.Message {
|
||||
var rest []backend.Message
|
||||
for _, m := range cb.messages {
|
||||
if m.Role != "system" {
|
||||
|
|
@ -270,7 +270,7 @@ func (cb *ContextBuilder) OlderMessages() []backend.Message {
|
|||
}
|
||||
|
||||
// TailWindow returns non-system messages within the protected tail window.
|
||||
func (cb *ContextBuilder) TailWindow() []backend.Message {
|
||||
func (cb *contextBuilder) TailWindow() []backend.Message {
|
||||
var rest []backend.Message
|
||||
for _, m := range cb.messages {
|
||||
if m.Role != "system" {
|
||||
|
|
@ -283,7 +283,7 @@ func (cb *ContextBuilder) TailWindow() []backend.Message {
|
|||
|
||||
// ApproxTokens returns a rough token estimate for the bounded history
|
||||
// using the 4-chars-per-token heuristic.
|
||||
func (cb *ContextBuilder) ApproxTokens() int {
|
||||
func (cb *contextBuilder) ApproxTokens() int {
|
||||
return totalChars(cb.BoundedHistory()) / 4
|
||||
}
|
||||
|
||||
|
|
@ -347,8 +347,8 @@ func sanitizeHistory(msgs []backend.Message) []backend.Message {
|
|||
return result
|
||||
}
|
||||
|
||||
// ContextStats describes the current state of the context window.
|
||||
type ContextStats struct {
|
||||
// contextStats describes the current state of the context window.
|
||||
type contextStats struct {
|
||||
StoredMessages int
|
||||
BoundedMessages int
|
||||
ApproxTokens int
|
||||
|
|
@ -357,13 +357,13 @@ type ContextStats struct {
|
|||
HardLimit int
|
||||
}
|
||||
|
||||
func (cb *ContextBuilder) Stats() ContextStats {
|
||||
func (cb *contextBuilder) Stats() contextStats {
|
||||
bounded := cb.BoundedHistory()
|
||||
evicted := len(cb.messages) - len(bounded)
|
||||
if evicted < 0 {
|
||||
evicted = 0
|
||||
}
|
||||
return ContextStats{
|
||||
return contextStats{
|
||||
StoredMessages: len(cb.messages),
|
||||
BoundedMessages: len(bounded),
|
||||
ApproxTokens: (totalChars(bounded) + cb.cfg.FixedOverheadChars) / 4,
|
||||
|
|
@ -374,7 +374,7 @@ func (cb *ContextBuilder) Stats() ContextStats {
|
|||
}
|
||||
|
||||
// ContextStatsString returns a one-line human-readable summary.
|
||||
func (cb *ContextBuilder) ContextStatsString() string {
|
||||
func (cb *contextBuilder) ContextStatsString() string {
|
||||
s := cb.Stats()
|
||||
evictedStr := ""
|
||||
if s.Evicted > 0 {
|
||||
|
|
@ -386,7 +386,7 @@ func (cb *ContextBuilder) ContextStatsString() string {
|
|||
|
||||
// FormatContextDebug returns a multi-line breakdown of the bounded history
|
||||
// useful for debug output.
|
||||
func (cb *ContextBuilder) FormatContextDebug() string {
|
||||
func (cb *contextBuilder) FormatContextDebug() string {
|
||||
var sb strings.Builder
|
||||
s := cb.Stats()
|
||||
sb.WriteString(fmt.Sprintf("=== context window: ~%d tokens | %d bounded / %d stored | soft=%d hard=%d ===\n",
|
||||
|
|
@ -21,7 +21,6 @@ import (
|
|||
execpkg "ollie/internal/exec"
|
||||
"ollie/internal/mcp"
|
||||
"ollie/internal/tools"
|
||||
"ollie/pkg/core"
|
||||
)
|
||||
|
||||
const systemPromptBase = `Use the fewest words possible. No preamble, filler, or narration ("Let me...", "I'll now...", "Great!"). No explanations of actions taken. No summaries of completed work. No reasoning unless asked. If the answer is one word, write one word.
|
||||
|
|
@ -43,9 +42,9 @@ Tool call examples:
|
|||
Run named tool: {"tool": "discover_skill.sh", "args": ["keyword"]}
|
||||
Pipeline: {"pipe": [{"code": "cat file.txt"}, {"code": "grep foo"}]}`
|
||||
|
||||
// BuildFirstPrompt seeds the first user message with the project file listing
|
||||
// buildFirstPrompt seeds the first user message with the project file listing
|
||||
// and README so the agent has immediate context.
|
||||
func BuildFirstPrompt(input string) string {
|
||||
func buildFirstPrompt(input string) string {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return input
|
||||
|
|
@ -120,8 +119,8 @@ func BuildFirstPrompt(input string) string {
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
// SystemPrompt builds the full system prompt for a given tool set.
|
||||
func SystemPrompt(allTools []backend.Tool) string {
|
||||
// systemPrompt builds the full system prompt for a given tool set.
|
||||
func systemPrompt(allTools []backend.Tool) string {
|
||||
cwd, _ := os.Getwd()
|
||||
now := time.Now().Format("2006-01-02 15:04:05 MST")
|
||||
names := make([]string, len(allTools))
|
||||
|
|
@ -133,8 +132,8 @@ func SystemPrompt(allTools []backend.Tool) string {
|
|||
"\nAvailable tools: " + strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
// BuiltinTools is the set of tools provided by the agent core.
|
||||
var BuiltinTools = []backend.Tool{
|
||||
// builtinTools is the set of tools provided by the agent core.
|
||||
var builtinTools = []backend.Tool{
|
||||
{
|
||||
Name: "execute_code",
|
||||
Description: "Run inline code in a sandboxed environment.",
|
||||
|
|
@ -215,16 +214,16 @@ var BuiltinTools = []backend.Tool{
|
|||
|
||||
// AgentEnv holds the runtime state derived from an agent config file.
|
||||
type AgentEnv struct {
|
||||
McpExec *tools.Executor
|
||||
Tools []backend.Tool
|
||||
Exec ToolExecutor
|
||||
Confirm *ConfirmFn
|
||||
Hooks core.Hooks
|
||||
SystemPrompt string
|
||||
GenParams backend.GenerationParams
|
||||
mcpExec *tools.Executor
|
||||
tools []backend.Tool
|
||||
exec toolExecutor
|
||||
confirm *confirmFn
|
||||
Hooks Hooks
|
||||
systemPrompt string
|
||||
genParams backend.GenerationParams
|
||||
CtxOverhead int
|
||||
Messages []string
|
||||
InvalidateCaches func()
|
||||
invalidateCaches func()
|
||||
}
|
||||
|
||||
// BuildAgentEnv constructs an AgentEnv from a config file and a builtin executor.
|
||||
|
|
@ -260,9 +259,9 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) AgentEnv {
|
|||
serverOf[t.Name] = t.Server
|
||||
}
|
||||
|
||||
allTools := append(mcpToolsToBackend(mcpTools), BuiltinTools...)
|
||||
allTools := append(mcpToolsToBackend(mcpTools), builtinTools...)
|
||||
|
||||
hooks := core.Hooks{}
|
||||
hooks := Hooks{}
|
||||
agentPrompt := ""
|
||||
trustedTools := map[string]struct{}{}
|
||||
var genParams backend.GenerationParams
|
||||
|
|
@ -282,9 +281,9 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) AgentEnv {
|
|||
}
|
||||
}
|
||||
|
||||
sp := SystemPrompt(allTools)
|
||||
sp := systemPrompt(allTools)
|
||||
if agentPrompt != "" {
|
||||
sp = SystemPrompt(allTools) + "\n\n" + agentPrompt
|
||||
sp = systemPrompt(allTools) + "\n\n" + agentPrompt
|
||||
}
|
||||
|
||||
overhead := len(sp)
|
||||
|
|
@ -292,13 +291,13 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) AgentEnv {
|
|||
overhead += len(t.Name) + len(t.Description) + len(t.Parameters)
|
||||
}
|
||||
|
||||
builtinNames := make(map[string]struct{}, len(BuiltinTools))
|
||||
for _, t := range BuiltinTools {
|
||||
builtinNames := make(map[string]struct{}, len(builtinTools))
|
||||
for _, t := range builtinTools {
|
||||
builtinNames[t.Name] = struct{}{}
|
||||
}
|
||||
|
||||
var confirmFn ConfirmFn
|
||||
confirmPtr := &confirmFn
|
||||
var cfn confirmFn
|
||||
confirmPtr := &cfn
|
||||
|
||||
fileReadCache := make(map[string]bool)
|
||||
toolCallSeen := make(map[string]bool)
|
||||
|
|
@ -312,7 +311,7 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) AgentEnv {
|
|||
return extractMCPText(raw), nil
|
||||
}
|
||||
if _, ok := builtinNames[name]; ok {
|
||||
var cfn ConfirmFn
|
||||
var cfn confirmFn
|
||||
if _, trusted := trustedTools[name]; !trusted {
|
||||
cfn = *confirmPtr
|
||||
}
|
||||
|
|
@ -386,44 +385,23 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) AgentEnv {
|
|||
}
|
||||
|
||||
return AgentEnv{
|
||||
McpExec: mcpExec,
|
||||
Tools: allTools,
|
||||
Exec: execFn,
|
||||
Confirm: confirmPtr,
|
||||
mcpExec: mcpExec,
|
||||
tools: allTools,
|
||||
exec: execFn,
|
||||
confirm: confirmPtr,
|
||||
Hooks: hooks,
|
||||
SystemPrompt: sp,
|
||||
GenParams: genParams,
|
||||
systemPrompt: sp,
|
||||
genParams: genParams,
|
||||
CtxOverhead: overhead,
|
||||
Messages: messages,
|
||||
InvalidateCaches: func() {
|
||||
invalidateCaches: func() {
|
||||
clear(fileReadCache)
|
||||
clear(toolCallSeen)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AgentConfigPath resolves the config file path for a named agent.
|
||||
func AgentConfigPath(agentsDir, name string) string {
|
||||
p := agentsDir + "/" + name + ".json"
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
if name == "default" {
|
||||
home, _ := os.UserHomeDir()
|
||||
return home + "/.config/ollie/config.json"
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// NewSessionID generates a unique session identifier.
|
||||
func NewSessionID() string {
|
||||
b := make([]byte, 3)
|
||||
rand.Read(b) //nolint:errcheck
|
||||
return time.Now().Format("20060102-150405") + "-" + fmt.Sprintf("%06x", b)
|
||||
}
|
||||
|
||||
// DefaultModelForBackend returns a sensible default model for the given backend label.
|
||||
func DefaultModelForBackend(name string) string {
|
||||
func defaultModelForBackend(name string) string {
|
||||
switch name {
|
||||
case "anthropic":
|
||||
return "claude-sonnet-4-5"
|
||||
|
|
@ -431,13 +409,12 @@ func DefaultModelForBackend(name string) string {
|
|||
return "deepseek/deepseek-v3.2"
|
||||
case "kiro", "codewhisperer":
|
||||
return "auto"
|
||||
default: // ollama, local, groq, mistral, together, etc.
|
||||
default:
|
||||
return "qwen3.5:9b"
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveBackendName returns a short human-readable backend label from env vars.
|
||||
func ResolveBackendName() string {
|
||||
func resolveBackendName() string {
|
||||
which := os.Getenv("OLLIE_BACKEND")
|
||||
if which == "" {
|
||||
which = "ollama"
|
||||
|
|
@ -464,9 +441,30 @@ func ResolveBackendName() string {
|
|||
}
|
||||
}
|
||||
|
||||
// agentConfigPath resolves the config file path for a named agent.
|
||||
func agentConfigPath(agentsDir, name string) string {
|
||||
p := agentsDir + "/" + name + ".json"
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
if name == "default" {
|
||||
home, _ := os.UserHomeDir()
|
||||
return home + "/.config/ollie/config.json"
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// newSessionID generates a unique session identifier.
|
||||
func newSessionID() string {
|
||||
b := make([]byte, 3)
|
||||
rand.Read(b) //nolint:errcheck
|
||||
return time.Now().Format("20060102-150405") + "-" + fmt.Sprintf("%06x", b)
|
||||
}
|
||||
|
||||
|
||||
// infoEvent wraps a plain-text message as an info Event.
|
||||
func infoEvent(text string) core.Event {
|
||||
return core.Event{Role: "info", Content: text + "\n"}
|
||||
func infoEvent(text string) Event {
|
||||
return Event{Role: "info", Content: text + "\n"}
|
||||
}
|
||||
|
||||
// actionHandle holds the cancel function for the current agent turn.
|
||||
|
|
@ -474,7 +472,7 @@ type actionHandle struct {
|
|||
cancel context.CancelCauseFunc
|
||||
}
|
||||
|
||||
// AgentCoreConfig is the configuration for creating an AgentCore.
|
||||
// AgentCoreConfig is the configuration for creating an agentCore.
|
||||
type AgentCoreConfig struct {
|
||||
Backend backend.Backend
|
||||
BackendName string
|
||||
|
|
@ -488,12 +486,12 @@ type AgentCoreConfig struct {
|
|||
BuiltinExec *execpkg.Executor
|
||||
}
|
||||
|
||||
// AgentCore is the Core implementation. It owns all agent and session state
|
||||
// agentCore is the Core implementation. It owns all agent and session state
|
||||
// but has no knowledge of how output is rendered.
|
||||
type AgentCore struct {
|
||||
type agentCore struct {
|
||||
session *Session
|
||||
loopcfg Config
|
||||
hooks core.Hooks
|
||||
loopcfg loopConfig
|
||||
hooks Hooks
|
||||
modelName string
|
||||
backendName string
|
||||
agentName string
|
||||
|
|
@ -502,26 +500,26 @@ type AgentCore struct {
|
|||
sessionID string
|
||||
mcpExec *tools.Executor
|
||||
builtinExec *execpkg.Executor
|
||||
confirmPtr *ConfirmFn
|
||||
confirmPtr *confirmFn
|
||||
ctxOverhead int
|
||||
invalidateCaches func()
|
||||
currentAction atomic.Pointer[actionHandle]
|
||||
}
|
||||
|
||||
var _ core.Core = (*AgentCore)(nil) // compile-time interface check
|
||||
var _ Core = (*agentCore)(nil) // compile-time interface check
|
||||
|
||||
// NewAgentCore creates an AgentCore from the given configuration.
|
||||
func NewAgentCore(cfg AgentCoreConfig) *AgentCore {
|
||||
loopcfg := Config{
|
||||
// NewAgentCore creates an agentCore from the given configuration.
|
||||
func NewAgentCore(cfg AgentCoreConfig) Core {
|
||||
loopcfg := loopConfig{
|
||||
Backend: cfg.Backend,
|
||||
Model: cfg.ModelName,
|
||||
SystemPrompt: cfg.Env.SystemPrompt,
|
||||
Tools: cfg.Env.Tools,
|
||||
Exec: cfg.Env.Exec,
|
||||
systemPrompt: cfg.Env.systemPrompt,
|
||||
Tools: cfg.Env.tools,
|
||||
Exec: cfg.Env.exec,
|
||||
MaxSteps: 20,
|
||||
GenerationParams: cfg.Env.GenParams,
|
||||
GenerationParams: cfg.Env.genParams,
|
||||
}
|
||||
return &AgentCore{
|
||||
return &agentCore{
|
||||
session: cfg.Session,
|
||||
loopcfg: loopcfg,
|
||||
hooks: cfg.Env.Hooks,
|
||||
|
|
@ -531,32 +529,32 @@ func NewAgentCore(cfg AgentCoreConfig) *AgentCore {
|
|||
agentsDir: cfg.AgentsDir,
|
||||
sessionsDir: cfg.SessionsDir,
|
||||
sessionID: cfg.SessionID,
|
||||
mcpExec: cfg.Env.McpExec,
|
||||
mcpExec: cfg.Env.mcpExec,
|
||||
builtinExec: cfg.BuiltinExec,
|
||||
confirmPtr: cfg.Env.Confirm,
|
||||
confirmPtr: cfg.Env.confirm,
|
||||
ctxOverhead: cfg.Env.CtxOverhead,
|
||||
invalidateCaches: cfg.Env.InvalidateCaches,
|
||||
invalidateCaches: cfg.Env.invalidateCaches,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AgentCore) prompt() string {
|
||||
func (s *agentCore) prompt() string {
|
||||
return fmt.Sprintf("[%s :: %s] ", s.backendName, s.agentName)
|
||||
}
|
||||
|
||||
// Prompt returns the display prompt string for the current session state.
|
||||
func (s *AgentCore) Prompt() string { return s.prompt() }
|
||||
func (s *agentCore) Prompt() string { return s.prompt() }
|
||||
|
||||
func (s *AgentCore) saveSession() {
|
||||
func (s *agentCore) saveSession() {
|
||||
if s.session == nil || s.sessionID == "" || s.sessionsDir == "" {
|
||||
return
|
||||
}
|
||||
path := s.sessionsDir + "/" + s.sessionID + ".json"
|
||||
if err := s.session.SaveTo(path, s.sessionID, s.agentName); err != nil {
|
||||
if err := s.session.saveTo(path, s.sessionID, s.agentName); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "session save:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AgentCore) getActionCancel() context.CancelCauseFunc {
|
||||
func (s *agentCore) getActionCancel() context.CancelCauseFunc {
|
||||
if a := s.currentAction.Load(); a != nil {
|
||||
return a.cancel
|
||||
}
|
||||
|
|
@ -565,7 +563,7 @@ func (s *AgentCore) getActionCancel() context.CancelCauseFunc {
|
|||
|
||||
// Interrupt cancels the current in-progress agent turn.
|
||||
// Returns true if an action was running and was cancelled.
|
||||
func (s *AgentCore) Interrupt(cause error) bool {
|
||||
func (s *agentCore) Interrupt(cause error) bool {
|
||||
if cancel := s.getActionCancel(); cancel != nil {
|
||||
cancel(cause)
|
||||
return true
|
||||
|
|
@ -576,19 +574,19 @@ func (s *AgentCore) Interrupt(cause error) bool {
|
|||
// Submit implements Core. It processes one line of user input: slash commands
|
||||
// and shell shortcuts are dispatched immediately via handler; any other input
|
||||
// starts an agent turn that streams events to handler.
|
||||
func (s *AgentCore) Submit(ctx context.Context, input string, handler core.EventHandler) {
|
||||
func (s *agentCore) Submit(ctx context.Context, input string, handler EventHandler) {
|
||||
if s.handleCommand(ctx, input, handler) {
|
||||
return
|
||||
}
|
||||
|
||||
s.hooks.Run(core.HookUserPromptSubmit)
|
||||
s.hooks.Run(HookUserPromptSubmit)
|
||||
|
||||
if s.session == nil {
|
||||
s.session = NewSessionWithConfig(BuildFirstPrompt(input), ContextConfig{
|
||||
s.session = newSessionWithConfig(buildFirstPrompt(input), contextConfig{
|
||||
FixedOverheadChars: s.ctxOverhead,
|
||||
})
|
||||
} else {
|
||||
s.session.AppendUserMessage(input)
|
||||
s.session.appendUserMessage(input)
|
||||
}
|
||||
|
||||
actCtx, actCancel := context.WithCancelCause(ctx)
|
||||
|
|
@ -598,25 +596,25 @@ func (s *AgentCore) Submit(ctx context.Context, input string, handler core.Event
|
|||
s.loopcfg.Output = handler
|
||||
*s.confirmPtr = nil // auto-approve all confirmations for now
|
||||
|
||||
s.hooks.Run(core.HookAgentSpawn)
|
||||
s.hooks.Run(HookAgentSpawn)
|
||||
|
||||
err := Run(actCtx, s.loopcfg, s.session)
|
||||
err := run(actCtx, s.loopcfg, s.session)
|
||||
actCancel(nil)
|
||||
s.currentAction.CompareAndSwap(handle, nil)
|
||||
|
||||
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, core.ErrInterrupted) {
|
||||
handler(core.Event{Role: "error", Content: err.Error()})
|
||||
s.session.Rollback()
|
||||
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, ErrInterrupted) {
|
||||
handler(Event{Role: "error", Content: err.Error()})
|
||||
s.session.rollback()
|
||||
}
|
||||
|
||||
handler(core.Event{Role: "newline"})
|
||||
handler(Event{Role: "newline"})
|
||||
|
||||
s.hooks.Run(core.HookStop)
|
||||
s.hooks.Run(HookStop)
|
||||
|
||||
s.saveSession()
|
||||
}
|
||||
|
||||
func (s *AgentCore) handleCommand(ctx context.Context, input string, handler core.EventHandler) bool {
|
||||
func (s *agentCore) handleCommand(ctx context.Context, input string, handler EventHandler) bool {
|
||||
if strings.HasPrefix(input, "!") {
|
||||
cmdStr := strings.TrimSpace(input[1:])
|
||||
if cmdStr == "" {
|
||||
|
|
@ -657,8 +655,8 @@ func (s *AgentCore) handleCommand(ctx context.Context, input string, handler cor
|
|||
return true
|
||||
}
|
||||
s.loopcfg.Backend = be
|
||||
s.backendName = ResolveBackendName()
|
||||
s.loopcfg.Model = DefaultModelForBackend(s.backendName)
|
||||
s.backendName = resolveBackendName()
|
||||
s.loopcfg.Model = defaultModelForBackend(s.backendName)
|
||||
s.modelName = s.loopcfg.Model
|
||||
handler(infoEvent(fmt.Sprintf("switched backend to: %s (model: %s)", s.backendName, s.modelName)))
|
||||
return true
|
||||
|
|
@ -703,7 +701,7 @@ func (s *AgentCore) handleCommand(ctx context.Context, input string, handler cor
|
|||
return true
|
||||
}
|
||||
name := args[0]
|
||||
cfgPath := AgentConfigPath(s.agentsDir, name)
|
||||
cfgPath := agentConfigPath(s.agentsDir, name)
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
handler(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err)))
|
||||
|
|
@ -713,18 +711,18 @@ func (s *AgentCore) handleCommand(ctx context.Context, input string, handler cor
|
|||
s.mcpExec.Close()
|
||||
}
|
||||
env := BuildAgentEnv(cfg, s.builtinExec)
|
||||
s.mcpExec = env.McpExec
|
||||
s.mcpExec = env.mcpExec
|
||||
s.hooks = env.Hooks
|
||||
s.loopcfg.SystemPrompt = env.SystemPrompt
|
||||
s.loopcfg.Tools = env.Tools
|
||||
s.loopcfg.Exec = env.Exec
|
||||
s.loopcfg.GenerationParams = env.GenParams
|
||||
s.loopcfg.systemPrompt = env.systemPrompt
|
||||
s.loopcfg.Tools = env.tools
|
||||
s.loopcfg.Exec = env.exec
|
||||
s.loopcfg.GenerationParams = env.genParams
|
||||
s.ctxOverhead = env.CtxOverhead
|
||||
s.confirmPtr = env.Confirm
|
||||
s.invalidateCaches = env.InvalidateCaches
|
||||
s.confirmPtr = env.confirm
|
||||
s.invalidateCaches = env.invalidateCaches
|
||||
s.agentName = name
|
||||
s.session = nil
|
||||
s.sessionID = NewSessionID()
|
||||
s.sessionID = newSessionID()
|
||||
for _, msg := range env.Messages {
|
||||
handler(infoEvent(msg))
|
||||
}
|
||||
|
|
@ -736,7 +734,7 @@ func (s *AgentCore) handleCommand(ctx context.Context, input string, handler cor
|
|||
handler(infoEvent("nothing to compact"))
|
||||
return true
|
||||
}
|
||||
n, summary, err := s.session.Compact(ctx, s.loopcfg.Backend, s.loopcfg.Model)
|
||||
n, summary, err := s.session.compact(ctx, s.loopcfg.Backend, s.loopcfg.Model)
|
||||
if err != nil {
|
||||
handler(infoEvent("compact error: " + err.Error()))
|
||||
} else if n == 0 {
|
||||
|
|
@ -744,7 +742,7 @@ func (s *AgentCore) handleCommand(ctx context.Context, input string, handler cor
|
|||
} else {
|
||||
handler(infoEvent(fmt.Sprintf("compacted %d messages", n)))
|
||||
if summary != "" {
|
||||
handler(core.Event{Role: "newline"})
|
||||
handler(Event{Role: "newline"})
|
||||
handler(infoEvent(summary))
|
||||
}
|
||||
s.saveSession()
|
||||
|
|
@ -759,7 +757,7 @@ func (s *AgentCore) handleCommand(ctx context.Context, input string, handler cor
|
|||
handler(infoEvent("no active session"))
|
||||
return true
|
||||
}
|
||||
handler(infoEvent(strings.TrimRight(s.session.ContextDebug(), "\n")))
|
||||
handler(infoEvent(strings.TrimRight(s.session.contextDebug(), "\n")))
|
||||
return true
|
||||
|
||||
case "/history":
|
||||
|
|
@ -767,7 +765,7 @@ func (s *AgentCore) handleCommand(ctx context.Context, input string, handler cor
|
|||
handler(infoEvent("no active session"))
|
||||
return true
|
||||
}
|
||||
for _, msg := range s.session.History() {
|
||||
for _, msg := range s.session.history() {
|
||||
preview := msg.Content
|
||||
if len(preview) > 200 {
|
||||
preview = preview[:200] + "..."
|
||||
|
|
@ -778,7 +776,7 @@ func (s *AgentCore) handleCommand(ctx context.Context, input string, handler cor
|
|||
|
||||
case "/clear":
|
||||
s.session = nil
|
||||
s.sessionID = NewSessionID()
|
||||
s.sessionID = newSessionID()
|
||||
if s.invalidateCaches != nil {
|
||||
s.invalidateCaches()
|
||||
}
|
||||
|
|
@ -883,7 +881,7 @@ func extractMCPText(raw json.RawMessage) string {
|
|||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
func dispatchBuiltinExec(ctx context.Context, name string, e *execpkg.Executor, confirm ConfirmFn, args json.RawMessage) (string, error) {
|
||||
func dispatchBuiltinExec(ctx context.Context, name string, e *execpkg.Executor, confirm confirmFn, args json.RawMessage) (string, error) {
|
||||
switch name {
|
||||
case "file_read":
|
||||
return dispatchFileRead(confirm, args)
|
||||
|
|
@ -924,7 +922,7 @@ func execArgs(args json.RawMessage) (code, language, sandbox string, timeout int
|
|||
return
|
||||
}
|
||||
|
||||
func dispatchExecuteCode(ctx context.Context, e *execpkg.Executor, confirm ConfirmFn, args json.RawMessage) (string, error) {
|
||||
func dispatchExecuteCode(ctx context.Context, e *execpkg.Executor, confirm confirmFn, args json.RawMessage) (string, error) {
|
||||
code, language, sandbox, timeout, err := execArgs(args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("execute_code: bad args: %w", err)
|
||||
|
|
@ -938,7 +936,7 @@ func dispatchExecuteCode(ctx context.Context, e *execpkg.Executor, confirm Confi
|
|||
return e.Execute(ctx, code, language, timeout, sandbox, false)
|
||||
}
|
||||
|
||||
func dispatchExecuteTool(ctx context.Context, e *execpkg.Executor, confirm ConfirmFn, args json.RawMessage) (string, error) {
|
||||
func dispatchExecuteTool(ctx context.Context, e *execpkg.Executor, confirm confirmFn, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Tool string `json:"tool"`
|
||||
Args []string `json:"args"`
|
||||
|
|
@ -982,7 +980,7 @@ func dispatchExecuteTool(ctx context.Context, e *execpkg.Executor, confirm Confi
|
|||
return e.Execute(ctx, code, language, timeout, sandbox, true)
|
||||
}
|
||||
|
||||
func dispatchExecutePipe(ctx context.Context, e *execpkg.Executor, confirm ConfirmFn, args json.RawMessage) (string, error) {
|
||||
func dispatchExecutePipe(ctx context.Context, e *execpkg.Executor, confirm confirmFn, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Pipe []execpkg.PipeStep `json:"pipe"`
|
||||
Timeout int `json:"timeout"`
|
||||
|
|
@ -1012,7 +1010,7 @@ func dispatchExecutePipe(ctx context.Context, e *execpkg.Executor, confirm Confi
|
|||
return e.Execute(ctx, code, "bash", timeout, sandbox, true)
|
||||
}
|
||||
|
||||
func dispatchFileRead(confirm ConfirmFn, args json.RawMessage) (string, error) {
|
||||
func dispatchFileRead(confirm confirmFn, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
|
@ -1037,7 +1035,7 @@ func dispatchFileRead(confirm ConfirmFn, args json.RawMessage) (string, error) {
|
|||
return strings.TrimRight(out.String(), "\n"), nil
|
||||
}
|
||||
|
||||
func dispatchFileWrite(confirm ConfirmFn, args json.RawMessage) (string, error) {
|
||||
func dispatchFileWrite(confirm confirmFn, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package core
|
||||
package agent
|
||||
|
||||
import "os/exec"
|
||||
|
||||
|
|
@ -9,29 +9,28 @@ import (
|
|||
"time"
|
||||
|
||||
"ollie/internal/backend"
|
||||
"ollie/pkg/core"
|
||||
)
|
||||
|
||||
const maxRateLimitRetries = 3
|
||||
|
||||
type ToolExecutor func(ctx context.Context, name string, args json.RawMessage) (string, error)
|
||||
type toolExecutor func(ctx context.Context, name string, args json.RawMessage) (string, error)
|
||||
|
||||
type Config struct {
|
||||
type loopConfig struct {
|
||||
Backend backend.Backend
|
||||
Model string
|
||||
Tools []backend.Tool
|
||||
Exec ToolExecutor
|
||||
Confirm ConfirmFn
|
||||
Exec toolExecutor
|
||||
Confirm confirmFn
|
||||
MaxSteps int
|
||||
Output core.EventHandler
|
||||
SystemPrompt string
|
||||
Output EventHandler
|
||||
systemPrompt string
|
||||
GenerationParams backend.GenerationParams
|
||||
}
|
||||
|
||||
// ConfirmFn requests user confirmation for an action. Returns true if approved.
|
||||
type ConfirmFn func(prompt string) bool
|
||||
// confirmFn requests user confirmation for an action. Returns true if approved.
|
||||
type confirmFn func(prompt string) bool
|
||||
|
||||
func Run(ctx context.Context, cfg Config, state State) error {
|
||||
func run(ctx context.Context, cfg loopConfig, state state) error {
|
||||
maxSteps := cfg.MaxSteps
|
||||
if maxSteps <= 0 {
|
||||
maxSteps = 1
|
||||
|
|
@ -41,9 +40,9 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
hitLimit := false
|
||||
|
||||
for step := range maxSteps {
|
||||
history := state.History()
|
||||
if cfg.SystemPrompt != "" {
|
||||
history = append([]backend.Message{{Role: "system", Content: cfg.SystemPrompt}}, history...)
|
||||
history := state.history()
|
||||
if cfg.systemPrompt != "" {
|
||||
history = append([]backend.Message{{Role: "system", Content: cfg.systemPrompt}}, history...)
|
||||
}
|
||||
|
||||
// Stream the assistant's response, retrying on HTTP 429.
|
||||
|
|
@ -76,7 +75,7 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
for ev := range ch {
|
||||
if ev.Content != "" {
|
||||
content.WriteString(ev.Content)
|
||||
emit(cfg, core.Event{Role: "assistant", Content: ev.Content})
|
||||
emit(cfg, Event{Role: "assistant", Content: ev.Content})
|
||||
}
|
||||
toolCalls = append(toolCalls, ev.ToolCalls...)
|
||||
if ev.Done {
|
||||
|
|
@ -99,11 +98,11 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
|
||||
// Announce and execute tool calls.
|
||||
msg := backend.Message{Role: "assistant", Content: content.String(), ToolCalls: toolCalls}
|
||||
var results []ToolResult
|
||||
var results []toolResult
|
||||
|
||||
for _, tc := range toolCalls {
|
||||
if tc.Name == "" {
|
||||
results = append(results, ToolResult{
|
||||
results = append(results, toolResult{
|
||||
ToolCallID: tc.ID,
|
||||
Name: tc.Name,
|
||||
Content: "error: empty tool name",
|
||||
|
|
@ -111,7 +110,7 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
})
|
||||
continue
|
||||
}
|
||||
emit(cfg, core.Event{Role: "call", Name: tc.Name, Content: string(tc.Arguments)})
|
||||
emit(cfg, Event{Role: "call", Name: tc.Name, Content: string(tc.Arguments)})
|
||||
|
||||
var result string
|
||||
var isErr bool
|
||||
|
|
@ -128,23 +127,23 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
isErr = true
|
||||
}
|
||||
|
||||
results = append(results, ToolResult{
|
||||
results = append(results, toolResult{
|
||||
ToolCallID: tc.ID,
|
||||
Name: tc.Name,
|
||||
Content: result,
|
||||
IsError: isErr,
|
||||
})
|
||||
emit(cfg, core.Event{Role: "tool", Name: tc.Name, Content: result})
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: result})
|
||||
}
|
||||
|
||||
|
||||
|
||||
if err := state.Update(msg, results); err != nil {
|
||||
if err := state.update(msg, results); err != nil {
|
||||
return fmt.Errorf("step %d update: %w", step, err)
|
||||
}
|
||||
|
||||
if len(toolCalls) == 0 {
|
||||
if err := state.MarkComplete(); err != nil {
|
||||
if err := state.markComplete(); err != nil {
|
||||
return fmt.Errorf("mark complete: %w", err)
|
||||
}
|
||||
break
|
||||
|
|
@ -157,13 +156,13 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
|
||||
// Surface stall: only when the step limit was hit without completing.
|
||||
if hitLimit {
|
||||
emit(cfg, core.Event{Role: "stalled", Content: "max steps"})
|
||||
emit(cfg, Event{Role: "stalled", Content: "max steps"})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func emit(cfg Config, msg core.Event) {
|
||||
func emit(cfg loopConfig, msg Event) {
|
||||
if cfg.Output != nil {
|
||||
cfg.Output(msg)
|
||||
}
|
||||
|
|
@ -172,7 +171,7 @@ func emit(cfg Config, msg core.Event) {
|
|||
// retryCountdown emits one "retry" OutputMsg per second, counting down from
|
||||
// wait, so the UI can display a live countdown. Returns ctx.Err() if the
|
||||
// context is cancelled before the wait elapses.
|
||||
func retryCountdown(ctx context.Context, cfg Config, wait time.Duration) error {
|
||||
func retryCountdown(ctx context.Context, cfg loopConfig, wait time.Duration) error {
|
||||
deadline := time.Now().Add(wait)
|
||||
for {
|
||||
remaining := time.Until(deadline)
|
||||
|
|
@ -180,7 +179,7 @@ func retryCountdown(ctx context.Context, cfg Config, wait time.Duration) error {
|
|||
return nil
|
||||
}
|
||||
secs := int(remaining.Seconds()) + 1
|
||||
emit(cfg, core.Event{Role: "retry", Content: fmt.Sprintf("%d", secs)})
|
||||
emit(cfg, Event{Role: "retry", Content: fmt.Sprintf("%d", secs)})
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
|
|
@ -18,7 +18,7 @@ type PersistedSession struct {
|
|||
}
|
||||
|
||||
// SaveTo writes the full message history to path as JSON.
|
||||
func (s *Session) SaveTo(path, id, agentName string) error {
|
||||
func (s *Session) saveTo(path, id, agentName string) error {
|
||||
ps := PersistedSession{
|
||||
ID: id,
|
||||
Agent: agentName,
|
||||
|
|
@ -32,9 +32,9 @@ func (s *Session) SaveTo(path, id, agentName string) error {
|
|||
}
|
||||
|
||||
// RestoreSession reconstructs a Session from a persisted message list,
|
||||
// applying cfg to the ContextBuilder.
|
||||
func RestoreSession(messages []backend.Message, cfg ContextConfig) *Session {
|
||||
s := &Session{ctx: NewContextBuilder(cfg)}
|
||||
// applying cfg to the contextBuilder.
|
||||
func RestoreSession(messages []backend.Message, ctxOverhead int) *Session {
|
||||
s := &Session{ctx: newContextBuilder(contextConfig{FixedOverheadChars: ctxOverhead})}
|
||||
for _, m := range messages {
|
||||
s.ctx.Append(m)
|
||||
}
|
||||
|
|
@ -50,26 +50,26 @@ func RestoreSession(messages []backend.Message, cfg ContextConfig) *Session {
|
|||
// Session is an ephemeral in-memory state backend.
|
||||
// It lives only for the duration of the process; nothing is persisted.
|
||||
//
|
||||
// History() returns a bounded context window via ContextBuilder to prevent
|
||||
// prompt explosion across multi-step agent loops. Use NewSessionWithConfig
|
||||
// History() returns a bounded context window via contextBuilder to prevent
|
||||
// prompt explosion across multi-step agent loops. Use newSessionWithConfig
|
||||
// to override the default limits.
|
||||
type Session struct {
|
||||
goal string
|
||||
ctx *ContextBuilder
|
||||
ctx *contextBuilder
|
||||
complete bool
|
||||
}
|
||||
|
||||
// NewSession creates a Session with default context window limits.
|
||||
func NewSession(goal string) *Session {
|
||||
return NewSessionWithConfig(goal, ContextConfig{})
|
||||
// newSession creates a Session with default context window limits.
|
||||
func newSession(goal string) *Session {
|
||||
return newSessionWithConfig(goal, contextConfig{})
|
||||
}
|
||||
|
||||
// NewSessionWithConfig creates a Session with explicit context window limits.
|
||||
// Pass a zero-value ContextConfig to use all defaults.
|
||||
func NewSessionWithConfig(goal string, cfg ContextConfig) *Session {
|
||||
// newSessionWithConfig creates a Session with explicit context window limits.
|
||||
// Pass a zero-value contextConfig to use all defaults.
|
||||
func newSessionWithConfig(goal string, cfg contextConfig) *Session {
|
||||
s := &Session{
|
||||
goal: goal,
|
||||
ctx: NewContextBuilder(cfg),
|
||||
ctx: newContextBuilder(cfg),
|
||||
}
|
||||
s.ctx.Append(backend.Message{Role: "user", Content: goal})
|
||||
return s
|
||||
|
|
@ -79,13 +79,13 @@ func (s *Session) Goal() string { return s.goal }
|
|||
|
||||
// History returns the bounded history window safe for passing to the backend.
|
||||
// A compaction notice is injected when older messages have been evicted.
|
||||
func (s *Session) History() []backend.Message {
|
||||
func (s *Session) history() []backend.Message {
|
||||
return s.ctx.BoundedHistoryWithNotice()
|
||||
}
|
||||
|
||||
func (s *Session) IsComplete() bool { return s.complete }
|
||||
func (s *Session) isComplete() bool { return s.complete }
|
||||
|
||||
func (s *Session) Update(assistant backend.Message, results []ToolResult) error {
|
||||
func (s *Session) update(assistant backend.Message, results []toolResult) error {
|
||||
s.ctx.Append(assistant)
|
||||
for _, r := range results {
|
||||
s.ctx.Append(backend.Message{
|
||||
|
|
@ -97,7 +97,7 @@ func (s *Session) Update(assistant backend.Message, results []ToolResult) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) MarkComplete() error {
|
||||
func (s *Session) markComplete() error {
|
||||
s.complete = true
|
||||
return nil
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@ func (s *Session) MarkComplete() error {
|
|||
// an LLM call, replacing them with a single summary system message.
|
||||
// Unlike the previous eviction-based approach, this works for any session size.
|
||||
// Returns (n compacted, summary text, error); n==0 means nothing to compact.
|
||||
func (s *Session) Compact(ctx context.Context, b backend.Backend, model string) (int, string, error) {
|
||||
func (s *Session) compact(ctx context.Context, b backend.Backend, model string) (int, string, error) {
|
||||
older := s.ctx.OlderMessages()
|
||||
if len(older) == 0 {
|
||||
return 0, "", nil
|
||||
|
|
@ -157,7 +157,7 @@ func (s *Session) Compact(ctx context.Context, b backend.Backend, model string)
|
|||
|
||||
// Rollback removes any trailing non-user messages from history, discarding
|
||||
// an incomplete assistant turn caused by an interruption.
|
||||
func (s *Session) Rollback() {
|
||||
func (s *Session) rollback() {
|
||||
s.complete = false
|
||||
msgs := s.ctx.Messages()
|
||||
i := len(msgs)
|
||||
|
|
@ -167,22 +167,22 @@ func (s *Session) Rollback() {
|
|||
s.ctx.Truncate(i)
|
||||
}
|
||||
// flag so the loop will run again on the next call to Loop.Run.
|
||||
func (s *Session) AppendUserMessage(content string) {
|
||||
func (s *Session) appendUserMessage(content string) {
|
||||
s.complete = false
|
||||
s.ctx.Append(backend.Message{Role: "user", Content: content})
|
||||
}
|
||||
|
||||
// ContextStats returns stats about the current context window.
|
||||
func (s *Session) ContextStats() ContextStats {
|
||||
// contextStats returns stats about the current context window.
|
||||
func (s *Session) contextStats() contextStats {
|
||||
return s.ctx.Stats()
|
||||
}
|
||||
|
||||
// ContextStatsString returns a one-line human-readable context summary.
|
||||
func (s *Session) ContextStatsString() string {
|
||||
func (s *Session) contextStatsString() string {
|
||||
return s.ctx.ContextStatsString()
|
||||
}
|
||||
|
||||
// ContextDebug returns a multi-line breakdown of the bounded history.
|
||||
func (s *Session) ContextDebug() string {
|
||||
func (s *Session) contextDebug() string {
|
||||
return s.ctx.FormatContextDebug()
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package core
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//go:build !windows
|
||||
|
||||
package core
|
||||
package agent
|
||||
|
||||
import "syscall"
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//go:build windows
|
||||
|
||||
package core
|
||||
package agent
|
||||
|
||||
import "os"
|
||||
|
||||
|
|
@ -2,31 +2,28 @@ package agent
|
|||
|
||||
import "ollie/internal/backend"
|
||||
|
||||
// ToolResult holds the output of a single tool call.
|
||||
type ToolResult struct {
|
||||
// toolResult holds the output of a single tool call.
|
||||
type toolResult struct {
|
||||
ToolCallID string // may be empty (Ollama does not always set this)
|
||||
Name string
|
||||
Content string
|
||||
IsError bool
|
||||
}
|
||||
|
||||
// State is the interface both ephemeral and bead-backed state must satisfy.
|
||||
// state is the interface both ephemeral and bead-backed state must satisfy.
|
||||
// The loop reads from it on Observe, writes to it on Update.
|
||||
type State interface {
|
||||
// Goal returns the prompt or task description driving this session.
|
||||
Goal() string
|
||||
|
||||
type state interface {
|
||||
// History returns the full conversation history for the current session.
|
||||
// The first entry is always the initial user message containing the goal.
|
||||
History() []backend.Message
|
||||
history() []backend.Message
|
||||
|
||||
// Update appends the assistant's reply and any tool results to the history.
|
||||
// Called once per loop iteration after Act completes.
|
||||
Update(assistant backend.Message, results []ToolResult) error
|
||||
update(assistant backend.Message, results []toolResult) error
|
||||
|
||||
// MarkComplete records that the goal has been achieved.
|
||||
MarkComplete() error
|
||||
markComplete() error
|
||||
|
||||
// IsComplete returns true once MarkComplete has been called.
|
||||
IsComplete() bool
|
||||
isComplete() bool
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package core
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -8,15 +8,15 @@ import (
|
|||
// ErrInterrupted is returned when the user cancels an agent turn (Ctrl-C).
|
||||
var ErrInterrupted = errors.New("interrupted")
|
||||
|
||||
// Event is a typed output event emitted by the Core during an agent turn
|
||||
// or in response to a command.
|
||||
// Event is a typed output event emitted during an agent turn or in response
|
||||
// to a command.
|
||||
type Event struct {
|
||||
Role string
|
||||
Name string
|
||||
Content string
|
||||
}
|
||||
|
||||
// EventHandler receives events from the Core.
|
||||
// EventHandler receives events from the agent.
|
||||
type EventHandler func(Event)
|
||||
|
||||
// Core is the interface between a frontend (TUI, HTTP handler, etc.) and the
|
||||
Reference in New Issue