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"
|
||||
|
||||
"ollie/internal/agent"
|
||||
"ollie/internal/agentcore"
|
||||
|
||||
"ollie/internal/backend"
|
||||
"ollie/internal/config"
|
||||
execpkg "ollie/internal/exec"
|
||||
|
|
@ -36,10 +36,10 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
backendName := agentcore.ResolveBackendName()
|
||||
backendName := agent.ResolveBackendName()
|
||||
modelName := os.Getenv("OLLIE_MODEL")
|
||||
if modelName == "" {
|
||||
modelName = agentcore.DefaultModelForBackend(backendName)
|
||||
modelName = agent.DefaultModelForBackend(backendName)
|
||||
}
|
||||
|
||||
builtinExec := execpkg.New(
|
||||
|
|
@ -52,7 +52,7 @@ func main() {
|
|||
agentName = "default"
|
||||
}
|
||||
|
||||
sessionID := agentcore.NewSessionID()
|
||||
sessionID := agent.NewSessionID()
|
||||
var resumeMessages []backend.Message
|
||||
if *sessionFlag != "" {
|
||||
sessionPath := sessionsDir + "/" + *sessionFlag + ".json"
|
||||
|
|
@ -76,9 +76,9 @@ func main() {
|
|||
agentName = extraArgs[0]
|
||||
}
|
||||
|
||||
cfgPath := agentcore.AgentConfigPath(agentsDir, agentName)
|
||||
cfgPath := agent.AgentConfigPath(agentsDir, agentName)
|
||||
cfg, cfgErr := config.Load(cfgPath)
|
||||
env := agentcore.BuildAgentEnv(cfg, builtinExec)
|
||||
env := agent.BuildAgentEnv(cfg, builtinExec)
|
||||
|
||||
var initialSession *agent.Session
|
||||
if len(resumeMessages) > 0 {
|
||||
|
|
@ -87,7 +87,7 @@ func main() {
|
|||
})
|
||||
}
|
||||
|
||||
agentCore := agentcore.NewAgentCore(agentcore.AgentCoreConfig{
|
||||
agentCore := agent.NewAgentCore(agent.AgentCoreConfig{
|
||||
Backend: be,
|
||||
BackendName: backendName,
|
||||
ModelName: modelName,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package agentcore
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -16,7 +16,6 @@ import (
|
|||
|
||||
"crypto/rand"
|
||||
|
||||
"ollie/internal/agent"
|
||||
"ollie/internal/backend"
|
||||
"ollie/internal/config"
|
||||
execpkg "ollie/internal/exec"
|
||||
|
|
@ -218,8 +217,8 @@ var BuiltinTools = []backend.Tool{
|
|||
type AgentEnv struct {
|
||||
McpExec *tools.Executor
|
||||
Tools []backend.Tool
|
||||
Exec agent.ToolExecutor
|
||||
Confirm *agent.ConfirmFn
|
||||
Exec ToolExecutor
|
||||
Confirm *ConfirmFn
|
||||
Hooks core.Hooks
|
||||
SystemPrompt string
|
||||
GenParams backend.GenerationParams
|
||||
|
|
@ -298,7 +297,7 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) AgentEnv {
|
|||
builtinNames[t.Name] = struct{}{}
|
||||
}
|
||||
|
||||
var confirmFn agent.ConfirmFn
|
||||
var confirmFn ConfirmFn
|
||||
confirmPtr := &confirmFn
|
||||
|
||||
fileReadCache := make(map[string]bool)
|
||||
|
|
@ -313,7 +312,7 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) AgentEnv {
|
|||
return extractMCPText(raw), nil
|
||||
}
|
||||
if _, ok := builtinNames[name]; ok {
|
||||
var cfn agent.ConfirmFn
|
||||
var cfn ConfirmFn
|
||||
if _, trusted := trustedTools[name]; !trusted {
|
||||
cfn = *confirmPtr
|
||||
}
|
||||
|
|
@ -484,7 +483,7 @@ type AgentCoreConfig struct {
|
|||
AgentsDir string
|
||||
SessionsDir string
|
||||
SessionID string
|
||||
Session *agent.Session
|
||||
Session *Session
|
||||
Env AgentEnv
|
||||
BuiltinExec *execpkg.Executor
|
||||
}
|
||||
|
|
@ -492,8 +491,8 @@ type AgentCoreConfig struct {
|
|||
// AgentCore is the Core implementation. It owns all agent and session state
|
||||
// but has no knowledge of how output is rendered.
|
||||
type AgentCore struct {
|
||||
session *agent.Session
|
||||
loopcfg agent.Config
|
||||
session *Session
|
||||
loopcfg Config
|
||||
hooks core.Hooks
|
||||
modelName string
|
||||
backendName string
|
||||
|
|
@ -503,7 +502,7 @@ type AgentCore struct {
|
|||
sessionID string
|
||||
mcpExec *tools.Executor
|
||||
builtinExec *execpkg.Executor
|
||||
confirmPtr *agent.ConfirmFn
|
||||
confirmPtr *ConfirmFn
|
||||
ctxOverhead int
|
||||
invalidateCaches func()
|
||||
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.
|
||||
func NewAgentCore(cfg AgentCoreConfig) *AgentCore {
|
||||
loopcfg := agent.Config{
|
||||
loopcfg := Config{
|
||||
Backend: cfg.Backend,
|
||||
Model: cfg.ModelName,
|
||||
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)
|
||||
|
||||
if s.session == nil {
|
||||
s.session = agent.NewSessionWithConfig(BuildFirstPrompt(input), agent.ContextConfig{
|
||||
s.session = NewSessionWithConfig(BuildFirstPrompt(input), ContextConfig{
|
||||
FixedOverheadChars: s.ctxOverhead,
|
||||
})
|
||||
} else {
|
||||
|
|
@ -601,7 +600,7 @@ func (s *AgentCore) Submit(ctx context.Context, input string, handler core.Event
|
|||
|
||||
s.hooks.Run(core.HookAgentSpawn)
|
||||
|
||||
err := agent.Run(actCtx, s.loopcfg, s.session)
|
||||
err := Run(actCtx, s.loopcfg, s.session)
|
||||
actCancel(nil)
|
||||
s.currentAction.CompareAndSwap(handle, nil)
|
||||
|
||||
|
|
@ -805,7 +804,7 @@ func (s *AgentCore) handleCommand(ctx context.Context, input string, handler cor
|
|||
}
|
||||
label := id
|
||||
if data, readErr := os.ReadFile(s.sessionsDir + "/" + e.Name()); readErr == nil {
|
||||
var ps agent.PersistedSession
|
||||
var ps PersistedSession
|
||||
if json.Unmarshal(data, &ps) == nil {
|
||||
goal := ""
|
||||
for _, msg := range ps.Messages {
|
||||
|
|
@ -884,7 +883,7 @@ func extractMCPText(raw json.RawMessage) string {
|
|||
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 {
|
||||
case "file_read":
|
||||
return dispatchFileRead(confirm, args)
|
||||
|
|
@ -925,7 +924,7 @@ func execArgs(args json.RawMessage) (code, language, sandbox string, timeout int
|
|||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
Tool string `json:"tool"`
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
Pipe []execpkg.PipeStep `json:"pipe"`
|
||||
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)
|
||||
}
|
||||
|
||||
func dispatchFileRead(confirm agent.ConfirmFn, args json.RawMessage) (string, error) {
|
||||
func dispatchFileRead(confirm ConfirmFn, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
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
|
||||
}
|
||||
|
||||
func dispatchFileWrite(confirm agent.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"`
|
||||
|
|
@ -9,19 +9,12 @@ 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 OutputFn func(msg OutputMsg)
|
||||
|
||||
type OutputMsg struct {
|
||||
Role string
|
||||
Name string
|
||||
Content string
|
||||
Usage backend.Usage
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Backend backend.Backend
|
||||
|
|
@ -30,7 +23,7 @@ type Config struct {
|
|||
Exec ToolExecutor
|
||||
Confirm ConfirmFn
|
||||
MaxSteps int
|
||||
Output OutputFn
|
||||
Output core.EventHandler
|
||||
SystemPrompt string
|
||||
GenerationParams backend.GenerationParams
|
||||
}
|
||||
|
|
@ -77,18 +70,16 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
|
||||
var content strings.Builder
|
||||
var toolCalls []backend.ToolCall
|
||||
var usage backend.Usage
|
||||
var stopReason string
|
||||
var done bool
|
||||
|
||||
for ev := range ch {
|
||||
if 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...)
|
||||
if ev.Done {
|
||||
usage = ev.Usage
|
||||
stopReason = ev.StopReason
|
||||
done = true
|
||||
break
|
||||
|
|
@ -120,7 +111,7 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
})
|
||||
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 isErr bool
|
||||
|
|
@ -143,13 +134,10 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
Content: result,
|
||||
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 {
|
||||
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.
|
||||
if hitLimit {
|
||||
emit(cfg, OutputMsg{Role: "stalled", Content: "max steps"})
|
||||
emit(cfg, core.Event{Role: "stalled", Content: "max steps"})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func emit(cfg Config, msg OutputMsg) {
|
||||
func emit(cfg Config, msg core.Event) {
|
||||
if cfg.Output != nil {
|
||||
cfg.Output(msg)
|
||||
}
|
||||
|
|
@ -192,7 +180,7 @@ func retryCountdown(ctx context.Context, cfg Config, wait time.Duration) error {
|
|||
return nil
|
||||
}
|
||||
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 {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ package core
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"ollie/internal/agent"
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
type EventHandler = agent.OutputFn
|
||||
type EventHandler func(Event)
|
||||
|
||||
// Core is the interface between a frontend (TUI, HTTP handler, etc.) and the
|
||||
// agent engine. All output from the agent is delivered via EventHandler.
|
||||
|
|
|
|||
Reference in New Issue