FromDir
Go · package github.com/muthuishere/toolnexus/golang/agents · SPEC §7E · golang/agents/home.go
type FromDirOptions struct { Does string // routing description (default "persona agent from <dir>") Name string // agent name (default filepath.Base(dir)) Model string // model id (default "inherit") Tools []tn.Tool // extra tools beyond the memory builtin Memory *bool // nil ⇒ enabled; point at false to omit the memory tool}
func FromDir(dir string, opts FromDirOptions) *AgentFromDir is the one-call persona constructor: the directory is the agent. It calls
ComposeSoul to build the soul from dir’s bootstrap files,
wires MemoryTool(dir) into the tool list (unless
opts.Memory points at false), and hands both to agents.New.
The returned *Agent runs like any other — Run for a one-shot, AsTool to drop it into a
classic toolkit, or StartAgent for a heartbeat loop.
When to use it
Section titled “When to use it”Reach for FromDir whenever a persona’s identity lives in files on disk — the openclaw
convention of AGENTS.md / SOUL.md / IDENTITY.md / USER.md / TOOLS.md /
HEARTBEAT.md / MEMORY.md. It’s the standard way to boot a long-lived, home-directory-backed
agent: point it at a folder, get back something runnable, with durable memory already wired.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — a persona from a bare directory
Section titled “1. The smallest useful call — a persona from a bare directory”No bootstrap files at all still works: the soul is empty, the name defaults to the directory’s
base name, the model defaults to "inherit", and the memory tool is wired anyway.
package main
import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os"
"github.com/muthuishere/toolnexus/golang/agents")
func main() { dir, err := os.MkdirTemp("", "kip-*") if err != nil { log.Fatal(err) } defer os.RemoveAll(dir)
kip := agents.FromDir(dir, agents.FromDirOptions{Model: "m-kip"}) if kip.Name != filepathBase(dir) { log.Fatalf("expected name to default to the dir base, got %q", kip.Name) }
transport := roundTripFunc(func(req *http.Request) (*http.Response, error) { body, _ := json.Marshal(map[string]any{ "choices": []any{map[string]any{"message": map[string]any{"role": "assistant", "content": "hi"}}}, "usage": map[string]any{"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, }) return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewReader(body))}, nil }) r, _ := kip.Run(agents.Options{Transport: transport}, "hello") if r.Status != "done" || r.Text != "hi" { log.Fatalf("unexpected: %+v", r) }
fmt.Println("ok:", kip.Name, "answered", r.Text)}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }
func filepathBase(p string) string { for i := len(p) - 1; i >= 0; i-- { if p[i] == '/' { return p[i+1:] } } return p}2. The realistic case — bootstrap files shape the soul, and the model answers with it
Section titled “2. The realistic case — bootstrap files shape the soul, and the model answers with it”SOUL.md on disk becomes part of the request the stub LLM receives — proving the composed
soul actually reaches the provider as the system prompt.
package main
import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "path/filepath" "strings"
"github.com/muthuishere/toolnexus/golang/agents")
func main() { dir, err := os.MkdirTemp("", "archivist-*") if err != nil { log.Fatal(err) } defer os.RemoveAll(dir)
if err := os.WriteFile(filepath.Join(dir, "SOUL.md"), []byte("You are Archivist, terse and precise."), 0o644); err != nil { log.Fatal(err) }
archivist := agents.FromDir(dir, agents.FromDirOptions{Name: "archivist", Does: "keeps records", Model: "m-archivist"})
var sawSoul bool transport := roundTripFunc(func(req *http.Request) (*http.Response, error) { b, _ := io.ReadAll(req.Body) if strings.Contains(string(b), "Archivist, terse and precise") { sawSoul = true } body, _ := json.Marshal(map[string]any{ "choices": []any{map[string]any{"message": map[string]any{"role": "assistant", "content": "logged"}}}, "usage": map[string]any{"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, }) return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewReader(body))}, nil })
r, _ := archivist.Run(agents.Options{Transport: transport}, "file this record") if r.Status != "done" || r.Text != "logged" { log.Fatalf("unexpected: %+v", r) } if !sawSoul { log.Fatal("expected the composed SOUL.md text in the request the provider received") }
fmt.Println("ok:", archivist.Name, "ran with its composed soul")}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }3. The full surface — extra tools, and turning the memory builtin off
Section titled “3. The full surface — extra tools, and turning the memory builtin off”opts.Memory set to a pointer-to-false yields a read-only persona: no memory tool at all,
even though a home directory is wired.
package main
import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os"
"github.com/muthuishere/toolnexus/golang/agents")
func main() { dir, err := os.MkdirTemp("", "readonly-*") if err != nil { log.Fatal(err) } defer os.RemoveAll(dir)
noMemory := false readonly := agents.FromDir(dir, agents.FromDirOptions{ Name: "readonly", Model: "m-readonly", Memory: &noMemory, })
// A separate persona with memory enabled (the default) DOES get the memory tool — // the mock LLM below inspects each request to prove the toggle actually removes it. withMemory := agents.FromDir(dir, agents.FromDirOptions{Name: "withmemory", Model: "m-with"})
transport := roundTripFunc(func(req *http.Request) (*http.Response, error) { b, _ := io.ReadAll(req.Body) hasMemoryDecl := containsToolName(string(b), "memory") content := "no-memory-tool-declared" if hasMemoryDecl { content = "memory-tool-declared" } body, _ := json.Marshal(map[string]any{ "choices": []any{map[string]any{"message": map[string]any{"role": "assistant", "content": content}}}, "usage": map[string]any{"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, }) return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewReader(body))}, nil })
r1, _ := readonly.Run(agents.Options{Transport: transport}, "go") if r1.Text != "no-memory-tool-declared" { log.Fatalf("expected the readonly persona to declare no memory tool, got %q", r1.Text) }
r2, _ := withMemory.Run(agents.Options{Transport: transport}, "go") if r2.Text != "memory-tool-declared" { log.Fatalf("expected the default persona to declare a memory tool, got %q", r2.Text) }
fmt.Println("ok: Memory:&false omits the tool; the default wires it")}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }
func containsToolName(body, name string) bool { return len(body) > 0 && indexOfSub(body, "\""+name+"\"") >= 0}
func indexOfSub(s, sub string) int { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return i } } return -1}FromDirOptions fields
Section titled “FromDirOptions fields”| Field | Type | Default | What it does |
|---|---|---|---|
Does |
string |
"persona agent from <dir>" |
The routing description the delegating model (or a parent’s task tool) sees. |
Name |
string |
filepath.Base(dir) |
The agent’s name. |
Model |
string |
"inherit" |
The model id passed to agents.Spec.Model. |
Tools |
[]tn.Tool |
none | Extra tools beyond the memory builtin. |
Memory |
*bool |
nil (enabled) |
Point at false to omit MemoryTool(dir) — a read-only persona. |
See also
Section titled “See also”ComposeSoul— Build a persona’s system prompt from its home directory: identity, memory, skills.MemoryTool— The opt-in built-in that lets a persona write durable notes to its own home.agents.New— The lower-level constructorFromDirwraps.