From 705a7226a2a6b9efc7762512733bd526ff764eea Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Sat, 8 Aug 2026 15:56:26 +0200 Subject: [PATCH] all: merge thin packages and small files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- agent/agent.go | 59 ++++++ agent/build_runtime.go | 194 ------------------ agent/compaction.go | 26 --- agent/history.go | 46 +++++ agent/loop.go | 29 ++- agent/models.go | 32 --- agent/new.go | 68 ------ agent/runtime.go | 188 +++++++++++++++++ ...{build_runtime_test.go => runtime_test.go} | 0 agent/state.go | 24 --- detach/process.go => toolsrv/detach.go | 10 +- toolsrv/server.go | 3 +- toolsrv/shell.go | 13 +- 13 files changed, 329 insertions(+), 363 deletions(-) delete mode 100644 agent/build_runtime.go delete mode 100644 agent/compaction.go delete mode 100644 agent/models.go delete mode 100644 agent/new.go rename agent/{build_runtime_test.go => runtime_test.go} (100%) delete mode 100644 agent/state.go rename detach/process.go => toolsrv/detach.go (89%) diff --git a/agent/agent.go b/agent/agent.go index 5a0bcd6..259c4f1 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -621,3 +621,62 @@ func (ag *Agent) DismissDetached(pid int) bool { } 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 +} diff --git a/agent/build_runtime.go b/agent/build_runtime.go deleted file mode 100644 index 53ade92..0000000 --- a/agent/build_runtime.go +++ /dev/null @@ -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 -} diff --git a/agent/compaction.go b/agent/compaction.go deleted file mode 100644 index 9ff78f6..0000000 --- a/agent/compaction.go +++ /dev/null @@ -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 "" -} diff --git a/agent/history.go b/agent/history.go index 2f41c07..411b402 100644 --- a/agent/history.go +++ b/agent/history.go @@ -5,9 +5,13 @@ import ( "crypto/rand" "encoding/json" "fmt" + "os" "strings" "ollie/backend" + "ollie/paths" + + "gopkg.in/yaml.v3" ) const ( @@ -539,3 +543,45 @@ func NewResponseID() string { return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", 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 +} diff --git a/agent/loop.go b/agent/loop.go index ed1849d..9985d5c 100644 --- a/agent/loop.go +++ b/agent/loop.go @@ -105,16 +105,16 @@ func toolOutputFormat(rt *Runtime, name string) string { return "" } -func toolMemoryTier(rt *Runtime, name string) ResultTier { +func toolMemoryTier(rt *Runtime, name string) MemoryTier { if ti, ok := rt.ToolMeta[name]; ok { switch ti.Tier { case "cold": - return TierCold + return MemoryCold case "warm": - return TierWarm + return MemoryWarm } } - return TierHot + return MemoryHot } // 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. emit(ctx, Event{Role: "tool", Name: call.Name, OutputFormat: toolOutputFormat(rt, call.Name)}) } - tier := TierHot + tier := MemoryHot if !isErr { tier = toolMemoryTier(rt, call.Name) } @@ -1092,3 +1092,22 @@ func retryCountdown(ctx TurnCtx, wait time.Duration) error { 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 +} diff --git a/agent/models.go b/agent/models.go deleted file mode 100644 index 34a74c7..0000000 --- a/agent/models.go +++ /dev/null @@ -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 -} diff --git a/agent/new.go b/agent/new.go deleted file mode 100644 index 854f6c4..0000000 --- a/agent/new.go +++ /dev/null @@ -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 -} diff --git a/agent/runtime.go b/agent/runtime.go index 32937cf..7ad42bf 100644 --- a/agent/runtime.go +++ b/agent/runtime.go @@ -1,10 +1,198 @@ 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 +} + // Runtime holds the swappable per-agent configuration. It contains everything // 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 diff --git a/agent/build_runtime_test.go b/agent/runtime_test.go similarity index 100% rename from agent/build_runtime_test.go rename to agent/runtime_test.go diff --git a/agent/state.go b/agent/state.go deleted file mode 100644 index b9aaaab..0000000 --- a/agent/state.go +++ /dev/null @@ -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 -} diff --git a/detach/process.go b/toolsrv/detach.go similarity index 89% rename from detach/process.go rename to toolsrv/detach.go index 6e91ebe..8464b2c 100644 --- a/detach/process.go +++ b/toolsrv/detach.go @@ -1,4 +1,4 @@ -package detach +package toolsrv import ( "sync" @@ -7,9 +7,9 @@ import ( 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. -type Process struct { +type DetachedProcess struct { PID int Command string Started time.Time @@ -30,7 +30,7 @@ type InfoData struct { ExitCode int } -func (p *Process) Info() InfoData { +func (p *DetachedProcess) Info() InfoData { p.Mu.Lock() defer p.Mu.Unlock() return InfoData{ @@ -43,7 +43,7 @@ func (p *Process) Info() InfoData { } // Output returns the current contents of the ring buffer. -func (p *Process) Output() string { +func (p *DetachedProcess) Output() string { p.Mu.Lock() defer p.Mu.Unlock() return p.Ring.String() diff --git a/toolsrv/server.go b/toolsrv/server.go index 1a005c9..25e2321 100644 --- a/toolsrv/server.go +++ b/toolsrv/server.go @@ -9,7 +9,6 @@ import ( "syscall" "time" - "ollie/detach" "ollie/paths" ) @@ -73,7 +72,7 @@ type Server struct { // Detached process management detachMu sync.Mutex 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 OnExit func(pid int, exitCode int) // hook: called when a detached process exits } diff --git a/toolsrv/shell.go b/toolsrv/shell.go index 28e000e..c26554e 100644 --- a/toolsrv/shell.go +++ b/toolsrv/shell.go @@ -17,7 +17,6 @@ import ( "syscall" "time" - "ollie/detach" "ollie/paths" "ollie/sandbox" ) @@ -241,10 +240,10 @@ func (s *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeo if len(cmdStr) > 80 { cmdStr = cmdStr[:77] + "..." } - ring := detach.NewRingBuffer(detach.RingBufSize) + ring := NewRingBuffer(RingBufSize) pid := int(time.Now().UnixNano() & 0x7FFFFFFF) // synthetic PID - proc := &detach.Process{ + proc := &DetachedProcess{ PID: pid, Command: cmdStr, Started: time.Now(), @@ -327,7 +326,7 @@ func (s *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeo if len(cmdStr) > 80 { cmdStr = cmdStr[:77] + "..." } - ring := detach.NewRingBuffer(detach.RingBufSize) + ring := NewRingBuffer(RingBufSize) pid := int(time.Now().UnixNano() & 0x7FFFFFFF) // 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.mu.Unlock() - proc := &detach.Process{ + proc := &DetachedProcess{ PID: pid, Command: cmdStr, Started: time.Now(), @@ -601,14 +600,14 @@ func (s *Server) executeWithStdin(ctx context.Context, code, language string, ti if len(cmdStr) > 80 { cmdStr = cmdStr[:77] + "..." } - ring := detach.NewRingBuffer(detach.RingBufSize) + ring := NewRingBuffer(RingBufSize) // Splice: future output goes to ring buffer instead of outputBuf. lw.mu.Lock() lw.w = ring lw.stream = nil lw.mu.Unlock() - proc := &detach.Process{ + proc := &DetachedProcess{ PID: cmd.Process.Pid, Command: cmdStr, Started: time.Now(),