Home.FromDir
C# · package Toolnexus · SPEC §7E · Agents/Home.cs
namespace Toolnexus.Agents;
public static class Home{ public static Agent FromDir(string dir, FromDirOptions? opts = null);}
public sealed class FromDirOptions{ public string? Does { get; set; } // routing description; default "persona agent from <dir>" public string? Name { get; set; } // agent name; default the directory's base name public string? Model { get; set; } // default "inherit" public List<ITool>? Tools { get; set; } // extra tools beyond the memory builtin public bool Memory { get; set; } = true; // set false to omit the memory tool (read-only persona)}The directory is the agent (SPEC §7E). FromDir calls
Home.ComposeSoul on dir to build the frozen system-prompt
snapshot, wires Home.MemoryTool over the same directory
(unless Memory = false), and returns a plain Agent — RunAsync
it like any agent, or hand it to Home.StartAgent for a heartbeat.
When to use it
Section titled “When to use it”You keep a persona’s identity in files on disk — SOUL.md for voice, AGENTS.md for operating
instructions, MEMORY.md for what it has learned — and want one call that turns that folder into
a runnable Agent with its durable-notes tool already attached. This is the on-ramp for every
long-lived persona: a support bot, a research assistant, anything that should remember things
across sessions without you hand-wiring AgentSpec yourself.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call
Section titled “1. The smallest useful call”using Toolnexus;using Toolnexus.Agents;
var dir = Directory.CreateTempSubdirectory("toolnexus-home-").FullName;File.WriteAllText(Path.Combine(dir, "SOUL.md"), "You are Kavi, a terse research assistant.");
var kavi = Home.FromDir(dir);
var llm = new MockLlm(_ => Task.FromResult(MockLlm.Text("hi from Kavi")));var rtOpts = new RuntimeOptions { ApiKey = "test-key", Handler = llm, BaseUrl = "http://runtime.invalid", Style = "openai", Model = "mock" };
var result = await kavi.RunAsync(rtOpts, "say hi");
if (result.Status != "done") throw new Exception(result.Status);if (result.Text != "hi from Kavi") throw new Exception(result.Text);if (kavi.Name != Path.GetFileName(dir)) throw new Exception(kavi.Name); // default name = directory base name
Console.WriteLine($"ok: {kavi.Name} -> {result.Text}");
sealed class MockLlm : System.Net.Http.HttpMessageHandler{ readonly Func<System.Net.Http.HttpRequestMessage, Task<System.Net.Http.HttpResponseMessage>> _respond; public MockLlm(Func<System.Net.Http.HttpRequestMessage, Task<System.Net.Http.HttpResponseMessage>> respond) => _respond = respond; protected override Task<System.Net.Http.HttpResponseMessage> SendAsync( System.Net.Http.HttpRequestMessage request, CancellationToken ct) => _respond(request);
public static System.Net.Http.HttpResponseMessage Text(string content) => Json(System.Text.Json.JsonSerializer.Serialize(new { choices = new[] { new { message = new { role = "assistant", content } } }, usage = new { prompt_tokens = 5, completion_tokens = 3, total_tokens = 8 }, }));
public static System.Net.Http.HttpResponseMessage ToolCall(string id, string name, object args) => Json(System.Text.Json.JsonSerializer.Serialize(new { choices = new[] { new { message = new { role = "assistant", content = (string?)null, tool_calls = new[] { new { id, type = "function", function = new { name, arguments = System.Text.Json.JsonSerializer.Serialize(args) } } } } }, }, usage = new { prompt_tokens = 10, completion_tokens = 5, total_tokens = 15 }, }));
static System.Net.Http.HttpResponseMessage Json(string body) => new(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(body, System.Text.Encoding.UTF8, "application/json"), };}2. The realistic case — the memory tool is wired in by default
Section titled “2. The realistic case — the memory tool is wired in by default”using System.Linq;using Toolnexus;using Toolnexus.Agents;
var dir = Directory.CreateTempSubdirectory("toolnexus-home-").FullName;File.WriteAllText(Path.Combine(dir, "SOUL.md"), "You are Kavi.");
var kavi = Home.FromDir(dir); // Memory defaults to true — the `memory` tool is auto-attached
if (kavi.Spec.Uses is null || !kavi.Spec.Uses.Any(t => t.Name == "memory")) throw new Exception("expected the memory tool to be included by default");
var calls = 0;var llm = new MockLlm(_ =>{ calls++; return Task.FromResult(calls == 1 ? MockLlm.ToolCall("c1", "memory", new { action = "add", text = "the user prefers terse replies" }) : MockLlm.Text("noted"));});var rtOpts = new RuntimeOptions { ApiKey = "test-key", Handler = llm, BaseUrl = "http://runtime.invalid", Style = "openai", Model = "mock" };
var result = await kavi.RunAsync(rtOpts, "remember that I like short answers");
if (result.Status != "done") throw new Exception(result.Status);if (result.Text != "noted") throw new Exception(result.Text);
// The memory tool writes straight to disk — MEMORY.md now carries the entry.var memoryFile = Path.Combine(dir, "MEMORY.md");if (!File.Exists(memoryFile)) throw new Exception("MEMORY.md was not written");if (!File.ReadAllText(memoryFile).Contains("the user prefers terse replies")) throw new Exception("entry missing");
Console.WriteLine($"ok: {result.Text} (wrote {new FileInfo(memoryFile).Length} bytes to MEMORY.md)");
sealed class MockLlm : System.Net.Http.HttpMessageHandler{ readonly Func<System.Net.Http.HttpRequestMessage, Task<System.Net.Http.HttpResponseMessage>> _respond; public MockLlm(Func<System.Net.Http.HttpRequestMessage, Task<System.Net.Http.HttpResponseMessage>> respond) => _respond = respond; protected override Task<System.Net.Http.HttpResponseMessage> SendAsync( System.Net.Http.HttpRequestMessage request, CancellationToken ct) => _respond(request);
public static System.Net.Http.HttpResponseMessage Text(string content) => Json(System.Text.Json.JsonSerializer.Serialize(new { choices = new[] { new { message = new { role = "assistant", content } } }, usage = new { prompt_tokens = 5, completion_tokens = 3, total_tokens = 8 }, }));
public static System.Net.Http.HttpResponseMessage ToolCall(string id, string name, object args) => Json(System.Text.Json.JsonSerializer.Serialize(new { choices = new[] { new { message = new { role = "assistant", content = (string?)null, tool_calls = new[] { new { id, type = "function", function = new { name, arguments = System.Text.Json.JsonSerializer.Serialize(args) } } } } }, }, usage = new { prompt_tokens = 10, completion_tokens = 5, total_tokens = 15 }, }));
static System.Net.Http.HttpResponseMessage Json(string body) => new(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(body, System.Text.Encoding.UTF8, "application/json"), };}3. Full surface — every FromDirOptions field
Section titled “3. Full surface — every FromDirOptions field”using System.Linq;using Toolnexus;using Toolnexus.Agents;
var dir = Directory.CreateTempSubdirectory("toolnexus-home-").FullName;File.WriteAllText(Path.Combine(dir, "SOUL.md"), "You are a base identity.");
var extra = NativeTool.Of("ping", "replies pong", new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?>() }, (IDictionary<string, object?> _) => "pong");
var agent = Home.FromDir(dir, new Home.FromDirOptions{ Does = "a fully configured persona", Name = "custom-name", Model = "gpt-4o-mini", Tools = new List<ITool> { extra }, Memory = false, // opt out of the memory builtin — a read-only persona});
if (agent.Name != "custom-name") throw new Exception(agent.Name);if (agent.Spec.Does != "a fully configured persona") throw new Exception(agent.Spec.Does);if (agent.Spec.Model != "gpt-4o-mini") throw new Exception(agent.Spec.Model);if (agent.Spec.Uses is null || agent.Spec.Uses.Count != 1 || agent.Spec.Uses[0].Name != "ping") throw new Exception("expected exactly the extra tool, no memory tool");if (agent.Spec.Soul is null || !agent.Spec.Soul.Contains("You are a base identity.")) throw new Exception(agent.Spec.Soul);
Console.WriteLine($"ok: {agent.Name} does '{agent.Spec.Does}' with tools [{string.Join(",", agent.Spec.Uses.Select(t => t.Name))}]");Fields (FromDirOptions)
Section titled “Fields (FromDirOptions)”| Field | Type | What it is |
|---|---|---|
Does |
string? |
Routing description; defaults to "persona agent from <dir>". |
Name |
string? |
Agent name; defaults to the directory’s base name. |
Model |
string? |
Model id; defaults to "inherit" (the runtime’s default). |
Tools |
List<ITool>? |
Extra tools beyond the memory builtin. |
Memory |
bool |
Set false to omit the memory tool (a read-only persona). Default true. |
See also
Section titled “See also”Home.ComposeSoul— Build a persona’s system prompt from its home directory: identity, memory, skills.Home.MemoryTool— The opt-in built-in that lets a persona write durable notes to its own home.Agent— WhatFromDirreturns: its own toolkit, prompt, and budget, runnable standalone or as a tool.