An agent is a definition row plus a private state directory. Everything about what it can do lives in one type, AgentDefinition (agents/protocol/src/index.ts:38-53):
type AgentDefinition = {
name: string
systemPrompt: string | AgentPromptBlock[]
modelProvider: string
model: string
reasoningLevel?: ReasoningLevel
tools?: string[]
signingKey?: string
signingKeys?: string[]
metadata?: Record<string, unknown>
}The agents table stores this as CBOR alongside state_dir and status (idle | running | stopped | error). The state directory holds the agent's memory tree and its session attachments.
The system prompt is stored as a Seed block tree (HMBlockNode[]) rather than a string, then rendered to markdown for the model — so an agent's instructions are editable content, not an opaque blob. Bounds are enforced at normalization: 64 KiB of prompt, 32 tools, 16 KiB of metadata (see Limits).
Grants: one array, two jobs
definition.tools is the entire permission model, and it does two things at once.
It narrows the callable set. The verbs are always on and are never grants; what tools gates is which entries of callableToolRegistry — search, web_search, navigate, execute — this agent may reach through call. An undefined tools array grants every service callable (enabledCallableTools, agents/src/api-service.ts:313-319).
It carries the publish grant. The pseudo-tool name 'publish' gates signed public writing — hm:// and ipfs:// — through write. Memory writes are never gated, by design. Refusal is explicit: 403 'Publishing is not enabled for this agent. The owner can grant "Publish Seed content" in its tool settings.' Legacy tool names (write, memory_publish_document, ipfs_write, attachment_to_ipfs) still count as the publish grant so older agent definitions keep working (:306-311).
Two consequences worth internalizing:
Authoring a tool is not an escape hatch. A lambda call requires the same execute grant its runtime needs (:7860-7867) — otherwise writing a tool document would route around an owner who turned code execution off.
A delegated child's tools can only reduce authority, never widen it beyond the parent's own callable set.
Signing identities
An agent has no hypermedia identity of its own. It signs as whichever signing identity its signingKey/signingKeys name — those fields hold secret names, not keys. A signing identity is an Ed25519 seed encrypted in the secrets table under the name hm-account-<first 16 chars of account id>, tagged metadata.kind = 'hm-account-key'.
Four actions manage them, and the difference between two of them is the interesting part:
CreateSigningIdentity generates a fresh key and publishes a profile plus a home document for it.
ImportSigningIdentity takes an existing key's raw 32-byte seed (decrypted client-side from a .hmkey.json) and publishes nothing at all. The reasoning is in the code (agents/src/api-service.ts:1134-1141): an imported account usually already exists on the network with a profile its owner published, and generating one here would overwrite the real thing. Re-importing the same key is a 409.
UpdateSigningIdentity renames and republishes the profile, optionally uploading a new avatar to IPFS.
DeleteSigningIdentity removes the stored secret only; nothing is unpublished from the network.
When an agent gets a signing identity, the server also creates a default user-mention trigger so that @-mentioning that account starts a session (#createDefaultMentionTrigger, :964-984).
Reasoning level
reasoningLevel is one of minimal | low | medium | high | xhigh and is passed to Pi as thinkingLevel. Which levels a model actually accepts is per model generation, and the table in agents/protocol/src/reasoning.ts is empirically verified against live provider APIs rather than scraped — see Providers for the matrix and the gotcha that motivated it.
Related
Providers — models, keys, and OAuth
Tools as Documents — what a grant lets through
Security — trust boundaries and gaps
The Space — what an agent has
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime