Skip to content

Concepts

Milieu is an AI agent ecosystem in which agents perceive and act through command-line interfaces. Much of the design follows from that: a CLI can be allowlisted before it runs and recorded after it runs.

This document defines the entities Milieu is built from, what each one is for, and how they fit together. Command syntax is in REFERENCE; the mechanics of the running system are in ARCHITECTURE. Read this when you are new, or when you have to decide between two things that sound alike — which layer a document belongs in, whether to open a session or send a message.

Everything here describes Milieu as built today. Planned work is in ROADMAP.

Contents

1. Where Milieu sits

Two kinds of AI system are worth telling apart. One is the agent a person already has in front of them — Claude Desktop, Gemini Enterprise, OpenAI, Microsoft Cowork, Amazon Quick — running on a model you did not choose, in an environment you do not administer. The other is the enterprise side, where work touches systems of record, and the hard part is not capability but authority: which credential, reaching which host, on whose behalf, and what the record says afterwards.

Milieu is the enterprise side. It runs as a VM or container of its own, split into a trusted zone that holds the credentials and an untrusted zone where the agents actually work, with a network edge that a user’s own agent can call into.

    flowchart LR
    ENT[("Enterprise systems<br/>REST · CLI · SQL · MCP servers")]

    subgraph HOST["Milieu — VM or container"]
      direction LR
      subgraph TZ["Trusted zone"]
        GW["Gateway broker<br/>secrets · API access<br/>own CA · own audit log"]
      end
      subgraph UZ["Untrusted zone"]
        AG["Agents<br/>sandboxed · allowlisted CLIs<br/>no ambient secrets"]
      end
      EDGE["API edge<br/>REST · A2A<br/>bearer key over TLS"]
    end

    LLM1["Model backend<br/>Claude, or any<br/>OpenAI-compatible endpoint"]
    UA["User agent<br/>Claude Desktop · Gemini Enterprise<br/>OpenAI · MS Cowork · Amazon Quick"]
    LLM2["Its own model<br/>not configured by Milieu"]

    ENT <--> GW
    GW <-->|mTLS, credential injected here| AG
    AG <--> EDGE
    EDGE <-->|network| UA
    AG -.-> LLM1
    UA -.-> LLM2

    classDef trusted fill:#e0e7ff,stroke:#6366f1
    classDef untrusted fill:#fee2e2,stroke:#ef4444
    class GW trusted
    class AG untrusted
  

Each part of that picture is something that ships:

In the pictureIn MilieuRuns as
Enterprise systemsUpstreams reached only through a named, operator-declared routegateway routes: rest, cli, sqlite, postgres, mcp
Trusted zone — secrets and API accessThe gateway broker: holds the credentials, is its own CA, authorizes each call by client-certificate CN, and keeps its own hash-chained audit logmilieu gateway serve, deployed apart from the host
Untrusted zone — AI and agentsAgent sandboxes: a granted CLI allowlist, a fixed PATH, a per-host egress allowlist, no ambient secretsbwrap on Linux, sbexec on macOS
VM or containerOne binary over one state tree under $MILIEU_HOMEContainerfile, systemd, or Kubernetes
The network edgeREST API and A2A, both authenticated by a bearer key over TLS; A2A publishes a public agent card per agentmilieu api serve, milieu a2a serve
LLM model, ClaudeA pluggable backend: claude, openrouter for any OpenAI-compatible endpoint including Azure, LiteLLM, vLLM or a local llama-server, and echo for offline runsMILIEU_LLM
The user agent’s modelOutside Milieu entirely

One label belongs on the other edge. MCP is something Milieu consumes, not something it serves: the gateway has an mcp backend that brokers an operator-allowlisted tool set from an external MCP server, projected to a skill as <route>__<tool>, so it sits between the trusted zone and the enterprise systems. A user’s agent reaching in arrives over REST or A2A instead.

A2A goes both ways, but not symmetrically. Milieu serves it in full and consumes it one exchange at a time: the gateway’s a2a backend brokers message/send to a remote peer, so an agent here can put a question to an agent elsewhere without holding that peer’s credential. What it cannot yet do is remember the conversation — carrying a peer’s contextId across runs needs durable gateway state, and is on the roadmap.

The asymmetry is the point of the layout. Inside its sandbox an agent is comparatively free: it composes whatever granted CLIs it likes, writes its own memory and transcripts, and decides for itself how to reach an answer. The policy lives at the two edges instead — what it may execute and which hosts it may reach on one side, and a credential it never holds on the other. Everything in the rest of this document is either something an agent works with inside that box, or something that defines one of its edges.

2. The shape of the system

Milieu is built around three principles, and most of the entities below serve at least one of them:

  • Observability — every action is visible: one audit row per invocation, a transcript per run, durable messages, readable state on disk.
  • Explainability — an agent’s answer can be reconstructed from inspectable inputs: its briefs, its skill, and the knowledge and memory layers it read.
  • Accountability — every action binds to an identity, and the record is append-only and hash-chained.
    mindmap
  root((Milieu))
    Agent
      Identity
        code
        role
        credentials
      Personality
        personality.md
        responsibility.md
      Knowledge
        axioms
        articles
        news
        events
      Skills
        SKILL.md
        allowed-tools
        bundled resources
    Work
      Run
        act
        wake
      Session
        external conversation
        turns
      Message
        outbox
        inbox
        reply depth
      Project
        instructions
        members
        layer
    Substrate
      Layers
        global
        project
        agent
      Records
        transcript
        audit log
        blob store
      Boundaries
        sandbox
        API edge
        gateway broker
    Control
      milieu CLI
      milieu daemon
      milieu apply
  

Those concepts run on one binary invoked per command, over one on-disk state tree:

    architecture-beta
    group edge[Callers]
    service op(server)[Operator or CI] in edge
    service client(internet)[External program] in edge

    group host[Milieu host]
    service api(server)[API server] in host
    service daemon(server)[Daemon] in host
    service agent(server)[Agent run sandboxed] in host
    service store(database)[Knowledge and memory] in host
    service audit(disk)[Audit log] in host
    service blobs(disk)[Blob store] in host

    group trusted[Trusted zone]
    service gw(server)[Gateway broker] in trusted
    service upstream(cloud)[Upstream systems] in trusted

    op:R --> L:daemon
    client:R --> L:api
    api:B --> T:agent
    daemon:B --> T:agent
    agent:R --> L:store
    agent:B --> T:blobs
    agent:B --> T:audit
    agent:R --> L:gw
    gw:R --> L:upstream
  

Only the servers are resident processes. An agent is a directory; it runs for the duration of one skill invocation and then exits. Most of what follows is therefore a file you can read, diff and back up.

3. Agent

An agent is an identity that can be asked to do work. On disk it is a directory, var/agents/<code>/: no process, no port, and no memory beyond what it writes down.

Four facets define one:

FacetConcretelyAnswers
Identityagent.toml — code, role, permissions, network allowlist, gateway identityWho is this, and what may it do?
Personalitypersonality.md, responsibility.mdHow does it behave, and what does it own?
Knowledgeits knowledge/ and memory/ layersWhat does it know and remember?
Skillsskills/<name>.md plus the permissions those tools needWhat can it do?

The facets vary independently: two agents can share a skill and differ in permissions, or share permissions and differ in responsibility. The agent code is what the rest of the system attributes to — audit rows, API keys, gateway certificates, messages and sessions all name one.

4. Role, personality, responsibility

These three are easy to confuse.

  • Role is a capability category recorded in agent.toml and enforced by the code: administrator (may dispatch work to any agent), associate (the default, does the work), assistant (a narrower associate, by convention), and auditor (read-only, and unable to send messages — the one role rule the code enforces today).
  • Responsibility (responsibility.md) is prose describing the job within that capability: scope, boundaries, and what “done” looks like.
  • Personality (personality.md) covers how the agent communicates: tone, voice, length, demeanour.

Both briefs are prepended to the system prompt on every run, above project instructions and the skill body. The role is checked by the system; the briefs only steer the model. Keep them consistent — an auditor’s brief should not claim it resolves tickets, since it cannot send the message that would resolve one.

5. Skill

A skill is a capability in Anthropic’s Agent Skills format: skills/<name>.md, or a directory skills/<name>/SKILL.md with bundled resources beside it. YAML frontmatter declares name, description and optionally model, max-iterations, allowed-tools, version, license and metadata. The body is the system prompt.

Skills load progressively. The catalogue an agent sees is names and descriptions; a run loads one body; bundled resources are read only when the skill asks for them through the skill tool. act --auto lets the model choose from the catalogue, and the choice is recorded in the audit log.

Every run records two identifiers for the skill it ran: the version from frontmatter, and a digest — the SHA-256 of the skill as loaded, covering the manifest and every bundled resource. A version is a claim the author can forget to update, so the digest is what tells you which skill answered.

allowed-tools declares tools rather than granting them. It is intersected with the agent’s permissions, so a skill that asks for curl on an agent that has no curl grant does not get curl.

6. Permission, tool, sandbox

A permission is a rule attached to an agent. Rules match argument tokens rather than a flattened string: cmd for any invocation, cmd(a b) for an exact argv, cmd(a b:*) for a prefix, cmd(:*) for any argv.

A tool is what the model may call during a run, and there are two kinds:

  • Sandboxed commands — a real CLI, executed by the runner inside a sandbox backend (bwrap on Linux, sbexec on macOS, none only if you opt in). Commands resolve against a fixed sandbox PATH rather than the caller’s $PATH, so the binary that was permission-checked is the binary that runs.
  • Built-ins — handled in-process and audited with sandbox builtin: knowledge and skill (read-only), transcript (read-only recall, permission-gated), memory (can write, permission-gated), session (declares that a reply is a question), gateway (a brokered call), and code (a short Starlark program).

code runs a short Starlark program. Starlark has no standard library, so a program reaches only the functions milieu injects, and which ones those are is derived from the agent’s own grants. run() inside a script goes through the same runner and writes its own audit record. Its purpose is to collapse a chain of dependent tool calls into a single step, which costs fewer tokens and less latency than one round trip per element.

Inside the sandbox the agent’s home is writable, protected directories are read-only, extra host paths appear only through operator-authored [[mount]] blocks, and network egress is limited to a per-host allowlist. No secrets are passed in; reaching a credentialed service is what the gateway is for.

7. Knowledge

Knowledge is hand-authored markdown that agents read. It holds the curated part of what an agent knows, filed by how stable it is rather than by topic:

KindDirectoryChangesExample
Axiomsaxioms/almost never“Refunds are issued to the original payment method.”
Articlesarticles/on reviewa runbook, a policy, a data dictionary
Newsnews/often, expires“Ongoing: East region outage”
Eventsevents/append-onlya discrete occurrence or signal

A file may carry OKF frontmatter — the Open Knowledge Format — with type, title, tags, status, stale_after, generated, verified and supersedes. It is optional throughout: a file without frontmatter counts as stable, unverified and never stale, so an existing base keeps working untouched.

Two pieces of that metadata keep a base usable as it ages. stale_after marks time-bound items, so a finished outage notice stops outranking live policy; stale and non-stable items are labelled rather than hidden in ls, search and get, including in the tool output the model sees, so an agent reads the caveat before it answers from the document. And a newer concept retires older ones by listing their ids under supersedes, which makes “what is still in force” a query (search --current) rather than something you have to know.

Search runs per layer and is hybrid by default: an FTS5 lexical index, plus a semantic vector index when an embedder is configured, fused by reciprocal-rank fusion. Editing a file does not update the index — milieu knowledge reindex does.

8. Memory

Memory is tool-written notes that an agent keeps. It is layered like knowledge but authored the other way round: knowledge is edited by people and then indexed, while memory is written by the agent mid-run through the permission-gated memory tool, or by milieu memory save.

A memory is one file holding one fact, with a name, a description used to judge relevance at recall time, and an advisory type (user, feedback, project, reference). Each layer keeps an auto-maintained MEMORY.md index.

The index reaches every run: it is prepended to the system prompt, so an agent always knows what it has notes about. The bodies cost something to read, so they are fetched on demand with memory(search:*) and memory(get:*), and memory(save:*) records something new.

Use knowledge for what the organisation asserts and memory for what one agent learned. If a person should review it before an agent repeats it, it belongs in knowledge.

9. Layers: global, project, agent

Knowledge and memory are each scoped in three layers:

    block-beta
  columns 3
  A["agent — most specific\nvar/agents/&lt;code&gt;/  · ungated"]:3
  B["project — membership-gated\nvar/projects/&lt;code&gt;/  · only for members"]:3
  C["global — shared by everyone\nvar/global/  · always included"]:3
  space:3
  D["--as &lt;code&gt; adds the agent layer"] E["--in &lt;project&gt; adds the project layer"] F["global is always in the union"]
  style A fill:#dbeafe,stroke:#3b82f6
  style B fill:#e0e7ff,stroke:#6366f1
  style C fill:#ede9fe,stroke:#8b5cf6
  

A read applies two rules:

  • Union — every applicable layer is merged, and search merges results across them.
  • Shadow — on a path or name clash the more specific layer wins (agent > project > global), and get returns the most-specific match.

A global articles/refund-policy.md is the house default; a project can shadow it with its own, and an agent can shadow that again. Nothing is copied — the specific file hides the general one for that reader, and stops hiding it if you delete it.

Each layer has its own index, so reindex --scope agent|project|global rebuilds one layer rather than all of them.

10. Project

A project is a named workspace with three parts:

  • Instructions (instructions.md), prepended to the skill body on --in runs, holding the standing context for this piece of work.
  • A member roster that gates who may act in the project and who may read its knowledge and memory. A non-member gets a refusal rather than an empty result.
  • A project layer of knowledge and memory, between the agent and global layers.

Projects scope work without cloning agents. One support agent run --in acme reads ACME’s clauses and follows ACME’s instructions; the same agent run without --in sees only its own layer and global.

11. Run: one turn of work

A run is one skill executed as one agent: input in, reply out, transcript written, audit row appended. Every entry point — milieu act, the REST API run routes, the A2A endpoint, and the daemon’s wake — goes through the same code, so a turn is scoped, executed and recorded the same way whichever door the caller came in by.

    sequenceDiagram
    autonumber
    actor Caller
    participant Act as act (one turn)
    participant Ctx as Context assembly
    participant LLM as Model
    participant Run as Runner + sandbox
    participant Rec as Transcript + audit

    Caller->>Act: input, --as mira, --in acme, skill
    Act->>Act: resolve skill · check project membership
    Act->>Ctx: briefs + project instructions + MEMORY.md index + skill body
    Ctx-->>LLM: system prompt + replayed session turns + input
    loop while the model calls tools
        LLM->>Run: tool_use (allowed-tools ∩ permissions)
        Run->>Run: sandbox or built-in
        Run-->>Rec: one audit row per call
        Run-->>LLM: tool result
    end
    LLM-->>Act: reply
    Act->>Rec: transcript (prompt, turns, tools, tokens, skill digest)
    Act-->>Caller: reply
  

A run’s context is fixed and inspectable: briefs, project instructions, memory index, knowledge layers, skill body. Each of those is a file on disk, which is what makes an answer reconstructible weeks later.

12. Message, route, wake, ask

Agents collaborate by message rather than by calling each other. Each agent has two SQLite databases in its home — out.db for what it has sent and in.db for what has been delivered to it — and three verbs move a message between them:

  • send writes a row to the sender’s outbox.
  • route copies undelivered outbox rows into recipients’ inboxes.
  • wake drains an inbox, runs a skill per unread message, and writes the reply to the outbox.

milieu daemon runs the last two on a loop.

    sequenceDiagram
    autonumber
    participant Arun as arun (administrator)
    participant Out as arun/out.db
    participant D as daemon (route · wake)
    participant In as mira/in.db
    participant Mira as mira (associate)

    Arun->>Out: send "triage ticket 4821" (+ blob digest)
    D->>Out: poll undelivered
    D->>In: deliver (reply_depth 0)
    D->>Mira: wake — run respond on the unread message
    Mira->>Mira: knowledge · memory · tools
    Mira-->>In: mark consumed only after the reply is sent
    Mira->>D: reply (reply_depth 1)
    D->>Arun: route back
    Note over D,Mira: reply_depth caps reply chains<br/>at MaxReplyDepth, which is 5
  

A message is marked consumed only after its reply has been sent, so a crashed wake re-processes it instead of dropping it. Every message carries a reply_depth that wake increments on reply and stops at 5, which ends what would otherwise be an endless exchange between two agents.

ask is the synchronous form over the same machinery: it sends a message stamped with a random correlation id and polls the caller’s inbox for the matching Re: reply. The ask: subject family is reserved so the daemon never auto-replies to a reply.

Attachments travel by reference. The bytes go once into the content-addressed blob store, the message carries a digest and media type, and wake hands them to the recipient’s skill as image or document blocks.

The same flow as an event model — what triggers what, what gets written, and what can be read back afterwards:

    flowchart LR
    subgraph T["Trigger"]
      OP["operator, API client,<br/>or another agent"]
    end
    subgraph C["Command"]
      SEND["send"]:::cmd
      ROUTE["route"]:::cmd
      WAKE["wake"]:::cmd
    end
    subgraph E["Durable record"]
      OUTBOX[("out.db row")]:::evt
      INBOX[("in.db row")]:::evt
      TR[("transcript")]:::evt
      AUD[("audit row")]:::evt
    end
    subgraph R["Read model"]
      IB["message inbox"]:::rm
      REP["audit report"]:::rm
      RECALL["transcript recall"]:::rm
    end

    OP --> SEND --> OUTBOX --> ROUTE --> INBOX --> WAKE
    WAKE --> TR
    WAKE --> AUD
    INBOX --> IB
    AUD --> REP
    TR --> RECALL
    WAKE -->|reply| OUTBOX

    classDef cmd fill:#dbeafe,stroke:#3b82f6
    classDef evt fill:#dcfce7,stroke:#22c55e
    classDef rm fill:#fef3c7,stroke:#f59e0b
  

13. Session

A session is the durable record of one external conversation: the turns a client sent, the replies it received, and enough identity to pick the conversation up later. Without one, every run starts cold.

    stateDiagram-v2
    [*] --> open: session open --topic billing
    open --> open: client turn → agent turn
    open --> input_required: agent turn declares<br/>session request-input
    input_required --> open: the client answers
    open --> closed: session close
    input_required --> closed: session close
    closed --> [*]: still readable, refuses new turns

    note right of input_required
      State is derived from the turns
      rather than stored.
    end note
  

Sessions live in var/sessions/, outside the sandbox home, so an agent cannot write its own. An agent that could edit its history could rewrite its next prompt; because it cannot, resuming from a session is safe.

The state in the diagram is derived from the turns rather than stored: closed once the client closes it, input-required while the newest turn is an agent turn that declared it was asking a question, open otherwise. The client’s next message clears input-required, so there is no flag to reset and none that can drift from the turns.

A session pins one topic and one agent. A client talking to three agents opens three sessions, and delegation between those agents stays internal.

A conversation outlives a context window, so a resumed run replays the most recent turns that fit a byte budget, always starting at a client turn, and tells the model when its view is only a suffix. milieu compact folds the older turns into a summary instead of dropping them. The summary is a derived file beside the turns and no turn is ever rewritten, so session show keeps printing the whole conversation. Each pass costs a model call, which is why it is a command rather than something that happens on its own.

Sessions record conversations with something outside Milieu. Agents talking to each other is internal state: messages and transcripts, bounded by reply_depth.

14. Transcript, audit log, blob

Three records with three different jobs:

TranscriptSessionAudit log
Recordshow the agent produced a reply — system prompt, turns, tool calls, tokenswhat the client said and heardthat an invocation happened, with metadata
Lives invar/agents/<code>/state/<topic>/var/sessions/<id>/var/audit/audit.jsonl
Agent may write ityes — it is the agent’s own working memorynono, appended by the system
Tamper-evidentnonoyes — SHA-256 hash chain
Fed into later runsrecalled by index, read on demandreplayednever
Retentionswept by cleanup transcriptkept until swept explicitlyappend-only, kept

The audit log holds one JSON row per CLI invocation and per built-in tool call, with seq, agent, cmd, args, exit_code, timings, sandbox backend, and the chain fields prev_hash and hash. Editing, reordering or deleting any record breaks every following hash, and milieu audit verify reports the first break. Rows for calls that reached a model carry the tokens they spent, so audit report attributes spend per agent; cost is computed at report time from a price table rather than frozen into the log.

The chain is unkeyed, which bounds what it proves. It detects tampering by anyone who cannot rewrite the file, but an operator with write access can recompute the whole chain, and lopping records off the end leaves a shorter chain that still verifies. Anchor the latest hash somewhere external if that matters to you.

The blob store (var/blobs/) is content-addressed by SHA-256 and shared across agents, so identical attachments are stored once and referenced by digest. That is how an attachment rides a message between agents without the bytes being copied.

15. Trust boundaries: sandbox, API, gateway

Milieu has three trust boundaries, each with its own identity mechanism and its own log, and they are separate trust domains by design.

    sequenceDiagram
    autonumber
    participant Ext as External program
    participant API as API server
    participant Agent as Agent sandbox
    participant Proxy as Local proxy
    participant GW as Gateway broker
    participant Up as Upstream

    Ext->>API: bearer key sk_… over TLS
    Note over API: key is stored only as a SHA-256 hash,<br/>it resolves to exactly one agent
    API->>Agent: run a skill as that agent
    Note over Agent: least privilege — CLI allowlist,<br/>fixed PATH, host allowlist,<br/>no ambient secrets
    Agent->>Proxy: gateway tool call, loopback only
    Proxy->>GW: mutual TLS, Ed25519
    Note over GW: authorizes by client-cert CN,<br/>holds the credential
    GW->>Up: request with the injected credential
    Up-->>GW: response
    GW-->>Agent: response — the secret never entered the sandbox
  
  • Sandbox boundary. The agent runs with least privilege and no ambient secrets; everything it may execute was granted explicitly.
  • API edge. External programs drive Milieu as an agent over one-way TLS, authenticated by a bearer key that is stored only as its hash and can be revoked. The key is interaction-only: no agent or project management, no command execution. A caller sees only its own sessions, and another agent’s id returns 404 rather than 403, so probing reveals nothing.
  • Gateway. A broker in a separate trusted zone holds the secrets. The agent talks to a loopback proxy, the proxy re-originates over mutual TLS, and the gateway authorizes the call by client-certificate CN, injects the credential, and logs the call — never the secret — to its own hash-chained log. Routes are REST, CLI, SQL named queries, or a brokered MCP server.

Credentials stay in the gateway, identity is carried by the certificate, and the sandbox holds neither.

16. Manifest and the control plane

There are three control surfaces over the same services:

  • milieu setup is a guided first run: it asks a few questions, writes milieu.yaml, and applies it.
  • milieu <cmd> is the imperative CLI, for exploration and one-off work.
  • milieu apply -f milieu.yaml is the declarative control plane, the kubectl analog. It takes a multi-document YAML manifest (apiVersion: milieu.dev/v1) of Agent, Project, Knowledge, Gateway, API, Web and Settings documents and reconciles them by calling the same services the CLI does.

Reconcile is upsert with no prune: declared resources are set to match the manifest, and resources absent from it are left alone. Secrets stay out of the manifest — apply mints API keys and prints them once.

    journey
    title An operator's first hour
    section Install
      make build: 5: Operator
      milieu setup — answer a few questions: 4: Operator
      Review the generated milieu.yaml: 4: Operator
    section Shape the team
      Edit personality.md and responsibility.md: 3: Operator
      Write the first skill: 3: Operator
      Grant only the permissions it needs: 4: Operator
    section Give it something to know
      Seed global knowledge: 4: Operator
      knowledge reindex: 5: Operator
      Create a project and add members: 4: Operator
    section Put it to work
      act — one turn, read the transcript: 5: Operator
      ask — one agent delegates to another: 4: Operator, Agent
      Start the daemon: 5: Operator
    section Keep it honest
      audit report — who spent what: 5: Operator
      audit verify — the chain is intact: 5: Operator, Auditor
      Re-apply the manifest on every change: 4: Operator
  

Explore with the CLI, then keep the result in the manifest. Anything that should survive a rebuild belongs there, and in version control.

17. How the entities relate

    erDiagram
    AGENT ||--o{ SKILL : "carries"
    AGENT ||--o{ PERMISSION : "is granted"
    AGENT ||--|| ROLE : "has"
    AGENT ||--|| BRIEFS : "prepends"
    AGENT ||--o{ KNOWLEDGE_LAYER : "owns"
    AGENT ||--o{ MEMORY_LAYER : "owns"
    AGENT }o--o{ PROJECT : "is member of"
    PROJECT ||--|| KNOWLEDGE_LAYER : "owns"
    PROJECT ||--|| MEMORY_LAYER : "owns"
    PROJECT ||--|| INSTRUCTIONS : "prepends"
    GLOBAL ||--|| KNOWLEDGE_LAYER : "owns"
    GLOBAL ||--|| MEMORY_LAYER : "owns"
    AGENT ||--o{ RUN : "performs"
    SKILL ||--o{ RUN : "is executed by"
    RUN ||--|| TRANSCRIPT : "writes"
    RUN ||--o{ AUDIT_ROW : "appends"
    RUN }o--o| SESSION : "continues"
    SESSION ||--o{ TURN : "holds"
    AGENT ||--o{ MESSAGE : "sends and receives"
    MESSAGE }o--o{ BLOB : "references by digest"
    RUN ||--o{ TOOL_CALL : "makes"
    TOOL_CALL }o--|| PERMISSION : "is gated by"
    TOOL_CALL }o--o| GATEWAY_ROUTE : "may broker through"
    KNOWLEDGE_LAYER ||--|| INDEX : "is searched via"
    MEMORY_LAYER ||--|| MEMORY_INDEX : "is summarised by"
  

The diagram falls into three groups: who the agent is (role, briefs, permissions), what it can read (layers, indexes, projects), and what happened (run, transcript, audit, session, message).

18. Best practices

Agents and roles

  • Split agents by responsibility, not by skill count. Two narrow agents that message each other are easier to audit than one agent holding twelve permissions.
  • Keep the briefs short and specific. “You own first contact. You classify and route — you do not resolve.” works better than a paragraph of adjectives.
  • Give a team an auditor. It cannot send messages, so it cannot be drafted into the work it reviews.
  • Do not write a brief that contradicts the role. The role wins, and the agent looks broken.

Skills

  • Write the description for the selector. Under --auto it is all the model has to choose from.
  • Declare allowed-tools narrowly. It is intersected with grants anyway, and a tight declaration documents intent.
  • Push detail into bundled resources. Keep the body about how to decide, and let reference files carry the tables and examples that are loaded only when needed.
  • Have the skill say when it is stuck. If it cannot finish without something only the client can supply, session request-input marks the turn, and the conversation shows up as outstanding instead of sitting in a queue looking answered.
  • Compare digests, not versions, when behaviour changes unexpectedly between runs.

Permissions and the sandbox

  • Grant the narrowest rule that works: curl(https://api.example.com:*) rather than curl, and an exact rule when the command line is fixed.
  • Keep MILIEU_SANDBOX=none to throwaway environments. It is opt-in and never auto-selected for a reason.
  • Use code when a task is a loop. One Starlark program over N items costs far less than N tool round trips, and it cannot exceed the agent’s grants.
  • Do not pass secrets into the sandbox. An agent that needs a credential needs a gateway route.

Knowledge

  • File by stability, not topic. Something that changes weekly is news, not an article; something that will still hold next year may be an axiom.
  • Put stale_after on anything time-bound, and use --fresh or --current in the skills that read it. Unbounded news is how a base rots.
  • Supersede rather than delete. Listing the retired ids keeps the history answerable and the current answer unambiguous.
  • Reindex whenever content changes — the index does not follow the file. Wire it into whatever step publishes the content.
  • Have a person verify what matters. A human:<id> entry under verified is what makes an item human-reviewed, and --verified is what lets a skill insist on it.

Memory

  • One fact per file, with a description worth matching against. The description is what the index shows and what relevance is judged on.
  • Grant memory(save:*) with intent. An agent that writes memory is editing its own future prompts.
  • Do not put in memory what belongs in knowledge. If other agents need it, or a person should approve it, author it as knowledge.

Layers and projects

  • Treat global as the house default and shadow it where you must. Copying a global article into ten agent layers guarantees nine of them go stale.
  • Create a project as soon as a second agent needs the same context. Instructions plus roster plus layer is the bundle you were about to assemble by hand.
  • Keep membership tight. The roster gates project knowledge and memory, not just who may act.
  • Reindex per scope: --scope project after seeding a project layer, and leave global alone.

Sessions and conversations

  • Open a session for anything a client will continue.
  • Let the client supply the id when it has one. The format matches the A2A contextId shape, so ids line up across systems.
  • Schedule compact for long conversations rather than letting the replay budget quietly drop the beginning.
  • Sweep idle sessions explicitly. Bare cleanup never touches them.

Messaging

  • Use ask when you need the answer now, and send plus the daemon when you do not.
  • Design within reply_depth. Chains cap at 5, and a workflow needing more hops wants a different shape.
  • Let attachments ride by digest rather than inlining bytes into a message body.

Operations

  • Keep the manifest in version control and re-apply on every change. Upsert with no prune means the manifest is a floor rather than a fence, so pair it with review.
  • Run audit verify on a schedule, and anchor the latest hash somewhere the operator of the host cannot rewrite.
  • Read audit report per agent rather than in aggregate. Spend per agent is the earliest sign that a skill has started looping.
  • Never put var/ on NFS. SQLite WAL needs proper POSIX locks.
  • Back up one path: everything lives under $MILIEU_HOME.

Where to next