Merge agentcore into agent; decouple pkg/core from internal/agent
AgentCore (the Core interface impl) belongs in internal/agent alongside the loop, session, and state it orchestrates — not in a separate one-file package. To break the resulting import cycle (agent → pkg/core → agent), define Event and EventHandler natively in pkg/core instead of aliasing from internal/agent. The agent loop now imports pkg/core for these types. Usage token tracking is internal to the loop and no longer emitted as an event (no consumer used it). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
992c47dd5b
commit
c8d2a7a907
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"ollie/internal/agent"
|
"ollie/internal/agent"
|
||||||
"ollie/internal/agentcore"
|
|
||||||
"ollie/internal/backend"
|
"ollie/internal/backend"
|
||||||
"ollie/internal/config"
|
"ollie/internal/config"
|
||||||
execpkg "ollie/internal/exec"
|
execpkg "ollie/internal/exec"
|
||||||
|
|
@ -36,10 +36,10 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
backendName := agentcore.ResolveBackendName()
|
backendName := agent.ResolveBackendName()
|
||||||
modelName := os.Getenv("OLLIE_MODEL")
|
modelName := os.Getenv("OLLIE_MODEL")
|
||||||
if modelName == "" {
|
if modelName == "" {
|
||||||
modelName = agentcore.DefaultModelForBackend(backendName)
|
modelName = agent.DefaultModelForBackend(backendName)
|
||||||
}
|
}
|
||||||
|
|
||||||
builtinExec := execpkg.New(
|
builtinExec := execpkg.New(
|
||||||
|
|
@ -52,7 +52,7 @@ func main() {
|
||||||
agentName = "default"
|
agentName = "default"
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID := agentcore.NewSessionID()
|
sessionID := agent.NewSessionID()
|
||||||
var resumeMessages []backend.Message
|
var resumeMessages []backend.Message
|
||||||
if *sessionFlag != "" {
|
if *sessionFlag != "" {
|
||||||
sessionPath := sessionsDir + "/" + *sessionFlag + ".json"
|
sessionPath := sessionsDir + "/" + *sessionFlag + ".json"
|
||||||
|
|
@ -76,9 +76,9 @@ func main() {
|
||||||
agentName = extraArgs[0]
|
agentName = extraArgs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgPath := agentcore.AgentConfigPath(agentsDir, agentName)
|
cfgPath := agent.AgentConfigPath(agentsDir, agentName)
|
||||||
cfg, cfgErr := config.Load(cfgPath)
|
cfg, cfgErr := config.Load(cfgPath)
|
||||||
env := agentcore.BuildAgentEnv(cfg, builtinExec)
|
env := agent.BuildAgentEnv(cfg, builtinExec)
|
||||||
|
|
||||||
var initialSession *agent.Session
|
var initialSession *agent.Session
|
||||||
if len(resumeMessages) > 0 {
|
if len(resumeMessages) > 0 {
|
||||||
|
|
@ -87,7 +87,7 @@ func main() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
agentCore := agentcore.NewAgentCore(agentcore.AgentCoreConfig{
|
agentCore := agent.NewAgentCore(agent.AgentCoreConfig{
|
||||||
Backend: be,
|
Backend: be,
|
||||||
BackendName: backendName,
|
BackendName: backendName,
|
||||||
ModelName: modelName,
|
ModelName: modelName,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package agentcore
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -16,7 +16,6 @@ import (
|
||||||
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
|
||||||
"ollie/internal/agent"
|
|
||||||
"ollie/internal/backend"
|
"ollie/internal/backend"
|
||||||
"ollie/internal/config"
|
"ollie/internal/config"
|
||||||
execpkg "ollie/internal/exec"
|
execpkg "ollie/internal/exec"
|
||||||
|
|
@ -218,8 +217,8 @@ var BuiltinTools = []backend.Tool{
|
||||||
type AgentEnv struct {
|
type AgentEnv struct {
|
||||||
McpExec *tools.Executor
|
McpExec *tools.Executor
|
||||||
Tools []backend.Tool
|
Tools []backend.Tool
|
||||||
Exec agent.ToolExecutor
|
Exec ToolExecutor
|
||||||
Confirm *agent.ConfirmFn
|
Confirm *ConfirmFn
|
||||||
Hooks core.Hooks
|
Hooks core.Hooks
|
||||||
SystemPrompt string
|
SystemPrompt string
|
||||||
GenParams backend.GenerationParams
|
GenParams backend.GenerationParams
|
||||||
|
|
@ -298,7 +297,7 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) AgentEnv {
|
||||||
builtinNames[t.Name] = struct{}{}
|
builtinNames[t.Name] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var confirmFn agent.ConfirmFn
|
var confirmFn ConfirmFn
|
||||||
confirmPtr := &confirmFn
|
confirmPtr := &confirmFn
|
||||||
|
|
||||||
fileReadCache := make(map[string]bool)
|
fileReadCache := make(map[string]bool)
|
||||||
|
|
@ -313,7 +312,7 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) AgentEnv {
|
||||||
return extractMCPText(raw), nil
|
return extractMCPText(raw), nil
|
||||||
}
|
}
|
||||||
if _, ok := builtinNames[name]; ok {
|
if _, ok := builtinNames[name]; ok {
|
||||||
var cfn agent.ConfirmFn
|
var cfn ConfirmFn
|
||||||
if _, trusted := trustedTools[name]; !trusted {
|
if _, trusted := trustedTools[name]; !trusted {
|
||||||
cfn = *confirmPtr
|
cfn = *confirmPtr
|
||||||
}
|
}
|
||||||
|
|
@ -484,7 +483,7 @@ type AgentCoreConfig struct {
|
||||||
AgentsDir string
|
AgentsDir string
|
||||||
SessionsDir string
|
SessionsDir string
|
||||||
SessionID string
|
SessionID string
|
||||||
Session *agent.Session
|
Session *Session
|
||||||
Env AgentEnv
|
Env AgentEnv
|
||||||
BuiltinExec *execpkg.Executor
|
BuiltinExec *execpkg.Executor
|
||||||
}
|
}
|
||||||
|
|
@ -492,8 +491,8 @@ type AgentCoreConfig struct {
|
||||||
// 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.
|
// but has no knowledge of how output is rendered.
|
||||||
type AgentCore struct {
|
type AgentCore struct {
|
||||||
session *agent.Session
|
session *Session
|
||||||
loopcfg agent.Config
|
loopcfg Config
|
||||||
hooks core.Hooks
|
hooks core.Hooks
|
||||||
modelName string
|
modelName string
|
||||||
backendName string
|
backendName string
|
||||||
|
|
@ -503,7 +502,7 @@ type AgentCore struct {
|
||||||
sessionID string
|
sessionID string
|
||||||
mcpExec *tools.Executor
|
mcpExec *tools.Executor
|
||||||
builtinExec *execpkg.Executor
|
builtinExec *execpkg.Executor
|
||||||
confirmPtr *agent.ConfirmFn
|
confirmPtr *ConfirmFn
|
||||||
ctxOverhead int
|
ctxOverhead int
|
||||||
invalidateCaches func()
|
invalidateCaches func()
|
||||||
currentAction atomic.Pointer[actionHandle]
|
currentAction atomic.Pointer[actionHandle]
|
||||||
|
|
@ -513,7 +512,7 @@ var _ core.Core = (*AgentCore)(nil) // compile-time interface check
|
||||||
|
|
||||||
// NewAgentCore creates an AgentCore from the given configuration.
|
// NewAgentCore creates an AgentCore from the given configuration.
|
||||||
func NewAgentCore(cfg AgentCoreConfig) *AgentCore {
|
func NewAgentCore(cfg AgentCoreConfig) *AgentCore {
|
||||||
loopcfg := agent.Config{
|
loopcfg := Config{
|
||||||
Backend: cfg.Backend,
|
Backend: cfg.Backend,
|
||||||
Model: cfg.ModelName,
|
Model: cfg.ModelName,
|
||||||
SystemPrompt: cfg.Env.SystemPrompt,
|
SystemPrompt: cfg.Env.SystemPrompt,
|
||||||
|
|
@ -585,7 +584,7 @@ func (s *AgentCore) Submit(ctx context.Context, input string, handler core.Event
|
||||||
s.hooks.Run(core.HookUserPromptSubmit)
|
s.hooks.Run(core.HookUserPromptSubmit)
|
||||||
|
|
||||||
if s.session == nil {
|
if s.session == nil {
|
||||||
s.session = agent.NewSessionWithConfig(BuildFirstPrompt(input), agent.ContextConfig{
|
s.session = NewSessionWithConfig(BuildFirstPrompt(input), ContextConfig{
|
||||||
FixedOverheadChars: s.ctxOverhead,
|
FixedOverheadChars: s.ctxOverhead,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -601,7 +600,7 @@ func (s *AgentCore) Submit(ctx context.Context, input string, handler core.Event
|
||||||
|
|
||||||
s.hooks.Run(core.HookAgentSpawn)
|
s.hooks.Run(core.HookAgentSpawn)
|
||||||
|
|
||||||
err := agent.Run(actCtx, s.loopcfg, s.session)
|
err := Run(actCtx, s.loopcfg, s.session)
|
||||||
actCancel(nil)
|
actCancel(nil)
|
||||||
s.currentAction.CompareAndSwap(handle, nil)
|
s.currentAction.CompareAndSwap(handle, nil)
|
||||||
|
|
||||||
|
|
@ -805,7 +804,7 @@ func (s *AgentCore) handleCommand(ctx context.Context, input string, handler cor
|
||||||
}
|
}
|
||||||
label := id
|
label := id
|
||||||
if data, readErr := os.ReadFile(s.sessionsDir + "/" + e.Name()); readErr == nil {
|
if data, readErr := os.ReadFile(s.sessionsDir + "/" + e.Name()); readErr == nil {
|
||||||
var ps agent.PersistedSession
|
var ps PersistedSession
|
||||||
if json.Unmarshal(data, &ps) == nil {
|
if json.Unmarshal(data, &ps) == nil {
|
||||||
goal := ""
|
goal := ""
|
||||||
for _, msg := range ps.Messages {
|
for _, msg := range ps.Messages {
|
||||||
|
|
@ -884,7 +883,7 @@ func extractMCPText(raw json.RawMessage) string {
|
||||||
return strings.Join(parts, "\n")
|
return strings.Join(parts, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func dispatchBuiltinExec(ctx context.Context, name string, e *execpkg.Executor, confirm agent.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 {
|
switch name {
|
||||||
case "file_read":
|
case "file_read":
|
||||||
return dispatchFileRead(confirm, args)
|
return dispatchFileRead(confirm, args)
|
||||||
|
|
@ -925,7 +924,7 @@ func execArgs(args json.RawMessage) (code, language, sandbox string, timeout int
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func dispatchExecuteCode(ctx context.Context, e *execpkg.Executor, confirm agent.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)
|
code, language, sandbox, timeout, err := execArgs(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("execute_code: bad args: %w", err)
|
return "", fmt.Errorf("execute_code: bad args: %w", err)
|
||||||
|
|
@ -939,7 +938,7 @@ func dispatchExecuteCode(ctx context.Context, e *execpkg.Executor, confirm agent
|
||||||
return e.Execute(ctx, code, language, timeout, sandbox, false)
|
return e.Execute(ctx, code, language, timeout, sandbox, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dispatchExecuteTool(ctx context.Context, e *execpkg.Executor, confirm agent.ConfirmFn, args json.RawMessage) (string, error) {
|
func dispatchExecuteTool(ctx context.Context, e *execpkg.Executor, confirm ConfirmFn, args json.RawMessage) (string, error) {
|
||||||
var a struct {
|
var a struct {
|
||||||
Tool string `json:"tool"`
|
Tool string `json:"tool"`
|
||||||
Args []string `json:"args"`
|
Args []string `json:"args"`
|
||||||
|
|
@ -983,7 +982,7 @@ func dispatchExecuteTool(ctx context.Context, e *execpkg.Executor, confirm agent
|
||||||
return e.Execute(ctx, code, language, timeout, sandbox, true)
|
return e.Execute(ctx, code, language, timeout, sandbox, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dispatchExecutePipe(ctx context.Context, e *execpkg.Executor, confirm agent.ConfirmFn, args json.RawMessage) (string, error) {
|
func dispatchExecutePipe(ctx context.Context, e *execpkg.Executor, confirm ConfirmFn, args json.RawMessage) (string, error) {
|
||||||
var a struct {
|
var a struct {
|
||||||
Pipe []execpkg.PipeStep `json:"pipe"`
|
Pipe []execpkg.PipeStep `json:"pipe"`
|
||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
|
|
@ -1013,7 +1012,7 @@ func dispatchExecutePipe(ctx context.Context, e *execpkg.Executor, confirm agent
|
||||||
return e.Execute(ctx, code, "bash", timeout, sandbox, true)
|
return e.Execute(ctx, code, "bash", timeout, sandbox, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dispatchFileRead(confirm agent.ConfirmFn, args json.RawMessage) (string, error) {
|
func dispatchFileRead(confirm ConfirmFn, args json.RawMessage) (string, error) {
|
||||||
var a struct {
|
var a struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
}
|
}
|
||||||
|
|
@ -1038,7 +1037,7 @@ func dispatchFileRead(confirm agent.ConfirmFn, args json.RawMessage) (string, er
|
||||||
return strings.TrimRight(out.String(), "\n"), nil
|
return strings.TrimRight(out.String(), "\n"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dispatchFileWrite(confirm agent.ConfirmFn, args json.RawMessage) (string, error) {
|
func dispatchFileWrite(confirm ConfirmFn, args json.RawMessage) (string, error) {
|
||||||
var a struct {
|
var a struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
|
@ -9,19 +9,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/internal/backend"
|
"ollie/internal/backend"
|
||||||
|
"ollie/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxRateLimitRetries = 3
|
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 OutputFn func(msg OutputMsg)
|
|
||||||
|
|
||||||
type OutputMsg struct {
|
|
||||||
Role string
|
|
||||||
Name string
|
|
||||||
Content string
|
|
||||||
Usage backend.Usage
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Backend backend.Backend
|
Backend backend.Backend
|
||||||
|
|
@ -30,7 +23,7 @@ type Config struct {
|
||||||
Exec ToolExecutor
|
Exec ToolExecutor
|
||||||
Confirm ConfirmFn
|
Confirm ConfirmFn
|
||||||
MaxSteps int
|
MaxSteps int
|
||||||
Output OutputFn
|
Output core.EventHandler
|
||||||
SystemPrompt string
|
SystemPrompt string
|
||||||
GenerationParams backend.GenerationParams
|
GenerationParams backend.GenerationParams
|
||||||
}
|
}
|
||||||
|
|
@ -77,18 +70,16 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
||||||
|
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
var toolCalls []backend.ToolCall
|
var toolCalls []backend.ToolCall
|
||||||
var usage backend.Usage
|
|
||||||
var stopReason string
|
var stopReason string
|
||||||
var done bool
|
var done bool
|
||||||
|
|
||||||
for ev := range ch {
|
for ev := range ch {
|
||||||
if ev.Content != "" {
|
if ev.Content != "" {
|
||||||
content.WriteString(ev.Content)
|
content.WriteString(ev.Content)
|
||||||
emit(cfg, OutputMsg{Role: "assistant", Content: ev.Content})
|
emit(cfg, core.Event{Role: "assistant", Content: ev.Content})
|
||||||
}
|
}
|
||||||
toolCalls = append(toolCalls, ev.ToolCalls...)
|
toolCalls = append(toolCalls, ev.ToolCalls...)
|
||||||
if ev.Done {
|
if ev.Done {
|
||||||
usage = ev.Usage
|
|
||||||
stopReason = ev.StopReason
|
stopReason = ev.StopReason
|
||||||
done = true
|
done = true
|
||||||
break
|
break
|
||||||
|
|
@ -120,7 +111,7 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
emit(cfg, OutputMsg{Role: "call", Name: tc.Name, Content: string(tc.Arguments)})
|
emit(cfg, core.Event{Role: "call", Name: tc.Name, Content: string(tc.Arguments)})
|
||||||
|
|
||||||
var result string
|
var result string
|
||||||
var isErr bool
|
var isErr bool
|
||||||
|
|
@ -143,13 +134,10 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
||||||
Content: result,
|
Content: result,
|
||||||
IsError: isErr,
|
IsError: isErr,
|
||||||
})
|
})
|
||||||
emit(cfg, OutputMsg{Role: "tool", Name: tc.Name, Content: result})
|
emit(cfg, core.Event{Role: "tool", Name: tc.Name, Content: result})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit usage when we have real token counts.
|
|
||||||
if usage.InputTokens > 0 || usage.OutputTokens > 0 {
|
|
||||||
emit(cfg, OutputMsg{Role: "usage", Usage: usage})
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
return fmt.Errorf("step %d update: %w", step, err)
|
||||||
|
|
@ -169,13 +157,13 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
||||||
|
|
||||||
// Surface stall: only when the step limit was hit without completing.
|
// Surface stall: only when the step limit was hit without completing.
|
||||||
if hitLimit {
|
if hitLimit {
|
||||||
emit(cfg, OutputMsg{Role: "stalled", Content: "max steps"})
|
emit(cfg, core.Event{Role: "stalled", Content: "max steps"})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func emit(cfg Config, msg OutputMsg) {
|
func emit(cfg Config, msg core.Event) {
|
||||||
if cfg.Output != nil {
|
if cfg.Output != nil {
|
||||||
cfg.Output(msg)
|
cfg.Output(msg)
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +180,7 @@ func retryCountdown(ctx context.Context, cfg Config, wait time.Duration) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
secs := int(remaining.Seconds()) + 1
|
secs := int(remaining.Seconds()) + 1
|
||||||
emit(cfg, OutputMsg{Role: "retry", Content: fmt.Sprintf("%d", secs)})
|
emit(cfg, core.Event{Role: "retry", Content: fmt.Sprintf("%d", secs)})
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ package core
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"ollie/internal/agent"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInterrupted is returned when the user cancels an agent turn (Ctrl-C).
|
// ErrInterrupted is returned when the user cancels an agent turn (Ctrl-C).
|
||||||
|
|
@ -12,10 +10,14 @@ var ErrInterrupted = errors.New("interrupted")
|
||||||
|
|
||||||
// Event is a typed output event emitted by the Core during an agent turn
|
// Event is a typed output event emitted by the Core during an agent turn
|
||||||
// or in response to a command.
|
// or in response to a command.
|
||||||
type Event = agent.OutputMsg
|
type Event struct {
|
||||||
|
Role string
|
||||||
|
Name string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
// EventHandler receives events from the Core.
|
// EventHandler receives events from the Core.
|
||||||
type EventHandler = agent.OutputFn
|
type EventHandler func(Event)
|
||||||
|
|
||||||
// Core is the interface between a frontend (TUI, HTTP handler, etc.) and the
|
// Core is the interface between a frontend (TUI, HTTP handler, etc.) and the
|
||||||
// agent engine. All output from the agent is delivered via EventHandler.
|
// agent engine. All output from the agent is delivered via EventHandler.
|
||||||
|
|
|
||||||
Reference in New Issue