# v0.8 Onboarding Agent — 设计原则

## 为什么写这份

第一次 Lab session 遇到 Moonshot/Kimi 一堆 quirks 后，临时打了几个补丁。
但用户指出："修复应该是通用的，不是只针对某一个东西做优化"。

这份文档是从那次反思中提炼出来的原则，目的是约束我们后续在加任何模型、任何 quirk、任何 fix 时都遵守同样的形态。

---

## 5 个原则

### 1. 模型 quirks 不进 Agent 核心代码

**坏**：
```ts
// agent.ts
if (modelId.startsWith("kimi-k2")) {
  body.enable_thinking = false;
}
if (modelId.startsWith("o1")) {
  body.temperature = 1;
}
```

**好**：
```ts
// agent.ts
const profile = getModelProfile(modelId);
const adjusted = applyProfileToRequestBody(body, profile);
```

quirks 都进 `electron/ai/modelProfiles.ts`，是数据不是代码。新加一个供应商 = 加一个 entry，不动 agent 核心。

### 2. Prompt 内容必须跨 tokenizer 兼容

任何送给 LLM 的字符串（system prompt / tool descriptions / user messages）都过一遍 `sanitizeForBroadCompat()`：
- 弯引号 → 直引号
- em-dash / en-dash → ASCII `-`
- `≥` `≤` `≠` `≈` → `>=` `<=` `!=` `~=`
- 零宽字符 → 删除
- 全角空格 / nbsp / en-space → 普通空格
- 箭头 → `->`
- 省略号 → `...`
- bullet → `*`

UI 文本可以保留花体排版。这只在送给 LLM 之前过一遍。

### 3. Tool schema 只用 JSON Schema 最小子集

LLM provider 的 JSON Schema 实现千差万别。即使是号称 OpenAI 兼容的，也经常不支持高级特性。

**禁用**：
- `z.union([z.string(), z.number(), z.boolean()])` → JSON Schema `"type": ["string","number","boolean"]` 数组型 type，Moonshot 等 tokenizer 会崩
- `z.intersection`, `z.discriminatedUnion` → 复杂 `allOf`/`oneOf` 嵌套
- `format: "uri"`, `format: "email"` 等 → 部分 provider 不识别

**用**：
- `z.string()`, `z.number()`, `z.boolean()`, `z.array()`, `z.object()`, `z.enum()`
- 校验放在 tool execute 内部（Zod 完整能力可以用），不放在 schema 里
- 多类型字段统一用 string，运行时按 `type` 字段 parse

### 4. Malformed tool-call JSON 必须能自愈

任何模型都可能输出截断或语法错误的 tool args。AI SDK 提供 `experimental_repairToolCall` hook：
```ts
experimental_repairToolCall: async ({ toolCall, error, messages }) => {
  // 让同一个模型看着错误自己修
  const repaired = await generateText({ model, ... });
  return { ...toolCall, args: repaired.text };
}
```

这是通用兜底 —— 不针对任何特定 model，所有 model 都受益。

### 5. Onboarding wizard 测真实能力，不查列表

用户挑了底层 Agent 模型后，wizard 不能简单地查 hardcoded "推荐列表"。
要拿**真实 tool schema + 真实 fixture** 跑一次，看模型能不能：

1. 调对一个简单 tool（`echo({message: string})`）
2. 处理我们的标准 tool schema 大小
3. 不在中途崩

通过的才接受，没通过的明确告诉用户 "这个模型 tool calling 不稳，请换 GPT-4o / Claude / Gemini 等"。

---

## 实现这些原则的当前位置

| 原则 | 实现文件 | 测试覆盖 |
|---|---|---|
| 1. 模型 profile | `electron/ai/modelProfiles.ts` | `modelProfiles.test.ts` (12 tests) |
| 2. Prompt 净化 | `electron/ai/promptSanitize.ts` | `promptSanitize.test.ts` (11 tests) |
| 3. Tool schema 最小子集 | `electron/ai/onboarding/tools.ts` (rules in comments) | 文档约束 |
| 4. Tool repair | `electron/ai/onboarding/agent.ts` 的 `experimental_repairToolCall` | Lab fixture |
| 5. 能力检测 | TODO: M5 Onboarding wizard | TODO |

---

## 加新 quirky provider 的清单

只需 1 步：在 `modelProfiles.ts` 的 `PROFILES` 数组加一个 entry。

例：发现 X 公司的 "xyz-pro" 模型必须传 `enable_system_role: true`：
```ts
{
  description: "XYZ Pro - requires enable_system_role",
  match: (id) => /^xyz-pro/i.test(id),
  extraBody: { enable_system_role: true },
  defaultMaxTokens: 4096,
  agentSuitability: "good",
}
```

完。不动 agent.ts、不动 tools.ts、不动 buildAiSdkModel.ts。

---

## 加新 prompt 净化规则的清单

只需 1 步：在 `promptSanitize.ts` 的 `sanitizeForBroadCompat` 加一行 `.replace()`，附测试用例。

例：发现某 tokenizer 不认 `←`：已在规则里（→ `->`）。

---

## 反模式（PR 时拒绝）

- ❌ `if (modelId === "xxx") { ... }` 散落在 agent.ts 或 tools.ts
- ❌ tool schema 用 z.union / z.intersection / format
- ❌ 直接给 LLM 字符串没过 sanitize
- ❌ "我先 hardcode 修这一个，以后再通用" — 不允许，从一开始就要通用
- ❌ Try/catch 吞 LLM 错误而不写 repair hook
