mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-07 15:12:18 +08:00
Co-authored-by: SuYao <sy20010504@gmail.com> Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com> Signed-off-by: jdzhang <625013594@qq.com> Signed-off-by: zhangjiadi225 <625013594@qq.com>
388 lines
17 KiB
Markdown
388 lines
17 KiB
Markdown
# Data Layer — Reviewer Cluster
|
|
|
|
## Scope
|
|
|
|
| Subpath | What changed |
|
|
|---|---|
|
|
| `src/main/data/db/schemas/` | Agent / session / workspace / agent-message tables restructured |
|
|
| `src/main/data/services/` | `AgentSessionService.ts` + `AgentWorkspaceService.ts` new; `AgentService.ts`, `AgentSessionMessageService.ts`, `MessageService.ts` heavy rewrites |
|
|
| `src/main/data/api/handlers/` | `agentSessions.ts` + workspace handlers new; `agents.ts` slimmed (~100 LOC); `messages.ts` extended; `assistants.ts` + `topics.ts` extended |
|
|
| `src/main/data/migration/v2/migrators/` | `AgentsMigrator.ts` + `AgentsDbMappings.ts` rewrites; `ChatMigrator.ts` parts conversion; `ProviderModelMigrator.ts` `adapterFamily` backfill |
|
|
| `src/shared/data/types/` | `agentMessage.ts` + `uiParts.ts` new; `agent.ts` slimmed via Zod inference; `message.ts` heavy rewrite (parts model) |
|
|
| `src/shared/agents/` | `agentSlashCommands.ts` new (builtin SDK command list, off the data layer) |
|
|
| `src/shared/data/api/schemas/` | agent session + workspace schemas new; `agents.ts` slimmed by 126 LOC; `messages.ts` + `assistants.ts` + `providers.ts` extended |
|
|
|
|
Total surface: ~94 files modified across `src/main/data/` and
|
|
`src/shared/data/`.
|
|
|
|
## Intent
|
|
|
|
The AI pipeline refactor exposed three v1 data-layer constraints:
|
|
|
|
1. **Agent and Session conflated cognitive config.** Each v1 session
|
|
carried its own copy of `mcps` / `allowedTools` / `configuration` /
|
|
accessible paths. Two sessions of the same agent could drift, and
|
|
the renderer had to chase both rows to render an agent profile.
|
|
2. **Workspace was a string column on agents.** A single
|
|
`accessible_paths: string[]` field on each agent. Sessions inherited
|
|
it. There was no row identity for "this directory" — renaming,
|
|
reordering, or reusing a directory across agents meant duplicating
|
|
the string.
|
|
3. **Agent session messages stored `blocks: MessageBlock[]`** — Cherry
|
|
v1's custom block model. AI SDK v6 produces `UIMessage.parts`
|
|
directly. Continuing to convert one to the other on every read /
|
|
write meant maintaining a translation layer that has zero v2-side
|
|
consumers.
|
|
|
|
The refactor:
|
|
|
|
- **Split.** `agent` keeps cognitive config; `agent_session` keeps
|
|
per-session state + workspace binding. Sessions reference their agent
|
|
by FK with `onDelete: 'set null'` so orphan sessions can still render.
|
|
- **Normalize.** `agent_workspace` is a separate table with unique
|
|
`path` index and a `type` discriminator (`user` or `system`). Sessions
|
|
FK to workspaces with `onDelete: 'cascade'`; deleting a workspace row
|
|
deletes sessions bound to it.
|
|
- **Parts.** `agent_session_message.content` stores
|
|
`AgentPersistedMessage` (AI-SDK-native `parts`) directly. The
|
|
`blocks` field is gone end-to-end; the migrator converts legacy rows
|
|
in place.
|
|
|
|
See [Multi-model & data shape changes](#multi-model--data-shape-changes)
|
|
below for the multi-model + plan/small model split that motivated the
|
|
agent schema rewrite.
|
|
|
|
## Schema diff (production-shape)
|
|
|
|
### `agent` (rewritten — `src/main/data/db/schemas/agent.ts`)
|
|
|
|
```ts
|
|
export const agentTable = sqliteTable('agent', {
|
|
id: uuidPrimaryKey(),
|
|
type: text().notNull(), // 'claude-code', future agent kinds
|
|
name: text().notNull(),
|
|
description: text().notNull().default(''),
|
|
instructions: text().notNull(),
|
|
|
|
// Multi-model: chat / plan / small as three separate FK columns to userModel.
|
|
// Plan model = used for high-level planning when the agent supports it;
|
|
// small model = used for cheap helper calls (compaction, summaries).
|
|
model: text().references(() => userModelTable.id, { onDelete: 'set null' }),
|
|
planModel: text().references(() => userModelTable.id, { onDelete: 'set null' }),
|
|
smallModel: text().references(() => userModelTable.id, { onDelete: 'set null' }),
|
|
|
|
mcps: text({ mode: 'json' }).$type<string[]>().notNull().default('[]'),
|
|
allowedTools: text({ mode: 'json' }).$type<string[]>().notNull().default('[]'),
|
|
configuration: text({ mode: 'json' }).$type<Record<string, unknown>>().notNull().default('{}'),
|
|
...orderKeyColumns,
|
|
...createUpdateDeleteTimestamps
|
|
})
|
|
```
|
|
|
|
Removed from this table (relative to v1): `accessible_paths`,
|
|
per-session config fields, `enableAutoTools`, model ids as strings.
|
|
|
|
`configuration` is intentionally `.loose()` (Zod passthrough) at the
|
|
schema layer — see `AgentConfigurationSchema` for the typed keys (avatar,
|
|
permission_mode, max_turns, scheduler_*, heartbeat_*, soul_enabled,
|
|
env_vars). Unknown extras are preserved across read/write so older
|
|
/newer app versions don't silently drop fields.
|
|
|
|
### `agent_session` (rewritten — `agentSession.ts`)
|
|
|
|
```ts
|
|
export const agentSessionTable = sqliteTable('agent_session', {
|
|
id: uuidPrimaryKey(),
|
|
agentId: text().references(() => agentTable.id, { onDelete: 'set null' }),
|
|
name: text().notNull(),
|
|
description: text().notNull().default(''),
|
|
workspaceId: text()
|
|
.notNull()
|
|
.references(() => agentWorkspaceTable.id, { onDelete: 'cascade' }),
|
|
...orderKeyColumns,
|
|
...createUpdateTimestamps
|
|
})
|
|
```
|
|
|
|
Removed (relative to v1): every cognitive-config field. The renderer
|
|
fetches them via `useAgent(session.agentId)`.
|
|
|
|
**Insert-only workspace.** `UpdateSessionDto` deliberately does not
|
|
include `workspaceId` — a running session can't be re-pointed at a new
|
|
directory. Newly created sessions must bind an explicit workspace source:
|
|
either `{ type: 'user', workspaceId }` for an existing user workspace, or
|
|
`{ type: 'system' }` for a deterministic app-owned system workspace row.
|
|
|
|
### `agent_workspace` (new — `agentWorkspace.ts`)
|
|
|
|
```ts
|
|
export const agentWorkspaceTable = sqliteTable('agent_workspace', {
|
|
id: uuidPrimaryKey(),
|
|
name: text().notNull(),
|
|
path: text().notNull(),
|
|
type: text().notNull(), // CHECK type IN ('user', 'system')
|
|
...orderKeyColumns,
|
|
...createUpdateTimestamps
|
|
}, t => [
|
|
uniqueIndex('agent_workspace_path_unique_idx').on(t.path),
|
|
check('agent_workspace_type_check', sql`${t.type} IN (...)`),
|
|
...
|
|
])
|
|
```
|
|
|
|
`path` is the unique key. `AgentWorkspaceService` is DB-only: it
|
|
normalizes paths, creates/reuses user workspace rows, creates deterministic
|
|
system workspace rows, and never touches the filesystem. Filesystem
|
|
directory validation and creation live on the Claude Code runtime consumer
|
|
for user workspaces, and only app-owned system workspace directories are
|
|
auto-created.
|
|
|
|
### `agent_session_message` (rewritten — `agentSessionMessage.ts`)
|
|
|
|
```ts
|
|
export const agentSessionMessageTable = sqliteTable('agent_session_message', {
|
|
id: uuidPrimaryKeyOrdered(),
|
|
sessionId: text().notNull().references(() => agentSessionTable.id, { onDelete: 'cascade' }),
|
|
role: text().notNull(),
|
|
content: text({ mode: 'json' }).$type<AgentPersistedMessage>().notNull(),
|
|
// Opaque runtime resume token; null when the session never ran or was reset.
|
|
runtimeResumeToken: text(),
|
|
metadata: text({ mode: 'json' }).$type<Record<string, unknown>>(),
|
|
...createUpdateTimestamps
|
|
})
|
|
```
|
|
|
|
`content` is now `AgentPersistedMessage` (parts model). The legacy
|
|
`blocks` column is gone. `runtimeResumeToken` is an opaque recovery token
|
|
owned by the active agent runtime driver; Claude Code maps it to its SDK
|
|
session id, while other drivers may use a different resume token or none.
|
|
|
|
### `agent_channel` + `agent_channel_task` (new — `agentChannel.ts`)
|
|
|
|
Channel adapters for Discord / Slack / Telegram / Feishu / WeChat / QQ.
|
|
`config` is JSON, `permissionMode` is constrained via `check` to the
|
|
Claude Agent SDK's permission modes, and `agent_channel_task` is the
|
|
join table to scheduled `agent_task` rows.
|
|
|
|
### `agent_task`, `agent_task_run_log`, `agent_global_skill`, `agent_skill`
|
|
|
|
Agent-domain scheduled-task storage. Each `agent_task` carries cron /
|
|
interval / one-time scheduling plus a prompt; `agent_task_run_log`
|
|
stores per-run outputs. `agent_global_skill` + `agent_skill` model the
|
|
(currently flat) "skill" catalog.
|
|
|
|
### Relationship to the generic `job` / `job_schedule` stack
|
|
|
|
`job` and `job_schedule` are the **generic** scheduling backbone owned
|
|
by `JobManager` + `SchedulerService` + `JobService` /
|
|
`JobScheduleService`. They are not removed — they were added on
|
|
`origin/v2` in parallel with this branch's agent work and arrived here
|
|
via the v2 merge. See
|
|
[`docs/references/job-and-scheduler/`](../../../docs/references/job-and-scheduler/)
|
|
for the design.
|
|
|
|
The `agent_task` tables are layered ABOVE the job stack — the agent
|
|
scheduler will register typed handlers against `JobManager` and store
|
|
its own task metadata in `agent_task`. Reviewing changes here should
|
|
treat the two as complementary, not as a replacement.
|
|
|
|
## Service-layer changes
|
|
|
|
### New services
|
|
|
|
- **`AgentSessionService.ts`**. Cursor-paginated list with order keys,
|
|
transactional create that requires an explicit workspace source. User
|
|
sources must reference user workspace rows; system sources create a
|
|
deterministic session-owned workspace row. Workspace binding remains
|
|
insert-only.
|
|
- **`AgentWorkspaceService.ts`**. DB-only workspace row access, path
|
|
normalization, find-or-create for user workspace rows, deterministic
|
|
system workspace row creation, and reorder. It owns no filesystem
|
|
side effects; runtime consumers validate user directories and create
|
|
only app-owned system directories when needed.
|
|
|
|
### Heavy rewrites
|
|
|
|
- **`AgentService.ts`** (+241 LOC change). Foreign-keyed model fields
|
|
expand into joined reads; cognitive config snapshot
|
|
(`getAgentForRun(sessionId)`) consolidates fields the AI pipeline needs.
|
|
See commit `f2229a881 refactor(agents): harden agent model field to
|
|
UniqueModelId end-to-end`.
|
|
- **`AgentSessionMessageService.ts`** (+215 LOC). Cursor-paginated
|
|
history (newest-first); persists `AgentPersistedMessage`; idempotent
|
|
upsert keyed on `(sessionId, content.id)` so retried persistence
|
|
doesn't double-insert.
|
|
- **`MessageService.ts`** (+390 LOC). Tree operations under the v2 parts
|
|
model — `createUserMessageWithPlaceholders` (transactional), tree path
|
|
reads, sibling groups, branch active-path tracking. Drops every
|
|
`blocks` reference.
|
|
|
|
## DataApi handlers
|
|
|
|
| Endpoint | Status | Notes |
|
|
|---|---|---|
|
|
| `GET/POST /agent-sessions`, `/agent-sessions/:id` | new | session CRUD |
|
|
| `GET /agent-sessions/:id/messages` | new | cursor-paginated |
|
|
| `GET /agent-workspaces`, `/agent-workspaces/:id`, reorder endpoints | new | workspace reads + reorder |
|
|
| `/agents/*` | slimmed | ~100 LOC removed; legacy order endpoints gone |
|
|
| `/messages/*` | extended | parts read/write, tree path, sibling helpers |
|
|
| `/topics/*` | extended | branch-aware active-node tracking |
|
|
| `/jobs/*`, `/job_schedules/*` | added on `origin/v2` (out of scope here) | see `docs/references/job-and-scheduler/` |
|
|
|
|
## Migration (v1 → v2)
|
|
|
|
### `AgentsMigrator`
|
|
|
|
Reads the legacy standalone `agents.db` and folds it into the main
|
|
SQLite database. Source tables → targets:
|
|
|
|
| Source | Target |
|
|
|---|---|
|
|
| `agents` | `agent` (+ join: model id → `user_model`) |
|
|
| `sessions` | `agent_session` |
|
|
| `sessions.accessible_paths[0]` | `agent_workspace` (one workspace per session, first valid path) |
|
|
| `session_messages` | `agent_session_message` (with `transformBlocksToParts`) |
|
|
| `skills` | `agent_global_skill` |
|
|
| `agent_skills` | `agent_skill` |
|
|
| `scheduled_tasks` | `agent_task` |
|
|
| `task_run_logs` | `agent_task_run_log` |
|
|
| `channels` | `agent_channel` |
|
|
| `channel_task_subscriptions` | `agent_channel_task` |
|
|
|
|
Key points:
|
|
|
|
- **First workspace wins.** Only `accessible_paths[0]` is migrated to a
|
|
workspace row. Additional paths are not preserved. See
|
|
[`2026-05-19-agent-session-primary-workspace.md`](../breaking-changes/2026-05-19-agent-session-primary-workspace.md).
|
|
- **`blocks` → `parts`** for legacy session messages, via the same
|
|
`transformBlocksToParts` that `ChatMigrator` uses for the chat tree.
|
|
- **Defensive default backfill.** `notNullCol(name, defaultExpr)` in
|
|
`AgentsDbMappings` covers the case where legacy rows have NULL in
|
|
columns that are `NOT NULL` in v2; a plain `SELECT col` would
|
|
otherwise hit `SQLITE_CONSTRAINT_NOTNULL`.
|
|
- **Order keys.** `generateOrderKeySequence` synthesizes
|
|
fractional-indexing order keys for every migrated row so the v2
|
|
reorder UX works on migrated data.
|
|
|
|
### `ChatMigrator`
|
|
|
|
The `transformBlocksToParts` helper is shared with `AgentsMigrator`.
|
|
Both produce `CherryMessagePart[]`; no legacy `blocks` survives the
|
|
migration.
|
|
|
|
### `ProviderModelMigrator`
|
|
|
|
Backfills `adapterFamily` per endpoint config. See
|
|
[`adapter-family.md`](./adapter-family.md).
|
|
|
|
## Shared types & API schemas
|
|
|
|
### `src/shared/data/types/agentMessage.ts` (new)
|
|
|
|
The `AgentPersistedMessage` shape stored on `agent_session_message.content`.
|
|
|
|
### `src/shared/data/types/uiParts.ts` (new)
|
|
|
|
Lifted the UI part type definitions out of `message.ts` so the agents
|
|
domain can consume them without taking the full chat-message dependency
|
|
graph.
|
|
|
|
### `src/shared/data/types/agent.ts` (slimmed)
|
|
|
|
Replaced hand-written types with Zod-inferred types from `api/schemas/agents.ts`.
|
|
|
|
### `src/shared/data/types/message.ts` (rewritten)
|
|
|
|
Removed the legacy `blocks` field and the type machinery built around
|
|
it. `CherryMessagePart`, `CherryUIMessage`, `Message`,
|
|
`AssistantMessageStatus` are the v2 vocabulary.
|
|
|
|
### `src/shared/data/api/schemas/agentSessions.ts` (new)
|
|
|
|
Entity + DTO schemas for sessions. `UpdateSessionSchema` deliberately
|
|
omits `workspaceId` to enforce insert-only binding.
|
|
|
|
### `src/shared/data/api/schemas/agentWorkspaces.ts` (new)
|
|
|
|
Entity + DTO schemas for workspaces. Path validation matches
|
|
the shared main-process `normalizeWorkspacePath` helper.
|
|
|
|
### `src/shared/data/api/schemas/agents.ts` (slimmed)
|
|
|
|
`AgentEntity` derived from the new schema. The 126-LOC reduction comes
|
|
from dropping per-session fields that moved to `sessions.ts`.
|
|
|
|
## Multi-model & data shape changes
|
|
|
|
Three places where the data model now distinguishes models:
|
|
|
|
1. **Agent.** Three FKs to `userModel`: `model` (default chat),
|
|
`planModel` (planning), `smallModel` (helper calls). Migration maps
|
|
v1's single `model_id` to all three (the renderer overrides
|
|
plan / small as the user configures them).
|
|
2. **Message.** `modelId: UniqueModelId` (`providerId::modelId`) is the
|
|
v2 model identifier; v1's `provider: string` + `model_id: string`
|
|
merge into this. Persistent chats already used UniqueModelId; agent
|
|
messages now use it too.
|
|
3. **Multi-model assistant turn.** `siblings_group_id` (already on the
|
|
message tree) groups parallel assistant replies. The migrator
|
|
preserves existing sibling groups; the stream-manager's persistent
|
|
provider allocates new ones for fresh multi-model turns.
|
|
|
|
## Invariants reviewers should check
|
|
|
|
1. **Cognitive config lives on the agent, not the session.** A
|
|
`UpdateSessionDto` adding `model`/`mcps`/`allowedTools` is wrong —
|
|
those changes go through `UpdateAgentDto`.
|
|
2. **Workspace is insert-only on sessions.** No code path should call
|
|
`sessionService.update(id, { workspaceId: ... })`. The schema rejects
|
|
this; reviewer should also catch any handler / hook that bypasses the
|
|
schema.
|
|
3. **Workspace deletion cascades to sessions.** `agent_session.workspaceId`
|
|
is non-null and uses `onDelete: 'cascade'`. A system workspace row is
|
|
owned one-to-one by its session and is deleted with that session; user
|
|
workspace rows must not be deleted when deleting one bound session.
|
|
4. **`blocks` field is gone.** Any newly added code that reads
|
|
`data.blocks` or `message.blocks` is wrong. The migration is the only
|
|
place legacy `blocks` ever appears, and it converts to `parts`.
|
|
5. **`runtime_resume_token` (opaque runtime resume token) is null until first
|
|
run.** Persistence writes it; reads must accept null.
|
|
6. **Tool IDs are `${serverName}__${toolName}`** (double underscore) in
|
|
the `allowedTools` JSON arrays. `mcps` is `string[]` of server ids.
|
|
7. **`agent_task` is not the generic `job` stack.** Review of agent
|
|
tasks here should not touch `JobManager` / `SchedulerService` /
|
|
`job_schedule` — those land via `origin/v2` independently.
|
|
|
|
## Validation
|
|
|
|
- `services/__tests__/AgentService.test.ts`, `AgentSessionService.test.ts`,
|
|
`AgentWorkspaceService.test.ts`,
|
|
`MessageService.test.ts`, `AgentChannelService.test.ts`,
|
|
`AgentTaskService.test.ts`.
|
|
- `migration/v2/migrators/__tests__/AgentsMigrator.test.ts`,
|
|
`AgentsMigrator.transforms.test.ts`,
|
|
`mappings/__tests__/AgentsDbMappings.test.ts`,
|
|
`remapAgentPrefixIds.test.ts`.
|
|
- `migration/v2/migrators/__tests__/ChatMigrator.test.ts` for
|
|
`transformBlocksToParts`.
|
|
- `src/shared/data/api/schemas/__tests__/agents.test.ts`,
|
|
`agentWorkspaces.test.ts`.
|
|
- `api/handlers/__tests__/agents.test.ts`,
|
|
`temporaryChats.test.ts`, `temporaryChats.integration.test.ts`.
|
|
|
|
## Follow-ups (out of scope)
|
|
|
|
- `agents.db` lives as a separate SQLite file pre-migration. After v2
|
|
GA the legacy file should be deleted; for now the migrator just reads
|
|
it.
|
|
- The `_skills` tables (`agent_global_skill`, `agent_skill`) currently
|
|
mirror the v1 catalog 1:1. A future "skill registry" pass may
|
|
re-shape both.
|
|
- `MessageService.ts` is at 1163 LOC — splitting into tree / sibling /
|
|
branch helpers is queued for a follow-up.
|
|
- Wiring `agent_task` execution onto the generic `JobManager` (instead
|
|
of an agent-local scheduler) — depends on the
|
|
[`job-and-scheduler`](../../../docs/references/job-and-scheduler/)
|
|
stack landing on this branch via merge and is not reviewed in this
|
|
cluster.
|