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.
|
||||
func (ag *Agent) emit(ev Event) {
|
||||
if ag.output != nil {
|
||||
|
|
@ -311,19 +306,6 @@ func (ag *Agent) HasHistory() bool {
|
|||
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.
|
||||
func (ag *Agent) ToolCallCount() int64 {
|
||||
return ag.toolCallCount.Load()
|
||||
|
|
@ -511,19 +493,6 @@ func (ag *Agent) toolsNeedRefresh() bool {
|
|||
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.
|
||||
func (ag *Agent) CtxSz() string {
|
||||
if ag.history == nil {
|
||||
|
|
@ -606,15 +575,10 @@ func (ag *Agent) ListModels() []string {
|
|||
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.
|
||||
// Exported for use by the 9P filesystem layer to sync tool registries.
|
||||
func (ag *Agent) ToolServer() toolsrv.Runner {
|
||||
return ag.execServer()
|
||||
return ag.runtime.ToolServer
|
||||
}
|
||||
|
||||
// Queue pushes a prompt onto the agent's FIFO.
|
||||
|
|
@ -634,18 +598,9 @@ func (ag *Agent) BroadcastChange() {
|
|||
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.
|
||||
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 {
|
||||
return d.Detach()
|
||||
}
|
||||
|
|
@ -664,7 +619,7 @@ type DetachedInfo struct {
|
|||
|
||||
// ListDetached returns info about all detached processes.
|
||||
func (ag *Agent) ListDetached() []DetachedInfo {
|
||||
srv := ag.execServer()
|
||||
srv := ag.runtime.ToolServer
|
||||
if srv == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -701,7 +656,7 @@ func (ag *Agent) ListDetached() []DetachedInfo {
|
|||
|
||||
// SignalDetached sends a signal to a detached process.
|
||||
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 {
|
||||
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.
|
||||
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) }
|
||||
if og, ok := srv.(outputGetter); ok {
|
||||
return og.GetDetachedOutput(pid)
|
||||
|
|
@ -725,7 +680,7 @@ func (ag *Agent) GetDetachedOutput(pid int) (string, error) {
|
|||
|
||||
// DismissDetached removes a finished detached process.
|
||||
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 }
|
||||
if d, ok := srv.(dismisser); ok {
|
||||
return d.DismissDetached(pid)
|
||||
|
|
|
|||
139
agent/history.go
139
agent/history.go
|
|
@ -5,7 +5,6 @@ import (
|
|||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"ollie/backend"
|
||||
|
|
@ -33,31 +32,6 @@ Be concise. Capture what another LLM needs to seamlessly continue.`
|
|||
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
|
||||
// current position in a task. Injected at the top of every turn so the
|
||||
// model doesn't rely on full history recall.
|
||||
|
|
@ -97,104 +71,6 @@ func (ts *TaskState) render() 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.
|
||||
// Used for multi-agent session restore where each agent has separate messages.
|
||||
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
|
||||
// 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 {
|
||||
return s.messages
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ package agent
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"ollie/paths"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
|
@ -19,8 +20,7 @@ type modelsConfig struct {
|
|||
// loadModelsConfig reads and parses the models config file.
|
||||
// Returns zero value if the file doesn't exist or is invalid.
|
||||
func loadModelsConfig() modelsConfig {
|
||||
path := filepath.Join(cfgDir(), "models.yaml")
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(paths.CfgDir() + "/models.yaml")
|
||||
if err != nil {
|
||||
return modelsConfig{}
|
||||
}
|
||||
|
|
@ -30,12 +30,3 @@ func loadModelsConfig() modelsConfig {
|
|||
}
|
||||
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"}
|
||||
}
|
||||
|
||||
// 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)
|
||||
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":
|
||||
var params struct {
|
||||
Name string `json:"name"`
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ func writeSessionCtl(ctx HandlerCtx, data []byte) error {
|
|||
},
|
||||
"save": func(_ []string, _ HandlerCtx) error {
|
||||
if ctx.Session.Core != nil {
|
||||
ctx.Session.Core.SaveSession("")
|
||||
ctx.Session.Core.Save()
|
||||
}
|
||||
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.
|
||||
func Shutdown(root *Tree) {
|
||||
session.Shutdown()
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ func New(cfg Config) *Session {
|
|||
name = cfg.SessionID[:idx]
|
||||
}
|
||||
|
||||
a := &Session{
|
||||
s := &Session{
|
||||
id: cfg.SessionID,
|
||||
name: name,
|
||||
env: make(map[string]string),
|
||||
|
|
@ -185,18 +185,18 @@ func New(cfg Config) *Session {
|
|||
SessionID: cfg.SessionID,
|
||||
StartupMsgs: rt.Messages,
|
||||
ReadPlanStep: cfg.ReadPlanStep,
|
||||
Save: a.saveSession,
|
||||
Flush: a.flushSave,
|
||||
Save: s.saveSession,
|
||||
Flush: s.flushSave,
|
||||
})
|
||||
|
||||
a.agents = append(a.agents, ag)
|
||||
ag.SetSessionEnv(a.id)
|
||||
return a
|
||||
s.agents = append(s.agents, ag)
|
||||
ag.SetSessionEnv(s.id)
|
||||
return s
|
||||
}
|
||||
|
||||
// FindAgent returns the agent matching the given name or ID, or nil.
|
||||
func (a *Session) FindAgent(nameOrID string) *agent.Agent {
|
||||
for _, ag := range a.agents {
|
||||
func (s *Session) FindAgent(nameOrID string) *agent.Agent {
|
||||
for _, ag := range s.agents {
|
||||
if ag.Name() == nameOrID || ag.ID() == nameOrID {
|
||||
return ag
|
||||
}
|
||||
|
|
@ -205,37 +205,37 @@ func (a *Session) FindAgent(nameOrID string) *agent.Agent {
|
|||
}
|
||||
|
||||
// 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.
|
||||
func (a *Session) AgentAt(idx int) *agent.Agent {
|
||||
if idx < 0 || idx >= len(a.agents) {
|
||||
func (s *Session) AgentAt(idx int) *agent.Agent {
|
||||
if idx < 0 || idx >= len(s.agents) {
|
||||
return nil
|
||||
}
|
||||
return a.agents[idx]
|
||||
return s.agents[idx]
|
||||
}
|
||||
|
||||
// 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 the save and flush callbacks for use in
|
||||
// agent.AgentCfg.Save and Flush when adding agents to this session.
|
||||
func (a *Session) SaveFuncs() (save func(), flush func()) {
|
||||
return a.saveSession, a.flushSave
|
||||
func (s *Session) SaveFuncs() (save func(), flush func()) {
|
||||
return s.saveSession, s.flushSave
|
||||
}
|
||||
|
||||
// AddAgent appends an agent to the session.
|
||||
func (a *Session) AddAgent(ag *agent.Agent) {
|
||||
a.agents = append(a.agents, ag)
|
||||
ag.SetSessionEnv(a.id)
|
||||
func (s *Session) AddAgent(ag *agent.Agent) {
|
||||
s.agents = append(s.agents, ag)
|
||||
ag.SetSessionEnv(s.id)
|
||||
}
|
||||
|
||||
// RemoveAgent removes an agent by ID and closes it.
|
||||
// Returns false if the agent was not found.
|
||||
func (a *Session) RemoveAgent(id string) bool {
|
||||
func (s *Session) RemoveAgent(id string) bool {
|
||||
idx := -1
|
||||
for i, ag := range a.agents {
|
||||
for i, ag := range s.agents {
|
||||
if ag.ID() == id {
|
||||
idx = i
|
||||
break
|
||||
|
|
@ -244,61 +244,61 @@ func (a *Session) RemoveAgent(id string) bool {
|
|||
if idx < 0 {
|
||||
return false
|
||||
}
|
||||
a.agents[idx].Close()
|
||||
a.agents = append(a.agents[:idx], a.agents[idx+1:]...)
|
||||
s.agents[idx].Close()
|
||||
s.agents = append(s.agents[:idx], s.agents[idx+1:]...)
|
||||
return true
|
||||
}
|
||||
|
||||
// Close releases resources for this session.
|
||||
func (a *Session) Close() {
|
||||
a.log.Debug("Close() session=%q", a.id)
|
||||
a.flushSave()
|
||||
for _, ag := range a.agents {
|
||||
func (s *Session) Close() {
|
||||
s.log.Debug("Close() session=%q", s.id)
|
||||
s.flushSave()
|
||||
for _, ag := range s.agents {
|
||||
ag.Close()
|
||||
}
|
||||
if a.id != "" {
|
||||
os.RemoveAll(filepath.Join(ollieTmpDir(), a.id)) //nolint:errcheck
|
||||
if s.id != "" {
|
||||
os.RemoveAll(filepath.Join(ollieTmpDir(), s.id)) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
// SetEnv stores a session-scoped variable and propagates to all agents.
|
||||
func (a *Session) SetEnv(key, value string) {
|
||||
a.envMu.Lock()
|
||||
a.env[key] = value
|
||||
a.envMu.Unlock()
|
||||
for _, ag := range a.agents {
|
||||
func (s *Session) SetEnv(key, value string) {
|
||||
s.envMu.Lock()
|
||||
s.env[key] = value
|
||||
s.envMu.Unlock()
|
||||
for _, ag := range s.agents {
|
||||
ag.SetEnv(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Remote returns the remote target (e.g., SSH host) if any.
|
||||
func (a *Session) Remote() string {
|
||||
return a.remote
|
||||
func (s *Session) Remote() string {
|
||||
return s.remote
|
||||
}
|
||||
|
||||
// SetRemote sets the remote target for this session.
|
||||
func (a *Session) SetRemote(remote string) {
|
||||
a.remote = remote
|
||||
func (s *Session) SetRemote(remote string) {
|
||||
s.remote = remote
|
||||
}
|
||||
|
||||
// SetSessionID renames the session.
|
||||
func (a *Session) SetSessionID(newID string) error {
|
||||
oldID := a.id
|
||||
func (s *Session) SetSessionID(newID string) error {
|
||||
oldID := s.id
|
||||
if oldID == newID {
|
||||
return nil
|
||||
}
|
||||
if a.sessionsDir != "" && oldID != "" {
|
||||
if s.sessionsDir != "" && oldID != "" {
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
a.id = newID
|
||||
for _, ag := range a.agents {
|
||||
s.id = newID
|
||||
for _, ag := range s.agents {
|
||||
ag.RenamePreamble(oldID, newID)
|
||||
}
|
||||
oldTemp := filepath.Join(ollieTmpDir(), oldID)
|
||||
|
|
@ -306,161 +306,160 @@ func (a *Session) SetSessionID(newID string) error {
|
|||
if _, err := os.Stat(oldTemp); err == nil {
|
||||
os.Rename(oldTemp, newTemp) //nolint:errcheck
|
||||
}
|
||||
for _, ag := range a.agents {
|
||||
for _, ag := range s.agents {
|
||||
ag.SetSessionEnv(newID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Session) activeSessionPath(id, suffix string) string {
|
||||
return filepath.Join(a.sessionsDir, "active", id+suffix)
|
||||
func (s *Session) activeSessionPath(id, suffix string) string {
|
||||
return filepath.Join(s.sessionsDir, "active", id+suffix)
|
||||
}
|
||||
|
||||
func (a *Session) saveSession() {
|
||||
a.saveMu.Lock()
|
||||
a.saveDirty = true
|
||||
if a.saveTimer == nil {
|
||||
a.saveTimer = time.AfterFunc(2*time.Second, a.flushSave)
|
||||
func (s *Session) saveSession() {
|
||||
s.saveMu.Lock()
|
||||
s.saveDirty = true
|
||||
if s.saveTimer == nil {
|
||||
s.saveTimer = time.AfterFunc(2*time.Second, s.flushSave)
|
||||
}
|
||||
a.saveMu.Unlock()
|
||||
s.saveMu.Unlock()
|
||||
}
|
||||
|
||||
func (a *Session) flushSave() {
|
||||
a.saveMu.Lock()
|
||||
dirty := a.saveDirty
|
||||
a.saveDirty = false
|
||||
if a.saveTimer != nil {
|
||||
a.saveTimer.Stop()
|
||||
a.saveTimer = nil
|
||||
func (s *Session) flushSave() {
|
||||
s.saveMu.Lock()
|
||||
dirty := s.saveDirty
|
||||
s.saveDirty = false
|
||||
if s.saveTimer != nil {
|
||||
s.saveTimer.Stop()
|
||||
s.saveTimer = nil
|
||||
}
|
||||
a.saveMu.Unlock()
|
||||
if !dirty || a.id == "" || a.sessionsDir == "" {
|
||||
s.saveMu.Unlock()
|
||||
if !dirty || s.id == "" || s.sessionsDir == "" {
|
||||
return
|
||||
}
|
||||
// Use PersistSession for consistent format (writes Name field correctly)
|
||||
if err := PersistSession(a.Name()); err != nil {
|
||||
a.log.Error("session save: %v", err)
|
||||
if err := PersistSession(s.Name()); err != nil {
|
||||
s.log.Error("session save: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SaveSession writes the current session state.
|
||||
// The path argument is ignored; session is saved via PersistSession.
|
||||
func (a *Session) SaveSession(path string) error {
|
||||
return PersistSession(a.Name())
|
||||
// Save writes the current session state via PersistSession.
|
||||
func (s *Session) Save() error {
|
||||
return PersistSession(s.Name())
|
||||
}
|
||||
|
||||
// --- Identity accessors ---
|
||||
|
||||
// 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.
|
||||
func (a *Session) Name() string {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.name
|
||||
func (s *Session) Name() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.name
|
||||
}
|
||||
|
||||
// SetName sets the friendly name.
|
||||
func (a *Session) SetName(name string) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.name = name
|
||||
func (s *Session) SetName(name string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.name = name
|
||||
}
|
||||
|
||||
// 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).
|
||||
func (a *Session) SetUname(uname string) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.uname = uname
|
||||
func (s *Session) SetUname(uname string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.uname = uname
|
||||
}
|
||||
|
||||
// --- Context lifecycle ---
|
||||
|
||||
// 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.
|
||||
func (a *Session) Cancel() {
|
||||
if a.cancel != nil {
|
||||
a.cancel()
|
||||
func (s *Session) Cancel() {
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// SetContext sets the session context and cancel func.
|
||||
func (a *Session) SetContext(ctx context.Context, cancel context.CancelFunc) {
|
||||
a.ctx = ctx
|
||||
a.cancel = cancel
|
||||
func (s *Session) SetContext(ctx context.Context, cancel context.CancelFunc) {
|
||||
s.ctx = ctx
|
||||
s.cancel = cancel
|
||||
}
|
||||
|
||||
// --- Tool server lifecycle ---
|
||||
|
||||
// Proc returns the tool server process.
|
||||
func (a *Session) Proc() *toolsrv.Process {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.proc
|
||||
func (s *Session) Proc() *toolsrv.Process {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.proc
|
||||
}
|
||||
|
||||
// SetProc sets the tool server process.
|
||||
func (a *Session) SetProc(proc *toolsrv.Process) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.proc = proc
|
||||
func (s *Session) SetProc(proc *toolsrv.Process) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.proc = proc
|
||||
}
|
||||
|
||||
// Keeper returns the process keeper.
|
||||
func (a *Session) Keeper() *toolsrv.ProcessKeeper {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.keeper
|
||||
func (s *Session) Keeper() *toolsrv.ProcessKeeper {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.keeper
|
||||
}
|
||||
|
||||
// SetKeeper sets the process keeper.
|
||||
func (a *Session) SetKeeper(keeper *toolsrv.ProcessKeeper) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.keeper = keeper
|
||||
func (s *Session) SetKeeper(keeper *toolsrv.ProcessKeeper) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.keeper = keeper
|
||||
}
|
||||
|
||||
// ToolsConn returns the tool server connection.
|
||||
func (a *Session) ToolsConn() toolsrv.Runner {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.toolsConn
|
||||
func (s *Session) ToolsConn() toolsrv.Runner {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.toolsConn
|
||||
}
|
||||
|
||||
// SetToolsConn sets the tool server connection.
|
||||
func (a *Session) SetToolsConn(conn toolsrv.Runner) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.toolsConn = conn
|
||||
func (s *Session) SetToolsConn(conn toolsrv.Runner) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.toolsConn = conn
|
||||
}
|
||||
|
||||
// IsPaused returns true if the session is paused.
|
||||
func (a *Session) IsPaused() bool {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.paused
|
||||
func (s *Session) IsPaused() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.paused
|
||||
}
|
||||
|
||||
// SetPaused sets the paused state directly (used during restore).
|
||||
func (a *Session) SetPaused(paused bool) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.paused = paused
|
||||
func (s *Session) SetPaused(paused bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.paused = paused
|
||||
}
|
||||
|
||||
// IsConnected returns true if the tool server connection is alive.
|
||||
// Returns false if the session is paused.
|
||||
func (a *Session) IsConnected() bool {
|
||||
a.mu.RLock()
|
||||
paused := a.paused
|
||||
conn := a.toolsConn
|
||||
a.mu.RUnlock()
|
||||
func (s *Session) IsConnected() bool {
|
||||
s.mu.RLock()
|
||||
paused := s.paused
|
||||
conn := s.toolsConn
|
||||
s.mu.RUnlock()
|
||||
if paused || conn == nil {
|
||||
return false
|
||||
}
|
||||
|
|
@ -473,61 +472,61 @@ func (a *Session) IsConnected() bool {
|
|||
// Pause stops the tool server to save resources.
|
||||
// It cancels the session context (stopping all agent operations),
|
||||
// kills the tool server process, and marks the session as paused.
|
||||
func (a *Session) Pause() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.paused {
|
||||
func (s *Session) Pause() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.paused {
|
||||
return fmt.Errorf("session already paused")
|
||||
}
|
||||
// Cancel the session context to stop all agent operations.
|
||||
if a.cancel != nil {
|
||||
a.cancel()
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
}
|
||||
// Close the tool server process.
|
||||
if a.keeper != nil {
|
||||
a.keeper.Close()
|
||||
} else if a.proc != nil {
|
||||
a.proc.Close()
|
||||
if s.keeper != nil {
|
||||
s.keeper.Close()
|
||||
} else if s.proc != nil {
|
||||
s.proc.Close()
|
||||
}
|
||||
// Close any existing connection.
|
||||
if a.toolsConn != nil {
|
||||
if c, ok := a.toolsConn.(interface{ Close() }); ok {
|
||||
if s.toolsConn != nil {
|
||||
if c, ok := s.toolsConn.(interface{ Close() }); ok {
|
||||
c.Close()
|
||||
}
|
||||
a.toolsConn = nil
|
||||
s.toolsConn = nil
|
||||
}
|
||||
a.paused = true
|
||||
s.paused = true
|
||||
// Persist the paused state.
|
||||
go a.saveSession()
|
||||
go s.saveSession()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resume restarts the tool server after a Pause.
|
||||
// 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.
|
||||
func (a *Session) Resume() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if !a.paused {
|
||||
func (s *Session) Resume() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.paused {
|
||||
return fmt.Errorf("session not paused")
|
||||
}
|
||||
|
||||
// Create a fresh context since the old one was cancelled on pause.
|
||||
ctx, cancel := context.WithCancel(serverCtx)
|
||||
a.ctx = ctx
|
||||
a.cancel = cancel
|
||||
s.ctx = ctx
|
||||
s.cancel = cancel
|
||||
|
||||
// If we have a keeper, use it to respawn. Otherwise set up from scratch.
|
||||
if a.keeper != nil {
|
||||
a.keeper.SetContext(ctx)
|
||||
conn, err := a.keeper.Dial()
|
||||
if s.keeper != nil {
|
||||
s.keeper.SetContext(ctx)
|
||||
conn, err := s.keeper.Dial()
|
||||
if err != nil {
|
||||
return fmt.Errorf("resume failed: %w", err)
|
||||
}
|
||||
a.toolsConn = conn
|
||||
s.toolsConn = conn
|
||||
} else {
|
||||
// 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()
|
||||
if cwd == "" {
|
||||
cwd, _ = os.Getwd()
|
||||
|
|
@ -535,18 +534,18 @@ func (a *Session) Resume() error {
|
|||
|
||||
// First agent spawns the tool server, others reuse
|
||||
var reuseFrom *InfraConfig
|
||||
if a.proc != nil {
|
||||
if s.proc != nil {
|
||||
reuseFrom = &InfraConfig{
|
||||
Proc: a.proc,
|
||||
Keeper: a.keeper,
|
||||
ToolsConn: a.toolsConn,
|
||||
Proc: s.proc,
|
||||
Keeper: s.keeper,
|
||||
ToolsConn: s.toolsConn,
|
||||
}
|
||||
}
|
||||
|
||||
infra, err := SetupToolServer(ToolServerConfig{
|
||||
Ctx: ctx,
|
||||
CWD: cwd,
|
||||
RemoteTarget: a.remote,
|
||||
RemoteTarget: s.remote,
|
||||
Yolo: pkgYolo,
|
||||
ReuseFrom: reuseFrom,
|
||||
})
|
||||
|
|
@ -556,45 +555,45 @@ func (a *Session) Resume() error {
|
|||
|
||||
// First agent sets session infra
|
||||
if i == 0 {
|
||||
a.proc = infra.Proc
|
||||
a.keeper = infra.Keeper
|
||||
a.toolsConn = infra.ToolsConn
|
||||
s.proc = infra.Proc
|
||||
s.keeper = infra.Keeper
|
||||
s.toolsConn = infra.ToolsConn
|
||||
}
|
||||
|
||||
ag.SetToolServer(infra.NewToolServer, infra.ToolsConn)
|
||||
}
|
||||
}
|
||||
|
||||
a.paused = false
|
||||
s.paused = false
|
||||
// Persist the resumed state.
|
||||
go a.saveSession()
|
||||
go s.saveSession()
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Tool loading ---
|
||||
|
||||
// SetDisallowTools sets the tool disallow list.
|
||||
func (a *Session) SetDisallowTools(disallow map[string]struct{}) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.disallowTools = disallow
|
||||
func (s *Session) SetDisallowTools(disallow map[string]struct{}) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.disallowTools = disallow
|
||||
}
|
||||
|
||||
// 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)
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
a.mu.RLock()
|
||||
_, blocked := a.disallowTools[name]
|
||||
a.mu.RUnlock()
|
||||
s.mu.RLock()
|
||||
_, blocked := s.disallowTools[name]
|
||||
s.mu.RUnlock()
|
||||
if blocked {
|
||||
return fmt.Errorf("tool %q is disallowed for this session", name)
|
||||
}
|
||||
var runner toolsrv.Runner
|
||||
if a.toolsConn != nil {
|
||||
runner = a.toolsConn
|
||||
if s.toolsConn != nil {
|
||||
runner = s.toolsConn
|
||||
} else if ag != nil {
|
||||
runner = ag.ToolServer()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,42 +151,6 @@ func (c *Conn) DismissDetached(pid int) bool {
|
|||
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.
|
||||
func (c *Conn) Close() {
|
||||
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)
|
||||
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":
|
||||
ok := ts.srv.Detach()
|
||||
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) {
|
||||
ts := newTestServer(t)
|
||||
defer ts.srv.Close()
|
||||
|
|
@ -700,17 +594,6 @@ func TestRPC_PipeConnection(t *testing.T) {
|
|||
// Test set cwd
|
||||
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
|
||||
cancel()
|
||||
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