all: merge thin packages and small files
- Merge detach/ into toolsrv/detach.go (single consumer) Rename Process → DetachedProcess to avoid collision with spawn.go - Merge agent/state.go into loop.go (types used only there) - Merge agent/runtime.go into runtime.go (formerly build_runtime.go) - Merge agent/compaction.go + models.go into history.go - Merge agent/new.go into agent.go (constructor lives with struct) - Rename ResultTier → MemoryTier (TierHot/Warm/Cold → MemoryHot/Warm/Cold) - Rename build_runtime.go → runtime.go
This commit is contained in:
parent
8152dec075
commit
705a7226a2
|
|
@ -621,3 +621,62 @@ func (ag *Agent) DismissDetached(pid int) bool {
|
||||||
}
|
}
|
||||||
return ag.runtime.ToolServer.DismissDetached(pid)
|
return ag.runtime.ToolServer.DismissDetached(pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AgentCfg is the configuration for constructing a new Agent.
|
||||||
|
type AgentCfg struct {
|
||||||
|
ID string // unique agent identity (uname)
|
||||||
|
SessionID string // id of session the agent belongs to
|
||||||
|
Profile string // config profile name (e.g. "default" → agents/default.json)
|
||||||
|
History *History
|
||||||
|
Runtime *Runtime
|
||||||
|
AgentsDir string
|
||||||
|
Cwd string // working directory for tool execution
|
||||||
|
SystemPrompt string
|
||||||
|
EnvBlock string
|
||||||
|
PromptEnvExtra []string
|
||||||
|
NewToolServer func() *toolsrv.Conn
|
||||||
|
NewBackend func(string) (backend.Backend, error)
|
||||||
|
Output EventHandler
|
||||||
|
Log *olog.Logger
|
||||||
|
AuditLog *olog.Logger
|
||||||
|
StartupMsgs []string
|
||||||
|
ReadPlanStep func() string
|
||||||
|
Save func()
|
||||||
|
Flush func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAgent constructs an Agent from the given configuration.
|
||||||
|
func NewAgent(cfg AgentCfg) *Agent {
|
||||||
|
auditLog := cfg.AuditLog
|
||||||
|
if auditLog == nil && cfg.Log != nil {
|
||||||
|
cfg.Log.Warn("AuditLog not provided for agent %s, deriving from Log", cfg.ID)
|
||||||
|
auditLog = cfg.Log.Sub("audit")
|
||||||
|
}
|
||||||
|
|
||||||
|
ag := &Agent{
|
||||||
|
history: cfg.History,
|
||||||
|
runtime: cfg.Runtime,
|
||||||
|
profile: cfg.Profile,
|
||||||
|
name: cfg.ID, // display name defaults to uname
|
||||||
|
agentsDir: cfg.AgentsDir,
|
||||||
|
id: cfg.ID,
|
||||||
|
cwd: cfg.Cwd,
|
||||||
|
systemPrompt: cfg.SystemPrompt,
|
||||||
|
envBlock: cfg.EnvBlock,
|
||||||
|
promptEnvExtra: cfg.PromptEnvExtra,
|
||||||
|
newToolServer: cfg.NewToolServer,
|
||||||
|
newBackend: cfg.NewBackend,
|
||||||
|
output: cfg.Output,
|
||||||
|
log: cfg.Log,
|
||||||
|
auditLog: auditLog,
|
||||||
|
sessionID: cfg.SessionID,
|
||||||
|
startupMessages: cfg.StartupMsgs,
|
||||||
|
readPlanStep: cfg.ReadPlanStep,
|
||||||
|
save: cfg.Save,
|
||||||
|
flush: cfg.Flush,
|
||||||
|
state: "idle",
|
||||||
|
}
|
||||||
|
ag.changeCond = sync.NewCond(&ag.changeMu)
|
||||||
|
ag.wireToolsChanged()
|
||||||
|
return ag
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,194 +0,0 @@
|
||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"ollie/backend"
|
|
||||||
"ollie/prompts"
|
|
||||||
"ollie/toolsrv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BuildRuntime constructs a Runtime from a pre-configured Dispatcher and
|
|
||||||
// optional agent config. cwd sets the working directory reported in the
|
|
||||||
// system prompt; if empty, the process working directory is used.
|
|
||||||
// env provides additional environment variables injected into prompt resolution
|
|
||||||
// subprocesses (e.g. OLLIE_SESSION_ID=xxx).
|
|
||||||
// The caller is responsible for registering all servers on d before calling this.
|
|
||||||
func BuildRuntime(cfg *AgentConfig, srv *toolsrv.Conn, cwd string, env []string, systemPrompt, envBlock string) *Runtime {
|
|
||||||
var messages []string
|
|
||||||
|
|
||||||
var allToolInfos []toolsrv.ToolInfo
|
|
||||||
var allTools []backend.Tool
|
|
||||||
|
|
||||||
if srv != nil && (cfg == nil || cfg.ToolsEnabled()) {
|
|
||||||
var listErr error
|
|
||||||
allToolInfos, listErr = srv.ListTools()
|
|
||||||
if listErr != nil {
|
|
||||||
messages = append(messages, fmt.Sprintf("list tools: %v", listErr))
|
|
||||||
}
|
|
||||||
// Only loaded tools become backend callable tools.
|
|
||||||
allTools = toolInfosToBackend(allToolInfos)
|
|
||||||
}
|
|
||||||
|
|
||||||
var agentPrompt string
|
|
||||||
var genParams backend.GenerationParams
|
|
||||||
var maxSteps int
|
|
||||||
if cfg != nil {
|
|
||||||
if resolved, err := resolvePrompt(cfg.Prompt, cwd, env); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "resolve prompt: %v\n", err)
|
|
||||||
} else {
|
|
||||||
agentPrompt = resolved
|
|
||||||
}
|
|
||||||
genParams = cfg.GenParams()
|
|
||||||
maxSteps = cfg.MaxSteps
|
|
||||||
if len(cfg.AllowTools) > 0 && srv != nil {
|
|
||||||
srv.SetAllowTools(cfg.AllowTools)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exec := func(ctx context.Context, name string, args json.RawMessage) (string, []backend.ContentBlock, error) {
|
|
||||||
if srv == nil {
|
|
||||||
return "", nil, fmt.Errorf("no tool server available")
|
|
||||||
}
|
|
||||||
raw, err := srv.CallTool(ctx, name, args)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
text, blocks, isErr := extractToolResult(raw)
|
|
||||||
if isErr {
|
|
||||||
return "", nil, fmt.Errorf("%s", text)
|
|
||||||
}
|
|
||||||
return text, blocks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build ToolMeta lookup from the tool listing — single source of truth
|
|
||||||
// for all per-tool metadata (tier, readOnly, outputFormat, resetsCounter).
|
|
||||||
toolMeta := make(map[string]toolsrv.ToolInfo, len(allToolInfos))
|
|
||||||
for _, ti := range allToolInfos {
|
|
||||||
toolMeta[ti.Name] = ti
|
|
||||||
}
|
|
||||||
|
|
||||||
var compactionModel string
|
|
||||||
if cfg != nil {
|
|
||||||
compactionModel = cfg.CompactionModel
|
|
||||||
}
|
|
||||||
|
|
||||||
preamble := buildPreamble(systemPrompt, envBlock, agentPrompt, allToolInfos)
|
|
||||||
|
|
||||||
return &Runtime{
|
|
||||||
ToolServer: srv,
|
|
||||||
Tools: allTools,
|
|
||||||
ToolMeta: toolMeta,
|
|
||||||
Exec: exec,
|
|
||||||
Preamble: preamble,
|
|
||||||
GenParams: genParams,
|
|
||||||
MaxSteps: maxSteps,
|
|
||||||
CompactionModel: compactionModel,
|
|
||||||
Messages: messages,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildPreamble constructs the complete system prompt from its components.
|
|
||||||
// The preamble evolves when:
|
|
||||||
// - Working directory changes (envBlock updates)
|
|
||||||
// - Tools are loaded/unloaded (allToolInfos changes)
|
|
||||||
// - Agent profile switches (agentPrompt changes)
|
|
||||||
// This function should be called whenever any component changes.
|
|
||||||
func buildPreamble(systemPrompt, envBlock, agentPrompt string, allToolInfos []toolsrv.ToolInfo) string {
|
|
||||||
var sb strings.Builder
|
|
||||||
|
|
||||||
// 1. Core system prompt (identity, rules, operating principles)
|
|
||||||
sb.WriteString(systemPrompt)
|
|
||||||
sb.WriteByte('\n')
|
|
||||||
|
|
||||||
// 2. Environment context (working directory, platform, date, etc.)
|
|
||||||
sb.WriteString(envBlock)
|
|
||||||
sb.WriteByte('\n')
|
|
||||||
|
|
||||||
// 3. Agent-specific prompt (role, task instructions from config)
|
|
||||||
sb.WriteString(agentPrompt)
|
|
||||||
sb.WriteByte('\n')
|
|
||||||
|
|
||||||
// 4. Compact tool surface listing (name + description)
|
|
||||||
var toolListing strings.Builder
|
|
||||||
for _, ti := range allToolInfos {
|
|
||||||
if ti.Description != "" && ti.Server == "" {
|
|
||||||
fmt.Fprintf(&toolListing, "- **%s** — %s\n", ti.Name, ti.Description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Full tool documentation (prompts, schemas, examples)
|
|
||||||
var toolDocs strings.Builder
|
|
||||||
for _, ti := range allToolInfos {
|
|
||||||
if ti.Prompt != "" && ti.Server == "" {
|
|
||||||
toolDocs.WriteString(ti.Prompt)
|
|
||||||
toolDocs.WriteString("\n\n")
|
|
||||||
if len(ti.InputSchema) > 0 && string(ti.InputSchema) != "{}" {
|
|
||||||
fmt.Fprintf(&toolDocs, "**Schema**:\n```json\n%s\n```\n\n", ti.InputSchema)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Render tool sections
|
|
||||||
if toolListing.Len() > 0 || toolDocs.Len() > 0 {
|
|
||||||
toolSections := prompts.ToolSections(toolListing.String(), toolDocs.String())
|
|
||||||
if toolSections != "" {
|
|
||||||
sb.WriteString("\n")
|
|
||||||
sb.WriteString(toolSections)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Tool discovery instructions (9P filesystem)
|
|
||||||
sb.WriteString("\n")
|
|
||||||
sb.WriteString(prompts.ToolDiscoveryPrompt)
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func toolInfosToBackend(infos []toolsrv.ToolInfo) []backend.Tool {
|
|
||||||
out := make([]backend.Tool, len(infos))
|
|
||||||
for i, t := range infos {
|
|
||||||
out[i] = backend.Tool{
|
|
||||||
Name: t.Name,
|
|
||||||
Description: t.Description,
|
|
||||||
Parameters: t.InputSchema,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractToolResult(raw json.RawMessage) (text string, contentBlocks []backend.ContentBlock, isError bool) {
|
|
||||||
var result struct {
|
|
||||||
IsError bool `json:"isError"`
|
|
||||||
Content []struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Text string `json:"text"`
|
|
||||||
MediaType string `json:"media_type"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
} `json:"content"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(raw, &result); err != nil {
|
|
||||||
return string(raw), nil, false
|
|
||||||
}
|
|
||||||
var parts []string
|
|
||||||
for _, c := range result.Content {
|
|
||||||
switch c.Type {
|
|
||||||
case "text":
|
|
||||||
parts = append(parts, c.Text)
|
|
||||||
case "image":
|
|
||||||
contentBlocks = append(contentBlocks, backend.ContentBlock{
|
|
||||||
Type: "image",
|
|
||||||
ImageSource: &backend.ImageSource{
|
|
||||||
Type: "base64",
|
|
||||||
MediaType: c.MediaType,
|
|
||||||
Data: c.Data,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(parts, "\n"), contentBlocks, result.IsError
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"ollie/backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
// resolveCompactionModel returns the model to use for compaction.
|
|
||||||
// Priority: agent config > OLLIE_COMPACTION_MODEL env > per-backend default from models.yaml > session's current model.
|
|
||||||
// Returns "" if no override is configured (use the session's current model).
|
|
||||||
func resolveCompactionModel(cfgModel string, b backend.Backend) string {
|
|
||||||
if cfgModel != "" {
|
|
||||||
return cfgModel
|
|
||||||
}
|
|
||||||
if env := os.Getenv("OLLIE_COMPACTION_MODEL"); env != "" {
|
|
||||||
return env
|
|
||||||
}
|
|
||||||
cfg := loadModelsConfig()
|
|
||||||
if b != nil {
|
|
||||||
if m, ok := cfg.Compaction[b.Name()]; ok {
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
@ -5,9 +5,13 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ollie/backend"
|
"ollie/backend"
|
||||||
|
"ollie/paths"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -539,3 +543,45 @@ func NewResponseID() string {
|
||||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||||
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveCompactionModel returns the model to use for compaction.
|
||||||
|
// Priority: agent config > OLLIE_COMPACTION_MODEL env > per-backend default from models.yaml > session's current model.
|
||||||
|
// Returns "" if no override is configured (use the session's current model).
|
||||||
|
func resolveCompactionModel(cfgModel string, b backend.Backend) string {
|
||||||
|
if cfgModel != "" {
|
||||||
|
return cfgModel
|
||||||
|
}
|
||||||
|
if env := os.Getenv("OLLIE_COMPACTION_MODEL"); env != "" {
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
cfg := loadModelsConfig()
|
||||||
|
if b != nil {
|
||||||
|
if m, ok := cfg.Compaction[b.Name()]; ok {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// modelsConfig holds the parsed contents of ~/.config/ollie/models.yaml.
|
||||||
|
type modelsConfig struct {
|
||||||
|
Compaction map[string]string `yaml:"compaction"`
|
||||||
|
Completion struct {
|
||||||
|
Model string `yaml:"model"`
|
||||||
|
Backend string `yaml:"backend"`
|
||||||
|
} `yaml:"completion"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadModelsConfig reads and parses the models config file.
|
||||||
|
// Returns zero value if the file doesn't exist or is invalid.
|
||||||
|
func loadModelsConfig() modelsConfig {
|
||||||
|
data, err := os.ReadFile(paths.CfgDir() + "/models.yaml")
|
||||||
|
if err != nil {
|
||||||
|
return modelsConfig{}
|
||||||
|
}
|
||||||
|
var cfg modelsConfig
|
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return modelsConfig{}
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,16 +105,16 @@ func toolOutputFormat(rt *Runtime, name string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func toolMemoryTier(rt *Runtime, name string) ResultTier {
|
func toolMemoryTier(rt *Runtime, name string) MemoryTier {
|
||||||
if ti, ok := rt.ToolMeta[name]; ok {
|
if ti, ok := rt.ToolMeta[name]; ok {
|
||||||
switch ti.Tier {
|
switch ti.Tier {
|
||||||
case "cold":
|
case "cold":
|
||||||
return TierCold
|
return MemoryCold
|
||||||
case "warm":
|
case "warm":
|
||||||
return TierWarm
|
return MemoryWarm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return TierHot
|
return MemoryHot
|
||||||
}
|
}
|
||||||
|
|
||||||
// TurnCtx holds per-turn closures and state that vary between turns within
|
// TurnCtx holds per-turn closures and state that vary between turns within
|
||||||
|
|
@ -431,7 +431,7 @@ func run(rt *Runtime, ctx TurnCtx, h *History) error {
|
||||||
// if the agent turn ends or is interrupted.
|
// if the agent turn ends or is interrupted.
|
||||||
emit(ctx, Event{Role: "tool", Name: call.Name, OutputFormat: toolOutputFormat(rt, call.Name)})
|
emit(ctx, Event{Role: "tool", Name: call.Name, OutputFormat: toolOutputFormat(rt, call.Name)})
|
||||||
}
|
}
|
||||||
tier := TierHot
|
tier := MemoryHot
|
||||||
if !isErr {
|
if !isErr {
|
||||||
tier = toolMemoryTier(rt, call.Name)
|
tier = toolMemoryTier(rt, call.Name)
|
||||||
}
|
}
|
||||||
|
|
@ -1092,3 +1092,22 @@ func retryCountdown(ctx TurnCtx, wait time.Duration) error {
|
||||||
const defaultToolResultMaxBytes = 131072
|
const defaultToolResultMaxBytes = 131072
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// MemoryTier classifies how long a tool result stays in the hot message list.
|
||||||
|
type MemoryTier int
|
||||||
|
|
||||||
|
const (
|
||||||
|
MemoryHot MemoryTier = iota // stays verbatim in messages (default)
|
||||||
|
MemoryWarm // summarized on next compaction pass
|
||||||
|
MemoryCold // immediately summarized at update time
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
ContentBlocks []backend.ContentBlock // image blocks extracted from tool output
|
||||||
|
IsError bool
|
||||||
|
Tier MemoryTier // default MemoryHot; tools may self-classify
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"ollie/paths"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// modelsConfig holds the parsed contents of ~/.config/ollie/models.yaml.
|
|
||||||
type modelsConfig struct {
|
|
||||||
Compaction map[string]string `yaml:"compaction"`
|
|
||||||
Completion struct {
|
|
||||||
Model string `yaml:"model"`
|
|
||||||
Backend string `yaml:"backend"`
|
|
||||||
} `yaml:"completion"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadModelsConfig reads and parses the models config file.
|
|
||||||
// Returns zero value if the file doesn't exist or is invalid.
|
|
||||||
func loadModelsConfig() modelsConfig {
|
|
||||||
data, err := os.ReadFile(paths.CfgDir() + "/models.yaml")
|
|
||||||
if err != nil {
|
|
||||||
return modelsConfig{}
|
|
||||||
}
|
|
||||||
var cfg modelsConfig
|
|
||||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
||||||
return modelsConfig{}
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
68
agent/new.go
68
agent/new.go
|
|
@ -1,68 +0,0 @@
|
||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"ollie/backend"
|
|
||||||
olog "ollie/log"
|
|
||||||
"ollie/toolsrv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AgentCfg is the configuration for constructing a new Agent.
|
|
||||||
type AgentCfg struct {
|
|
||||||
ID string // unique agent identity (uname)
|
|
||||||
SessionID string // id of session the agent belongs to
|
|
||||||
Profile string // config profile name (e.g. "default" → agents/default.json)
|
|
||||||
History *History
|
|
||||||
Runtime *Runtime
|
|
||||||
AgentsDir string
|
|
||||||
Cwd string // working directory for tool execution
|
|
||||||
SystemPrompt string
|
|
||||||
EnvBlock string
|
|
||||||
PromptEnvExtra []string
|
|
||||||
NewToolServer func() *toolsrv.Conn
|
|
||||||
NewBackend func(string) (backend.Backend, error)
|
|
||||||
Output EventHandler
|
|
||||||
Log *olog.Logger
|
|
||||||
AuditLog *olog.Logger
|
|
||||||
StartupMsgs []string
|
|
||||||
ReadPlanStep func() string
|
|
||||||
Save func()
|
|
||||||
Flush func()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAgent constructs an Agent from the given configuration.
|
|
||||||
func NewAgent(cfg AgentCfg) *Agent {
|
|
||||||
auditLog := cfg.AuditLog
|
|
||||||
if auditLog == nil && cfg.Log != nil {
|
|
||||||
cfg.Log.Warn("AuditLog not provided for agent %s, deriving from Log", cfg.ID)
|
|
||||||
auditLog = cfg.Log.Sub("audit")
|
|
||||||
}
|
|
||||||
|
|
||||||
ag := &Agent{
|
|
||||||
history: cfg.History,
|
|
||||||
runtime: cfg.Runtime,
|
|
||||||
profile: cfg.Profile,
|
|
||||||
name: cfg.ID, // display name defaults to uname
|
|
||||||
agentsDir: cfg.AgentsDir,
|
|
||||||
id: cfg.ID,
|
|
||||||
cwd: cfg.Cwd,
|
|
||||||
systemPrompt: cfg.SystemPrompt,
|
|
||||||
envBlock: cfg.EnvBlock,
|
|
||||||
promptEnvExtra: cfg.PromptEnvExtra,
|
|
||||||
newToolServer: cfg.NewToolServer,
|
|
||||||
newBackend: cfg.NewBackend,
|
|
||||||
output: cfg.Output,
|
|
||||||
log: cfg.Log,
|
|
||||||
auditLog: auditLog,
|
|
||||||
sessionID: cfg.SessionID,
|
|
||||||
startupMessages: cfg.StartupMsgs,
|
|
||||||
readPlanStep: cfg.ReadPlanStep,
|
|
||||||
save: cfg.Save,
|
|
||||||
flush: cfg.Flush,
|
|
||||||
state: "idle",
|
|
||||||
}
|
|
||||||
ag.changeCond = sync.NewCond(&ag.changeMu)
|
|
||||||
ag.wireToolsChanged()
|
|
||||||
return ag
|
|
||||||
}
|
|
||||||
188
agent/runtime.go
188
agent/runtime.go
|
|
@ -1,10 +1,198 @@
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"ollie/backend"
|
"ollie/backend"
|
||||||
|
"ollie/prompts"
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BuildRuntime constructs a Runtime from a pre-configured Dispatcher and
|
||||||
|
// optional agent config. cwd sets the working directory reported in the
|
||||||
|
// system prompt; if empty, the process working directory is used.
|
||||||
|
// env provides additional environment variables injected into prompt resolution
|
||||||
|
// subprocesses (e.g. OLLIE_SESSION_ID=xxx).
|
||||||
|
// The caller is responsible for registering all servers on d before calling this.
|
||||||
|
func BuildRuntime(cfg *AgentConfig, srv *toolsrv.Conn, cwd string, env []string, systemPrompt, envBlock string) *Runtime {
|
||||||
|
var messages []string
|
||||||
|
|
||||||
|
var allToolInfos []toolsrv.ToolInfo
|
||||||
|
var allTools []backend.Tool
|
||||||
|
|
||||||
|
if srv != nil && (cfg == nil || cfg.ToolsEnabled()) {
|
||||||
|
var listErr error
|
||||||
|
allToolInfos, listErr = srv.ListTools()
|
||||||
|
if listErr != nil {
|
||||||
|
messages = append(messages, fmt.Sprintf("list tools: %v", listErr))
|
||||||
|
}
|
||||||
|
// Only loaded tools become backend callable tools.
|
||||||
|
allTools = toolInfosToBackend(allToolInfos)
|
||||||
|
}
|
||||||
|
|
||||||
|
var agentPrompt string
|
||||||
|
var genParams backend.GenerationParams
|
||||||
|
var maxSteps int
|
||||||
|
if cfg != nil {
|
||||||
|
if resolved, err := resolvePrompt(cfg.Prompt, cwd, env); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "resolve prompt: %v\n", err)
|
||||||
|
} else {
|
||||||
|
agentPrompt = resolved
|
||||||
|
}
|
||||||
|
genParams = cfg.GenParams()
|
||||||
|
maxSteps = cfg.MaxSteps
|
||||||
|
if len(cfg.AllowTools) > 0 && srv != nil {
|
||||||
|
srv.SetAllowTools(cfg.AllowTools)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exec := func(ctx context.Context, name string, args json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
|
if srv == nil {
|
||||||
|
return "", nil, fmt.Errorf("no tool server available")
|
||||||
|
}
|
||||||
|
raw, err := srv.CallTool(ctx, name, args)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
text, blocks, isErr := extractToolResult(raw)
|
||||||
|
if isErr {
|
||||||
|
return "", nil, fmt.Errorf("%s", text)
|
||||||
|
}
|
||||||
|
return text, blocks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build ToolMeta lookup from the tool listing — single source of truth
|
||||||
|
// for all per-tool metadata (tier, readOnly, outputFormat, resetsCounter).
|
||||||
|
toolMeta := make(map[string]toolsrv.ToolInfo, len(allToolInfos))
|
||||||
|
for _, ti := range allToolInfos {
|
||||||
|
toolMeta[ti.Name] = ti
|
||||||
|
}
|
||||||
|
|
||||||
|
var compactionModel string
|
||||||
|
if cfg != nil {
|
||||||
|
compactionModel = cfg.CompactionModel
|
||||||
|
}
|
||||||
|
|
||||||
|
preamble := buildPreamble(systemPrompt, envBlock, agentPrompt, allToolInfos)
|
||||||
|
|
||||||
|
return &Runtime{
|
||||||
|
ToolServer: srv,
|
||||||
|
Tools: allTools,
|
||||||
|
ToolMeta: toolMeta,
|
||||||
|
Exec: exec,
|
||||||
|
Preamble: preamble,
|
||||||
|
GenParams: genParams,
|
||||||
|
MaxSteps: maxSteps,
|
||||||
|
CompactionModel: compactionModel,
|
||||||
|
Messages: messages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildPreamble constructs the complete system prompt from its components.
|
||||||
|
// The preamble evolves when:
|
||||||
|
// - Working directory changes (envBlock updates)
|
||||||
|
// - Tools are loaded/unloaded (allToolInfos changes)
|
||||||
|
// - Agent profile switches (agentPrompt changes)
|
||||||
|
// This function should be called whenever any component changes.
|
||||||
|
func buildPreamble(systemPrompt, envBlock, agentPrompt string, allToolInfos []toolsrv.ToolInfo) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
// 1. Core system prompt (identity, rules, operating principles)
|
||||||
|
sb.WriteString(systemPrompt)
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
|
||||||
|
// 2. Environment context (working directory, platform, date, etc.)
|
||||||
|
sb.WriteString(envBlock)
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
|
||||||
|
// 3. Agent-specific prompt (role, task instructions from config)
|
||||||
|
sb.WriteString(agentPrompt)
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
|
||||||
|
// 4. Compact tool surface listing (name + description)
|
||||||
|
var toolListing strings.Builder
|
||||||
|
for _, ti := range allToolInfos {
|
||||||
|
if ti.Description != "" && ti.Server == "" {
|
||||||
|
fmt.Fprintf(&toolListing, "- **%s** — %s\n", ti.Name, ti.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Full tool documentation (prompts, schemas, examples)
|
||||||
|
var toolDocs strings.Builder
|
||||||
|
for _, ti := range allToolInfos {
|
||||||
|
if ti.Prompt != "" && ti.Server == "" {
|
||||||
|
toolDocs.WriteString(ti.Prompt)
|
||||||
|
toolDocs.WriteString("\n\n")
|
||||||
|
if len(ti.InputSchema) > 0 && string(ti.InputSchema) != "{}" {
|
||||||
|
fmt.Fprintf(&toolDocs, "**Schema**:\n```json\n%s\n```\n\n", ti.InputSchema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Render tool sections
|
||||||
|
if toolListing.Len() > 0 || toolDocs.Len() > 0 {
|
||||||
|
toolSections := prompts.ToolSections(toolListing.String(), toolDocs.String())
|
||||||
|
if toolSections != "" {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
sb.WriteString(toolSections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Tool discovery instructions (9P filesystem)
|
||||||
|
sb.WriteString("\n")
|
||||||
|
sb.WriteString(prompts.ToolDiscoveryPrompt)
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolInfosToBackend(infos []toolsrv.ToolInfo) []backend.Tool {
|
||||||
|
out := make([]backend.Tool, len(infos))
|
||||||
|
for i, t := range infos {
|
||||||
|
out[i] = backend.Tool{
|
||||||
|
Name: t.Name,
|
||||||
|
Description: t.Description,
|
||||||
|
Parameters: t.InputSchema,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractToolResult(raw json.RawMessage) (text string, contentBlocks []backend.ContentBlock, isError bool) {
|
||||||
|
var result struct {
|
||||||
|
IsError bool `json:"isError"`
|
||||||
|
Content []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
MediaType string `json:"media_type"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
} `json:"content"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &result); err != nil {
|
||||||
|
return string(raw), nil, false
|
||||||
|
}
|
||||||
|
var parts []string
|
||||||
|
for _, c := range result.Content {
|
||||||
|
switch c.Type {
|
||||||
|
case "text":
|
||||||
|
parts = append(parts, c.Text)
|
||||||
|
case "image":
|
||||||
|
contentBlocks = append(contentBlocks, backend.ContentBlock{
|
||||||
|
Type: "image",
|
||||||
|
ImageSource: &backend.ImageSource{
|
||||||
|
Type: "base64",
|
||||||
|
MediaType: c.MediaType,
|
||||||
|
Data: c.Data,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "\n"), contentBlocks, result.IsError
|
||||||
|
}
|
||||||
|
|
||||||
// Runtime holds the swappable per-agent configuration. It contains everything
|
// Runtime holds the swappable per-agent configuration. It contains everything
|
||||||
// that changes on an /agent switch but is stable across turns within the same
|
// that changes on an /agent switch but is stable across turns within the same
|
||||||
// agent. The agent struct stores a pointer to the active Runtime; switching
|
// agent. The agent struct stores a pointer to the active Runtime; switching
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"ollie/backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResultTier classifies how long a tool result stays in the hot message list.
|
|
||||||
type ResultTier int
|
|
||||||
|
|
||||||
const (
|
|
||||||
TierHot ResultTier = iota // stays verbatim in messages (default)
|
|
||||||
TierWarm // summarized on next compaction pass
|
|
||||||
TierCold // immediately summarized at update time
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
ContentBlocks []backend.ContentBlock // image blocks extracted from tool output
|
|
||||||
IsError bool
|
|
||||||
Tier ResultTier // default TierHot; tools may self-classify
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package detach
|
package toolsrv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
@ -7,9 +7,9 @@ import (
|
||||||
|
|
||||||
const RingBufSize = 64 * 1024 // 64KB ring buffer per detached process
|
const RingBufSize = 64 * 1024 // 64KB ring buffer per detached process
|
||||||
|
|
||||||
// Process represents a process that the agent has detached from
|
// DetachedProcess represents a process that the agent has detached from
|
||||||
// but which continues running. The user can view its output and signal it.
|
// but which continues running. The user can view its output and signal it.
|
||||||
type Process struct {
|
type DetachedProcess struct {
|
||||||
PID int
|
PID int
|
||||||
Command string
|
Command string
|
||||||
Started time.Time
|
Started time.Time
|
||||||
|
|
@ -30,7 +30,7 @@ type InfoData struct {
|
||||||
ExitCode int
|
ExitCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Process) Info() InfoData {
|
func (p *DetachedProcess) Info() InfoData {
|
||||||
p.Mu.Lock()
|
p.Mu.Lock()
|
||||||
defer p.Mu.Unlock()
|
defer p.Mu.Unlock()
|
||||||
return InfoData{
|
return InfoData{
|
||||||
|
|
@ -43,7 +43,7 @@ func (p *Process) Info() InfoData {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output returns the current contents of the ring buffer.
|
// Output returns the current contents of the ring buffer.
|
||||||
func (p *Process) Output() string {
|
func (p *DetachedProcess) Output() string {
|
||||||
p.Mu.Lock()
|
p.Mu.Lock()
|
||||||
defer p.Mu.Unlock()
|
defer p.Mu.Unlock()
|
||||||
return p.Ring.String()
|
return p.Ring.String()
|
||||||
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/detach"
|
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -73,7 +72,7 @@ type Server struct {
|
||||||
// Detached process management
|
// Detached process management
|
||||||
detachMu sync.Mutex
|
detachMu sync.Mutex
|
||||||
detachCh chan struct{} // signal to detach the currently running process
|
detachCh chan struct{} // signal to detach the currently running process
|
||||||
detached []*detach.Process
|
detached []*DetachedProcess
|
||||||
OnDetach func(pid int, cmd string) // hook: called when a process is detached
|
OnDetach func(pid int, cmd string) // hook: called when a process is detached
|
||||||
OnExit func(pid int, exitCode int) // hook: called when a detached process exits
|
OnExit func(pid int, exitCode int) // hook: called when a detached process exits
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/detach"
|
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
"ollie/sandbox"
|
"ollie/sandbox"
|
||||||
)
|
)
|
||||||
|
|
@ -241,10 +240,10 @@ func (s *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeo
|
||||||
if len(cmdStr) > 80 {
|
if len(cmdStr) > 80 {
|
||||||
cmdStr = cmdStr[:77] + "..."
|
cmdStr = cmdStr[:77] + "..."
|
||||||
}
|
}
|
||||||
ring := detach.NewRingBuffer(detach.RingBufSize)
|
ring := NewRingBuffer(RingBufSize)
|
||||||
pid := int(time.Now().UnixNano() & 0x7FFFFFFF) // synthetic PID
|
pid := int(time.Now().UnixNano() & 0x7FFFFFFF) // synthetic PID
|
||||||
|
|
||||||
proc := &detach.Process{
|
proc := &DetachedProcess{
|
||||||
PID: pid,
|
PID: pid,
|
||||||
Command: cmdStr,
|
Command: cmdStr,
|
||||||
Started: time.Now(),
|
Started: time.Now(),
|
||||||
|
|
@ -327,7 +326,7 @@ func (s *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeo
|
||||||
if len(cmdStr) > 80 {
|
if len(cmdStr) > 80 {
|
||||||
cmdStr = cmdStr[:77] + "..."
|
cmdStr = cmdStr[:77] + "..."
|
||||||
}
|
}
|
||||||
ring := detach.NewRingBuffer(detach.RingBufSize)
|
ring := NewRingBuffer(RingBufSize)
|
||||||
pid := int(time.Now().UnixNano() & 0x7FFFFFFF)
|
pid := int(time.Now().UnixNano() & 0x7FFFFFFF)
|
||||||
|
|
||||||
// Splice: future output goes to ring buffer, stop streaming
|
// Splice: future output goes to ring buffer, stop streaming
|
||||||
|
|
@ -336,7 +335,7 @@ func (s *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeo
|
||||||
lw.stream = nil
|
lw.stream = nil
|
||||||
lw.mu.Unlock()
|
lw.mu.Unlock()
|
||||||
|
|
||||||
proc := &detach.Process{
|
proc := &DetachedProcess{
|
||||||
PID: pid,
|
PID: pid,
|
||||||
Command: cmdStr,
|
Command: cmdStr,
|
||||||
Started: time.Now(),
|
Started: time.Now(),
|
||||||
|
|
@ -601,14 +600,14 @@ func (s *Server) executeWithStdin(ctx context.Context, code, language string, ti
|
||||||
if len(cmdStr) > 80 {
|
if len(cmdStr) > 80 {
|
||||||
cmdStr = cmdStr[:77] + "..."
|
cmdStr = cmdStr[:77] + "..."
|
||||||
}
|
}
|
||||||
ring := detach.NewRingBuffer(detach.RingBufSize)
|
ring := NewRingBuffer(RingBufSize)
|
||||||
// Splice: future output goes to ring buffer instead of outputBuf.
|
// Splice: future output goes to ring buffer instead of outputBuf.
|
||||||
lw.mu.Lock()
|
lw.mu.Lock()
|
||||||
lw.w = ring
|
lw.w = ring
|
||||||
lw.stream = nil
|
lw.stream = nil
|
||||||
lw.mu.Unlock()
|
lw.mu.Unlock()
|
||||||
|
|
||||||
proc := &detach.Process{
|
proc := &DetachedProcess{
|
||||||
PID: cmd.Process.Pid,
|
PID: cmd.Process.Pid,
|
||||||
Command: cmdStr,
|
Command: cmdStr,
|
||||||
Started: time.Now(),
|
Started: time.Now(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue