DESIGN.md@agent-plugin · git:20260918.f8cebc6 · 2026-09-18 · sha256 32da48d36da09564

DESIGN.md@agent-plugin git:20260918.f8cebc6A

Immutable. This exact content is served forever at /api/v1/blob/32da48d36da09564.

# Design — agent-plugin

> 狀態:設計稿 v0.2(2026-09-12),供實作前審閱。
> 定位:給本地 agent 用的能力型 skills pack,不是單一 mega-skill。
> 2026-09-16:4.6 `agent-memory` 已對齊 markdown 協定,不再使用 SQLite / vector。

## 0. 目標與邊界

### 目標

- 提供八個開箱即用的本地 agent 能力:
  1. `generate-image`
  2. `generate-video`
  3. `read-image`
  4. `read-video`
  5. `read-audio`
  6. `agent-memory`
  7. `generate-text`
  8. `generate-audio`
- 每個能力都是獨立 skill,可單獨安裝、單獨觸發、單獨升級。
- 共用一套 runtime adapter 與 storage schema,避免七份重複邏輯。
- **Phase 1 provider 明確鎖定 OpenRouter**,先完成 image / video / audio endpoint 對接,再開放其他 provider。
- 支援三種部署形態:
  1. **Skills-only**:只放 `SKILL.md`,agent 直接讀。
  2. **Plugin pack**:包成 `.codex-plugin` / marketplace entry。
  3. **MCP server**:把能力暴露成 tools,給支援 MCP 的 agent 使用。

### 非目標

- 不綁定任何單一 agent runtime。
- 不把所有能力塞進一個巨型 prompt。
- 不預設必須上雲。預設 local-first,雲端只是 adapter。
- 不做通用 agent framework,只做「能力 + 檔案 + 狀態」的最小可交付層。

## 1. 設計原則

1. **能力獨立,基礎共用**
   - 七個 skill 各自獨立,方便單獨觸發。
   - storage / provider / policy 共用,避免重複。

2. **Local-first**
   - 媒體讀取、記憶優先使用本地工具。
   - 只有需要更強生成或理解時,才經 provider adapter 呼叫雲端。

3. **可驗收輸出**
   - 每個 skill 都有固定輸出契約。
   - 生成類輸出檔案 + metadata。
   - 理解類輸出結構化觀察 + 引用。

4. **可替換 provider**
   - skill 只定義「做什麼」,不定義「用哪個模型」。
   - provider 透過 adapter 註冊,未來換模型不改 skill。

5. **可降級**
   - 若模型不可用,至少回退到 metadata、OCR、ffmpeg。
   - 不允許無證據的幻覺輸出。

## 2. 架構

```text
User intent
    ↓
Skill trigger layer
  generate-image / generate-video
  read-image / read-video / read-audio
  agent-memory
    ↓
Skill workflow layer
  fixed input → fixed steps → fixed output contract
    ↓
Capability adapter layer
  provider adapters: local model / cloud model / CLI tools
    ↓
Storage & state layer
  media store / memory store / logs
```

### 2.1 三層分離

1. **Skill 層**
   - 只描述「何時觸發、怎麼做、輸出什麼」。
   - 不直接綁定 provider。

2. **Adapter 層**
   - 定義統一能力介面。
   - 每個 provider 實作同一組 contract。

3. **Storage 層**
   - 統一放置生成物、記憶與 log。
   - 支援 project-local 或 user-global 兩種 scope。

### 2.2 Phase 1 OpenRouter surface

Phase 1 不實作多 provider routing,只做一個 `openrouter` adapter,並把官方 endpoint 映射到 pack skills。
Adapter 支援模型探索、能力驗證、provider passthrough 預檢、text generation 與 TTS:

| Skill | OpenRouter endpoint | Mode |
|---|---|---|
| `generate-image` | `POST /api/v1/images` | Synchronous, base64 response |
| `generate-video` | `POST /api/v1/videos` + poll + download | Asynchronous job |
| `read-image` | `POST /api/v1/chat/completions` with `image_url` | Vision understanding |
| `read-video` | `POST /api/v1/chat/completions` with `video_url` | Video understanding |
| `read-audio` | `POST /api/v1/audio/transcriptions` | Dedicated STT |
| `read-audio` analysis mode | `POST /api/v1/chat/completions` with `input_audio` | Audio reasoning |
| `generate-text` | `POST /api/v1/chat/completions` | Text generation |
| `generate-audio` | `POST /api/v1/audio/speech` | TTS |
| model discovery | `/models`, `/images/models`, `/videos/models` | Capability discovery |

兼容策略:

1. OpenAI 使用 dedicated image/audio/chat endpoint。
2. Gemini 使用 vision/video/audio chat multimodal parts;local file 走 base64 data URL,public video 只允許 AI Studio route 支援的 YouTube URL。
3. Qwen 與 OpenAI 相容面走 generic text/vision chat completions。
4. MiniMax / Hailuo、Google Veo、Bytedance Seedance 走 `/videos` job state machine,並依模型 metadata 驗證 duration/resolution/aspect ratio/audio/frame input。
5. Input reference 能力依模型 metadata 判斷:image→image 檢查 image input modality 與 reference range;image→video 檢查 first/last frame metadata。text→image、text→video、image→image、image→video 為 Phase 1 支援矩陣。
6. Generic video→video editing 不在 Phase 1 generic contract 內;若模型需要影片參考,必須等 OpenRouter 提供統一欄位或新增 provider-specific adapter。
7. Provider 特有參數一律放在 `provider.options`,若有 `allowed_passthrough_parameters` 就先本地驗證。
8. Gemini agentic reasoning 保存在 artifact,可用 `--reasoning-file` 回傳續問。

完整 endpoint contract、request/response shape、error policy 與 privacy gate 在:

```text
references/openrouter.md
```

Phase 1 的本地狀態必須至少保存:

1. API request provenance:endpoint、model、usage、cost。
2. Video job state:`jobId`、`pollingUrl`、status、submittedAt。
3. Downloaded artifact:local path、media type、validation result。
4. Explicit cloud approval record.

## 3. 目錄結構

```text
agent-plugin/
  pack.json
  generate-image/
    SKILL.md
    references/
  generate-video/
    SKILL.md
    references/
  read-image/
    SKILL.md
    references/
  read-video/
    SKILL.md
    references/
  read-audio/
    SKILL.md
    references/
  agent-memory/
    SKILL.md
    references/
  shared/
    provider-contract.md
    storage-schema.md
    privacy-policy.md
    output-contract.md
  references/
    openrouter.md
  scripts/
    doctor.py
    providers/
    storage/
  templates/
    openrouter.config.json
    memory/
      working/INDEX.md
      semantic/INDEX.md
      episodic/INDEX.md
      procedural/INDEX.md
      personas/INDEX.md
```

### 3.1 `pack.json` 示意

```json
{
  "name": "agent-plugin",
  "version": "0.1.0",
  "runtime": "agent-skills-v1",
  "skills": [
    "generate-image",
    "generate-video",
    "read-image",
    "read-video",
    "read-audio",
    "agent-memory"
  ],
  "capabilities": {
    "image-generation": ["local", "cloud"],
    "video-generation": ["local", "cloud"],
    "media-understanding": ["local", "cloud"],
    "memory": ["local"]
  },
  "defaults": {
    "provider": "openrouter",
    "storageRoot": ".agents",
    "privacyMode": "local-first",
    "cloudApproval": "explicit"
  }
}
```

## 4. Skill 契約

每個 skill 的 `SKILL.md` 都應包含:

1. **Trigger**
   - 明確列出使用者會怎麼問。
   - 避免和相鄰 skill 混淆。

2. **Inputs**
   - 必填 / 選填參數。

3. **Workflow**
   - 固定步驟。
   - 模型不可用時的降級路徑。

4. **Output contract**
   - 固定欄位。
   - 引用來源或檔案。

5. **Provider**
   - 只依賴 adapter contract。
   - 不直接寫死模型名稱。

---

### 4.1 `generate-image`

#### 觸發

- 「幫我生一張圖」
- 「畫一個 icon / mockup / illustration」
- 「根據這張圖改成另一種風格」

#### 輸入

| 欄位 | 必填 | 說明 |
|------|------|------|
| prompt | 是 | 主題、風格、構圖 |
| aspectRatio | 否 | 預設 `1:1` |
| style | 否 | 攝影、插畫、UI、3D 等 |
| referenceImage | 否 | 圖生圖或編輯 |
| outputPath | 否 | 未指定則進 media store |

#### 流程

1. 判斷是「全新生成」還是「圖生圖 / 編輯」。
2. 組成 structured prompt:
   - subject
   - composition
   - style
   - lighting
   - quality
   - negative prompt
3. Phase 1 走 OpenRouter:
   - `POST /api/v1/images`
   - model 來自 `openrouter.config.json`
   - 先用 `/images/models/{model}/endpoints` 驗證參數
4. 生成圖片。
5. 驗證輸出:
   - 檔案存在
   - 可讀取
   - 尺寸正確
6. 寫入 metadata。

#### 輸出

```json
{
  "type": "image-generation",
  "outputPath": "media/images/xxx.png",
  "prompt": "...",
  "provider": "openrouter",
  "model": "...",
  "aspectRatio": "16:9",
  "seed": 123456,
  "createdAt": "2026-09-12T00:00:00Z"
}
```

#### 失敗模式

- Provider 不存在 → 提示安裝或改用 fallback。
- 檔案驗證失敗 → 不回報成功。
- 圖生圖超過尺寸限制 → 先降解析度或分區處理。

---

### 4.2 `generate-video`

#### 觸發

- 「幫我做一段影片」
- 「把這段 storyboard 變成 video」
- 「做一個 5 秒產品動畫」

#### 輸入

| 欄位 | 必填 | 說明 |
|------|------|------|
| storyboard | 是 | 鏡頭、節奏、文案 |
| duration | 否 | 預設 5 秒 |
| fps | 否 | 預設 24 |
| resolution | 否 | 預設 1080p |
| audio | 否 | 可選配音或音樂 |
| style | 否 | 動畫、電影感、產品演示 |

#### 流程

1. 把需求轉成 storyboard spec。
2. Phase 1 走 OpenRouter:
   - `POST /api/v1/videos`
   - 持久化 `jobId`、`pollingUrl` 與 status
   - poll `GET /api/v1/videos/{jobId}`
   - download `GET /api/v1/videos/{jobId}/content`
3. 生成 video。
4. 用 `ffprobe` 驗證:
   - duration
   - codec
   - fps
   - resolution
5. 若有音軌,驗證 audio stream。

#### 輸出

```json
{
  "type": "video-generation",
  "outputPath": "media/videos/xxx.mp4",
  "storyboard": "...",
  "provider": "openrouter",
  "model": "...",
  "duration": 5,
  "fps": 24,
  "resolution": "1920x1080",
  "createdAt": "2026-09-12T00:00:00Z"
}
```

#### 失敗模式

- Provider 不支援指定時長 → 提示可接受範圍。
- 影片驗證失敗 → 不回報成功。
- 只能生成圖片序列時,先交付 sequence,再問是否合成。

---

### 4.3 `read-image`

#### 觸發

- 「這張圖是什麼?」
- 「幫我 OCR」
- 「這張 UI 截圖有什麼問題?」
- 「這張圖表在講什麼?」

#### 輸入

| 欄位 | 必填 | 說明 |
|------|------|------|
| imagePath | 是 | 本地圖片路徑 |
| question | 否 | 使用者的問題 |
| detailLevel | 否 | `low` / `medium` / `high` |
| focus | 否 | `objects` / `text` / `chart` / `ui` |

#### 流程

1. 先用本地工具讀 metadata:
   - `sips`
   - `exiftool`
   - `identify`
2. 若需要 OCR,走 `tesseract` / `vision OCR`。
3. 若需要視覺理解,走 vision model。
4. 結果結構化:
   - objects
   - text
   - layout
   - colors
   - notable regions
5. 若使用者有問題,優先回答問題。

#### 輸出

```json
{
  "type": "image-reading",
  "inputPath": "input.png",
  "question": "...",
  "summary": "...",
  "observations": [
    {
      "kind": "text",
      "value": "...",
      "region": {"x": 0, "y": 0, "w": 100, "h": 20},
      "confidence": 0.94
    }
  ],
  "metadata": {
    "width": 1920,
    "height": 1080,
    "format": "png",
    "exif": {}
  }
}
```

#### 失敗模式

- 圖片過大 → 自動縮圖後再讀。
- OCR 不確定 → 標 confidence,不假裝確定。
- Vision model 不可用 → 只做 metadata + OCR,並明說限制。

---

### 4.4 `read-video`

#### 觸發

- 「這段影片在講什麼?」
- 「幫我整理這段影片重點」
- 「找出裡面有 logo 出現的時間點」

#### 輸入

| 欄位 | 必填 | 說明 |
|------|------|------|
| videoPath | 是 | 本地影片路徑 |
| question | 否 | 使用者的問題 |
| sampleRate | 否 | 每幾秒抽一幀 |
| focus | 否 | `scene` / `text` / `audio` / `motion` |

#### 流程

1. `ffprobe` 讀 metadata。
2. 抽關鍵幀。
3. 音訊轉 transcript。
4. OCR / shot detection。
5. 建立 timeline。
6. 依使用者問題合成摘要。

#### 輸出

```json
{
  "type": "video-reading",
  "inputPath": "input.mp4",
  "question": "...",
  "duration": 120,
  "scenes": [
    {
      "start": 0,
      "end": 10,
      "summary": "...",
      "objects": [],
      "text": [],
      "audio": "..."
    }
  ],
  "transcript": {
    "language": "zh",
    "segments": [
      {"start": 0, "end": 5, "text": "..."}
    ]
  }
}
```

#### 失敗模式

- 影片太長 → 先分段處理。
- 無音軌 → 只做視覺分析。
- 無法抽幀 → 只回 metadata,不硬編故事。

---

### 4.5 `read-audio`

#### 觸發

- 「這段錄音在說什麼?」
- 「幫我轉文字」
- 「這段音檔裡有誰在說話?」

#### 輸入

| 欄位 | 必填 | 說明 |
|------|------|------|
| audioPath | 是 | 本地音訊路徑 |
| question | 否 | 使用者的問題 |
| language | 否 | 自動偵測 |
| diarize | 否 | 是否區分說話者 |

#### 流程

1. `ffprobe` 讀 metadata。
2. Phase 1 轉 transcript 走 OpenRouter:
   - `POST /api/v1/audio/transcriptions`
   - `response_format: verbose_json`
   - 需要分析而非純轉文字時,改走 Chat Completions `input_audio`
3. 若需要,做 speaker diarization。
4. 依問題摘要。

#### 輸出

```json
{
  "type": "audio-reading",
  "inputPath": "input.m4a",
  "language": "zh",
  "duration": 90,
  "transcript": "...",
  "segments": [
    {"start": 0, "end": 10, "speaker": "S1", "text": "..."}
  ],
  "summary": "..."
}
```

#### 失敗模式

- ASR 不可用 → 只回 metadata,不編造內容。
- 語言混雜 → 分段處理。
- 說話者辨識失敗 → 保留 segment,但 speaker 欄位標 `unknown`。

---

### 4.6 `agent-memory`

Markdown 檔案協議,不是 provider、不是 SQLite、不是向量庫。
協定正文:`agent-plugin/agent-memory/SKILL.md`。

對齊業界最小集:CoALA 四職責(working / semantic / episodic / procedural);
Letta core vs archival;Claude Code 的 INDEX + 按需讀本文。Personas 是可選
core(協作契約),不是第五種認知記憶。

#### 觸發

- 「記住我喜歡簡短回覆」
- 「這個專案上次決策是什麼?」
- 「更新你的長期記憶」
- 「忘記這件事」
- 任務會跨 compaction / handoff
- 類似任務以前失敗過

#### 抽屜

| 層 | 目錄 | 何時載入 |
|---|---|---|
| Core | `personas/`、`working/` | Boot:最多一份 active persona;working 僅在任務會跨 compact/handoff 時建立 |
| Archival | `semantic/`、`episodic/`、`procedural/` | 需要時先讀 INDEX one-liner,再打開檔案 |

預設根目錄:`./.agents/memory/`。`~/.agents/memory/` 僅在使用者明確要求跨專案時使用,不自動同步。

#### 流程

1. Boot:persona INDEX(若有列)→ 必要時 working。不要一次讀五份 INDEX。
2. Recall:INDEX one-liner 決定要不要打開檔案。沒有命中就不要瀏覽資料夾。
3. Remember:過 write-back gate 後寫入恰好一個抽屜。
4. Forget:刪檔且刪 catalog 列。
5. Handoff:working 的 `handoff` 欄是 compact 後唯一要重讀的桌面摘要。

#### 輸出

每次記憶寫入都要明說:drawer、slug、`last_updated`、改了什麼、為何過閘。
沒有更新則在 handoff 說 `no memory update`。禁止靜默寫入。

#### 失敗模式

- 把聊天紀錄當 episodic → 只留 context / action / result / lesson。
- 把 skill 正文複製進 procedural → 只留 pointer。
- 範圍不明 → 預設 project,不寫入 `~/.agents`。
- 內容含秘密或敏感資料 → 不寫入。
- INDEX 沒有命中 → 不猜測,不掃資料夾。

## 5. Shared contracts

### 5.1 Provider contract

所有 provider 必須符合統一介面。

```text
image.generate(prompt, options) -> ImageResult
video.generate(storyboard, options) -> VideoResult
image.read(path, question, options) -> ImageReading
video.read(path, question, options) -> VideoReading
audio.read(path, question, options) -> AudioReading
```

Memory is not a provider method. Agents read and write markdown under
`.agents/memory/` per `agent-memory/SKILL.md`.

### 5.2 Storage schema

#### Memory files

Each memory entry is one markdown file plus one INDEX catalog row.
Skeletons and caps live in `agent-plugin/agent-memory/SKILL.md`.
Do not introduce a parallel JSON/SQLite record format.

### 5.3 Output contract

每個 skill 的回覆都應包含:

1. **結果摘要**
2. **檔案路徑**
3. **使用的 provider**
4. **限制**

## 6. Storage layout

Pack runtime artifacts use two compatible roots. The default is
project-local; the user-global root is opt-in for state that must be shared
across projects.

Default project-local:

```text
./.agents/
  media/
    images/
    videos/
    audio/
  artifacts/
  jobs/
    videos/
  approvals/
  memory/
    working/
    semantic/
    episodic/
    procedural/
    personas/
```

User-global opt-in:

```text
~/.agents/
  skills/           # installed skills, never runtime state
  media/
  artifacts/
  jobs/
  approvals/
  memory/
```

Selection rules:

1. `--storage-root ~/.agents` or a config value can select the global scope.
2. Project-local `.agents` remains the default so generated files stay near
   the project and are easy to ignore or delete.
3. `~/.agents/skills/` is reserved for skill installation; runtime must not
   overwrite or prune it.
4. Memory may use global scope only when its content is
   intentionally cross-project.

## 7. Privacy & permission

1. **Local-first**
   - 預設不上傳檔案。
   - 上雲需明確同意。

2. **Secrets**
   - API key 只放環境變數。
   - 不寫入 repo。

3. **Provenance**
   - 所有生成與理解結果都記錄 provider、模型、時間。

4. **Retention**
   - Semantic 條目可設 `expires`。
   - 支援 `forget`。
   - Working 在任務結束時刪除,不归档。

## 8. Packaging

### 8.1 Skills-only

適合任何支援 `SKILL.md` 的 agent。

```text
agent-plugin/
  generate-image/SKILL.md
  generate-video/SKILL.md
  read-image/SKILL.md
  read-video/SKILL.md
  read-audio/SKILL.md
  agent-memory/SKILL.md
```

### 8.2 Codex plugin

適合 Codex App:

```text
agent-plugin/
  .codex-plugin/
    plugin.json
  skills/
    generate-image/
    generate-video/
    read-image/
    read-video/
    read-audio/
    agent-memory/
```

### 8.3 MCP server

適合支援 MCP 的 agent:

```text
agent-plugin/
  mcp/
    server.py
    tools.json
```

## 9. 開發階段

### Phase 1:OpenRouter-first MVP

- 建 7 個 skill 目錄與 `SKILL.md`,觸發與輸出契約直接指向 OpenRouter adapter。
- 實作 shared contracts:
  - `provider-contract.md`
  - `storage-schema.md`
  - `privacy-policy.md`
- 實作 OpenRouter adapter:
  - `/images`
  - `/videos` submit / poll / download
  - `/chat/completions` image and video understanding
  - `/audio/transcriptions`
  - optional `/audio/speech`
- 建立 artifact 與 video job state schema。

### Phase 2:Local fallback runtime

- `ffmpeg` / `ffprobe`
- `sips` / `exiftool`
- `tesseract`
- 基本 metadata 讀取

### Phase 3:Model and cost guardrails

- Model discovery caching
- endpoint capability validation
- cost estimate before request
- upload size gate
- explicit cloud approval record

### Phase 4:Other provider adapters

- local ComfyUI / SD
- local ASR
- OpenAI direct / Sora / Runway / Pika
- provider registry with per-capability routing

### Phase 5:Packaging

- `.codex-plugin`
- marketplace entry
- optional MCP server

### Phase 6:MVP 驗收

1. 在乾淨 project 中安裝 pack,設定 `OPENROUTER_API_KEY`。
2. OpenRouter 端到端完成:
   - `generate-image`:`/images` -> decode -> save -> validate
   - `generate-video`:submit -> poll -> download -> `ffprobe`
   - `read-image`:Chat Completions vision -> structured observation
   - `read-video`:Chat Completions video -> timeline summary
   - `read-audio`:`/audio/transcriptions` -> transcript + segments
3. 關閉 cloud approval 後驗證 local fallback:
   - `read-image`:metadata + OCR
   - `read-video`:metadata + 抽幀
   - `read-audio`:metadata only,明確拒絕編造 transcript
   - `agent-memory`:寫入 / 讀取 / 刪除
4. 每個 skill 輸出一次固定格式 artifact,並記錄 provider / model / 路徑 / cost。

## 10. 待驗證假設

1. 八個 skill 是否足以涵蓋主要 local agent 媒體與記憶需求?
2. Provider adapter 是否夠穩定,能支援不同 runtime?
3. Memory scope 是否需要更細的分層?
   **2026-09-16 結論(80%):** 預設 `./.agents/memory/`。`~/.agents/memory/` 僅在使用者明確要求跨專案時使用,不自動同步。不新增 thread/user/global 三層 API。
4. 是否需要把 `read-video` 拆成 `video-summary` / `video-search`?

## 11. 開放問題

1. Phase 2 之後是否要提供 `ComfyUI` workflow?
2. Memory 是否要引入 decay / conflict resolution?
   **2026-09-16 結論(80%):** semantic 用 `expires` + in-place upsert + `was:`;personas 衝突先解決再寫;INDEX 超過 40 列先 archive。不做 Ebbinghaus 衰減或向量 rerank。
3. Video generation 是否需要 pre-request cost estimate?
4. 是否要為每個 skill 建 `evals.json`?