Agent State
std/agent_state is Harn’s durable, session-scoped scratch space for
agent orchestration. It gives a caller-owned root directory plus a
session id a small set of predictable operations:
- write text blobs atomically
- read them back later
- list keys deterministically
- delete keys
- persist a machine-readable handoff document
- reopen the same session from a later process with
agent_state_resume
The important design point is that the primitive is generic. Harn owns the durable-state substrate; host apps own their schema and naming conventions layered on top of it.
Import
import "std/agent_state"
Functions
| Function | Returns | Notes |
|---|---|---|
agent_state_init(root, options?) | state_handle | Creates or reopens a session-scoped state root under root/<session_id>/ |
agent_state_resume(root, session_id, options?) | state_handle | Reopens an existing session; errors if it does not exist |
agent_state_write(handle, key, content) | nil | Atomic temp-write plus rename |
agent_state_read(handle, key) | string or nil | Returns nil for missing keys |
agent_state_list(handle) | list<string> | Lexicographically sorted, recursive, deterministic |
agent_state_delete(handle, key) | nil | Missing keys are ignored |
agent_state_handoff(handle, summary) | nil | Writes a structured JSON handoff envelope to __handoff.json |
agent_state_handoff_key() | string | Returns the reserved handoff key name ("__handoff.json") |
Handle shape
agent_state_init(...) and agent_state_resume(...) return a tagged
dict:
{
_type: "state_handle",
backend: "filesystem",
root: "/absolute/root",
session_id: "session-123",
handoff_key: "__handoff.json",
conflict_policy: "ignore",
writer: {
writer_id: "worker-a",
stage_id: "worker-a",
session_id: "session-123",
worker_id: "worker-a"
}
}
The exact fields are stable on purpose. Other runtime features can build on the same handle semantics without introducing a second durable-state model.
Session ids
agent_state_init(root, options?) looks for options.session_id first.
If it is absent, Harn defaults to the active agent/workflow session id
when one exists. Outside an active agent context, Harn mints a fresh
UUIDv7.
That means common agent code can usually say:
import "std/agent_state"
pipeline default() {
let state = agent_state_init(".harn/state", {writer_id: "planner"})
agent_state_write(state, "plan.md", "# Plan")
}
and get a session-specific namespace automatically.
Keys and layout
Keys are always relative to the session root. Nested paths are fine:
import "std/agent_state"
pipeline default() {
let state = agent_state_init(".harn/state", {writer_id: "planner"})
agent_state_write(state, "plan.md", "# Plan")
agent_state_write(state, "evidence/files.json", "{\"paths\":[]}")
}
Rejected key forms:
- absolute paths
- any path containing
.. - reserved internal metadata paths
The default filesystem backend stores user content under:
<root>/<session_id>/<key>
with internal writer metadata stored separately under a hidden backend
directory. agent_state_list(...) only returns user-visible keys.
Atomic writes
agent_state_write(...) writes to a temp file in the target directory,
syncs it, then renames it into place. If the process crashes before the
rename, the old file remains intact and the partially-written temp file
never becomes the visible key.
This guarantees “no partial file at the target path”, which is the durability property the primitive is designed to expose.
Handoff documents
agent_state_handoff(handle, summary) stores a JSON envelope at
__handoff.json:
{
"_type": "agent_state_handoff",
"version": 1,
"session_id": "session-123",
"key": "__handoff.json",
"summary": {
"status": "ready"
}
}
Callers own the shape of summary. Harn owns the outer envelope and the
well-known key.
Two-writer discipline
Each handle can carry a writer identity and conflict policy:
let state = agent_state_init(".harn/state", {
session_id: "demo",
writer_id: "planner",
conflict_policy: "error"
})
Supported policies:
"ignore": accept overlapping writes silently"warn": accept the write and emit a runtime warning"error": reject the write before replacing the existing content
Conflict detection compares the previous writer id for that key with the current writer id. This is intentionally simple and deterministic: it is a guard rail against accidental stage overlap, not a full distributed locking protocol.
Backend seam
The default implementation is a filesystem backend, but the storage
layer is split behind a backend trait in
crates/harn-vm/src/stdlib/agent_state/backend.rs.
That trait is designed around:
- scope creation/resume
- atomic blob read/write/delete
- deterministic list
- conflict metadata on write
so future backends such as in-memory, SQLite, or remote stores can plug in without changing the Harn-facing handle semantics.
Example
import "std/agent_state"
pipeline default() {
let state = agent_state_init(".harn/state", {
session_id: "review-42",
writer_id: "triage"
})
agent_state_write(state, "plan.md", "# Plan\n- inspect PR")
agent_state_handoff(state, {
status: "needs_review",
next_stage: "implement"
})
let resumed = agent_state_resume(".harn/state", "review-42", {
writer_id: "implement"
})
println(agent_state_read(resumed, "plan.md"))
}