Runtime .agent compatibility
The standalone CleanSlice runtime (cleanslice/runtime) stores an agent as a portable .agent/ folder (SOUL, MEMORY, skills, sessions, secrets, config…). Agentfy.ai 2.0 should be compatible with that format so we can: import existing runtime agents, run the runtime's agent loop unchanged in our worker, and export/back up an agent for portability.
The .agent/ layout
.agent/
├── SOUL.md # persona / system prompt (managed file)
├── USER.md # per-user context
├── MEMORY.md # curated long-term memory
├── HEARTBEAT.md # heartbeat prompt
├── agent.config.json # IAgentConfig: maxIterations, maxTokens, accessStrategy,
│ # heartbeat, session(compaction), memory(limits/review),
│ # stopPhrases, managedFiles, syncSkills, tools{...}
├── memory/ # rolling daily memory (YYYY-MM-DD.md)
├── skills/<name>/SKILL.md # skills (markdown + frontmatter)
└── data/
├── access.json # users{ id: {status, inviteCode/accessCode, createdAt} }
├── secrets/<userId>.json # { "service:key": value } (e.g. gmail:app_password)
├── sessions/<chan>:<user>.jsonl # event log: {id,type,ts,data} per line
├── memory.sqlite # FTS index over memory
└── usage.json # token usage
# + browser-state/<profile>.json (live browser cookies/state)Mapping .agent/ ↔ Agentfy.ai entities
.agent/ | Agentfy.ai 2.0 (close-compat — same names) |
|---|---|
SOUL.md | agent.soul (persona / system prompt — stored verbatim) |
USER.md | agent.user (human/profile context — stored verbatim) |
HEARTBEAT.md | agent.heartbeat (heartbeat prompt) |
agent.config.json | agent.config — stored verbatim as IAgentConfig; limits / accessStrategy / runtimeProfile are read from it, not renamed away into other slices |
MEMORY.md + memory/*.md | agent/memory (curated = MEMORY.md, daily[date] = memory/YYYY-MM-DD.md — same text) |
data/memory.sqlite | — (FTS index — derived, rebuilt on boot; never stored/transferred) |
skills/<name>/SKILL.md | agent/skill (name + frontmatter + body verbatim) (slice to add — see gaps) |
data/sessions/<chan>:<uid>.jsonl | chat (events kept as-is {id,type,ts,data}; id→externalId) |
data/access.json | agent/access (keeps users{ id:{status,inviteCode/accessCode,createdAt} } shape) |
data/secrets/<userId>.json | agent/secret (keeps service:key→value map; re-encrypted at rest) |
data/usage.json | system/usage (BillingEvents) |
browser-state/*.json | app/account (connector state) / worker browser session |
workspace/ | worker /workspace (emptyDir) + outputs → system/file |
Close compatibility — store the runtime's own names
We deliberately keep the runtime's field names in the DB instead of renaming them into CleanSlice-conventional ones. SOUL.md is stored as soul, USER.md as user, MEMORY.md as memory, HEARTBEAT.md as heartbeat, and agent.config.json is persisted verbatim as config. The managed markdown files map 1:1 to columns of the same stem; the config JSON is queried in place (limits, accessStrategy, runtimeProfile) rather than exploded and renamed across setting / runtimeProfile / access.
Why: the adapter becomes a near-identity projection, not a semantic translation — round-trips are lossless, the runtime loop reads/writes the exact same names, and there is no rename layer to drift. When the runtime adds a new managed file later, it lands as a new same-named field with no mapping change. (Other slices like setting may still mirror/read values from config for cross-agent queries, but config stays the source of truth and is never rewritten.)
Strategy: .agent/ is the portable package + on-pod format
- Postgres is the normalized source of truth (Agent row, memory, chat, secrets, access…).
.agent/is a projection of that state — used as (a) the interchange/backup package and (b) the working directory on the pod when theworkerruns the runtime loop.- A bidirectional adapter (
agent/package) maps between the two; theworkermaterializes.agent/from the DB before a turn and syncs deltas back after.
This is what lets the worker reuse the runtime's loop + tool-executors unchanged — they read and write .agent/, and the adapter keeps the DB in sync.
Materialize / sync at runtime
turn needs a runtime
→ worker manager hydrates .agent/ from Postgres (+ object storage) into the pod workspace
→ runtime loop runs against .agent/ (unchanged — reads SOUL/MEMORY/skills, writes sessions/…)
→ on completion: sync deltas back
sessions/*.jsonl → chat (new messages)
MEMORY.md, memory/*.md → agent/memory
data/secrets/* → agent/secret (if changed)
data/usage.json → system/usage events
browser-state/* → app/account
→ workspace wiped; durable state already persistedReuse the runtime's existing diff-manifest S3 sync (pull-on-boot / push-on-change): the pod can pull a materialized .agent/ from object storage and push changes, so the sync layer is the one the runtime already ships — Agentfy.ai just owns the DB↔package projection.
Import / export
- Import a
.agent/folder → create/update Agentfy.ai entities (onboard an existing runtime agent). - Export an agent → a
.agent/folder (backup, portability, or run it on the standalone runtime). Round-trips because both sides speak the same package format.
Gaps to close for full compatibility
- Add
agent/skill(markdown skills with frontmatter) to the agent group — the runtime has skills; our L5 breakdown dropped it. Also confirmagent/secret,agent/access,agent/heartbeat,agent/cronsub-slices exist (they map 1:1 to.agent/). sessions/*.jsonl↔chat: define the event-type mapping (user/assistant/tool_call/tool_result/summary→ message rows + tool/summary records).access.json↔user: runtime keys users by channel id (e.g. Telegram id); map to Agentfy.aiuser/team identities.- Versioning: pin the
.agentschema version (the runtime evolves it) and keep the adapter tolerant of missing/extra files.
Where it lives
- Adapter:
api→agent/package(import/export + DB↔.agent/mapping). - Materializer/sync:
runtime/workerslice (hydrate before turn, sync deltas after), reusing the runtime's S3/diff sync. - Loop + tool-executors: the
workerapp borrows them fromcleanslice/runtimeand runs them against the materialized.agent/.