X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/36229ef68a30c5a117599037d79fa1fc708fbfef..6c7859cfea123479aba046614ee81028651ddaf5:/agent.go?ds=inline diff --git a/agent.go b/agent.go index 97d6685..da4b0b7 100644 --- a/agent.go +++ b/agent.go @@ -25,6 +25,28 @@ const ( agentIDBytes = 16 ) +// Agent is one agent and its directory. The directory holds config.json, the +// user's instructions in instructions.md, the chat, and the usage file. +type Agent struct { + dir string + config AgentConfig +} + +// AgentConfig holds the settings of the config.json of an agent. ID is random, +// made once when the file is made, and never changes. The cache key comes from +// the ID, so a rename keeps the prompt cache and two agents with one name do +// not share a key. Workdir is the directory where the commands of the agent +// run. Responsibility and Parent are set together for a child agent. +type AgentConfig struct { + Name string `json:"name"` + ID string `json:"id"` + Model string `json:"model"` + Effort string `json:"effort"` + Workdir string `json:"workdir"` + Responsibility string `json:"responsibility,omitempty"` + Parent string `json:"parent,omitempty"` +} + // openAgent reads the config.json of the agent in dir. When the file does not // exist, it makes the directory and writes the file with the defaults and a new // ID. A missing field of an existing file takes the default, but the ID must be @@ -39,7 +61,12 @@ func openAgent(dir string) (*Agent, error) { path := filepath.Join(dir, "config.json") data, err := os.ReadFile(path) if errors.Is(err, os.ErrNotExist) { - return createAgent(dir) + agent, err := createAgent(dir) + if err != nil { + return agent, fmt.Errorf("create agent: %w", err) + } + + return agent, nil } if err != nil { @@ -48,7 +75,7 @@ func openAgent(dir string) (*Agent, error) { config, err := defaultAgentConfig() if err != nil { - return nil, err + return nil, fmt.Errorf("prepare agent defaults: %w", err) } decoder := json.NewDecoder(bytes.NewReader(data)) @@ -73,13 +100,6 @@ func openAgent(dir string) (*Agent, error) { }, nil } -// Agent is one agent and its directory. The directory holds config.json, the -// user's instructions in instructions.md, the chat, and the usage file. -type Agent struct { - dir string - config AgentConfig -} - // Images returns the shared data-directory image store, also for a child. func (a *Agent) Images() *ImageStore { root := a.dir @@ -90,9 +110,14 @@ func (a *Agent) Images() *ImageStore { return &ImageStore{dir: filepath.Join(filepath.Dir(root), imageDirName)} } -// CacheKey returns the prompt cache key of every model call of the agent. -func (a *Agent) CacheKey() string { - return "onechat-" + a.config.ID +// Voices returns the shared data-directory audio store, also for a child. +func (a *Agent) Voices() *VoiceStore { + root := a.dir + if a.config.Parent != "" { + root = filepath.Dir(filepath.Dir(root)) + } + + return &VoiceStore{dir: filepath.Join(filepath.Dir(root), voiceDirName)} } // Prompt returns the system prompt of the agent with the user's instructions @@ -116,13 +141,24 @@ func (a *Agent) Prompt() (string, error) { return prompt(a.config.Name, path, instructions), nil } +// instructions reads the user's instructions from instructions.md. A missing +// file means no instructions. +func (a *Agent) instructions() (string, error) { + data, err := os.ReadFile(filepath.Join(a.dir, agentInstructionsFileName)) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return "", fmt.Errorf("read instructions: %w", err) + } + + return string(data), nil +} + // Tools returns the tools shared by the turns and compactions of this agent. A -// child has no WhatsApp, child creation, or route tools. +// child has no image output, WhatsApp, child creation, or route tools. func (a *Agent) Tools() []ModelTool { tools := promptTools() if a.config.Parent != "" { tools = slices.DeleteFunc(tools, func(tool ModelTool) bool { - return strings.HasPrefix(tool.Name, "whatsapp_") || tool.Name == "agent_create" || tool.Name == "route" || tool.Name == "unroute" || tool.Name == "routes" + return strings.HasPrefix(tool.Name, "whatsapp_") || tool.Name == "image_show" || tool.Name == "agent_create" || tool.Name == "route" || tool.Name == "unroute" || tool.Name == "routes" }) } @@ -188,6 +224,11 @@ func (a *Agent) Children() ([]*Agent, error) { return children, nil } +// CacheKey returns the prompt cache key of every model call of the agent. +func (a *Agent) CacheKey() string { + return "onechat-" + a.config.ID +} + // CreateChild writes a new direct child with its own ID and the model, effort, // and working directory of the root. Child and subagent names share a // namespace. The config is complete before an atomic rename publishes the @@ -274,32 +315,6 @@ func (a *Agent) SubagentPrompt() (string, error) { return promptSubagent(a.config.Name, instructions), nil } -// instructions reads the user's instructions from instructions.md. A missing -// file means no instructions. -func (a *Agent) instructions() (string, error) { - data, err := os.ReadFile(filepath.Join(a.dir, agentInstructionsFileName)) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return "", fmt.Errorf("read instructions: %w", err) - } - - return string(data), nil -} - -// AgentConfig holds the settings of the config.json of an agent. ID is random, -// made once when the file is made, and never changes. The cache key comes from -// the ID, so a rename keeps the prompt cache and two agents with one name do -// not share a key. Workdir is the directory where the commands of the agent -// run. Responsibility and Parent are set together for a child agent. -type AgentConfig struct { - Name string `json:"name"` - ID string `json:"id"` - Model string `json:"model"` - Effort string `json:"effort"` - Workdir string `json:"workdir"` - Responsibility string `json:"responsibility,omitempty"` - Parent string `json:"parent,omitempty"` -} - // validate makes sure that each setting has a valid value. func (c AgentConfig) validate() error { if c.Name == "" || strings.ContainsAny(c.Name, "\r\n") { @@ -330,6 +345,38 @@ func (c AgentConfig) validate() error { return nil } +// createAgent writes the config.json of a new agent in dir with the defaults +// and a new ID. +func createAgent(dir string) (*Agent, error) { + config, err := defaultAgentConfig() + if err != nil { + return nil, fmt.Errorf("prepare agent defaults: %w", err) + } + + agent, err := writeAgent(dir, config) + if err != nil { + return agent, fmt.Errorf("write new agent: %w", err) + } + + return agent, nil +} + +// defaultAgentConfig returns the settings of a new agent, without an ID. The +// commands run in the home directory of the user. +func defaultAgentConfig() (AgentConfig, error) { + home, err := os.UserHomeDir() + if err != nil { + return AgentConfig{}, fmt.Errorf("find working directory: %w", err) + } + + return AgentConfig{ + Name: "OneChat", + Model: "gpt-6.1-sol", + Effort: "medium", + Workdir: home, + }, nil +} + // agentChildNameValid accepts one word of 2 to 32 ASCII letters with an initial // capital letter. Child names describe their area. func agentChildNameValid(name string) bool { @@ -347,17 +394,6 @@ func agentChildNameValid(name string) bool { return true } -// createAgent writes the config.json of a new agent in dir with the defaults -// and a new ID. -func createAgent(dir string) (*Agent, error) { - config, err := defaultAgentConfig() - if err != nil { - return nil, err - } - - return writeAgent(dir, config) -} - // writeAgent gives config a new ID and writes it in dir. func writeAgent(dir string, config AgentConfig) (*Agent, error) { id := make([]byte, agentIDBytes) @@ -382,19 +418,3 @@ func writeAgent(dir string, config AgentConfig) (*Agent, error) { config: config, }, nil } - -// defaultAgentConfig returns the settings of a new agent, without an ID. The -// commands run in the home directory of the user. -func defaultAgentConfig() (AgentConfig, error) { - home, err := os.UserHomeDir() - if err != nil { - return AgentConfig{}, fmt.Errorf("find working directory: %w", err) - } - - return AgentConfig{ - Name: "OneChat", - Model: "gpt-6.1-sol", - Effort: "medium", - Workdir: home, - }, nil -}