Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ harnesses, models and tools into one agent that finishes real work in your
projects and apps. Ready from day one, yours to shape over time.

Cindy runs locally on your own machine, using your real files and logged-in
apps. The first supported harnesses are **Claude Code** and **Codex** — more are
being added, and a native harness is in the works. Models and harnesses mix
apps. The first supported harnesses are **Claude Code**, **Codex**, and **Pi**. **Grok Build** is an optional local harness when the `grok` CLI is on PATH. Models and harnesses mix
freely and can switch mid-task while your workspace, memory, skills and tools
stay continuous; one task can even be planned, executed in parallel, and
reviewed by agents on different harness × model combos. She can drive your
Expand Down
3 changes: 1 addition & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ Cindy 是一个开源、开箱即用的 AI Agent。她把多套 Harness、模型
持续成长的伙伴,在真实工程和软件里把任务做完。一开始就好用,任你打扮,任你培养。

Cindy 运行在你自己的电脑上,使用你本地的文件和已登录的应用。首批兼容
**Claude Code** 与 **Codex** 两套 Agent Harness——更多 Harness 正在接入,自研
Harness 也在酝酿。模型与 Harness 自由组合、同一任务中随时切换,工作现场、记忆、
**Claude Code**、**Codex** 与 **Pi** 三套 Agent Harness;本机 PATH 上有 `grok` 时还可选 **Grok Build**。模型与 Harness 自由组合、同一任务中随时切换,工作现场、记忆、
Skill 和工具始终连续;一个任务还可以由不同 Harness × 模型组合的多个 agent
规划、并行执行、独立 review。她能操作浏览器、电脑和手机,并支持从 IM 和
定时任务派活。
Expand Down
10 changes: 8 additions & 2 deletions apps/desktop/src/main/im/defaultSessionSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
import {
IM_DEFAULT_EFFORT_OVERRIDES,
IM_DEFAULT_SETTINGS,
isImDefaultAgentKind,
type ImDefaultAgentKind,
type ImDefaultAgentSettings,
type ImDefaultSettingsChannel,
} from '../../shared/imDefaultSettings.js';
Expand Down Expand Up @@ -72,7 +74,10 @@ export async function resolveImSessionDefaults(
const requestedSettings = raw.agents[requestedAgent];
const model = pickModel(requestedAgent, requestedSettings, config, providers);
const agentKind = model.agentKind;
const agentSettings = raw.agents[agentKind] ?? requestedSettings;
// IM 默认设置只为可选的三个 agent 存 per-agent 拷贝;渠道配置把会话落到目录之外的
// agent(grok-build)时,沿用请求 agent 的那份,来源/档位随后仍按落地模型 reconcile。
const agentSettings =
(isImDefaultAgentKind(agentKind) ? raw.agents[agentKind] : undefined) ?? requestedSettings;
// 先定来源再定 effort:effort 支持是 per-(来源, 模型) 的,保存的来源被停用改道后,
// 必须按**最终落地来源**的拷贝 reconcile —— 按第一份 connected 拷贝(可能正是那份
// 停用拷贝)算出的档位,启用替代来源未必支持,直建会话会被上游拒
Expand Down Expand Up @@ -122,7 +127,8 @@ export async function resolveDefaultProviderIdForModel(
}

function pickModel(
requestedAgent: AgentKind,
// 请求 agent 恒来自 IM 默认设置(三选一);兜底才可能落到渠道配置的其它 agent。
requestedAgent: ImDefaultAgentKind,
settings: ImDefaultAgentSettings,
config: ImOrchestratorConfig,
providers: ProviderView[] | null,
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/main/localDb/chatHistoryReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const messageRowid = sql<number>`"messages"."rowid"`;
// ── Types ───────────────────────────────────────────────────────────────────

export type HistoryOrder = 'asc' | 'desc';
export type HistoryAgentKind = 'cc' | 'codex' | 'pi';
/** sessions.agent_kind 的历史筛选取值;'cc' 是 Claude Code 的历史存储形态。 */
export type HistoryAgentKind = 'cc' | 'codex' | 'pi' | 'grok-build';

export interface HistoryCursor {
createdAt: number; // unix ms
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/main/localDb/chatHistorySearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
HistoryRole,
} from '@cindy/mcps';

import type { HistoryAgentKind } from './chatHistoryReader';
import { getDbClient } from './client/current';
import { messages as messagesTable, sessions as sessionsTable } from './schema';
import { messageToCamel } from './mapper';
Expand Down Expand Up @@ -86,7 +87,12 @@ interface HitMeta {

type SearchSessionStatus = 'active' | 'archived' | 'deleted';

interface SearchChatHistoryEngineArgs extends SearchChatHistoryArgs {
interface SearchChatHistoryEngineArgs extends Omit<SearchChatHistoryArgs, 'agentKind'> {
/**
* 桌面端会话搜索可按 grok-build 过滤;MCP wire 契约的 agentKind 只有三种,是这里的
* 真子集,工具层照旧直接传。过滤值就是 sessions.agent_kind 的存储形态。
*/
agentKind: HistoryAgentKind | null;
/**
* Optional host-side filters for product entry points that should only expose
* desktop-visible conversations. MCP callers omit these and keep the original
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/localDb/ipc/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { sessions } from '../schema';
import { readLatestSessionTerminal, type SessionTerminalHint } from '../sessionTerminal';
import { requireObject, requireString, throwIpcError } from '../../utils/ipcValidate';

const VALID_AGENT_KINDS: readonly HistoryAgentKind[] = ['cc', 'codex', 'pi'];
const VALID_AGENT_KINDS: readonly HistoryAgentKind[] = ['cc', 'codex', 'pi', 'grok-build'];
const VALID_ORDERS: readonly HistoryOrder[] = ['asc', 'desc'];
const VALID_ROLES: readonly HistoryRole[] = [
'user',
Expand Down
7 changes: 4 additions & 3 deletions apps/desktop/src/main/localDb/ipc/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
extractMessagePreview,
} from '../mapper';
import { throwIpcError, requireString } from '../../utils/ipcValidate';
import type { DbAgentKind } from '../../../shared/agentKindConversion';
import * as broadcastTap from '../../device-link/broadcast-tap';
import { createLogger } from '../../logger';
import { collectCindyMediaHashes, commitMessageMediaRefs } from '../../cindy-media/chatAttachments';
Expand Down Expand Up @@ -1001,7 +1002,7 @@ export async function commitContextRebuild(
meta: {
reason: 'context-overflow' | 'pi-prompt-timeout';
sourceUserClientId: string | null;
sourceAgentKind?: 'cc' | 'codex' | 'pi';
sourceAgentKind?: DbAgentKind;
sourceModel?: string | null;
sourceProviderId?: string | null;
expectedClearedAt?: number | null;
Expand Down Expand Up @@ -1418,7 +1419,7 @@ export async function createMessage(
* agentMeta 需要它;main 侧 SDK 事件落库路径必传,renderer pending echo 等
* 无 SDK 元信息的行留空(null 回落 session.agentKind)。
*/
agentKind?: 'cc' | 'codex' | 'pi' | null;
agentKind?: DbAgentKind | null;
createdAt?: number;
},
opts?: {
Expand Down Expand Up @@ -2511,7 +2512,7 @@ export interface ParkedEngineSession {
*/
export async function findParkedEngineSession(
sessionId: string,
targetDbKind: 'cc' | 'codex' | 'pi',
targetDbKind: DbAgentKind,
): Promise<ParkedEngineSession | null> {
const db = getDbClient().drizzle;
const [sessRow] = await db
Expand Down
16 changes: 11 additions & 5 deletions apps/desktop/src/main/localDb/ipc/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ import { buildSessionListFlightKey, runSessionListSingleFlight } from './session
import { throwIpcError, requireString, requireObject } from '../../utils/ipcValidate';
import { bindDeletedPiSubagentCleanupCancel } from './piSubagentDeletion';
import { resolveBusinessSessionId } from '../../sessionIds';
import { normalizeDbAgentKind } from '../../../shared/agentKindConversion';
import {
dbToMakerAgentKind,
normalizeDbAgentKind,
type DbAgentKind,
type MakerAgentKindWire,
} from '../../../shared/agentKindConversion';
import {
sessionToCamel,
sessionCreateToRow,
Expand Down Expand Up @@ -440,7 +445,7 @@ const REMOTE_PERSIST_FIELDS = new Set([
export async function applyAgentSwitchToSessionRow(
sessionId: string,
patch: {
agentKind: 'cc' | 'codex' | 'pi';
agentKind: DbAgentKind;
model: string;
providerId: string | null | undefined;
sdkSessionId?: string | null;
Expand Down Expand Up @@ -863,7 +868,7 @@ export interface OverwritableAutoTitleTarget {
* `reconcileCreateOptsAgainstDb` 处理的正是同一类漂移),用错 agent 会让标题
* 走错供应商 —— 纯 Codex / 纯 Claude 用户会因此只拿到 fallback 标题。
*/
agentKind: 'claude-code' | 'codex' | 'pi';
agentKind: MakerAgentKindWire;
/**
* 是否仍停在建会话时的裸默认标题。合成占位(纯附件消息)只允许覆写这一种 ——
* fork 占位与上一条附件写下的合成占位都要保留到用户真正打字为止。
Expand All @@ -878,8 +883,9 @@ export async function getOverwritableAutoTitle(
const db = getDbClient().drizzle;
const row = await selectSessionWithCount(db, id);
if (!row) return null;
const agentKind =
row.agentKind === 'codex' || row.agentKind === 'pi' ? row.agentKind : 'claude-code';
// 走映射正本:就地 ternary 会把 'cc' 之外的新引擎(grok-build)误判成 claude-code,
// 起名就跑去了错的供应商(见上方 agentKind 注释)。
const agentKind = dbToMakerAgentKind(row.agentKind);
const overwritable =
row.title === DEFAULT_DRAFT_SESSION_TITLE ||
(!!row.parentSessionId && row.title.startsWith(FORK_PLACEHOLDER_TITLE_PREFIX)) ||
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/main/localDb/mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type {
ScriptCapability,
PreRunHookRunResult,
} from '@cindy/maker-scheduler';
import type { DbAgentKind } from '../../shared/agentKindConversion.js';
import { normalizeSessionSource } from '../../shared/sessionSource.js';
import { normalizeWorkingDirForStorage } from '../../shared/workingDir.js';
import { isSyntheticTriggerText } from '../../shared/interruptedTurn.js';
Expand Down Expand Up @@ -435,7 +436,7 @@ export function messageCreateToRow(
content: unknown;
toolUseId?: string;
agentMeta?: AgentMeta | null;
agentKind?: 'cc' | 'codex' | 'pi' | null;
agentKind?: DbAgentKind | null;
createdAt?: number;
},
now: number,
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/main/localDb/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ export const subagentRuns = sqliteTable(
sessionId: text('session_id')
.notNull()
.references((): AnySQLiteColumn => sessions.id, { onDelete: 'cascade' }),
provider: text('provider', { enum: ['claude-code', 'codex', 'pi'] }).notNull(),
provider: text('provider', { enum: ['claude-code', 'codex', 'pi', 'grok-build'] }).notNull(),
logicalAgentId: text('logical_agent_id').notNull(),
parentToolUseId: text('parent_tool_use_id'),
/** JSON string[] containing task/tool aliases observed for this logical child. */
Expand Down Expand Up @@ -516,7 +516,7 @@ export const subagentRunAliases = sqliteTable(
sessionId: text('session_id')
.notNull()
.references((): AnySQLiteColumn => sessions.id, { onDelete: 'cascade' }),
provider: text('provider', { enum: ['claude-code', 'codex', 'pi'] }).notNull(),
provider: text('provider', { enum: ['claude-code', 'codex', 'pi', 'grok-build'] }).notNull(),
alias: text('alias').notNull(),
runId: text('run_id')
.notNull()
Expand Down Expand Up @@ -866,7 +866,7 @@ export const schedules = sqliteTable(
* 引擎 fireOne 优先用 intervalMs 算 nextFireAt;旧 cron 数据 0015 migration 自动回填。
*/
intervalMs: integer('interval_ms'),
agentKind: text('agent_kind', { enum: ['claude-code', 'codex', 'pi'] }).notNull(),
agentKind: text('agent_kind', { enum: ['claude-code', 'codex', 'pi', 'grok-build'] }).notNull(),
model: text('model'),
/**
* 显式选定的供应商(来源)id。NULL = 回落该 agent 原生默认来源(no-break,
Expand Down Expand Up @@ -985,7 +985,7 @@ export const sessionGoals = sqliteTable(
/** usageLimited 时记录的限额重置时刻(unix ms);到点自动续跑。其它状态为 null。 */
usageResetAt: integer('usage_reset_at'),
lastReason: text('last_reason'),
agentKind: text('agent_kind', { enum: ['claude-code', 'codex', 'pi'] }).notNull(),
agentKind: text('agent_kind', { enum: ['claude-code', 'codex', 'pi', 'grok-build'] }).notNull(),
startedAt: integer('started_at').notNull(),
updatedAt: integer('updated_at').notNull(),
},
Expand Down
14 changes: 10 additions & 4 deletions apps/desktop/src/main/maker-host/active-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,13 @@ export interface XdGatewayModelInfo {
/** AIGateway 缓存 token 单价(per token);参与「免费」判定与价格展示。 */
cacheReadInputTokenCost?: number;
cacheCreationInputTokenCost?: number;
/** 进哪些 runtime tab;v3 由服务端完整下发。 */
agents?: AgentKind[];
/**
* 进哪些 runtime tab;v3 由服务端完整下发。这里跟 shared/modelAccess 的
* `ModelAccessGatewayModel.agents` 逐字对齐(同一份服务端协议):网关目录只服务
* cc / codex / pi 三个 tab,grok-build 是本机 CLI、自带唯一内置模型,不进网关目录,
* 所以**不能**写成 `AgentKind[]` —— 那样两边协议类型会漂移。
*/
agents?: ('claude-code' | 'codex' | 'pi')[];
name?: string;
group?: string;
description?: string;
Expand Down Expand Up @@ -545,8 +550,9 @@ function modelRegistryMetaFields(
modelId: string,
): RegistryMetaFields | undefined {
// 模型 registry 的路由与 perAgent 覆盖只按 claude-code / codex 建键;Pi 是动态 BYOM,
// 无 registry per-agent 覆盖,按 agent 无关处理(取条目基线元数据)。
const registryAgent = agent === 'pi' ? undefined : agent;
// 无 registry per-agent 覆盖,按 agent 无关处理(取条目基线元数据)。Grok Build
// 只有一个内置模型、不进模型平面,同样按 agent 无关处理。
const registryAgent = agent === 'pi' || agent === 'grok-build' ? undefined : agent;
const catalog = base ?? BUNDLED_CATALOG;
const matched = findModelRegistryRoute(catalog.modelRegistry, providerId, modelId, registryAgent);
if (!matched) return undefined;
Expand Down
16 changes: 15 additions & 1 deletion apps/desktop/src/main/maker-host/catalog-to-descriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ interface SeenModelProjection {
includesUserProvider: boolean;
}

/** ModelDescriptor.newSessionDefault 的元素类型(maker-core 只为三个 wire agent 记种子)。 */
type NewSessionDefaultAgent = NonNullable<ModelDescriptor['newSessionDefault']>[number];

/**
* grok-build 不进新对话默认种子:它只有一个内置模型、不由目录供货,目录里出现该标记
* 只能是脏数据。这里丢弃而不是投影,避免下游按不存在的目录默认改路由。
*/
function isNewSessionDefaultAgent(agent: AgentKind): agent is NewSessionDefaultAgent {
return agent !== 'grok-build';
}

/** CatalogModel → ModelDescriptor。仅透传 ModelDescriptor 需要的字段;可选字段缺省时不写键。 */
function toDescriptor(
m: CatalogModel,
Expand Down Expand Up @@ -92,7 +103,9 @@ function toDescriptor(
if (m.defaultEnabled !== undefined) d.defaultEnabled = m.defaultEnabled;
// 新对话默认种子标记要透传:渲染层 getDefaultModelForVendor 据它优先选中被标记的模型。
// v3 可携带 Pi 自己的标记;消费端按 Agent 严格解释,不跨 Agent 借用默认策略。
if (m.newSessionDefault !== undefined) d.newSessionDefault = m.newSessionDefault;
if (m.newSessionDefault !== undefined) {
d.newSessionDefault = m.newSessionDefault.filter(isNewSessionDefaultAgent);
}
if (m.cost !== undefined) d.cost = m.cost;
if (m.maxOutput !== undefined) d.maxOutputTokens = m.maxOutput;
const supportsImageInput =
Expand Down Expand Up @@ -131,6 +144,7 @@ function mergeNewSessionDefaultMarker(
next: ModelDescriptor,
agent: AgentKind,
): ModelDescriptor {
if (!isNewSessionDefaultAgent(agent)) return first;
const hasNewMarker =
next.newSessionDefault?.includes(agent) === true &&
first.newSessionDefault?.includes(agent) !== true;
Expand Down
Loading