all: kill dead code, remove classifier system, clean up names
Dead code removed: - agent: firstSentence, Checkpoint, WaitForChange, InitCond, cfgDir, execServer, SaveTo, SaveFull, saveTo, saveToFull, sanitizeMessages, LoadPersistedAgent, RestoreHistory, Restore, PersistedAgent type - toolsrv: entire tier.go (MemoryTier, MemoryTierArgs, OutputFormat, CanParallelize on Server), same methods on Conn - cmd/ollie-remote: can_parallelize, memory_tier, memory_tier_args RPC handlers - toolsrv/rpc_integration_test: all classifier tests - fs/lifecycle: state(root) dead helper Renames: - session.Session receiver: 'a' → 's' throughout - SaveSession(path) → Save() (dead parameter removed) - execServer() eliminated — callers use ag.runtime.ToolServer directly The agent-package save/restore chain was entirely dead: PersistSession in session/persist.go reads agent accessors directly and never called SaveTo/SaveFull.
This commit is contained in:
parent
b6694d409a
commit
63c4cd6173
|
|
@ -224,11 +224,6 @@ func (ag *Agent) WaitChange(ctx context.Context, field, current string) (string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitCond initializes the changeCond. Must be called once after construction.
|
|
||||||
func (ag *Agent) InitCond() {
|
|
||||||
ag.changeCond = sync.NewCond(&ag.changeMu)
|
|
||||||
}
|
|
||||||
|
|
||||||
// emit sends an event to the agent's output handler.
|
// emit sends an event to the agent's output handler.
|
||||||
func (ag *Agent) emit(ev Event) {
|
func (ag *Agent) emit(ev Event) {
|
||||||
if ag.output != nil {
|
if ag.output != nil {
|
||||||
|
|
@ -311,19 +306,6 @@ func (ag *Agent) HasHistory() bool {
|
||||||
return ag.history != nil
|
return ag.history != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveTo saves the current history to the given path.
|
|
||||||
func (ag *Agent) SaveTo(path, name, cwd string) error {
|
|
||||||
if ag.history == nil {
|
|
||||||
return fmt.Errorf("no active session")
|
|
||||||
}
|
|
||||||
return ag.history.saveTo(path, name, ag.Name(), cwd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore restores agent history from a persisted session.
|
|
||||||
func (ag *Agent) Restore(ps *PersistedAgent) {
|
|
||||||
ag.history = RestoreHistory(ps)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToolCallCount returns the total number of tool calls executed.
|
// ToolCallCount returns the total number of tool calls executed.
|
||||||
func (ag *Agent) ToolCallCount() int64 {
|
func (ag *Agent) ToolCallCount() int64 {
|
||||||
return ag.toolCallCount.Load()
|
return ag.toolCallCount.Load()
|
||||||
|
|
@ -511,19 +493,6 @@ func (ag *Agent) toolsNeedRefresh() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveFull persists the full session state (history + metadata) to the given path.
|
|
||||||
func (ag *Agent) SaveFull(path, sessionID, sessionName, cwd, remote string) error {
|
|
||||||
// Switching profiles intentionally clears the conversation history, but
|
|
||||||
// the profile and display name still need to be persisted. Save an empty
|
|
||||||
// history in that case rather than dropping the entire metadata snapshot.
|
|
||||||
history := ag.history
|
|
||||||
if history == nil {
|
|
||||||
history = &History{}
|
|
||||||
}
|
|
||||||
return history.saveToFull(path, sessionID, sessionName, ag.id, ag.Name(), ag.profile,
|
|
||||||
ag.runtime.Backend.Name(), ag.runtime.Backend.Model(), cwd, remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CtxSz returns a human-readable context size string.
|
// CtxSz returns a human-readable context size string.
|
||||||
func (ag *Agent) CtxSz() string {
|
func (ag *Agent) CtxSz() string {
|
||||||
if ag.history == nil {
|
if ag.history == nil {
|
||||||
|
|
@ -606,15 +575,10 @@ func (ag *Agent) ListModels() []string {
|
||||||
return ag.runtime.Backend.Models(context.Background())
|
return ag.runtime.Backend.Models(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
// execServer returns the execute server, or nil if unavailable.
|
|
||||||
func (ag *Agent) execServer() toolsrv.Runner {
|
|
||||||
return ag.runtime.ToolServer
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToolServer returns the tool execution server, or nil if unavailable.
|
// ToolServer returns the tool execution server, or nil if unavailable.
|
||||||
// Exported for use by the 9P filesystem layer to sync tool registries.
|
// Exported for use by the 9P filesystem layer to sync tool registries.
|
||||||
func (ag *Agent) ToolServer() toolsrv.Runner {
|
func (ag *Agent) ToolServer() toolsrv.Runner {
|
||||||
return ag.execServer()
|
return ag.runtime.ToolServer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue pushes a prompt onto the agent's FIFO.
|
// Queue pushes a prompt onto the agent's FIFO.
|
||||||
|
|
@ -634,18 +598,9 @@ func (ag *Agent) BroadcastChange() {
|
||||||
ag.changeMu.Unlock()
|
ag.changeMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitForChange blocks until a state change is broadcast or ctx is cancelled.
|
|
||||||
func (ag *Agent) WaitForChange(ctx context.Context) {
|
|
||||||
ag.changeMu.Lock()
|
|
||||||
if ctx.Err() == nil {
|
|
||||||
ag.changeCond.Wait()
|
|
||||||
}
|
|
||||||
ag.changeMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detach detaches the current running process to background.
|
// Detach detaches the current running process to background.
|
||||||
func (ag *Agent) Detach() bool {
|
func (ag *Agent) Detach() bool {
|
||||||
if srv := ag.execServer(); srv != nil {
|
if srv := ag.runtime.ToolServer; srv != nil {
|
||||||
if d, ok := srv.(interface{ Detach() bool }); ok {
|
if d, ok := srv.(interface{ Detach() bool }); ok {
|
||||||
return d.Detach()
|
return d.Detach()
|
||||||
}
|
}
|
||||||
|
|
@ -664,7 +619,7 @@ type DetachedInfo struct {
|
||||||
|
|
||||||
// ListDetached returns info about all detached processes.
|
// ListDetached returns info about all detached processes.
|
||||||
func (ag *Agent) ListDetached() []DetachedInfo {
|
func (ag *Agent) ListDetached() []DetachedInfo {
|
||||||
srv := ag.execServer()
|
srv := ag.runtime.ToolServer
|
||||||
if srv == nil {
|
if srv == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -701,7 +656,7 @@ func (ag *Agent) ListDetached() []DetachedInfo {
|
||||||
|
|
||||||
// SignalDetached sends a signal to a detached process.
|
// SignalDetached sends a signal to a detached process.
|
||||||
func (ag *Agent) SignalDetached(pid int, signal int) error {
|
func (ag *Agent) SignalDetached(pid int, signal int) error {
|
||||||
if srv := ag.execServer(); srv != nil {
|
if srv := ag.runtime.ToolServer; srv != nil {
|
||||||
type signaler interface {
|
type signaler interface {
|
||||||
SignalDetached(int, syscall.Signal) error
|
SignalDetached(int, syscall.Signal) error
|
||||||
}
|
}
|
||||||
|
|
@ -714,7 +669,7 @@ func (ag *Agent) SignalDetached(pid int, signal int) error {
|
||||||
|
|
||||||
// GetDetachedOutput reads output from a detached process.
|
// GetDetachedOutput reads output from a detached process.
|
||||||
func (ag *Agent) GetDetachedOutput(pid int) (string, error) {
|
func (ag *Agent) GetDetachedOutput(pid int) (string, error) {
|
||||||
if srv := ag.execServer(); srv != nil {
|
if srv := ag.runtime.ToolServer; srv != nil {
|
||||||
type outputGetter interface{ GetDetachedOutput(int) (string, error) }
|
type outputGetter interface{ GetDetachedOutput(int) (string, error) }
|
||||||
if og, ok := srv.(outputGetter); ok {
|
if og, ok := srv.(outputGetter); ok {
|
||||||
return og.GetDetachedOutput(pid)
|
return og.GetDetachedOutput(pid)
|
||||||
|
|
@ -725,7 +680,7 @@ func (ag *Agent) GetDetachedOutput(pid int) (string, error) {
|
||||||
|
|
||||||
// DismissDetached removes a finished detached process.
|
// DismissDetached removes a finished detached process.
|
||||||
func (ag *Agent) DismissDetached(pid int) bool {
|
func (ag *Agent) DismissDetached(pid int) bool {
|
||||||
if srv := ag.execServer(); srv != nil {
|
if srv := ag.runtime.ToolServer; srv != nil {
|
||||||
type dismisser interface{ DismissDetached(int) bool }
|
type dismisser interface{ DismissDetached(int) bool }
|
||||||
if d, ok := srv.(dismisser); ok {
|
if d, ok := srv.(dismisser); ok {
|
||||||
return d.DismissDetached(pid)
|
return d.DismissDetached(pid)
|
||||||
|
|
|
||||||
139
agent/history.go
139
agent/history.go
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ollie/backend"
|
"ollie/backend"
|
||||||
|
|
@ -33,31 +32,6 @@ Be concise. Capture what another LLM needs to seamlessly continue.`
|
||||||
warmIndexSize = 10
|
warmIndexSize = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
// PersistedAgent is the on-disk format for a saved session.
|
|
||||||
type PersistedAgent struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
// SessionName is the mutable display name of the containing session.
|
|
||||||
SessionName string `json:"sessionName,omitempty"`
|
|
||||||
Uname string `json:"uname,omitempty"` // agent's immutable 9P identity
|
|
||||||
Agent string `json:"agent,omitempty"`
|
|
||||||
Profile string `json:"profile,omitempty"`
|
|
||||||
Backend string `json:"backend,omitempty"`
|
|
||||||
Model string `json:"model,omitempty"`
|
|
||||||
CWD string `json:"cwd,omitempty"`
|
|
||||||
Remote string `json:"remote,omitempty"`
|
|
||||||
Messages []backend.Message `json:"messages"`
|
|
||||||
TaskState *TaskState `json:"taskState,omitempty"`
|
|
||||||
// Usage and cost tracking, persisted across restarts.
|
|
||||||
TotalInputTokens int `json:"totalInputTokens,omitempty"`
|
|
||||||
TotalCachedInputTokens int `json:"totalCachedInputTokens,omitempty"`
|
|
||||||
TotalCacheCreationTokens int `json:"totalCacheCreationTokens,omitempty"`
|
|
||||||
TotalOutputTokens int `json:"totalOutputTokens,omitempty"`
|
|
||||||
TotalRequests int `json:"totalRequests,omitempty"`
|
|
||||||
Estimated bool `json:"estimated,omitempty"`
|
|
||||||
LastTurnCostUSD float64 `json:"lastTurnCostUSD,omitempty"`
|
|
||||||
SessionCostUSD float64 `json:"sessionCostUSD,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TaskState is a compact structured overlay that summarizes the agent's
|
// TaskState is a compact structured overlay that summarizes the agent's
|
||||||
// current position in a task. Injected at the top of every turn so the
|
// current position in a task. Injected at the top of every turn so the
|
||||||
// model doesn't rely on full history recall.
|
// model doesn't rely on full history recall.
|
||||||
|
|
@ -97,104 +71,6 @@ func (ts *TaskState) render() string {
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveTo writes the full message history to path as JSON.
|
|
||||||
func (s *History) saveTo(path, id, displayName, cwd string) error {
|
|
||||||
return s.saveToFull(path, id, "", "", displayName, "", "", "", cwd, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveToFull writes session state including backend/model info.
|
|
||||||
func sanitizeMessages(msgs []backend.Message) []backend.Message {
|
|
||||||
out := make([]backend.Message, 0, len(msgs))
|
|
||||||
for _, m := range msgs {
|
|
||||||
for j, tc := range m.ToolCalls {
|
|
||||||
if len(tc.Arguments) > 0 {
|
|
||||||
var dummy json.RawMessage
|
|
||||||
if err := json.Unmarshal(tc.Arguments, &dummy); err != nil {
|
|
||||||
// Corrupted arguments — replace with empty object so save succeeds.
|
|
||||||
m.ToolCalls[j].Arguments = json.RawMessage("{}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out = append(out, m)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *History) saveToFull(path, id, sessionName, uname, displayName, profile, backendName, modelName, cwd, remote string) error {
|
|
||||||
ps := PersistedAgent{
|
|
||||||
ID: id,
|
|
||||||
SessionName: sessionName,
|
|
||||||
Uname: uname,
|
|
||||||
Agent: displayName,
|
|
||||||
Profile: profile,
|
|
||||||
Backend: backendName,
|
|
||||||
Model: modelName,
|
|
||||||
CWD: cwd,
|
|
||||||
Remote: remote,
|
|
||||||
Messages: sanitizeMessages(s.messages),
|
|
||||||
TaskState: s.TaskState,
|
|
||||||
TotalInputTokens: s.TotalInputTokens,
|
|
||||||
TotalCachedInputTokens: s.TotalCachedInputTokens,
|
|
||||||
TotalCacheCreationTokens: s.TotalCacheCreationTokens,
|
|
||||||
TotalOutputTokens: s.TotalOutputTokens,
|
|
||||||
TotalRequests: s.TotalRequests,
|
|
||||||
Estimated: s.Estimated,
|
|
||||||
LastTurnCostUSD: s.LastTurnCostUSD,
|
|
||||||
SessionCostUSD: s.SessionCostUSD,
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(ps)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("session save: %w", err)
|
|
||||||
}
|
|
||||||
// Atomic write: write to temp file, then rename to preserve last good snapshot.
|
|
||||||
tmpPath := path + ".tmp"
|
|
||||||
if err := os.WriteFile(tmpPath, data, 0600); err != nil {
|
|
||||||
return fmt.Errorf("session save tmp: %w", err)
|
|
||||||
}
|
|
||||||
return os.Rename(tmpPath, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPersistedAgent reads a PersistedAgent from a JSON file.
|
|
||||||
func LoadPersistedAgent(path string) (*PersistedAgent, error) {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var ps PersistedAgent
|
|
||||||
if err := json.Unmarshal(data, &ps); err != nil {
|
|
||||||
return nil, fmt.Errorf("parse session %s: %w", path, err)
|
|
||||||
}
|
|
||||||
return &ps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RestoreHistory reconstructs a Session from a persisted message list.
|
|
||||||
func RestoreHistory(ps *PersistedAgent) *History {
|
|
||||||
s := &History{
|
|
||||||
messages: ps.Messages,
|
|
||||||
TaskState: ps.TaskState,
|
|
||||||
TotalInputTokens: ps.TotalInputTokens,
|
|
||||||
TotalCachedInputTokens: ps.TotalCachedInputTokens,
|
|
||||||
TotalCacheCreationTokens: ps.TotalCacheCreationTokens,
|
|
||||||
TotalOutputTokens: ps.TotalOutputTokens,
|
|
||||||
TotalRequests: ps.TotalRequests,
|
|
||||||
Estimated: ps.Estimated,
|
|
||||||
LastTurnCostUSD: ps.LastTurnCostUSD,
|
|
||||||
SessionCostUSD: ps.SessionCostUSD,
|
|
||||||
}
|
|
||||||
for i := range s.messages {
|
|
||||||
if s.messages[i].Role == "assistant" && s.messages[i].ID == "" {
|
|
||||||
s.messages[i].ID = NewResponseID()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, m := range ps.Messages {
|
|
||||||
if m.Role == "user" {
|
|
||||||
s.goal = m.Content
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// RestoreHistoryFromMessages reconstructs a History from just the message list.
|
// RestoreHistoryFromMessages reconstructs a History from just the message list.
|
||||||
// Used for multi-agent session restore where each agent has separate messages.
|
// Used for multi-agent session restore where each agent has separate messages.
|
||||||
func RestoreHistoryFromMessages(messages []backend.Message) *History {
|
func RestoreHistoryFromMessages(messages []backend.Message) *History {
|
||||||
|
|
@ -244,21 +120,6 @@ func newHistory(goal string) *History {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkpoint forks the session: returns a new Session that inherits the given
|
// Checkpoint forks the session: returns a new Session that inherits the given
|
||||||
// TaskState but starts with a clean message history. This enables narrow-context
|
|
||||||
// sub-agents that know what to do without inheriting all parent message noise.
|
|
||||||
func (s *History) Checkpoint(ts TaskState) *History {
|
|
||||||
child := &History{
|
|
||||||
goal: ts.Objective,
|
|
||||||
TaskState: &ts,
|
|
||||||
}
|
|
||||||
// Seed with a user message so the child has a valid initial turn.
|
|
||||||
child.messages = []backend.Message{{
|
|
||||||
Role: "user",
|
|
||||||
Content: ts.render(),
|
|
||||||
}}
|
|
||||||
return child
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *History) history() []backend.Message {
|
func (s *History) history() []backend.Message {
|
||||||
return s.messages
|
return s.messages
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
|
"ollie/paths"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
@ -19,8 +20,7 @@ type modelsConfig struct {
|
||||||
// loadModelsConfig reads and parses the models config file.
|
// loadModelsConfig reads and parses the models config file.
|
||||||
// Returns zero value if the file doesn't exist or is invalid.
|
// Returns zero value if the file doesn't exist or is invalid.
|
||||||
func loadModelsConfig() modelsConfig {
|
func loadModelsConfig() modelsConfig {
|
||||||
path := filepath.Join(cfgDir(), "models.yaml")
|
data, err := os.ReadFile(paths.CfgDir() + "/models.yaml")
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return modelsConfig{}
|
return modelsConfig{}
|
||||||
}
|
}
|
||||||
|
|
@ -30,12 +30,3 @@ func loadModelsConfig() modelsConfig {
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
// cfgDir returns the ollie config directory, respecting XDG_CONFIG_HOME.
|
|
||||||
func cfgDir() string {
|
|
||||||
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
|
|
||||||
return filepath.Join(xdg, "ollie")
|
|
||||||
}
|
|
||||||
home, _ := os.UserHomeDir()
|
|
||||||
return filepath.Join(home, ".config", "ollie")
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -389,16 +389,3 @@ func infoEvent(text string) Event {
|
||||||
return Event{Role: "info", Content: text + "\n"}
|
return Event{Role: "info", Content: text + "\n"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// firstSentence returns the first sentence of s (up to the first period or
|
|
||||||
// newline), trimmed. Falls back to s truncated at 80 chars if no sentence end found.
|
|
||||||
func firstSentence(s string) string {
|
|
||||||
for i, r := range s {
|
|
||||||
if r == '.' || r == '\n' {
|
|
||||||
return strings.TrimSpace(s[:i+1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(s) > 80 {
|
|
||||||
return s[:77] + "..."
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -334,40 +334,6 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
|
||||||
ok := srv.DismissDetached(params.PID)
|
ok := srv.DismissDetached(params.PID)
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
||||||
|
|
||||||
case "can_parallelize":
|
|
||||||
var params struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ok := srv.CanParallelize(params.Name)
|
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
|
||||||
|
|
||||||
case "memory_tier":
|
|
||||||
var params struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tier := srv.MemoryTier(params.Name)
|
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: tier})
|
|
||||||
|
|
||||||
case "memory_tier_args":
|
|
||||||
var params struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Args json.RawMessage `json:"args"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tier := srv.MemoryTierArgs(params.Name, params.Args)
|
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: tier})
|
|
||||||
|
|
||||||
case "tool_load":
|
case "tool_load":
|
||||||
var params struct {
|
var params struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,7 @@ func writeSessionCtl(ctx HandlerCtx, data []byte) error {
|
||||||
},
|
},
|
||||||
"save": func(_ []string, _ HandlerCtx) error {
|
"save": func(_ []string, _ HandlerCtx) error {
|
||||||
if ctx.Session.Core != nil {
|
if ctx.Session.Core != nil {
|
||||||
ctx.Session.Core.SaveSession("")
|
ctx.Session.Core.Save()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,6 @@ func (rs *RootState) RenameNode(oldName, newName string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func state(root *Tree) *RootState {
|
|
||||||
return root.Data.(*RootState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown interrupts all sessions, waits for idle, persists, and closes.
|
// Shutdown interrupts all sessions, waits for idle, persists, and closes.
|
||||||
func Shutdown(root *Tree) {
|
func Shutdown(root *Tree) {
|
||||||
session.Shutdown()
|
session.Shutdown()
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ func New(cfg Config) *Session {
|
||||||
name = cfg.SessionID[:idx]
|
name = cfg.SessionID[:idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
a := &Session{
|
s := &Session{
|
||||||
id: cfg.SessionID,
|
id: cfg.SessionID,
|
||||||
name: name,
|
name: name,
|
||||||
env: make(map[string]string),
|
env: make(map[string]string),
|
||||||
|
|
@ -185,18 +185,18 @@ func New(cfg Config) *Session {
|
||||||
SessionID: cfg.SessionID,
|
SessionID: cfg.SessionID,
|
||||||
StartupMsgs: rt.Messages,
|
StartupMsgs: rt.Messages,
|
||||||
ReadPlanStep: cfg.ReadPlanStep,
|
ReadPlanStep: cfg.ReadPlanStep,
|
||||||
Save: a.saveSession,
|
Save: s.saveSession,
|
||||||
Flush: a.flushSave,
|
Flush: s.flushSave,
|
||||||
})
|
})
|
||||||
|
|
||||||
a.agents = append(a.agents, ag)
|
s.agents = append(s.agents, ag)
|
||||||
ag.SetSessionEnv(a.id)
|
ag.SetSessionEnv(s.id)
|
||||||
return a
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindAgent returns the agent matching the given name or ID, or nil.
|
// FindAgent returns the agent matching the given name or ID, or nil.
|
||||||
func (a *Session) FindAgent(nameOrID string) *agent.Agent {
|
func (s *Session) FindAgent(nameOrID string) *agent.Agent {
|
||||||
for _, ag := range a.agents {
|
for _, ag := range s.agents {
|
||||||
if ag.Name() == nameOrID || ag.ID() == nameOrID {
|
if ag.Name() == nameOrID || ag.ID() == nameOrID {
|
||||||
return ag
|
return ag
|
||||||
}
|
}
|
||||||
|
|
@ -205,37 +205,37 @@ func (a *Session) FindAgent(nameOrID string) *agent.Agent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agents returns the full agent slice.
|
// Agents returns the full agent slice.
|
||||||
func (a *Session) Agents() []*agent.Agent { return a.agents }
|
func (s *Session) Agents() []*agent.Agent { return s.agents }
|
||||||
|
|
||||||
// AgentAt returns the agent at the given index, or nil.
|
// AgentAt returns the agent at the given index, or nil.
|
||||||
func (a *Session) AgentAt(idx int) *agent.Agent {
|
func (s *Session) AgentAt(idx int) *agent.Agent {
|
||||||
if idx < 0 || idx >= len(a.agents) {
|
if idx < 0 || idx >= len(s.agents) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return a.agents[idx]
|
return s.agents[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentCount returns the number of agents.
|
// AgentCount returns the number of agents.
|
||||||
func (a *Session) AgentCount() int { return len(a.agents) }
|
func (s *Session) AgentCount() int { return len(s.agents) }
|
||||||
|
|
||||||
// SaveFuncs returns save and flush callbacks suitable for use as
|
// SaveFuncs returns save and flush callbacks suitable for use as
|
||||||
// SaveFuncs returns the save and flush callbacks for use in
|
// SaveFuncs returns the save and flush callbacks for use in
|
||||||
// agent.AgentCfg.Save and Flush when adding agents to this session.
|
// agent.AgentCfg.Save and Flush when adding agents to this session.
|
||||||
func (a *Session) SaveFuncs() (save func(), flush func()) {
|
func (s *Session) SaveFuncs() (save func(), flush func()) {
|
||||||
return a.saveSession, a.flushSave
|
return s.saveSession, s.flushSave
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAgent appends an agent to the session.
|
// AddAgent appends an agent to the session.
|
||||||
func (a *Session) AddAgent(ag *agent.Agent) {
|
func (s *Session) AddAgent(ag *agent.Agent) {
|
||||||
a.agents = append(a.agents, ag)
|
s.agents = append(s.agents, ag)
|
||||||
ag.SetSessionEnv(a.id)
|
ag.SetSessionEnv(s.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveAgent removes an agent by ID and closes it.
|
// RemoveAgent removes an agent by ID and closes it.
|
||||||
// Returns false if the agent was not found.
|
// Returns false if the agent was not found.
|
||||||
func (a *Session) RemoveAgent(id string) bool {
|
func (s *Session) RemoveAgent(id string) bool {
|
||||||
idx := -1
|
idx := -1
|
||||||
for i, ag := range a.agents {
|
for i, ag := range s.agents {
|
||||||
if ag.ID() == id {
|
if ag.ID() == id {
|
||||||
idx = i
|
idx = i
|
||||||
break
|
break
|
||||||
|
|
@ -244,61 +244,61 @@ func (a *Session) RemoveAgent(id string) bool {
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
a.agents[idx].Close()
|
s.agents[idx].Close()
|
||||||
a.agents = append(a.agents[:idx], a.agents[idx+1:]...)
|
s.agents = append(s.agents[:idx], s.agents[idx+1:]...)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close releases resources for this session.
|
// Close releases resources for this session.
|
||||||
func (a *Session) Close() {
|
func (s *Session) Close() {
|
||||||
a.log.Debug("Close() session=%q", a.id)
|
s.log.Debug("Close() session=%q", s.id)
|
||||||
a.flushSave()
|
s.flushSave()
|
||||||
for _, ag := range a.agents {
|
for _, ag := range s.agents {
|
||||||
ag.Close()
|
ag.Close()
|
||||||
}
|
}
|
||||||
if a.id != "" {
|
if s.id != "" {
|
||||||
os.RemoveAll(filepath.Join(ollieTmpDir(), a.id)) //nolint:errcheck
|
os.RemoveAll(filepath.Join(ollieTmpDir(), s.id)) //nolint:errcheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetEnv stores a session-scoped variable and propagates to all agents.
|
// SetEnv stores a session-scoped variable and propagates to all agents.
|
||||||
func (a *Session) SetEnv(key, value string) {
|
func (s *Session) SetEnv(key, value string) {
|
||||||
a.envMu.Lock()
|
s.envMu.Lock()
|
||||||
a.env[key] = value
|
s.env[key] = value
|
||||||
a.envMu.Unlock()
|
s.envMu.Unlock()
|
||||||
for _, ag := range a.agents {
|
for _, ag := range s.agents {
|
||||||
ag.SetEnv(key, value)
|
ag.SetEnv(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remote returns the remote target (e.g., SSH host) if any.
|
// Remote returns the remote target (e.g., SSH host) if any.
|
||||||
func (a *Session) Remote() string {
|
func (s *Session) Remote() string {
|
||||||
return a.remote
|
return s.remote
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRemote sets the remote target for this session.
|
// SetRemote sets the remote target for this session.
|
||||||
func (a *Session) SetRemote(remote string) {
|
func (s *Session) SetRemote(remote string) {
|
||||||
a.remote = remote
|
s.remote = remote
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSessionID renames the session.
|
// SetSessionID renames the session.
|
||||||
func (a *Session) SetSessionID(newID string) error {
|
func (s *Session) SetSessionID(newID string) error {
|
||||||
oldID := a.id
|
oldID := s.id
|
||||||
if oldID == newID {
|
if oldID == newID {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if a.sessionsDir != "" && oldID != "" {
|
if s.sessionsDir != "" && oldID != "" {
|
||||||
for _, suffix := range []string{".json", ".compaction.jsonl"} {
|
for _, suffix := range []string{".json", ".compaction.jsonl"} {
|
||||||
oldPath := a.activeSessionPath(oldID, suffix)
|
oldPath := s.activeSessionPath(oldID, suffix)
|
||||||
if _, err := os.Stat(oldPath); err == nil {
|
if _, err := os.Stat(oldPath); err == nil {
|
||||||
if err := os.Rename(oldPath, a.activeSessionPath(newID, suffix)); err != nil {
|
if err := os.Rename(oldPath, s.activeSessionPath(newID, suffix)); err != nil {
|
||||||
return fmt.Errorf("rename %s: %w", suffix, err)
|
return fmt.Errorf("rename %s: %w", suffix, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.id = newID
|
s.id = newID
|
||||||
for _, ag := range a.agents {
|
for _, ag := range s.agents {
|
||||||
ag.RenamePreamble(oldID, newID)
|
ag.RenamePreamble(oldID, newID)
|
||||||
}
|
}
|
||||||
oldTemp := filepath.Join(ollieTmpDir(), oldID)
|
oldTemp := filepath.Join(ollieTmpDir(), oldID)
|
||||||
|
|
@ -306,161 +306,160 @@ func (a *Session) SetSessionID(newID string) error {
|
||||||
if _, err := os.Stat(oldTemp); err == nil {
|
if _, err := os.Stat(oldTemp); err == nil {
|
||||||
os.Rename(oldTemp, newTemp) //nolint:errcheck
|
os.Rename(oldTemp, newTemp) //nolint:errcheck
|
||||||
}
|
}
|
||||||
for _, ag := range a.agents {
|
for _, ag := range s.agents {
|
||||||
ag.SetSessionEnv(newID)
|
ag.SetSessionEnv(newID)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Session) activeSessionPath(id, suffix string) string {
|
func (s *Session) activeSessionPath(id, suffix string) string {
|
||||||
return filepath.Join(a.sessionsDir, "active", id+suffix)
|
return filepath.Join(s.sessionsDir, "active", id+suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Session) saveSession() {
|
func (s *Session) saveSession() {
|
||||||
a.saveMu.Lock()
|
s.saveMu.Lock()
|
||||||
a.saveDirty = true
|
s.saveDirty = true
|
||||||
if a.saveTimer == nil {
|
if s.saveTimer == nil {
|
||||||
a.saveTimer = time.AfterFunc(2*time.Second, a.flushSave)
|
s.saveTimer = time.AfterFunc(2*time.Second, s.flushSave)
|
||||||
}
|
}
|
||||||
a.saveMu.Unlock()
|
s.saveMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Session) flushSave() {
|
func (s *Session) flushSave() {
|
||||||
a.saveMu.Lock()
|
s.saveMu.Lock()
|
||||||
dirty := a.saveDirty
|
dirty := s.saveDirty
|
||||||
a.saveDirty = false
|
s.saveDirty = false
|
||||||
if a.saveTimer != nil {
|
if s.saveTimer != nil {
|
||||||
a.saveTimer.Stop()
|
s.saveTimer.Stop()
|
||||||
a.saveTimer = nil
|
s.saveTimer = nil
|
||||||
}
|
}
|
||||||
a.saveMu.Unlock()
|
s.saveMu.Unlock()
|
||||||
if !dirty || a.id == "" || a.sessionsDir == "" {
|
if !dirty || s.id == "" || s.sessionsDir == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Use PersistSession for consistent format (writes Name field correctly)
|
// Use PersistSession for consistent format (writes Name field correctly)
|
||||||
if err := PersistSession(a.Name()); err != nil {
|
if err := PersistSession(s.Name()); err != nil {
|
||||||
a.log.Error("session save: %v", err)
|
s.log.Error("session save: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveSession writes the current session state.
|
// Save writes the current session state via PersistSession.
|
||||||
// The path argument is ignored; session is saved via PersistSession.
|
func (s *Session) Save() error {
|
||||||
func (a *Session) SaveSession(path string) error {
|
return PersistSession(s.Name())
|
||||||
return PersistSession(a.Name())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Identity accessors ---
|
// --- Identity accessors ---
|
||||||
|
|
||||||
// ID returns the immutable session UUID.
|
// ID returns the immutable session UUID.
|
||||||
func (a *Session) ID() string { return a.id }
|
func (s *Session) ID() string { return s.id }
|
||||||
|
|
||||||
// Name returns the mutable friendly name.
|
// Name returns the mutable friendly name.
|
||||||
func (a *Session) Name() string {
|
func (s *Session) Name() string {
|
||||||
a.mu.RLock()
|
s.mu.RLock()
|
||||||
defer a.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
return a.name
|
return s.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetName sets the friendly name.
|
// SetName sets the friendly name.
|
||||||
func (a *Session) SetName(name string) {
|
func (s *Session) SetName(name string) {
|
||||||
a.mu.Lock()
|
s.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
a.name = name
|
s.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uname returns the immutable user principal.
|
// Uname returns the immutable user principal.
|
||||||
func (a *Session) Uname() string { return a.uname }
|
func (s *Session) Uname() string { return s.uname }
|
||||||
|
|
||||||
// SetUname sets the user principal (should only be called during init).
|
// SetUname sets the user principal (should only be called during init).
|
||||||
func (a *Session) SetUname(uname string) {
|
func (s *Session) SetUname(uname string) {
|
||||||
a.mu.Lock()
|
s.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
a.uname = uname
|
s.uname = uname
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Context lifecycle ---
|
// --- Context lifecycle ---
|
||||||
|
|
||||||
// Ctx returns the session context.
|
// Ctx returns the session context.
|
||||||
func (a *Session) Ctx() context.Context { return a.ctx }
|
func (s *Session) Ctx() context.Context { return s.ctx }
|
||||||
|
|
||||||
// Cancel cancels the session context.
|
// Cancel cancels the session context.
|
||||||
func (a *Session) Cancel() {
|
func (s *Session) Cancel() {
|
||||||
if a.cancel != nil {
|
if s.cancel != nil {
|
||||||
a.cancel()
|
s.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetContext sets the session context and cancel func.
|
// SetContext sets the session context and cancel func.
|
||||||
func (a *Session) SetContext(ctx context.Context, cancel context.CancelFunc) {
|
func (s *Session) SetContext(ctx context.Context, cancel context.CancelFunc) {
|
||||||
a.ctx = ctx
|
s.ctx = ctx
|
||||||
a.cancel = cancel
|
s.cancel = cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Tool server lifecycle ---
|
// --- Tool server lifecycle ---
|
||||||
|
|
||||||
// Proc returns the tool server process.
|
// Proc returns the tool server process.
|
||||||
func (a *Session) Proc() *toolsrv.Process {
|
func (s *Session) Proc() *toolsrv.Process {
|
||||||
a.mu.RLock()
|
s.mu.RLock()
|
||||||
defer a.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
return a.proc
|
return s.proc
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetProc sets the tool server process.
|
// SetProc sets the tool server process.
|
||||||
func (a *Session) SetProc(proc *toolsrv.Process) {
|
func (s *Session) SetProc(proc *toolsrv.Process) {
|
||||||
a.mu.Lock()
|
s.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
a.proc = proc
|
s.proc = proc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keeper returns the process keeper.
|
// Keeper returns the process keeper.
|
||||||
func (a *Session) Keeper() *toolsrv.ProcessKeeper {
|
func (s *Session) Keeper() *toolsrv.ProcessKeeper {
|
||||||
a.mu.RLock()
|
s.mu.RLock()
|
||||||
defer a.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
return a.keeper
|
return s.keeper
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetKeeper sets the process keeper.
|
// SetKeeper sets the process keeper.
|
||||||
func (a *Session) SetKeeper(keeper *toolsrv.ProcessKeeper) {
|
func (s *Session) SetKeeper(keeper *toolsrv.ProcessKeeper) {
|
||||||
a.mu.Lock()
|
s.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
a.keeper = keeper
|
s.keeper = keeper
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolsConn returns the tool server connection.
|
// ToolsConn returns the tool server connection.
|
||||||
func (a *Session) ToolsConn() toolsrv.Runner {
|
func (s *Session) ToolsConn() toolsrv.Runner {
|
||||||
a.mu.RLock()
|
s.mu.RLock()
|
||||||
defer a.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
return a.toolsConn
|
return s.toolsConn
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetToolsConn sets the tool server connection.
|
// SetToolsConn sets the tool server connection.
|
||||||
func (a *Session) SetToolsConn(conn toolsrv.Runner) {
|
func (s *Session) SetToolsConn(conn toolsrv.Runner) {
|
||||||
a.mu.Lock()
|
s.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
a.toolsConn = conn
|
s.toolsConn = conn
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPaused returns true if the session is paused.
|
// IsPaused returns true if the session is paused.
|
||||||
func (a *Session) IsPaused() bool {
|
func (s *Session) IsPaused() bool {
|
||||||
a.mu.RLock()
|
s.mu.RLock()
|
||||||
defer a.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
return a.paused
|
return s.paused
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPaused sets the paused state directly (used during restore).
|
// SetPaused sets the paused state directly (used during restore).
|
||||||
func (a *Session) SetPaused(paused bool) {
|
func (s *Session) SetPaused(paused bool) {
|
||||||
a.mu.Lock()
|
s.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
a.paused = paused
|
s.paused = paused
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsConnected returns true if the tool server connection is alive.
|
// IsConnected returns true if the tool server connection is alive.
|
||||||
// Returns false if the session is paused.
|
// Returns false if the session is paused.
|
||||||
func (a *Session) IsConnected() bool {
|
func (s *Session) IsConnected() bool {
|
||||||
a.mu.RLock()
|
s.mu.RLock()
|
||||||
paused := a.paused
|
paused := s.paused
|
||||||
conn := a.toolsConn
|
conn := s.toolsConn
|
||||||
a.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
if paused || conn == nil {
|
if paused || conn == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -473,61 +472,61 @@ func (a *Session) IsConnected() bool {
|
||||||
// Pause stops the tool server to save resources.
|
// Pause stops the tool server to save resources.
|
||||||
// It cancels the session context (stopping all agent operations),
|
// It cancels the session context (stopping all agent operations),
|
||||||
// kills the tool server process, and marks the session as paused.
|
// kills the tool server process, and marks the session as paused.
|
||||||
func (a *Session) Pause() error {
|
func (s *Session) Pause() error {
|
||||||
a.mu.Lock()
|
s.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if a.paused {
|
if s.paused {
|
||||||
return fmt.Errorf("session already paused")
|
return fmt.Errorf("session already paused")
|
||||||
}
|
}
|
||||||
// Cancel the session context to stop all agent operations.
|
// Cancel the session context to stop all agent operations.
|
||||||
if a.cancel != nil {
|
if s.cancel != nil {
|
||||||
a.cancel()
|
s.cancel()
|
||||||
}
|
}
|
||||||
// Close the tool server process.
|
// Close the tool server process.
|
||||||
if a.keeper != nil {
|
if s.keeper != nil {
|
||||||
a.keeper.Close()
|
s.keeper.Close()
|
||||||
} else if a.proc != nil {
|
} else if s.proc != nil {
|
||||||
a.proc.Close()
|
s.proc.Close()
|
||||||
}
|
}
|
||||||
// Close any existing connection.
|
// Close any existing connection.
|
||||||
if a.toolsConn != nil {
|
if s.toolsConn != nil {
|
||||||
if c, ok := a.toolsConn.(interface{ Close() }); ok {
|
if c, ok := s.toolsConn.(interface{ Close() }); ok {
|
||||||
c.Close()
|
c.Close()
|
||||||
}
|
}
|
||||||
a.toolsConn = nil
|
s.toolsConn = nil
|
||||||
}
|
}
|
||||||
a.paused = true
|
s.paused = true
|
||||||
// Persist the paused state.
|
// Persist the paused state.
|
||||||
go a.saveSession()
|
go s.saveSession()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume restarts the tool server after a Pause.
|
// Resume restarts the tool server after a Pause.
|
||||||
// It creates a fresh session context and respawns the tool server.
|
// It creates a fresh session context and respawns the tool server.
|
||||||
// For sessions restored in paused state (no keeper), it sets up the tool server from scratch.
|
// For sessions restored in paused state (no keeper), it sets up the tool server from scratch.
|
||||||
func (a *Session) Resume() error {
|
func (s *Session) Resume() error {
|
||||||
a.mu.Lock()
|
s.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if !a.paused {
|
if !s.paused {
|
||||||
return fmt.Errorf("session not paused")
|
return fmt.Errorf("session not paused")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a fresh context since the old one was cancelled on pause.
|
// Create a fresh context since the old one was cancelled on pause.
|
||||||
ctx, cancel := context.WithCancel(serverCtx)
|
ctx, cancel := context.WithCancel(serverCtx)
|
||||||
a.ctx = ctx
|
s.ctx = ctx
|
||||||
a.cancel = cancel
|
s.cancel = cancel
|
||||||
|
|
||||||
// If we have a keeper, use it to respawn. Otherwise set up from scratch.
|
// If we have a keeper, use it to respawn. Otherwise set up from scratch.
|
||||||
if a.keeper != nil {
|
if s.keeper != nil {
|
||||||
a.keeper.SetContext(ctx)
|
s.keeper.SetContext(ctx)
|
||||||
conn, err := a.keeper.Dial()
|
conn, err := s.keeper.Dial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("resume failed: %w", err)
|
return fmt.Errorf("resume failed: %w", err)
|
||||||
}
|
}
|
||||||
a.toolsConn = conn
|
s.toolsConn = conn
|
||||||
} else {
|
} else {
|
||||||
// Session was restored paused without infra - set up per-agent.
|
// Session was restored paused without infra - set up per-agent.
|
||||||
for i, ag := range a.agents {
|
for i, ag := range s.agents {
|
||||||
cwd := ag.Cwd()
|
cwd := ag.Cwd()
|
||||||
if cwd == "" {
|
if cwd == "" {
|
||||||
cwd, _ = os.Getwd()
|
cwd, _ = os.Getwd()
|
||||||
|
|
@ -535,18 +534,18 @@ func (a *Session) Resume() error {
|
||||||
|
|
||||||
// First agent spawns the tool server, others reuse
|
// First agent spawns the tool server, others reuse
|
||||||
var reuseFrom *InfraConfig
|
var reuseFrom *InfraConfig
|
||||||
if a.proc != nil {
|
if s.proc != nil {
|
||||||
reuseFrom = &InfraConfig{
|
reuseFrom = &InfraConfig{
|
||||||
Proc: a.proc,
|
Proc: s.proc,
|
||||||
Keeper: a.keeper,
|
Keeper: s.keeper,
|
||||||
ToolsConn: a.toolsConn,
|
ToolsConn: s.toolsConn,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
infra, err := SetupToolServer(ToolServerConfig{
|
infra, err := SetupToolServer(ToolServerConfig{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
CWD: cwd,
|
CWD: cwd,
|
||||||
RemoteTarget: a.remote,
|
RemoteTarget: s.remote,
|
||||||
Yolo: pkgYolo,
|
Yolo: pkgYolo,
|
||||||
ReuseFrom: reuseFrom,
|
ReuseFrom: reuseFrom,
|
||||||
})
|
})
|
||||||
|
|
@ -556,45 +555,45 @@ func (a *Session) Resume() error {
|
||||||
|
|
||||||
// First agent sets session infra
|
// First agent sets session infra
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
a.proc = infra.Proc
|
s.proc = infra.Proc
|
||||||
a.keeper = infra.Keeper
|
s.keeper = infra.Keeper
|
||||||
a.toolsConn = infra.ToolsConn
|
s.toolsConn = infra.ToolsConn
|
||||||
}
|
}
|
||||||
|
|
||||||
ag.SetToolServer(infra.NewToolServer, infra.ToolsConn)
|
ag.SetToolServer(infra.NewToolServer, infra.ToolsConn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.paused = false
|
s.paused = false
|
||||||
// Persist the resumed state.
|
// Persist the resumed state.
|
||||||
go a.saveSession()
|
go s.saveSession()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Tool loading ---
|
// --- Tool loading ---
|
||||||
|
|
||||||
// SetDisallowTools sets the tool disallow list.
|
// SetDisallowTools sets the tool disallow list.
|
||||||
func (a *Session) SetDisallowTools(disallow map[string]struct{}) {
|
func (s *Session) SetDisallowTools(disallow map[string]struct{}) {
|
||||||
a.mu.Lock()
|
s.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
a.disallowTools = disallow
|
s.disallowTools = disallow
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadTool loads a tool into the tool server.
|
// LoadTool loads a tool into the tool server.
|
||||||
func (a *Session) LoadTool(name string, ag *agent.Agent) error {
|
func (s *Session) LoadTool(name string, ag *agent.Agent) error {
|
||||||
name = strings.TrimSpace(name)
|
name = strings.TrimSpace(name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
a.mu.RLock()
|
s.mu.RLock()
|
||||||
_, blocked := a.disallowTools[name]
|
_, blocked := s.disallowTools[name]
|
||||||
a.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
if blocked {
|
if blocked {
|
||||||
return fmt.Errorf("tool %q is disallowed for this session", name)
|
return fmt.Errorf("tool %q is disallowed for this session", name)
|
||||||
}
|
}
|
||||||
var runner toolsrv.Runner
|
var runner toolsrv.Runner
|
||||||
if a.toolsConn != nil {
|
if s.toolsConn != nil {
|
||||||
runner = a.toolsConn
|
runner = s.toolsConn
|
||||||
} else if ag != nil {
|
} else if ag != nil {
|
||||||
runner = ag.ToolServer()
|
runner = ag.ToolServer()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,42 +151,6 @@ func (c *Conn) DismissDetached(pid int) bool {
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanParallelize implements ParallelClassifier.
|
|
||||||
func (c *Conn) CanParallelize(name string) bool {
|
|
||||||
params, _ := json.Marshal(map[string]string{"name": name})
|
|
||||||
resp, err := c.call("can_parallelize", params)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var ok bool
|
|
||||||
json.Unmarshal(resp, &ok)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemoryTier implements MemoryTierClassifier.
|
|
||||||
func (c *Conn) MemoryTier(name string) string {
|
|
||||||
params, _ := json.Marshal(map[string]string{"name": name})
|
|
||||||
resp, err := c.call("memory_tier", params)
|
|
||||||
if err != nil {
|
|
||||||
return "hot"
|
|
||||||
}
|
|
||||||
var tier string
|
|
||||||
json.Unmarshal(resp, &tier)
|
|
||||||
return tier
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemoryTierArgs implements MemoryTierClassifier.
|
|
||||||
func (c *Conn) MemoryTierArgs(name string, args json.RawMessage) string {
|
|
||||||
params, _ := json.Marshal(map[string]any{"name": name, "args": args})
|
|
||||||
resp, err := c.call("memory_tier_args", params)
|
|
||||||
if err != nil {
|
|
||||||
return "hot"
|
|
||||||
}
|
|
||||||
var tier string
|
|
||||||
json.Unmarshal(resp, &tier)
|
|
||||||
return tier
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close shuts down the connection and subprocess.
|
// Close shuts down the connection and subprocess.
|
||||||
func (c *Conn) Close() {
|
func (c *Conn) Close() {
|
||||||
c.fail(fmt.Errorf("connection closed"))
|
c.fail(fmt.Errorf("connection closed"))
|
||||||
|
|
|
||||||
|
|
@ -130,40 +130,6 @@ func (ts *testServer) handleRPC(ctx context.Context, req rpcRequest) rpcResponse
|
||||||
ts.srv.SetAllowTools(params.Names)
|
ts.srv.SetAllowTools(params.Names)
|
||||||
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`true`)}
|
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`true`)}
|
||||||
|
|
||||||
case "can_parallelize":
|
|
||||||
var params struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
||||||
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}
|
|
||||||
}
|
|
||||||
ok := ts.srv.CanParallelize(params.Name)
|
|
||||||
result, _ := json.Marshal(ok)
|
|
||||||
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: result}
|
|
||||||
|
|
||||||
case "memory_tier":
|
|
||||||
var params struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
||||||
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}
|
|
||||||
}
|
|
||||||
tier := ts.srv.MemoryTier(params.Name)
|
|
||||||
result, _ := json.Marshal(tier)
|
|
||||||
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: result}
|
|
||||||
|
|
||||||
case "memory_tier_args":
|
|
||||||
var params struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Args json.RawMessage `json:"args"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
||||||
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}
|
|
||||||
}
|
|
||||||
tier := ts.srv.MemoryTierArgs(params.Name, params.Args)
|
|
||||||
result, _ := json.Marshal(tier)
|
|
||||||
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: result}
|
|
||||||
|
|
||||||
case "detach":
|
case "detach":
|
||||||
ok := ts.srv.Detach()
|
ok := ts.srv.Detach()
|
||||||
result, _ := json.Marshal(ok)
|
result, _ := json.Marshal(ok)
|
||||||
|
|
@ -449,78 +415,6 @@ func TestRPC_SetAllowTools(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRPC_CanParallelize(t *testing.T) {
|
|
||||||
ts := newTestServer(t)
|
|
||||||
defer ts.srv.Close()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
params, _ := json.Marshal(map[string]string{"name": "unknown_tool"})
|
|
||||||
req := rpcRequest{JSONRPC: "2.0", ID: 1, Method: "can_parallelize", Params: params}
|
|
||||||
resp := ts.handleRPC(ctx, req)
|
|
||||||
|
|
||||||
if resp.Error != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", resp.Error.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
var result bool
|
|
||||||
if err := json.Unmarshal(resp.Result, &result); err != nil {
|
|
||||||
t.Fatalf("unmarshal result: %v", err)
|
|
||||||
}
|
|
||||||
// Unknown tools default to not parallel-read-safe
|
|
||||||
if result {
|
|
||||||
t.Error("expected false for unknown tool")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRPC_MemoryTier(t *testing.T) {
|
|
||||||
ts := newTestServer(t)
|
|
||||||
defer ts.srv.Close()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
params, _ := json.Marshal(map[string]string{"name": "unknown_tool"})
|
|
||||||
req := rpcRequest{JSONRPC: "2.0", ID: 1, Method: "memory_tier", Params: params}
|
|
||||||
resp := ts.handleRPC(ctx, req)
|
|
||||||
|
|
||||||
if resp.Error != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", resp.Error.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
var tier string
|
|
||||||
if err := json.Unmarshal(resp.Result, &tier); err != nil {
|
|
||||||
t.Fatalf("unmarshal result: %v", err)
|
|
||||||
}
|
|
||||||
// Unknown tools default to "hot"
|
|
||||||
if tier != "hot" {
|
|
||||||
t.Errorf("expected tier='hot', got %q", tier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRPC_MemoryTierArgs(t *testing.T) {
|
|
||||||
ts := newTestServer(t)
|
|
||||||
defer ts.srv.Close()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
params, _ := json.Marshal(map[string]any{
|
|
||||||
"name": "some_tool",
|
|
||||||
"args": json.RawMessage(`{"foo": "bar"}`),
|
|
||||||
})
|
|
||||||
req := rpcRequest{JSONRPC: "2.0", ID: 1, Method: "memory_tier_args", Params: params}
|
|
||||||
resp := ts.handleRPC(ctx, req)
|
|
||||||
|
|
||||||
if resp.Error != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", resp.Error.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
var tier string
|
|
||||||
if err := json.Unmarshal(resp.Result, &tier); err != nil {
|
|
||||||
t.Fatalf("unmarshal result: %v", err)
|
|
||||||
}
|
|
||||||
// Unknown tools default to "hot"
|
|
||||||
if tier != "hot" {
|
|
||||||
t.Errorf("expected tier='hot', got %q", tier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRPC_Detach_NoRunningProcess(t *testing.T) {
|
func TestRPC_Detach_NoRunningProcess(t *testing.T) {
|
||||||
ts := newTestServer(t)
|
ts := newTestServer(t)
|
||||||
defer ts.srv.Close()
|
defer ts.srv.Close()
|
||||||
|
|
@ -700,17 +594,6 @@ func TestRPC_PipeConnection(t *testing.T) {
|
||||||
// Test set cwd
|
// Test set cwd
|
||||||
conn.SetCWD("/tmp/test")
|
conn.SetCWD("/tmp/test")
|
||||||
|
|
||||||
// Test is_parallel_read
|
|
||||||
if conn.CanParallelize("unknown") {
|
|
||||||
t.Error("expected CanParallelize=false for unknown tool")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test result_tier
|
|
||||||
tier := conn.MemoryTier("unknown")
|
|
||||||
if tier != "hot" {
|
|
||||||
t.Errorf("expected tier='hot', got %q", tier)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
cancel()
|
cancel()
|
||||||
conn.Close()
|
conn.Close()
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
package toolsrv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MemoryTier implements MemoryTierClassifier. Looks up the tool's tier
|
|
||||||
// from the registry, then from its .meta file.
|
|
||||||
func (e *Server) MemoryTier(name string) string {
|
|
||||||
if e.toolRegistry != nil && e.sessionID != "" {
|
|
||||||
if info, ok := e.toolRegistry.Lookup(e.sessionID, name); ok && info.Tier != "" {
|
|
||||||
return info.Tier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m, err := LoadMetaFile(name)
|
|
||||||
if err == nil && m != nil && m.Tier != "" {
|
|
||||||
return m.Tier
|
|
||||||
}
|
|
||||||
return "hot"
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutputFormat returns the source-fence language for a tool's output.
|
|
||||||
// Empty means the output should be wrapped in a plaintext fence.
|
|
||||||
func (e *Server) OutputFormat(name string) string {
|
|
||||||
if e.toolRegistry != nil && e.sessionID != "" {
|
|
||||||
if info, ok := e.toolRegistry.Lookup(e.sessionID, name); ok {
|
|
||||||
return info.OutputFormat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m, err := LoadMetaFile(name)
|
|
||||||
if err == nil && m != nil {
|
|
||||||
if resolved := m.Resolve(); resolved != nil {
|
|
||||||
return resolved.OutputFormat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemoryTierArgs classifies the tier using both the outer tool name and its
|
|
||||||
// arguments. For shell, it returns warm. For promoted tools, it
|
|
||||||
// delegates to MemoryTier.
|
|
||||||
func (e *Server) MemoryTierArgs(name string, args json.RawMessage) string {
|
|
||||||
switch name {
|
|
||||||
case "shell":
|
|
||||||
return "warm"
|
|
||||||
default:
|
|
||||||
return e.MemoryTier(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanParallelize implements ParallelClassifier. Returns true when the
|
|
||||||
// named tool is marked readOnly in its .meta file.
|
|
||||||
func (e *Server) CanParallelize(name string) bool {
|
|
||||||
if e.toolRegistry != nil && e.sessionID != "" {
|
|
||||||
if info, ok := e.toolRegistry.Lookup(e.sessionID, name); ok {
|
|
||||||
return info.ReadOnly
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m, err := LoadMetaFile(name)
|
|
||||||
if err == nil && m != nil {
|
|
||||||
return m.ReadOnly
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue