A single-user local daemon that runs multi-agent task DAGs with persistent, hybrid memory. Apps are thin clients speaking newline-delimited JSON (NDJSON) over a local IPC socket — Unix domain socket on Unix, Windows named pipe on Windows, or TCP when forced.
- One daemon owns all state: sessions, task graphs, memory, worker pool.
- A coordinator schedules DAG nodes onto a shared worker pool with per-session quotas; each worker runs a minimal agent turn loop (provider -> tool calls -> report).
- Hybrid memory: structured facts plus cosine vector recall via a pluggable embedding backend.
- State persists to disk; in-progress graphs auto-resume on restart.
cargo runThe daemon binds its platform default endpoint — a Unix socket
(<data-dir>/agentd.sock) on Unix, a named pipe on Windows — and writes it to
<data-dir>/endpoint so clients can discover it. Override the data dir with
--data-dir or AGENTD_DATA_DIR, or force a specific transport with
--endpoint / AGENTD_ENDPOINT:
cargo run -- --endpoint unix:///tmp/agentd.sock # force UDS
AGENTD_ENDPOINT=tcp://127.0.0.1:0 cargo run # force TCP (any platform)The daemon logs agentd listening on <endpoint> when RUST_LOG=info (or
AGENTD_VERBOSE) is set.
There is no in-place reload — the model is stop-and-restart. Stop the daemon
gracefully by sending a Shutdown request (the server replies ShutdownAck
and exits its accept loop), by Ctrl-C, or on Unix by SIGTERM/SIGINT. The
daemon does not unlink its socket on shutdown (matching the lock file's
never-unlink-while-held rule); a stale socket file from a crash or graceful
stop is removed by the NEXT start, under the single-instance lock, before
binding. Windows pipes leave no filesystem artifact either way. Persisted
sessions/graphs/memory resume automatically on restart.
Without a config.toml, the daemon still starts, but provider calls fail
fast (no api_key). To actually run agents, drop a config.toml into the
data dir first — see below.
config.toml lives in the data dir (~/.agentd/config.toml by default):
[provider]
base_url = "https://api.openai.com/v1"
api_key = "sk-..."
model = "gpt-4o-mini"
# Optional: embedding overrides. Defaults to the provider endpoint's
# /embeddings route with the same model.
[embedding]
base_url = "https://api.openai.com/v1"
model = "text-embedding-3-small"
max_concurrency = 4 # worker pool size
per_session_quota = 2 # max nodes running concurrently per sessionEverything is optional except a working provider.api_key for real runs.
Transport is a local IPC socket — Unix domain socket on Unix, Windows named
pipe on Windows, or TCP when forced via --endpoint/AGENTD_ENDPOINT. All
backends carry the same frames: one JSON object per line, always carrying
"v": 1 and a numeric id that correlates request and reply. Requests that
don't match a known kind decode as Unknown and get an Unsupported
error — never a parse error.
| Request | Fields | Reply |
|---|---|---|
ping |
— | pong |
create_session |
name, project |
session_created / invalid_request |
list_sessions |
— | session_list |
open_session |
session_id |
session_opened / not_found |
submit_task_graph |
session_id, tasks[] |
graph_accepted / invalid_graph |
graph_status |
graph_id |
graph_status / not_found |
cancel_graph |
graph_id |
cancel_ack / not_found |
cancel_task |
graph_id, task_id |
cancel_ack / not_found |
send_message |
session_id, worker_id, text |
send_message_ack / not_found |
register_tool |
def |
tool_registered |
tool_call_result |
call_id, output |
tool_call_result_ack |
memory_store |
session_id, entry |
memory_stored / invalid_request |
memory_query |
session_id, text, limit |
memory_results / invalid_request |
stats |
— | stats |
shutdown |
— | shutdown_ack, then the daemon stops accepting |
| Kind | Payload | Meaning |
|---|---|---|
graph_updated |
graph_id, node_deltas[] |
node state transitions |
worker_turn |
session_id, worker_id, text_delta |
agent text output |
tool_call_request |
call_id, tool, input |
app must run a registered tool |
memory_injected |
session_id, memory_ids[] |
memory surfaced to a worker |
not_found, invalid_graph, task_failed, tool_error,
provider_error, unsupported, busy, invalid_request.
use agentd_client::AgentdClient;
use agentd_core::Task;
use agentd_protocol::{Event, ReplyKind, RequestKind};
fn main() -> anyhow::Result<()> {
let mut client = AgentdClient::connect_default()?; // resolves <data-dir>/endpoint
client.request(RequestKind::Ping)?; // -> Pong
let session_id = match client.request(RequestKind::CreateSession {
name: "app".into(),
project: "proj".into(),
})? {
ReplyKind::SessionCreated { meta } => meta.id,
_ => return Err(anyhow::anyhow!("create failed")),
};
let graph_id = match client.request(RequestKind::SubmitTaskGraph {
session_id,
tasks: vec![Task::new("t1", "summarize the docs", vec![], 2)],
})? {
ReplyKind::GraphAccepted { graph_id } => graph_id,
_ => return Err(anyhow::anyhow!("submit failed")),
};
let mut events = client.events();
loop {
match events.recv() {
Ok(Event::GraphUpdated { node_deltas, .. }) => println!("nodes: {node_deltas:?}"),
Ok(Event::ToolCallRequest { tool, input, .. }) => {
// run the tool in your app, then:
client.request(RequestKind::ToolCallResult {
call_id,
output: tool_output,
})?;
}
Ok(Event::WorkerTurn { text_delta, .. }) => println!("agent: {text_delta}"),
Ok(_) => {}
Err(_) => break, // daemon closed the connection
}
}
let _ = graph_id;
Ok(())
}See crates/agentd-client/tests/e2e.rs for a complete round-trip.
+------------+ NDJSON (UDS / pipe / TCP) +-------------------------------------------+
| App | --------------------------> | agentd daemon |
| (client) | <-------------------------- | |
+------------+ | server.rs accept loop, conns |
| state.rs shared DaemonState |
| coordinator.rs DAG scheduler + quotas |
| worker pool agent turn loops |
| agent.rs provider <-> tools |
| provider.rs LLM + streaming |
| tool_callbacks callback routing |
| memory.rs facts + vector recall |
| embedding.rs pluggable embedder |
| persistence.rs atomic disk writes |
+-------------------------------------------+
|
~/.agentd/ (lock, endpoint, sessions.json,
graphs/, memory/, config.toml)
agentd-core— data contracts (DTOs only, no I/O).agentd-protocol— request/reply/event codecs.agentd-transport—Endpoint/Listener/Stream/ClientStreamabstraction over UDS, Windows named pipes, and TCP.agentd-daemon— the server.agentd-client— blocking client crate for apps.
cargo test --workspace # full suite (all crates)
cargo test -p agentd-core # graph model, memory types
cargo test -p agentd-protocol # codecs
cargo test -p agentd-daemon # server, coordinator, memory, lock
cargo test -p agentd-client # e2e + restart + concurrency over a real socket
cargo clippy --all-targets # lints
cargo fmt # formattingTests never touch a real provider: integration tests inject
MockProvider and MockEmbedding from agentd-daemon::testutil.