diff --git a/openspec/changes/archive/2026-05-10-multi-agent-pipeline/.openspec.yaml b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/.openspec.yaml new file mode 100644 index 0000000..ac20efa --- /dev/null +++ b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-10 diff --git a/openspec/changes/archive/2026-05-10-multi-agent-pipeline/design.md b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/design.md new file mode 100644 index 0000000..fccdf32 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/design.md @@ -0,0 +1,116 @@ +## Context + +当前项目已有稳定的 Agent 架构: + +- **Web Worker** (`agent.worker.ts`) — 独立线程跑 LLM 调用,不阻塞 UI +- **ReAct 循环** — 最多 5 步:LLM 决定是否调工具 → 主线程执行 → 结果注入 → 继续 +- **工具 Round-trip** — Worker `postMessage` 请求主线程执行 `readMindmap` / `generateMindmapOps` +- **两种模式** — ENHANCE(对话生成了新内容,自动更新脑图)、MEDIATE(用户对 Agent 说话,更新脑图 + 流式回答) +- **`useMindmapAgent` hook** — 统一入口,外部只调 `initialize / enhanceMessage / mediateMessage` + +以上架构通过 30+ 轮迭代验证,稳定有效。问题出在工程实现层面:335 行单文件、零数据校验、prompt 内联。 + +## Goals / Non-Goals + +**Goals:** +- 提取 `BaseAgent` 抽象类,使 Agent 逻辑可单独测试 +- 从 `agent.worker.ts` 提取 `ReActRunner`,解耦消息路由和推理循环 +- System prompt 从 hook 移到独立文件 +- `applyOperations()` 前加 Zod 校验 +- 保持所有现有接口和行为不变 + +**Non-Goals:** +- 不改 ReAct 循环为多 Agent Pipeline(过度设计) +- 不拆包、不加 monorepo +- 不换 LLM 框架(继续用 Vercel AI SDK) +- 不引入新外部依赖 +- 不改变 postMessage 协议 +- 不改变 `useMindmapAgent` 对外的三个方法签名 + +## Decisions + +### Decision 1: 保持 ReAct 循环,提取到类 + +**选择**:`ReActRunner` 类封装循环逻辑,`agent.worker.ts` 只做消息路由。 + +```typescript +class ReActRunner { + constructor(private agent: BaseAgent, private tools: ToolSet) {} + + async run(userPrompt: string, options?: ReActOptions): Promise { + const messages = [{ role: 'user', content: userPrompt }] + for (let step = 0; step < options?.maxSteps ?? 5; step++) { + const result = await this.agent.callLLM({ messages, tools: this.tools }) + // ... 处理 toolCalls / toolResults + if (result.toolResults.length === 0) return result.text ?? '' + } + } +} +``` + +**不拆分 Planner/Writer/Reflector**:当前的 ReAct 循环只有 2-3 步,拆成 3 个独立 Agent 会增加 2 次额外 LLM 调用,且 Planner 看不到 Writer 的执行结果。一张专门化的 system prompt 在单次上下文里做完"计划→执行→回答"更高效。 + +### Decision 2: Zod 校验放在 applyOperations 前 + +**选择**:在 `agent-tools.ts` 的 `generateMindmapOps` 处理器中,`applyOperations()` 调用前加 `safeParse`。 + +```typescript +const OperationsArraySchema = z.array(z.object({ + type: z.enum(['add_child', 'update', 'delete_leaf', 'add_root']), + parentId: z.string().optional(), + nodeId: z.string().optional(), + label: z.string().optional(), + summary: z.string().optional(), + // ... 等 +})).max(10) + +export async function generateMindmapOps(input: { operations: unknown }) { + const parsed = OperationsArraySchema.safeParse(input.operations) + if (!parsed.success) { + return { error: `操作校验失败: ${parsed.error.message}`, success: false } + } + const newTree = applyOperations(tree, parsed.data) + // ... +} +``` + +LLM 输出不可信,这是数据完整性的最后一道防线。 + +### Decision 3: BaseAgent 是薄封装 + +**选择**:`BaseAgent` 只封装 `callLLM()` 和 `callTool()` 两个方法,不定义 `process()` 抽象方法。 + +当前只有一个 Agent,不需要复杂的多 Agent 继承体系。BaseAgent 提供: +- `this.ctx` — 统一的 AgentContext(model, endpoint, logger) +- `this.callLLM()` — 封装 generateText 调用 +- `this.callTool()` — 封装工具 round-trip + +未来如果需要加第二个 Agent,直接继承即可。 + +## File Structure + +``` +改动前 改动后 + +src/ src/ +├── workers/ ├── workers/ +│ └── agent.worker.ts (335行) │ └── agent.worker.ts (~120行) ← 只做消息路由 +├── hooks/ ├── hooks/ +│ └── useMindmapAgent.ts (324行) │ └── useMindmapAgent.ts (~280行) ← 移除内联 prompt +├── lib/ ├── lib/ +│ ├── agent/ │ ├── agent/ +│ │ ├── types.ts │ │ ├── types.ts +│ │ └── agent-tools.ts │ │ ├── agent-tools.ts (+ Zod) +│ └── llm-client.ts │ │ ├── system-prompt.ts ← 新增 +│ │ │ ├── base-agent.ts ← 新增 +│ │ │ └── schema.ts ← 新增 +│ │ └── llm-client.ts +``` + +## Risks / Trade-offs + +| Risk | Mitigation | +|------|-----------| +| 提取 ReActRunner 引入 bug | 保留原有逻辑 100%,只做提取不改变行为;全部回归测试 | +| Zod 校验误拒绝合法操作 | 用 `safeParse` 不抛异常;校验失败返回 descriptive error,不影响系统 | +| 新增文件增加复杂度 | 3 个新文件,每个 50-150 行,工程化的必要成本 | diff --git a/openspec/changes/archive/2026-05-10-multi-agent-pipeline/proposal.md b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/proposal.md new file mode 100644 index 0000000..2304a01 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/proposal.md @@ -0,0 +1,34 @@ +## Why + +当前 Agent 系统的骨架是对的(Web Worker + ReAct 循环 + 工具系统),但存在三个工程问题: + +1. **代码耦合**:`agent.worker.ts` 335 行,消息路由、ReAct 循环、工具注册、状态报告全部胶合在一个文件中,无法单独测试任何一部分 +2. **缺少数据校验**:LLM 输出的 operations 直接 `applyOperations()`,没有 Zod schema 验证。格式错误(缺字段、类型不对、ID 用 "1")会直接污染脑图数据 +3. **System prompt 硬编码**:176-198 行的 prompt 字符串嵌在 `useMindmapAgent.ts` hook 中,无法测试、无法切换、无法复用 + +不改变架构(不改 ReAct 循环、不改 Worker、不改两种模式),只做工程解耦 + 数据安全。 + +## What Changes + +- 新增 `BaseAgent` 类,封装 LLM 调用和工具交互 +- 从 `agent.worker.ts` 提取 `ReActRunner` 类,解耦消息路由和 ReAct 循环 +- System prompt 从 `useMindmapAgent.ts` 移到独立文件 `src/lib/agent/system-prompt.ts` +- `applyOperations()` 前加 **Zod schema 校验**,拒绝坏数据 +- `AgentStatus` 状态类型不变,ENHANCE_MESSAGE / MEDIATE_MESSAGE 两种模式不变 +- 不改 Worker、不改 useMindmapAgent 接口、不改 agent-tools.ts 工具处理器 + +## Capabilities + +### New Capabilities +- `agent-core`: BaseAgent 基类 + ReActRunner 循环提取,Agent 核心逻辑可测试化 +- `agent-schema`: Zod schema 校验层,在 operations 应用前做结构验证 + +### Modified Capabilities +- 无 + +## Impact + +- **新增文件**:`src/lib/agent/system-prompt.ts`、`src/lib/agent/base-agent.ts`、`src/lib/agent/schema.ts` +- **修改文件**:`src/workers/agent.worker.ts`(提取 ReAct 循环到类)、`src/hooks/useMindmapAgent.ts`(移除内联 prompt)、`src/lib/agent/agent-tools.ts`(加 Zod 校验) +- **无外部依赖变更** +- **删除文件**:无 diff --git a/openspec/changes/archive/2026-05-10-multi-agent-pipeline/specs/agent-core/spec.md b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/specs/agent-core/spec.md new file mode 100644 index 0000000..7c42445 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/specs/agent-core/spec.md @@ -0,0 +1,101 @@ +## ADDED Requirements + +### Requirement: BaseAgent provides unified LLM calling + +The system SHALL provide a `BaseAgent` class that encapsulates LLM invocation and tool interaction for all agent implementations. + +```typescript +abstract class BaseAgent { + protected ctx: AgentContext + abstract get name(): string + protected async callLLM(params: CallLLMParams): Promise + protected async callTool(name: string, args: unknown): Promise +} + +interface AgentContext { + model: string + systemPrompt: string + providerConfig: { apiEndpoint: string; apiKey: string } + onToolCall: (name: string, args: unknown) => Promise + onStatusReport: (status: AgentStatus, message?: string) => void +} +``` + +#### Scenario: Agent calls LLM with system prompt +- **WHEN** an agent invokes `this.callLLM({ messages, tools })` +- **THEN** the system SHALL send the messages + system prompt to the LLM +- **THEN** the response SHALL include text content and any tool calls + +#### Scenario: Agent calls a tool +- **WHEN** an agent invokes `this.callTool('readMindmap', {})` +- **THEN** the system SHALL forward the call to the main thread via `onToolCall` +- **THEN** the result SHALL be returned to the agent + +### Requirement: ReActRunner encapsulates reasoning loop + +The system SHALL provide a `ReActRunner` class that orchestrates the tool-use reasoning loop independently of message routing. + +```typescript +class ReActRunner { + constructor(agent: BaseAgent, tools: ToolDefinition[]) + async run(userPrompt: string, options?: { maxSteps?: number }): Promise +} +``` + +The ReActRunner SHALL: +- Accept a max of 5 steps per run +- Process tool calls and inject tool results back into the conversation +- Return the final text response when no more tool calls are needed +- Report status updates (thinking, reading_mindmap, generating_mindmap) at appropriate steps + +#### Scenario: ReActRunner completes with tool calls +- **WHEN** the LLM calls `readMindmap` then `generateMindmapOps` +- **THEN** the ReActRunner SHALL execute both tools in sequence +- **THEN** the ReActRunner SHALL return the final LLM text response + +#### Scenario: ReActRunner handles invalid tool call +- **WHEN** the LLM produces a tool call with invalid JSON parameters +- **THEN** the ReActRunner SHALL inject an error message into the conversation +- **THEN** the ReActRunner SHALL continue the loop (up to maxSteps) + +### Requirement: System prompt lives in a separate module + +The system prompt for the mindmap agent SHALL be defined in `src/lib/agent/system-prompt.ts` as an exported function. + +```typescript +export function buildMindmapAgentPrompt(): string +``` + +The prompt SHALL include: +- Tool descriptions (readMindmap, generateMindmapOps) +- Workflow guidance (read → think → execute → respond) +- Operational rules (editedByUser protection, ID naming rules, 10 ops max, summary ≤ 50 chars) + +#### Scenario: Prompt is imported by Worker and hook +- **WHEN** `agent.worker.ts` initializes the agent +- **THEN** it SHALL call `buildMindmapAgentPrompt()` to get the system prompt +- **THEN** `useMindmapAgent.ts` SHALL NOT contain inline prompt strings + +### Requirement: Worker file only handles message routing + +The `src/workers/agent.worker.ts` SHALL be reduced to message routing and orchestration only: + +``` +onmessage → switch(msg.type) + INIT → create BaseAgent + ReActRunner + ENHANCE → build user prompt → ReActRunner.run() + MEDIATE → build user prompt → ReActRunner.run() +``` + +The file SHALL NOT contain: +- ReAct loop logic (moved to ReActRunner) +- System prompt text (moved to system-prompt.ts) +- Tool definitions (already in agent.worker.ts as AI SDK tool objects, kept here) + +#### Scenario: Worker routes ENHANCE_MESSAGE +- **WHEN** Worker receives ENHANCE_MESSAGE +- **THEN** it SHALL construct a user prompt from messages → call `runner.run(prompt)` → post AGENT_COMPLETE + +#### Scenario: Worker routes MEDIATE_MESSAGE +- **WHEN** Worker receives MEDIATE_MESSAGE +- **THEN** it SHALL construct a user prompt → call `runner.run(prompt)` → post STREAM_TOKEN / STREAM_DONE diff --git a/openspec/changes/archive/2026-05-10-multi-agent-pipeline/specs/agent-schema/spec.md b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/specs/agent-schema/spec.md new file mode 100644 index 0000000..997e966 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/specs/agent-schema/spec.md @@ -0,0 +1,63 @@ +## ADDED Requirements + +### Requirement: Operations are validated before application + +The system SHALL validate all mindmap operations using Zod schema BEFORE calling `applyOperations()`. + +```typescript +import { z } from 'zod' + +export const MindmapOperationSchema = z.object({ + type: z.enum(['add_child', 'update', 'delete_leaf', 'add_root']), + parentId: z.string().optional(), + nodeId: z.string().optional(), + id: z.string().optional(), + label: z.string().optional(), + summary: z.string().optional(), + patch: z.object({ + label: z.string().optional(), + summary: z.string().optional(), + }).optional(), +}) + +export const OperationsArraySchema = z + .array(MindmapOperationSchema) + .max(10, '每次最多 10 个操作') +``` + +The validation SHALL be performed in the `generateMindmapOps` tool handler in `agent-tools.ts`. + +#### Scenario: Valid operations pass validation +- **WHEN** `generateMindmapOps` receives `{ operations: [{ type: 'add_child', parentId: 'n1a2b3c', label: 'TypeScript' }] }` +- **THEN** `OperationsArraySchema.safeParse()` SHALL return `success: true` +- **THEN** the operations SHALL be applied to the mindmap + +#### Scenario: Invalid type is rejected +- **WHEN** `generateMindmapOps` receives `{ operations: [{ type: 'invalid_type' }] }` +- **THEN** `safeParse()` SHALL return `success: false` +- **THEN** the tool SHALL return `{ error: '操作校验失败: ...', success: false }` +- **THEN** the mindmap SHALL NOT be modified + +#### Scenario: Missing required label on add_child is rejected +- **WHEN** `generateMindmapOps` receives `{ operations: [{ type: 'add_child', parentId: 'n1a2b3c' }] }` (no label) +- **THEN** validation SHALL succeed (label is optional in schema, the apply engine handles default) +- **THEN** the node SHALL be created with default label + +#### Scenario: Numeric IDs are rejected +- **WHEN** `generateMindmapOps` receives `{ operations: [{ type: 'add_child', id: '1', label: 'X' }] }` +- **THEN** validation SHALL succeed (Zod doesn't check semantics — the apply engine rejects id='1') +- **THEN** the operation SHALL be applied with the ID as-is (id validation is a future enhancement) + +#### Scenario: Operations exceed maximum +- **WHEN** `generateMindmapOps` receives 15 operations in one call +- **THEN** `OperationsArraySchema.safeParse()` SHALL return `success: false` +- **THEN** the error message SHALL indicate the operations count exceeded 10 + +### Requirement: Validation errors are surfaced to the Agent + +When Zod validation fails, the error SHALL be returned to the Worker so the ReAct loop can retry with corrected input. + +#### Scenario: Agent retries after validation error +- **WHEN** the ReActRunner receives a validation error from `generateMindmapOps` +- **THEN** the error SHALL be injected into the LLM conversation as a tool result +- **THEN** the LLM SHALL have the opportunity to correct its output in the next ReAct step diff --git a/openspec/changes/archive/2026-05-10-multi-agent-pipeline/tasks.md b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/tasks.md new file mode 100644 index 0000000..d120a4d --- /dev/null +++ b/openspec/changes/archive/2026-05-10-multi-agent-pipeline/tasks.md @@ -0,0 +1,43 @@ +## 1. System Prompt Extraction + +- [x] 1.1 Create `src/lib/agent/system-prompt.ts` — export `buildMindmapAgentPrompt()` function +- [x] 1.2 Copy the inline prompt from `useMindmapAgent.ts` (lines 176-198) into the new function +- [x] 1.3 Update `useMindmapAgent.ts` to import from system-prompt.ts instead of inline string +- [x] 1.4 Verify agent behavior is identical after extraction + +## 2. Zod Schema Validation + +- [x] 2.1 Create `src/lib/agent/schema.ts` — define `MindmapOperationSchema` and `OperationsArraySchema` +- [x] 2.2 Add `safeParse` call in `agent-tools.ts` `generateMindmapOps` handler before `applyOperations()` +- [x] 2.3 Return descriptive error on validation failure (with field path and message) +- [x] 2.4 Write unit tests for valid operations passing and invalid operations being rejected +- [x] 2.5 Write unit test for operations exceeding max count (10) + +## 3. BaseAgent Class + +- [x] 3.1 Create `src/lib/agent/base-agent.ts` — abstract BaseAgent with `callLLM()` and `callTool()` +- [x] 3.2 Define `AgentContext` interface (model, providerConfig, systemPrompt, onToolCall, onStatusReport) +- [x] 3.3 Write unit tests for BaseAgent (callLLM mock, callTool dispatch) + +## 4. ReActRunner Extraction + +- [x] 4.1 Create `src/lib/agent/ReActRunner.ts` — class that encapsulates the ReAct loop +- [x] 4.2 Extract the loop logic from `agent.worker.ts` lines 92-175 into ReActRunner +- [x] 4.3 Keep the same behavior: 5 max steps, tool result injection, invalid call retry +- [x] 4.4 Update `agent.worker.ts` to instantiate ReActRunner and call `.run()` from message handlers +- [x] 4.5 Verify ENHANCE_MESSAGE mode works identically +- [x] 4.6 Verify MEDIATE_MESSAGE mode works identically (streaming still functional) + +## 5. Worker File Cleanup + +- [x] 5.1 Verify `agent.worker.ts` is reduced to message routing only (< ~150 lines) +- [x] 5.2 Confirm all postMessage protocols (AGENT_STATUS, AGENT_COMPLETE, STREAM_TOKEN, etc.) unchanged +- [x] 5.3 Confirm tool definitions (AI SDK `tool()` objects) remain in worker (they are worker-specific) + +## 6. Regression Testing + +- [x] 6.1 Run existing test suite — verify no regressions +- [x] 6.2 Test ENHANCE_MESSAGE: AI responds → agent auto-updates mindmap +- [x] 6.3 Test MEDIATE_MESSAGE: user sends agent message → mindmap updates + answer streams +- [x] 6.4 Test Zod validation path: inject bad operations → verify rejection +- [x] 6.5 Test editedByUser protection: manually edit node → verify AI respects it diff --git a/openspec/specs/agent-core/spec.md b/openspec/specs/agent-core/spec.md new file mode 100644 index 0000000..7c42445 --- /dev/null +++ b/openspec/specs/agent-core/spec.md @@ -0,0 +1,101 @@ +## ADDED Requirements + +### Requirement: BaseAgent provides unified LLM calling + +The system SHALL provide a `BaseAgent` class that encapsulates LLM invocation and tool interaction for all agent implementations. + +```typescript +abstract class BaseAgent { + protected ctx: AgentContext + abstract get name(): string + protected async callLLM(params: CallLLMParams): Promise + protected async callTool(name: string, args: unknown): Promise +} + +interface AgentContext { + model: string + systemPrompt: string + providerConfig: { apiEndpoint: string; apiKey: string } + onToolCall: (name: string, args: unknown) => Promise + onStatusReport: (status: AgentStatus, message?: string) => void +} +``` + +#### Scenario: Agent calls LLM with system prompt +- **WHEN** an agent invokes `this.callLLM({ messages, tools })` +- **THEN** the system SHALL send the messages + system prompt to the LLM +- **THEN** the response SHALL include text content and any tool calls + +#### Scenario: Agent calls a tool +- **WHEN** an agent invokes `this.callTool('readMindmap', {})` +- **THEN** the system SHALL forward the call to the main thread via `onToolCall` +- **THEN** the result SHALL be returned to the agent + +### Requirement: ReActRunner encapsulates reasoning loop + +The system SHALL provide a `ReActRunner` class that orchestrates the tool-use reasoning loop independently of message routing. + +```typescript +class ReActRunner { + constructor(agent: BaseAgent, tools: ToolDefinition[]) + async run(userPrompt: string, options?: { maxSteps?: number }): Promise +} +``` + +The ReActRunner SHALL: +- Accept a max of 5 steps per run +- Process tool calls and inject tool results back into the conversation +- Return the final text response when no more tool calls are needed +- Report status updates (thinking, reading_mindmap, generating_mindmap) at appropriate steps + +#### Scenario: ReActRunner completes with tool calls +- **WHEN** the LLM calls `readMindmap` then `generateMindmapOps` +- **THEN** the ReActRunner SHALL execute both tools in sequence +- **THEN** the ReActRunner SHALL return the final LLM text response + +#### Scenario: ReActRunner handles invalid tool call +- **WHEN** the LLM produces a tool call with invalid JSON parameters +- **THEN** the ReActRunner SHALL inject an error message into the conversation +- **THEN** the ReActRunner SHALL continue the loop (up to maxSteps) + +### Requirement: System prompt lives in a separate module + +The system prompt for the mindmap agent SHALL be defined in `src/lib/agent/system-prompt.ts` as an exported function. + +```typescript +export function buildMindmapAgentPrompt(): string +``` + +The prompt SHALL include: +- Tool descriptions (readMindmap, generateMindmapOps) +- Workflow guidance (read → think → execute → respond) +- Operational rules (editedByUser protection, ID naming rules, 10 ops max, summary ≤ 50 chars) + +#### Scenario: Prompt is imported by Worker and hook +- **WHEN** `agent.worker.ts` initializes the agent +- **THEN** it SHALL call `buildMindmapAgentPrompt()` to get the system prompt +- **THEN** `useMindmapAgent.ts` SHALL NOT contain inline prompt strings + +### Requirement: Worker file only handles message routing + +The `src/workers/agent.worker.ts` SHALL be reduced to message routing and orchestration only: + +``` +onmessage → switch(msg.type) + INIT → create BaseAgent + ReActRunner + ENHANCE → build user prompt → ReActRunner.run() + MEDIATE → build user prompt → ReActRunner.run() +``` + +The file SHALL NOT contain: +- ReAct loop logic (moved to ReActRunner) +- System prompt text (moved to system-prompt.ts) +- Tool definitions (already in agent.worker.ts as AI SDK tool objects, kept here) + +#### Scenario: Worker routes ENHANCE_MESSAGE +- **WHEN** Worker receives ENHANCE_MESSAGE +- **THEN** it SHALL construct a user prompt from messages → call `runner.run(prompt)` → post AGENT_COMPLETE + +#### Scenario: Worker routes MEDIATE_MESSAGE +- **WHEN** Worker receives MEDIATE_MESSAGE +- **THEN** it SHALL construct a user prompt → call `runner.run(prompt)` → post STREAM_TOKEN / STREAM_DONE diff --git a/openspec/specs/agent-schema/spec.md b/openspec/specs/agent-schema/spec.md new file mode 100644 index 0000000..997e966 --- /dev/null +++ b/openspec/specs/agent-schema/spec.md @@ -0,0 +1,63 @@ +## ADDED Requirements + +### Requirement: Operations are validated before application + +The system SHALL validate all mindmap operations using Zod schema BEFORE calling `applyOperations()`. + +```typescript +import { z } from 'zod' + +export const MindmapOperationSchema = z.object({ + type: z.enum(['add_child', 'update', 'delete_leaf', 'add_root']), + parentId: z.string().optional(), + nodeId: z.string().optional(), + id: z.string().optional(), + label: z.string().optional(), + summary: z.string().optional(), + patch: z.object({ + label: z.string().optional(), + summary: z.string().optional(), + }).optional(), +}) + +export const OperationsArraySchema = z + .array(MindmapOperationSchema) + .max(10, '每次最多 10 个操作') +``` + +The validation SHALL be performed in the `generateMindmapOps` tool handler in `agent-tools.ts`. + +#### Scenario: Valid operations pass validation +- **WHEN** `generateMindmapOps` receives `{ operations: [{ type: 'add_child', parentId: 'n1a2b3c', label: 'TypeScript' }] }` +- **THEN** `OperationsArraySchema.safeParse()` SHALL return `success: true` +- **THEN** the operations SHALL be applied to the mindmap + +#### Scenario: Invalid type is rejected +- **WHEN** `generateMindmapOps` receives `{ operations: [{ type: 'invalid_type' }] }` +- **THEN** `safeParse()` SHALL return `success: false` +- **THEN** the tool SHALL return `{ error: '操作校验失败: ...', success: false }` +- **THEN** the mindmap SHALL NOT be modified + +#### Scenario: Missing required label on add_child is rejected +- **WHEN** `generateMindmapOps` receives `{ operations: [{ type: 'add_child', parentId: 'n1a2b3c' }] }` (no label) +- **THEN** validation SHALL succeed (label is optional in schema, the apply engine handles default) +- **THEN** the node SHALL be created with default label + +#### Scenario: Numeric IDs are rejected +- **WHEN** `generateMindmapOps` receives `{ operations: [{ type: 'add_child', id: '1', label: 'X' }] }` +- **THEN** validation SHALL succeed (Zod doesn't check semantics — the apply engine rejects id='1') +- **THEN** the operation SHALL be applied with the ID as-is (id validation is a future enhancement) + +#### Scenario: Operations exceed maximum +- **WHEN** `generateMindmapOps` receives 15 operations in one call +- **THEN** `OperationsArraySchema.safeParse()` SHALL return `success: false` +- **THEN** the error message SHALL indicate the operations count exceeded 10 + +### Requirement: Validation errors are surfaced to the Agent + +When Zod validation fails, the error SHALL be returned to the Worker so the ReAct loop can retry with corrected input. + +#### Scenario: Agent retries after validation error +- **WHEN** the ReActRunner receives a validation error from `generateMindmapOps` +- **THEN** the error SHALL be injected into the LLM conversation as a tool result +- **THEN** the LLM SHALL have the opportunity to correct its output in the next ReAct step diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index b520e87..11f0cda 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,3 +1,4 @@ +import * as React from 'react' import { Button as ButtonPrimitive } from '@base-ui/react/button' import { cva, type VariantProps } from 'class-variance-authority' @@ -40,19 +41,18 @@ const buttonVariants = cva( }, ) -function Button({ - className, - variant = 'default', - size = 'default', - ...props -}: ButtonPrimitive.Props & VariantProps) { - return ( - - ) -} +const Button = React.forwardRef>( + ({ className, variant = 'default', size = 'default', ...props }, ref) => { + return ( + + ) + }, +) +Button.displayName = 'Button' export { Button, buttonVariants } diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 69d4a0c..1814caa 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -2,17 +2,21 @@ import * as React from 'react' import { cn } from '@/lib/utils' -function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { - return ( -