04. 领域模型与磁盘布局
Craft Agents 采用“文件系统即工作空间”的路线。理解它的关键不是背路径,而是区分三件事:全局设备配置、可搬运 workspace 内容、每次会话的 provider/runtime 句柄。
4.1 总体层级
Section titled “4.1 总体层级”erDiagram GLOBAL_CONFIG ||--o{ WORKSPACE : registers WORKSPACE ||--o{ SESSION : owns WORKSPACE ||--o{ PROJECT : owns WORKSPACE ||--o{ SOURCE : defines WORKSPACE ||--o{ SKILL : defines WORKSPACE ||--o{ TASK_SPEC : defines PROJECT o|--o{ SESSION : groups SESSION o|--o{ SESSION : parent_or_branch TASK_SPEC ||--o{ TASK_RUN : instantiates TASK_RUN ||--o{ SESSION : dispatches_nodes SESSION o|--o{ MESSAGE : persistsProject → Session 是逻辑绑定,session 文件仍放 workspace 的统一 sessions 目录;TaskRun → Session 也是 metadata 关联,不把 child transcript 嵌进 run log。
4.2 全局配置目录
Section titled “4.2 全局配置目录”中央路径由 CONFIG_DIR 决定:
${CRAFT_CONFIG_DIR:-~/.craft-agent}/├── config.json 全局 workspace 列表、活动项、连接默认值等├── credentials.enc 设备绑定的加密凭据├── .server.lock server 单实例锁├── docs/ 内置帮助材料└── workspaces/ 默认 workspace 容器注意一个实现细节:多数新路径代码尊重 CRAFT_CONFIG_DIR,但少量 storage 文件仍直接从 homedir()/.craft-agent 派生默认路径。二次开发时要搜全局常量,避免多实例开发环境出现“一半读 override、一半读默认目录”的漂移。
全局层保存“这台安装知道哪些 workspace、有哪些 LLM connection、默认使用什么”,不应包含可随 workspace 一起分享的业务内容。
4.3 Workspace
Section titled “4.3 Workspace”Workspace 是最重要的可搬运边界。它有稳定 ID、slug/name、rootPath,并在任意磁盘位置存在;默认创建在 ~/.craft-agent/workspaces/。
workspaces/storage.ts 给出标准目录 helper:
<workspaceRoot>/├── config.json├── sessions/├── projects/├── sources/├── skills/├── tasks/├── automations.json 或 automation 相关配置├── statuses/ 状态定义/资源├── labels/ 标签定义/资源└── .codex-plugin/ 兼容/插件 manifestWorkspace config 保存默认工作目录、默认 connection/model、permission/thinking、source/feature 设置等。路径写入前转成 portable form,读取后展开,提升跨机器可搬运性。
Workspace 的两个身份
Section titled “Workspace 的两个身份”id:内部稳定引用;slug/rootPath:目录和可读定位。
不要用 basename(rootPath) 在所有地方替代 id;部分 legacy/helper 会这样推导,但跨路径移动或重命名时稳定性不同。
4.4 Project
Section titled “4.4 Project”Project 是 workspace 内的上下文分组:
projects/<projectSlug>/├── config.json├── MEMORY.md└── assets/config.json
Section titled “config.json”包含 project id/slug/name、可选工作目录、时间戳等。Session 存 projectId 而非 slug,以便项目改名时绑定不失效;加载时通过 id 扫描 project。
MEMORY.md
Section titled “MEMORY.md”这是 agent 维护的“项目经验”,不是全 transcript。加载时:
- 缺失/空文件返回
null; - 默认上限 5,000 tokens;
- 超限保留文件头部并追加截断标记;
- 提示 Agent 将最新/最重要内容写在顶部。
assets/
Section titled “assets/”保存项目级参考资料。Prompt 只注入清单/必要摘要,而不是无条件把所有二进制放入上下文。
4.5 Session 的物理结构
Section titled “4.5 Session 的物理结构”每个 session 是独立目录:
sessions/<sessionId>/├── session.jsonl├── data/│ ├── long_responses/│ └── ...工具生成数据├── plans/├── pi-turn-anchors.json Pi 分支锚点 sidecar(按实现需要)└── ...provider/后台任务附件权威定义在 sessions/types.ts:
session.jsonl 第 1 行:SessionHeadersession.jsonl 第 2 行起:每行一个 StoredMessage为什么不写一个大 JSON?
- 列表只读首行即可,不解析所有消息;
- message 可按行恢复,末尾半写坏行可以容错;
- diff/导出更自然;
- header 预计算列表所需字段,性能与透明性兼得。
4.6 SessionHeader 与 StoredSession
Section titled “4.6 SessionHeader 与 StoredSession”SessionConfig 是持久 metadata;StoredSession = SessionConfig + messages + tokenUsage;SessionHeader 再加入用于列表的预计算值。
SESSION_PERSISTENT_FIELDS 是序列化白名单。新增字段需要:
- 加入该数组;
- 加入
SessionConfig; - 若跨 RPC/UI,再加入 DTO/projection 类型和事件。
白名单比 {...session} 安全,因为 ManagedSession 中的 agent、Promise、Map、callback 不会意外落盘。
| 分组 | 代表字段 | 含义 |
|---|---|---|
| identity | id, workspaceRootPath |
产品身份与归属 |
| provider resume | sdkSessionId, sdkCwd |
SDK 恢复位置 |
| display/workflow | name, sessionStatus, labels, flag/archive/unread |
用户工作管理 |
| runtime config | source、permission、working dir、model/connection/thinking | 下次 runtime 重建输入 |
| plan | pendingPlanExecution |
人机 handoff 跨重启状态 |
| branching | branchFrom* |
父 transcript 与 provider anchor |
| transfer | transferredSessionSummary* |
跨服务器一次性恢复上下文 |
| hierarchy | projectId, parentSessionId, kanbanColumn |
UI/工作组织 |
| task conductor | taskSlug/runId/nodeId/count/draft |
DAG 关联 |
4.7 产品 Session ID 与 SDK Session ID
Section titled “4.7 产品 Session ID 与 SDK Session ID”这是最容易混淆的领域概念。
| ID | 谁生成 | 生命周期 | 用途 |
|---|---|---|---|
session.id |
Craft | 产品会话终身 | 目录名、RPC、UI、父子关系 |
sdkSessionId |
Claude/Pi runtime | provider transcript | resume/fork |
turnId |
provider/API | 单次 assistant turn | 关联 delta/text/tool |
message.id |
Craft/主进程 | 持久消息 | UI、annotation、branch cutoff |
sdkMessageId/anchor |
provider | provider turn entry | 严格分支锚点 |
用户可以迁移 Craft session,但远端机器未必拥有原 provider transcript;此时不能假装 sdkSessionId 可继续,系统需要 transfer summary 或 seeded fresh context。
4.8 sdkCwd 与 workingDirectory
Section titled “4.8 sdkCwd 与 workingDirectory”Session 创建时设置 sdkCwd,之后保持不变;workingDirectory 可由用户或 Agent 改。
原因:Claude 等 SDK 的 transcript 存储按 cwd hash 组织。如果每次 cd 都改变 SDK cwd,旧 session 文件可能找不到,resume/branch 会失效。
所以:
sdkCwd = provider session storage identity(固定)workingDirectory = 当前 Bash/Read/Write 的工作上下文(可变)源码注释:SessionConfig.sdkCwd;创建逻辑在 sessions/storage.ts。
4.9 Message:运行时与持久格式
Section titled “4.9 Message:运行时与持久格式”packages/core/src/types/message.ts 区分 Message 和 StoredMessage。运行时消息可能包含:
isStreaming、isPending;- tool display metadata;
- base64 attachment/content;
- UI 临时关联信息。
持久格式保存重建 transcript 所需内容,但会对过大 tool result 做保护,把完整内容移到 session data 文件并在消息里留摘要/路径。
Message role 不止 user/assistant:tool、plan、auth 等产品级消息也需要在 transcript 中表现。真正送给 provider 前还会再做 context conversion,不等于把 JSONL 每行原样传入。
4.10 Source
Section titled “4.10 Source”Source 目录约定:
sources/<slug>/├── config.json├── guide.md├── icon.*└── ...可选模板/辅助文件主类型 FolderSourceConfig 的 discriminant 是:
mcp:stdio、HTTP 或 SSE server;api:把 REST endpoint 动态包装为 MCP-style tools;local:本地目录/能力源。
凭据不放 config.json。配置只声明 auth 形态和非秘密 header;secret 存 credential vault,以 workspace/source identity 查找。
4.11 Skill
Section titled “4.11 Skill”Skill 以 skills/<slug>/SKILL.md 为核心,包含 frontmatter 与说明。它和 Source 的区别:
- Source 提供“可调用能力/数据”;
- Skill 提供“如何完成某类工作”的指令与引用;
- Skill 可以声明 required sources;
- Prompt 中出现
[skill:slug]后,prerequisite manager 要求 Agent 先读取对应文件,再开放后续工具。
这是一种渐进上下文加载:不把所有技能全文塞进每个 system prompt。
4.12 Status、Label 与 KanbanColumn
Section titled “4.12 Status、Label 与 KanbanColumn”三者不要混为一谈:
sessionStatus:用户可配置 workflow status id,例如 todo/in-progress/needs-review/done;labels:多值分类,可含id::value形式;kanbanColumn:任务板列位置,尤其用于 Task child;与 sessionStatus 独立。
代码注释明确 session status 是用户控制的,不应由普通 assistant completion 自动改。TaskRunner 是特殊编排器,会显式驱动 child status/column,以反映 DAG 进度。
4.13 Task spec 与 run
Section titled “4.13 Task spec 与 run”tasks/<taskSlug>/├── task.yaml└── runs/<runId>/ ├── task.yaml 启动时快照 ├── run.jsonl append-only run log └── nodes/<nodeId>/output.*Task 是声明式 DAG;run 是一次执行实例;每个可执行 node 仍是普通 child Session。这样 task board、历史、权限和消息都复用 session 基础设施。
4.14 Automation
Section titled “4.14 Automation”Automation 配置属于 workspace;history/event log 持久化执行结果。它通常不持有独立 Agent runtime,而是通过回调创建 session、发送 prompt 或 webhook。triggeredBy metadata 把新 session 关联回 automation name/event/timestamp。
4.15 Credential store
Section titled “4.15 Credential store”凭据独立保存在 ~/.craft-agent/credentials.enc:
- AES-256-GCM;
- PBKDF2 100,000 次,从机器稳定 ID + salt 派生 32 字节 key;
- 每次写随机 12 字节 IV;
- 文件 mode
0600,目录0700; - 兼容旧 hostname key,并在成功解密后迁移;
- 文件损坏/跨机器不能解密时要求重新认证。
源码:secure-storage.ts、saveStoreSync。
这也意味着 workspace bundle 默认不能“带走凭据”;迁移后要在目标设备重新绑定。
4.16 配置优先级
Section titled “4.16 配置优先级”典型 session runtime 值按“最具体覆盖最一般”解析:
显式 create/send 参数 > session 持久值 > project 默认/绑定 > workspace defaults > global defaults > 代码内安全默认值并非每个字段完全相同。例如 connection 一旦 agent 建立会锁定,后续 global default 变化不应悄悄换 provider;working directory 可以动态更新,但 sdkCwd 不跟随。
4.17 外部编辑与 watcher
Section titled “4.17 外部编辑与 watcher”文件系统优先允许:
- 用户用编辑器改配置;
- Agent 通过工具改配置;
- UI RPC 改配置;
- watcher 同时观察并更新 runtime。
因此必须有三种防线:
- 原子写,避免半文件;
- schema validator,避免无效配置直接进入运行时;
- signature/merge,避免保存旧内存对象覆盖外部刚改的 metadata。
4.18 本章小结
Section titled “4.18 本章小结”Craft 的领域模型以普通文件为事实源,但没有把“透明”误解成“随便读写”:JSONL header、持久字段白名单、portable path、原子写、外部变更合并和独立 secret vault,共同把文件系统提升成可靠应用存储。
下一章看内存中的另一半:SessionManager 如何把这些静态文件恢复成拥有 agent、队列、工具资源和事件出口的会话 Actor。