From: Anderson A. Giacomolli Date: Tue, 10 Nov 2026 04:27:07 +0000 (-0300) Subject: Define child agent areas and reporting rules X-Git-Url: http://git.giacomolli.uk/onechat/commitdiff_plain/77f29e78734dc16cfcea5f41b8e0185f8b33534a?ds=inline Define child agent areas and reporting rules Child configuration names its parent and responsibility together. Its prompt limits work to that area, reports only what needs attention, allows an empty report, and requires drafts instead of WhatsApp sends. Each child reads its own instructions file at each call. --- diff --git a/agent.go b/agent.go index 0d97cdf..790e18b 100644 --- a/agent.go +++ b/agent.go @@ -96,6 +96,10 @@ func (a *Agent) Prompt() (string, error) { return "", fmt.Errorf("find instructions path: %w", err) } + if a.config.Parent != "" { + return promptChild(a.config.Name, a.config.Parent, a.config.Responsibility, path, instructions), nil + } + return prompt(a.config.Name, path, instructions), nil } @@ -125,13 +129,15 @@ func (a *Agent) instructions() (string, error) { // 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. +// 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"` + 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. @@ -149,6 +155,14 @@ func (c AgentConfig) validate() error { return errors.New("agent needs a model and an effort") } + if (strings.TrimSpace(c.Responsibility) == "") != (c.Parent == "") { + return errors.New("a child needs both responsibility and parent") + } + + if strings.ContainsAny(c.Parent, "\r\n") { + return errors.New("parent must be one line of text") + } + if !filepath.IsAbs(c.Workdir) { return fmt.Errorf("workdir %q is not an absolute path", c.Workdir) } diff --git a/agent_test.go b/agent_test.go index be74b40..8b6775f 100644 --- a/agent_test.go +++ b/agent_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" @@ -126,6 +127,66 @@ func TestAgentPromptUsesAnAbsoluteInstructionsPath(t *testing.T) { require.Contains(t, text, "The user's instructions live in the file "+filepath.Join(dir, agentInstructionsFileName)+",") } +func TestChildPromptDefinesItsAreaAndReports(t *testing.T) { + dir := t.TempDir() + agent, err := openAgent(dir) + require.NoError(t, err) + + config := agent.config + config.Name = "Garden" + config.Parent = "OneChat" + config.Responsibility = "Keep the garden plans and decisions." + agentWriteTestConfig(t, dir, config) + require.NoError(t, os.WriteFile(filepath.Join(dir, agentInstructionsFileName), []byte("Ask before editing plans."), 0o600)) + + agent, err = openAgent(dir) + require.NoError(t, err) + + text, err := agent.Prompt() + require.NoError(t, err) + require.Contains(t, text, "You are Garden, a child agent of OneChat.") + require.Contains(t, text, "You are responsible for: Keep the garden plans and decisions.") + require.Contains(t, text, "Work only in this area.") + require.Contains(t, text, "Report to OneChat only what needs the parent or the") + require.Contains(t, text, "End your turn with an empty reply when nothing needs the parent or the user.") + require.Contains(t, text, "Send nothing to WhatsApp. Draft replies in your report instead") + require.Contains(t, text, filepath.Join(dir, agentInstructionsFileName)) + require.True(t, strings.HasSuffix(text, "Ask before editing plans.")) + require.NotContains(t, text, "You act again only when the user writes.") +} + +func TestChildConfigNeedsAParentAndAnArea(t *testing.T) { + testCases := []struct { + name string + parent string + responsibility string + }{ + { + name: "a parent needs an area", + parent: "OneChat", + }, + { + name: "an area needs a parent", + responsibility: "Keep the garden plans.", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + agent, err := openAgent(dir) + require.NoError(t, err) + + config := agent.config + config.Parent = tc.parent + config.Responsibility = tc.responsibility + agentWriteTestConfig(t, dir, config) + + _, err = openAgent(dir) + require.ErrorContains(t, err, "a child needs both responsibility and parent") + }) + } +} + // agentWriteTestConfig writes config as the config.json of the agent in dir. func agentWriteTestConfig(t *testing.T, dir string, config AgentConfig) { t.Helper() diff --git a/prompt.go b/prompt.go index 694ad68..934491f 100644 --- a/prompt.go +++ b/prompt.go @@ -192,6 +192,24 @@ func prompt(name, instructionsPath, instructions string) string { return promptWithInstructions(text, promptTaskSentence, promptInstructionsClause, instructions) } +// promptChild returns the prompt of a child with its area and reporting rules. +// Its instructions file belongs to the child, not to the parent. +func promptChild(name, parent, responsibility, instructionsPath, instructions string) string { + text := prompt(name, instructionsPath, "") + text = strings.Replace(text, "You are "+name+", an AI agent that works for one user in a single chat that never\nends.", "You are "+name+", a child agent of "+parent+".", 1) + text = strings.Replace(text, promptTaskSentence, `Do the tasks of your area yourself, with your tools. +You are responsible for: `+responsibility+` +Work only in this area. Report to `+parent+` only what needs the parent or the +user: a decision, a deadline, a problem, a question you cannot settle, or a +message outside your area. Your final reply is your report to the parent. +End your turn with an empty reply when nothing needs the parent or the user. +Send nothing to WhatsApp. Draft replies in your report instead, so the parent +can ask the user before sending.`, 1) + text = strings.Replace(text, "You act again only when the user writes.", "You act again when a message or your own schedule wakes you.", 1) + + return promptWithInstructions(text, "Do the tasks of your area yourself, with your tools.", promptInstructionsClause, instructions) +} + // promptSubagent returns the system prompt of a subagent of the agent name, // with the instructions of the user at its end, as prompt does. func promptSubagent(name, instructions string) string {