docs/references/ai/README.md
This is the entry point for Cherry Studio's AI pipeline: main-process provider calls, AI SDK chat execution, registered agent-session runtimes, and the renderer-side transport that connects to them.
| Document | What it covers |
|---|---|
| Core Architecture | End-to-end call flow: ai.stream.open IpcApi route → context provider → AiStreamManager → runtime → broadcast / persist |
| Stream Manager | Active-stream registry, listeners, reconnect, abort, queue/yield/continuation steering, persistence backends |
| Agent Session Runtime | Agent-session host/driver split, follow-up admission, resume persistence, and the registered Claude Code, Pi, and DSH drivers |
| Adding an Agent Runtime | Operational checklist for a new runtime: capability descriptor, driver package, registration points, design rules |
| Adapter Family | How provider.endpointConfigs[ep].adapterFamily picks the right @ai-sdk/* package per request |
| Document | What it covers |
|---|---|
| Agent Loop | Main-process Agent.stream(): single-pass stream, hook composition, observer pattern, error/abort semantics |
| Agent Prompt Layers | Agent System Prompt, workspace system.md, SOUL.md, precedence, update boundary, and variable lifecycle |
| Params Pipeline | buildAgentParams + RequestFeature model: how capabilities, plugins, tools, and provider-specific quirks are composed |
| Tool Registry | Built-in web/knowledge/file/image/MCP-resource tools, selected MCP tools, meta-tools, and deferred exposition |
| Chat Attachments | How attached files reach the model: native file parts when supported, capped extracted text otherwise, read_file for overflow paging |
| Provider Resolution | Provider.endpointConfigs schema, endpoint resolution chain, variant suffixes, custom provider extensions (aihubmix, newapi) |
| Model Retry & Fallback | ai-retry integration: same-model transient retry + user-configured fallback models, wrapModel hook, chat.retry.* preferences, embedding/rerank policies |
| Observability (trace / telemetry) | AiSdkSpanAdapter, root span propagation, OTel attribute shape, local span projection, sinks |
| AI Usage Records | Best-effort per-provider-invocation usage/cost analytics: capture ownership, immutable attribution snapshots, message projection, bounded query API, migration, freshness |
| Document | What it covers |
|---|---|
| IPC Transport | useChat + IpcChatTransport: sendMessages / reconnectToStream, dispatch service, topic-status mirror |
| Execution Overlay | TopicStreamSubscription + useExecutionOverlay: ref-counted attach, execution + anchor demux, one-shot readUIMessageStream per turn (the renderer half of the same merge function Main uses) |
| Tool Approval | Approval registry, Main-as-writer model, persistent decisions, useToolApproval hook |
Scope of the focused docs. The reference documents in this folder map the chat / stream pipeline (dispatch → stream manager → runtime → tools → persistence → renderer transport). The
channels/,skills/, andmcp/subsystems are mapped in the tree below but do not yet have dedicated deep-dive docs.
src/main/ai/
├── AiService.ts ← provider operations, built-in tool init, approval decisions
├── runtime/ ← AI execution backends + agent-session runtime registry
│ ├── aiSdk/ ← Agent class, loop, observers, params/features
│ ├── claudeCode/ ← Claude Code driver, warm query, SDK adapter
│ ├── pi/ ← Pi runtime connection and approval extension
│ └── dsh/ ← DeepSeek Harness runtime connection
├── agentSession/ ← agent-session topic host
│ └── AgentSessionRuntimeService.ts
├── agents/ ← AgentJobsService, AgentTaskJobHandler, runAgentTask, prompt, heartbeat, builtin/
├── channels/ ← ChannelManager + IM adapters (discord/feishu/qq/slack/telegram/wechat) + security/
├── streamManager/ ← AiStreamManager + listeners + persistence backends
│ ├── AiStreamManager.ts ← active-stream registry and dispatch owner
│ ├── context/ ← ChatContextProvider implementations + dispatch
│ ├── lifecycle/ ← chat / prompt-only stream lifecycles
│ ├── listeners/ ← WebContents / Persistence / SSE / channel-adapter
│ ├── persistence/ ← MessageService / TemporaryChat / Translation backends
│ └── pipeStreamLoop.ts ← shared chunk-pipe primitive
├── provider/ ← provider config, endpoint resolution, custom providers
│ ├── custom/ ← provider-specific adapters, transports, and wire profiles
│ ├── config.ts ← providerToAiSdkConfig (builder table)
│ ├── endpoint.ts ← resolveEffectiveEndpoint + adapterFamily routing
│ ├── extensions.ts ← ProviderExtension registrations
│ └── listModels.ts ← per-provider model listing
├── mcp/ ← McpRuntimeService / McpCatalogService, oauth/, built-in servers
│ └── servers/ ← in-memory MCP server implementations (browser, filesystem)
├── skills/ ← SkillService, SkillInstaller
├── contextBuild/ ← context-window policy, compression, persisted tool output
├── inference/ ← local embedding/OCR inference workers and model sources
├── tokens/ ← token estimation and modality profiles
├── tools/ ← unified tool registry
│ └── adapters/
│ ├── aiSdk/ ← registry.ts, repair.ts; builtin/ (web_search/web_fetch/kb_*),
│ │ mcp/ (server → ToolEntry sync), meta/ (tool_search/inspect/invoke;
│ │ tool_exec defined but not injected), exposition/ (shouldDefer + applyDefer)
│ └── claudeCode/ ← agentTools.ts (registry → Claude Code runtime)
├── observability/ ← AI trace adapters (aiSdk / claudeCode), local projection, sinks
├── messages/ ← UI part → AI SDK part conversion
├── types/ ← AppProviderId, merged extension types, request types
└── utils/ ← reasoning / model parameters / options / websearch helpers
useChat({ transport: IpcChatTransport }) calls sendMessages →
IpcApi ai.stream.open ({ topicId, trigger, userMessageParts, parentAnchorId?, mentionedModelIds? }).src/main/ipc/handlers/ai.ts resolves the caller's
WebContents, wraps it in a WebContentsListener, and delegates to
AiStreamManager.dispatch. Stream state stays in the manager; transport
registration stays in IpcApi.dispatchStreamRequest picks the first ChatContextProvider whose
canHandle(topicId) matches (persistent chat / temporary / agent
session) and calls prepareDispatch — that resolves models, persists
the user message, builds listeners, and returns a PreparedDispatch.AiStreamManager.send(input) starts a turn (no active stream): creates
an ActiveStream, launches one StreamExecution per model. (A chat
resubmit on a live topic is persisted + queued as a steer and takes the
inject path — the running turn yields and onExecutionDone chains a
continuation; an agent-session follow-up also injects, upserting listeners.)runExecutionLoop calls AiService.streamText(request, signal), which builds params (buildAgentParams) and constructs an Agent
composing hooks from RequestFeature[] (anthropic cache, gateway usage
normalisation, reasoning extraction, …), then calls agent.stream(messages, signal) to open the AI SDK stream and yield UIMessageChunks.
Agent-session runtime requests are the exception: AiService.streamText
routes them to AgentSessionRuntimeService.openTurnStream() so the
registered driver can own the concrete agent runtime.pipeStreamLoop tees the chunk stream: one branch broadcasts to listeners
(WebContents / SSE / channel-adapter / persistence), one branch runs
readUIMessageStream to accumulate a CherryUIMessage snapshot.PersistenceListener writes the final
message via the appropriate PersistenceBackend.useQuery('/topics/:topicId/messages')
and disposes its overlay.topicId.
A topic has at most one active stream; subscribers are equal — there's no
"owner" window.PersistenceListener writes on terminal
regardless of who is listening.approved/denied parts. It posts the decision over IPC and re-reads the
authoritative row. See Tool Approval.adapterFamily per endpoint.
Picking the SDK package never reads apiHost or provider id heuristics
at request time. See Adapter Family.AiService extends BaseServiceMessageService, ModelService,
ProviderService (called from main-side AI code)WebContentsListener
attaches to whatever windows are open