Импорт и экспорт агента
Как агент переезжает между папкой .agent/ отдельного рантайма и Postgres Agentfy.ai — в обе стороны. Это адаптер agent/package; тот же код обслуживает materialize/sync воркера. См. также Совместимость с .agent.
Импорт — .agent/ → Agentfy.ai DB
importAgent(dir, { teamId, agentId? }). Runtime-агент одно-тенантный, Agentfy.ai — мультитенантный, поэтому импорту нужен целевой teamId (и опц. agentId для обновления). Всё — идемпотентный upsert по натуральным ключам, по секциям в транзакции.
Порядок (родители раньше)
team (дан) → agent → { skill, memory, secret, access } → chatЧто куда
| Шаг | Источник (.agent/) | → Agentfy.ai | Как |
|---|---|---|---|
| 1 | SOUL.md | agent.soul | сохранить тело дословно (то же имя, без переименования) |
| 1 | agent.config.json | agent.config (дословно) | хранится как есть; runtimeProfile / лимиты / accessStrategy читаются из него (runtimeProfile дефолтится, если нет) |
| 1 | USER.md, HEARTBEAT.md | agent.user, agent.heartbeat | сохранить дословно (интервал heartbeat берётся из config) |
| 2 | skills/<n>/SKILL.md | строки skill (+ ассеты → system/file) | парс frontmatter + тело |
| 3 | MEMORY.md, memory/*.md | memory (curated + daily[date]) | тот же текст; daily по ключу agentId+date |
| 3 | data/memory.sqlite | — | перестраивается, не импортируется (re-embed из markdown) |
| 4 | data/secrets/<uid>.json | secret (только имена → pendingKeys) | AGNT2-74: пакет несёт ИМЕНА ключей, никогда значения; значения из старого файла выбрасываются, остаются только имена |
| 5 | data/access.json + accessStrategy | access (ACL + стратегия) | channel-user-id → user Agentfy.ai |
| 6 | data/sessions/<chan>:<uid>.jsonl | chat (тред + сообщения) | события → сообщения в порядке ts |
| 7 | data/usage.json | system/usage (опц. baseline) | обычно пропускаем (forward-looking) |
| 7 | browser-state/*.json | app/account (опц.) | чувствительное/эфемерное; часто пропускаем |
Маппинг событий сессии → chat
Каждая строка JSONL { id, type, ts, data }:
type события | → |
|---|---|
user | message(role=user, text, from) |
assistant | message(role=assistant, text) |
tool_call | запись вызова (name, params, toolUseId) |
tool_result | результат (toolUseId, result) |
summary | маркер компакта/саммари |
Оригинальный event.id сохраняется как externalId → повторный импорт идемпотентен (без дублей).
Сквозные правила трансформации
- ID: runtime-UUID →
externalId; Agentfy.ai генерит свой{slice}-uuid. - Время: epoch ms →
DateTime. - Секреты: только имена (AGNT2-74). Пакет никогда не несёт значение креда, поэтому импорт объявляет имена (
AgentSecret.pendingKeys), а значения вводятся руками на месте назначения. Пока этого не сделано, диалог импорта и экран секретов называют недостающие. - Производные индексы (
memory.sqlite, OpenSearch, pgvector) не переносятся — перестраиваются из канонического markdown/json/jsonl.
Экспорт — Agentfy.ai DB → .agent/
exportAgent(agentId, { userScope? }) — обратная проекция в папку .agent/ (tarball или директория). Round-trip работает, потому что обе стороны говорят на одном формате пакета.
| Источник (Agentfy.ai) | → .agent/ |
|---|---|
agent.soul | SOUL.md (тело как есть) |
agent.config (дословно) | agent.config.json |
agent.user · agent.heartbeat | USER.md · HEARTBEAT.md |
memory curated · daily | MEMORY.md · memory/YYYY-MM-DD.md |
skill (+ ассеты) | skills/<name>/SKILL.md |
secret (только имена, без расшифровки) | data/secrets/{shared|agent|user-<userId>}.json |
access | data/access.json |
chat threads → события | data/sessions/<chan>:<user>.jsonl |
system/usage (опц.) | data/usage.json |
куки app/account | browser-state/<profile>.json |
data/memory.sqliteне пишем — рантайм перестроит FTS из markdown на старте.- Результат можно
pull-нуть рантаймом (через S3) или запустить напрямую — агент «переехал».
Границы и за что пакет отвергается
Импорт материализует весь архив в памяти до того, как из него что-то прочитано, поэтому потолок — это свойство загрузки, а не агента, который в ней описан:
| загрузка | 25 МБ |
| записей | 20 000, включая каталоги |
| суммарно в распакованном виде | 128 МБ — проверяется дважды: против того, что объявляет директория (до распаковки хоть одного байта), и против того, что распаковщик реально производит |
| сжатие | только STORE и DEFLATE. bzip2/lzma отвергаются как неподдерживаемый метод, и это другое сообщение, чем «это не zip» |
У каждого отказа есть код, а не фраза. Причина едет в ответе об ошибке полем details.refusal, а приложение держит по одному тексту на код. Причина, добавленная на сервере, не может молча свалиться в общий хвост — код без текста красит тест.
Импорт — всё или ничего. Отвергнутый импорт оставляет ноль строк: полусобранного агента в списке не будет. Значения, которых не держит база — байт NUL, непарный суррогат, отметка времени вне диапазона timestamptz, — отвергаются на границе с указанием поля, а не доезжают до Postgres и возвращаются пятисоткой. (NUL — не враждебный ввод: ditto на папке в macOS кладёт в архив сайдкары AppleDouble.)
Ничто в пакете не может назвать путь за пределами .agent/ — ни в одну сторону. Запись, которая легла бы вне папки, отвергается на импорте; и ни одно значение из пакета — userId, имя навыка, дата памяти — не может вывести имя записи наружу на экспорте.
Типа агента в манифесте нет (см. Состав). concierge нельзя ни экспортировать, ни перезаписать загрузкой; закрыты обе стороны, потому что импортёр находит существующего агента по externalId.
Пакет приезжает загрузкой или из хранилища, привязанного к арендатору, — но никогда путём на сервере. Источника «прочитай вот этот каталог» в HTTP-контракте нет: путь от вызывающего позволил бы любому аутентифицированному участнику заставить сервер обойти и вычитать любой каталог, до которого дотягивается процесс.
Архив пишется так, чтобы его читали чужие распаковщики и чтобы он был стабилен. Имена записей несут UNIX-маркер платформы, поэтому не-ASCII имя навыка распаковывается штатными средствами macOS и Linux верно, а не мусором; отметки времени записей фиксированы, поэтому два экспорта неизменного агента — одни и те же байты.
Идемпотентность ключуется парой (teamId, externalId). Один и тот же пакет, импортированный дважды в одну команду, обновляет одного и того же агента; импортированный в две разные команды даёт двух независимых агентов, у которых просто общий origin id.
Round-trip
- Чисто round-trip:
SOUL/agent.config/MEMORY/skills/access(текст + JSON) и имена секретов вместе со scope. - Значения секретов — намеренное исключение (AGNT2-74): они никогда не попадают в пакет, поэтому переезд всегда заканчивается словами «введите эти N кредов».
- Sessions: без потерь, если маппинг типов событий полный (user/assistant/tool_call/tool_result/summary).
- Индексы: производные везде → регенерируются с каждой стороны, в пакете не передаются.
Где запускается
- CLI / admin-эндпоинт:
agentfy import ./.agent --team <teamId>·agentfy export <agentId> -o ./out. - Воркер (тот же адаптер): materialize = export (hydrate
.agent/на поде перед турном); delta-sync = import (синк дельт после турна). Переиспользует diff/S3-синк рантайма. - Код:
api→ адаптерagent/package.