diff --git a/.agents/skills/ai-context-generator/README.md b/.agents/skills/ai-context-generator/README.md deleted file mode 100644 index 7246ae6..0000000 --- a/.agents/skills/ai-context-generator/README.md +++ /dev/null @@ -1,252 +0,0 @@ -# AI Context Generator - -> A reusable Agent Skill for creating project knowledge bases optimized for coding agents. - ---- - -## What Is This? - -This skill helps you generate a `.ai-context/` directory structure that provides coding agents (like Claude Code, Cursor, etc.) with pre-generated project knowledge. The knowledge is organized by stability tiers, making it easy for agents to understand: - -- **What** the project is (PROJECT-ESSENCE.md) -- **How** components fit together (ARCHITECTURE.md) -- **Why** design decisions were made (DECISIONS.md) -- **What issues** are currently active (DYNAMICS.md) - ---- - -## Why Use This? - -**Problem:** Coding agents spend valuable tokens and time exploring code to understand project context. This exploration is repeated in every session. - -**Solution:** Pre-generated knowledge that: -- Reduces onboarding time from ~10 minutes to ~30 seconds -- Preserves institutional knowledge (decisions, constraints) -- Uses ~4000 tokens vs ~50,000+ for full code exploration -- Guides agents to relevant code locations faster - ---- - -## Quick Start - -### Option 1: Use with an AI Agent - -Simply tell your coding agent: - -``` -Use the ai-context-generator skill to setup .ai-context for this project -``` - -The agent will: -1. Read project files (AGENTS.md, README.md, package.json, etc.) -2. Generate the `.ai-context/` structure -3. Ask clarifying questions if needed -4. Create all necessary files - -### Option 2: Manual Generation - -1. Copy the `templates/` directory contents -2. Replace `{{PLACEHOLDERS}}` with your project details -3. Create the `.ai-context/` directory structure - ---- - -## Generated Structure - -``` -.ai-context/ -├── SKILL.md # Entry point with activation rules -├── DYNAMICS.md # Active issues & constraints -├── references/ -│ ├── PROJECT-ESSENCE.md # What & why (stable) -│ ├── ARCHITECTURE.md # Component relationships -│ └── DECISIONS.md # Design decisions & rationale -└── meta/ - ├── MAINTENANCE.md # How to maintain this knowledge - ├── templates/ # (Optional) Custom templates - └── scripts/ # (Optional) Maintenance scripts -``` - ---- - -## Stability Tiers - -| Tier | File | Stability | Tokens | Update Frequency | -|------|------|-----------|--------|------------------| -| 0 | PROJECT-ESSENCE.md | High | ~500 | Quarterly | -| 1 | ARCHITECTURE.md | Medium | ~1000 | Monthly | -| 2 | DECISIONS.md | Low | ~800 | Per decision | -| 3 | DYNAMICS.md | Dynamic | ~600 | As needed | - -Total budget: ~4000 tokens (within typical context limits) - ---- - -## Files Overview - -### SKILL.md -The entry point that tells agents: -- When to activate this knowledge -- Which file to read for specific needs -- How to keep knowledge updated - -### references/PROJECT-ESSENCE.md -One-page summary answering: -- What is this project? -- Why does it exist? -- Who is it for? -- What does it provide? - -### references/ARCHITECTURE.md -Component-level overview with: -- System diagram (ASCII or Mermaid) -- Component responsibilities -- Data flow descriptions -- Key dependencies - -### references/DECISIONS.md -Architecture Decision Records (ADRs) format: -- Context (the problem) -- Decision (the choice) -- Rationale (why) -- Trade-offs -- Implications - -### DYNAMICS.md -Living document tracking: -- Active issues (blockers) -- Known constraints -- Workarounds -- Recently resolved issues - -### meta/MAINTENANCE.md -Guide for keeping knowledge accurate: -- What triggers updates -- How to make updates -- Quality standards -- Anti-patterns to avoid - ---- - -## Integration with AGENTS.md - -This skill complements (not replaces) `AGENTS.md`: - -| File | Purpose | Author | -|------|---------|--------| -| `AGENTS.md` | How to work (commands, style, rules) | Project maintainers | -| `.ai-context/` | What the project is (architecture, decisions) | Generated | - -**Both should be read at session start for optimal agent performance.** - ---- - -## Customization - -### Custom Templates - -Copy templates to your project and modify: - -``` -your-project/ -├── .ai-context/ -│ └── meta/ -│ └── templates/ # Override defaults here -``` - -The skill will use local templates if they exist. - -### Custom Scripts - -Add automation scripts to `meta/scripts/`: - -| Script | Purpose | -|--------|---------| -| `check-drift.ts` | Detect documentation drift from code | -| `audit-dynamics.ts` | Flag stale issues | -| `generate-from-code.ts` | Auto-generate from code analysis | - ---- - -## Best Practices - -### Do: -- ✅ Generate once, maintain regularly -- ✅ Keep each file under 150 lines -- ✅ Use diagrams over paragraphs -- ✅ Update "Last updated" dates -- ✅ Remove resolved issues from DYNAMICS.md - -### Don't: -- ❌ Copy-paste code snippets (link to files instead) -- ❌ Document every file/function -- ❌ Include details that change frequently -- ❌ Let knowledge go stale - ---- - -## Example Usage - -### Scenario 1: New Project Setup - -``` -User: Setup ai-context for my new Express.js API project - -Agent: -1. Reads package.json, identifies Express.js + TypeScript -2. Scans src/ directory structure -3. Generates PROJECT-ESSENCE.md describing the API -4. Creates ARCHITECTURE.md with component diagram -5. Initializes empty DECISIONS.md and DYNAMICS.md -6. Asks about any non-obvious design choices -``` - -### Scenario 2: Existing Project - -``` -User: My project has 50k lines of code, help agents understand it faster - -Agent: -1. Reads AGENTS.md, README.md, existing docs/ -2. Analyzes directory structure for components -3. Extracts key architecture patterns -4. Generates concise knowledge base -5. Highlights areas needing clarification -``` - ---- - -## Compatibility - -- **Agent Skills Spec**: Fully compliant with [agentskills.io](https://agentskills.io/specification) -- **Claude Code**: Works with Claude Code's skill system -- **Cursor**: Compatible with Cursor's context system -- **Other Agents**: Portable to any agent supporting Agent Skills - ---- - -## Contributing - -To improve this skill: - -1. Fork and modify templates -2. Test with your projects -3. Submit improvements via PR - ---- - -## License - -MIT License — Use freely in any project. - ---- - -## References - -- [Agent Skills Specification](https://agentskills.io/specification) -- [Architecture Decision Records](https://adr.github.io/) -- [Project README Template](https://github.com/LinusBorg/project-readme-template) - ---- - -*Generate better project knowledge. Help agents work smarter.* \ No newline at end of file diff --git a/.agents/skills/ai-context-generator/SKILL.md b/.agents/skills/ai-context-generator/SKILL.md deleted file mode 100644 index 32f2f70..0000000 --- a/.agents/skills/ai-context-generator/SKILL.md +++ /dev/null @@ -1,216 +0,0 @@ ---- -name: ai-context-generator -description: | - Generates .ai-context knowledge base for coding agents. Activate when: (1) setting up a new project for AI-assisted development, (2) user asks to "create project knowledge" or "setup ai-context", (3) existing .ai-context needs regeneration. Creates tiered documentation structure optimized for agent comprehension and token efficiency. ---- - -# AI Context Generator - -> A reusable skill for creating project knowledge bases that help coding agents work faster and smarter. - ---- - -## 🎯 When to Use This Skill - -**Activate when:** -- Setting up a new project for AI-assisted development -- User requests: "create ai-context", "setup project knowledge", "generate .ai-context" -- Existing `.ai-context` is outdated and needs regeneration -- After major project restructuring - -**Do NOT activate when:** -- Project already has fresh `.ai-context` (check `SKILL.md` date) -- User asks for unrelated documentation -- Simple code tasks with clear existing context - ---- - -## 📋 What This Skill Generates - -Creates a `.ai-context/` directory with: - -``` -.ai-context/ -├── SKILL.md # Entry point with activation rules -├── DYNAMICS.md # Active issues & constraints (Dynamic) -├── references/ -│ ├── PROJECT-ESSENCE.md # What & why (High stability) -│ ├── ARCHITECTURE.md # Component relationships (Medium stability) -│ └── DECISIONS.md # Design decisions (Update on change) -└── meta/ - ├── MAINTENANCE.md # How to maintain this knowledge - ├── templates/ # (Optional) Custom templates - └── scripts/ # (Optional) Maintenance scripts -``` - -### Stability Tiers - -| Tier | File | Update Frequency | Token Budget | -|------|------|-------------------|--------------| -| 0 | PROJECT-ESSENCE.md | Quarterly / Major version | ~500 tokens | -| 1 | ARCHITECTURE.md | Monthly / Sprint | ~1000 tokens | -| 2 | DECISIONS.md | Per decision change | ~800 tokens | -| 3 | DYNAMICS.md | As needed (issues) | ~600 tokens | - ---- - -## 🔧 Generation Process - -### Step 1: Gather Project Intelligence - -Before generating, collect: - -``` -□ Read AGENTS.md (if exists) — operational rules -□ Read README.md — user-facing description -□ Read package.json — dependencies, scripts, entry points -□ Scan directory structure — identify components -□ Read docs/ or litho.docs/ — existing documentation -□ Identify key source files — main entry points -□ Note technology stack — frameworks, languages, platforms -``` - -### Step 2: Extract Knowledge - -**For PROJECT-ESSENCE.md:** -- What is this project? (one sentence) -- Why does it exist? (problem/solution) -- Who is it for? (target users) -- What does it provide? (key features) -- Core constraints? (security, compatibility) - -**For ARCHITECTURE.md:** -- System diagram (ASCII or Mermaid) -- Component responsibilities -- Data flow between components -- Key dependencies -- Important patterns - -**For DECISIONS.md:** -- Non-obvious design choices -- Trade-offs made -- Constraints accepted -- Decisions that might be revisited - -**For DYNAMICS.md:** -- Current blockers -- Known workarounds -- Temporary constraints -- Recently resolved issues (brief) - -### Step 3: Generate Files - -Use templates from `templates/` directory: - -1. Start with `SKILL.md` — entry point with activation rules -2. Generate `references/PROJECT-ESSENCE.md` — core identity -3. Generate `references/ARCHITECTURE.md` — component map -4. Generate `references/DECISIONS.md` — design rationale -5. Generate `DYNAMICS.md` — active issues -6. Generate `meta/MAINTENANCE.md` — upkeep guide - -### Step 4: Validate Quality - -``` -□ SKILL.md has clear activation triggers -□ PROJECT-ESSENCE.md readable in 2 minutes -□ ARCHITECTURE.md shows big picture (no code) -□ DECISIONS.md justified with rationale -□ DYNAMICS.md only contains current issues -□ All files dated at top -□ Total token budget < 4000 tokens -``` - ---- - -## 📝 Writing Principles - -### Do: -- ✅ Write for someone who knows nothing about the project -- ✅ Use diagrams over paragraphs -- ✅ Focus on "why" not "how" -- ✅ Keep files under 150 lines each -- ✅ Link between related sections -- ✅ Include "Last updated" dates - -### Don't: -- ❌ Copy-paste code snippets (link to files instead) -- ❌ Document every file/function -- ❌ Include details that change frequently -- ❌ Duplicate content across files -- ❌ Use jargon without context - ---- - -## 🔄 Integration with AGENTS.md - -``` -AGENTS.md = "How to work" (commands, style, rules) -.ai-context = "What the project is" (architecture, decisions, issues) -``` - -Both should be read at session start. They serve different purposes and should not overlap. - ---- - -## 📚 Template Reference - -Templates are provided in `templates/`: - -| Template | Purpose | -|----------|---------| -| `skill.md.tmpl` | SKILL.md with placeholder prompts | -| `essence.md.tmpl` | PROJECT-ESSENCE.md structure | -| `architecture.md.tmpl` | ARCHITECTURE.md with diagram prompts | -| `decisions.md.tmpl` | DECISIONS.md with ADR format | -| `dynamics.md.tmpl` | DYNAMICS.md with status tracking | -| `maintenance.md.tmpl` | MAINTENANCE.md guide | - ---- - -## 🛠️ Automation Scripts - -Scripts in `scripts/` can help with: - -| Script | Purpose | -|--------|---------| -| `generate.ts` | Interactive generation from templates | -| `check-drift.ts` | Compare documented vs actual structure | -| `audit-dynamics.ts` | Flag stale issues (>30 days) | - ---- - -## 💡 Example Usage - -**User:** "Setup ai-context for my project" - -**Agent:** -1. Activate this skill -2. Read AGENTS.md, README.md, package.json -3. Scan directory structure -4. Generate each file using templates -5. Ask clarifying questions if needed: - - "What's the main problem this project solves?" - - "Any non-obvious design decisions I should know about?" - - "Current blockers or workarounds?" - ---- - -## ⚠️ Important Notes - -- Generated knowledge is a **starting point**, not final truth -- Agent should verify against actual code during first session -- User should review generated content for accuracy -- Schedule regular audits (monthly recommended) - ---- - -## 📖 References - -- [Agent Skills Specification](https://agentskills.io/specification) -- [Architecture Decision Records](https://adr.github.io/) -- [Writing Readable Docs](references/WRITING-GUIDE.md) - ---- - -*This skill creates knowledge bases optimized for AI agents. For questions or improvements, see MAINTENANCE.md.* \ No newline at end of file diff --git a/.agents/skills/ai-context-generator/_meta.json b/.agents/skills/ai-context-generator/_meta.json deleted file mode 100644 index 78d056b..0000000 --- a/.agents/skills/ai-context-generator/_meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ownerId": "kn7a2vy7ys7aj3gzm2vvw7zc158311b2", - "slug": "ai-context-generator", - "version": "1.0.0", - "publishedAt": 1776327011803 -} \ No newline at end of file diff --git a/.agents/skills/ai-context-generator/references/WRITING-GUIDE.md b/.agents/skills/ai-context-generator/references/WRITING-GUIDE.md deleted file mode 100644 index 5dc6718..0000000 --- a/.agents/skills/ai-context-generator/references/WRITING-GUIDE.md +++ /dev/null @@ -1,357 +0,0 @@ -# Writing Guide for AI Context Knowledge Base - -> Practical guidance for creating documentation that coding agents can effectively use. Last updated: 2025-01-11. - ---- - -## Core Philosophy - -**Goal:** Create knowledge that helps agents work faster, not knowledge that documents everything. - -**Principle:** Agents have two sources of truth: -1. **Static Knowledge** (`.ai-context/`) — Mental model, context, rationale -2. **Dynamic Exploration** (code reading) — Current state, implementation details - -Write static knowledge to complement dynamic exploration, not replace it. - ---- - -## Writing for Agents vs Humans - -| Aspect | Human Documentation | Agent Knowledge | -|--------|---------------------|-----------------| -| Detail level | Comprehensive | Minimal, strategic | -| Code examples | Plentiful | Link to files instead | -| Update frequency | Per feature | Per structural change | -| Audience | Varies | Technical, literal | -| Format flexibility | Prose-heavy | Structured, scannable | - ---- - -## The Token Budget - -Each file has an implicit token budget. Respect it. - -| File | Budget | Why | -|------|--------|-----| -| PROJECT-ESSENCE.md | ~500 tokens | Must read every session | -| ARCHITECTURE.md | ~1000 tokens | Read when working across components | -| DECISIONS.md | ~800 tokens | Read when changing patterns | -| DYNAMICS.md | ~600 tokens | Read when debugging | -| SKILL.md | ~400 tokens | Loaded for discovery | - -**Total: ~3300 tokens** — Less than one typical function's worth of context. - ---- - -## Writing Techniques - -### 1. Prefer Diagrams Over Paragraphs - -❌ **Bad:** -``` -The system has a gateway that loads plugins. The gateway talks to cortex-mem-service -via HTTP. The service then connects to Qdrant for vector storage and the filesystem -for markdown storage. -``` - -✅ **Good:** -``` -┌─────────────┐ -│ Gateway │ -└──────┬──────┘ - │ HTTP - ▼ -┌─────────────────┐ -│ cortex-mem-svc │ -└────┬───────┬────┘ - │ │ - ▼ ▼ - Qdrant Files -``` - -### 2. Link, Don't Copy - -❌ **Bad:** -```markdown -The config format is: -```toml -[server] -port = 8085 -host = "localhost" -``` - -✅ **Good:** -```markdown -Configuration format: see `config.example.toml` or `src/config.ts` for schema. -``` - -**Why:** Code copies become stale. Links stay valid. - -### 3. State the Non-Obvious - -❌ **Bad:** -```markdown -We use TypeScript for type safety. -``` - -✅ **Good:** -```markdown -We use TypeScript strict mode. Avoid `any` — we prefer runtime validation via Zod -instead of type assertions. -``` - -**Why:** The first is obvious to any TypeScript user. The second captures project-specific practices. - -### 4. Use Tables for Comparisons - -❌ **Bad:** -```markdown -The memory plugin is for explicit calls while the context engine handles automatic -lifecycle hooks. They can both be installed together. -``` - -✅ **Good:** -```markdown -| Aspect | Memory Plugin | Context Engine | -|--------|---------------|----------------| -| Trigger | Explicit tool call | Automatic lifecycle | -| Control | Full | None | -| Co-install | Yes | Yes | -``` - -### 5. Date Everything - -Every file should have a "Last updated" or "Last reviewed" date at the top. - -**Why:** Agents need to know if knowledge might be stale. - ---- - -## Anti-Patterns - -### Anti-Pattern 1: The Encyclopedia - -```markdown -## File Structure - -/src/ - /components/ - /Button/ - Button.tsx # Button component - Button.test.tsx # Tests - styles.css # Styles - /Input/ - Input.tsx - ... -``` - -**Problem:** Becomes wrong immediately. Use `find_path` instead. - -**Fix:** -```markdown -Components live in `src/components/`. Each is self-contained with its own directory. -``` - ---- - -### Anti-Pattern 2: The Tutorial - -```markdown -## How to Add a New Tool - -1. Create a new file in `src/tools/` -2. Import the tool interface -3. Implement the execute method -4. Register in tool-registry.ts -5. Add tests -... -``` - -**Problem:** This is a procedure, not knowledge. Procedures belong in AGENTS.md. - -**Fix:** -```markdown -Tools are registered in `tool-registry.ts`. Each tool implements `ToolInterface`. -``` - ---- - -### Anti-Pattern 3: The Decision Dump - -```markdown -## ADR-042: Use tabs for indentation - -We decided to use tabs because... - -## ADR-043: Use semicolons - -We decided to use semicolons because... -``` - -**Problem:** Not all decisions matter equally. - -**Fix:** Only document decisions that: -- Have significant trade-offs -- Affect multiple components -- Might be questioned later -- Have non-obvious rationale - ---- - -### Anti-Pattern 4: The Issue Graveyard - -```markdown -## Fixed in 2024-01 - -The config parser was broken... - -## Fixed in 2024-02 - -The API endpoint was wrong... -``` - -**Problem:** Resolved issues hide active ones. - -**Fix:** DYNAMICS.md should only contain: -- Active blockers -- Known constraints -- Items under consideration -- Recently resolved (last 2 weeks, brief) - ---- - -## What to Put Where - -### PROJECT-ESSENCE.md — "What and Why" - -- What is this? (one sentence) -- Why does it exist? -- Who uses it? -- Key value proposition -- Core constraints -- Success metrics - -**Not:** Technical details, architecture, decisions - ---- - -### ARCHITECTURE.md — "How Things Connect" - -- System diagram -- Component responsibilities -- Data flow -- Key patterns -- Dependencies -- Configuration layers - -**Not:** Implementation details, every file, API specs - ---- - -### DECISIONS.md — "Why We Chose This" - -- Non-obvious choices -- Trade-offs accepted -- Constraints embraced -- Things we might revisit - -**Not:** Naming conventions, style choices, one-off decisions - ---- - -### DYNAMICS.md — "What's Happening Now" - -- Active blockers -- Workarounds in use -- Temporary constraints -- Items under review - -**Not:** History, resolved issues, wishlists - ---- - -## Review Checklist - -Before finalizing any file: - -``` -□ Is this knowledge that code exploration can't easily reveal? -□ Would this still be accurate in 3 months? -□ Is the token budget respected? -□ Is there a "Last updated" date? -□ Did I link instead of copy? -□ Did I state the non-obvious? -□ Is this scannable (tables, lists, diagrams)? -``` - ---- - -## Example Transformations - -### Before (Human-Style): - -```markdown -# Architecture - -This document describes the architecture of our system. The system is built using -TypeScript and runs on Node.js. We use Express for the HTTP server and PostgreSQL -for the database. The frontend is built with React. - -The main components are: -- API Layer: Handles HTTP requests -- Business Logic: Contains the core functionality -- Data Layer: Manages database access - -We chose PostgreSQL because it's reliable and has good JSON support. -``` - -### After (Agent-Optimized): - -```markdown -# Architecture - -> Last updated: 2025-01-11 - -## System Diagram - -``` -┌─────────────┐ ┌─────────────┐ ┌───────────┐ -│ Express │────▶│ Business │────▶│ PostgreSQL│ -│ :3000 │ │ Logic │ │ :5432 │ -└─────────────┘ └─────────────┘ └───────────┘ - │ - ▼ -┌─────────────┐ -│ React │ -│ Client │ -└─────────────┘ -``` - -## Components - -| Component | Entry Point | Responsibility | -|-----------|-------------|----------------| -| API Layer | `src/api/` | HTTP routing, auth | -| Business Logic | `src/core/` | Domain operations | -| Data Layer | `src/db/` | Queries, migrations | - -## Key Decisions - -See [DECISIONS.md](DECISIONS.md) for rationale on PostgreSQL, Express, etc. -``` - ---- - -## Summary - -1. **Minimize tokens** — Every token costs attention -2. **Link, don't copy** — Code changes, links stay valid -3. **State the non-obvious** — Obvious things don't need documentation -4. **Structure for scanning** — Tables, lists, diagrams -5. **Date everything** — Agents need to assess staleness -6. **Separate concerns** — Each file has a purpose - ---- - -_Writing for agents is writing for a literal, token-constrained, but technically competent reader who prefers structure over prose._ \ No newline at end of file diff --git a/.agents/skills/ai-context-generator/scripts/generate.ts b/.agents/skills/ai-context-generator/scripts/generate.ts deleted file mode 100644 index 9d96ab9..0000000 --- a/.agents/skills/ai-context-generator/scripts/generate.ts +++ /dev/null @@ -1,785 +0,0 @@ -#!/usr/bin/env bun -/** - * AI Context Generator - * - * Generates .ai-context knowledge base structure for coding agents. - * Run with: bun run generate.ts [project-path] - */ - -import * as fs from 'fs' -import * as path from 'path' - -// ============================================================================= -// Types & Interfaces -// ============================================================================= - -interface ProjectInfo { - name: string - description: string - problem: string - solution: string - targetUsers: Array<{ user: string; useCase: string }> - components: Array<{ name: string; purpose: string; entry?: string }> - decisions: Array<{ title: string; context: string; decision: string; rationale: string }> - issues: Array<{ title: string; status: 'active' | 'known'; impact: string; workaround: string }> - constraints: string[] -} - -interface TemplateData { - PROJECT_NAME: string - DATE: string - [key: string]: string | string[] | object[] -} - -// ============================================================================= -// Template Engine -// ============================================================================= - -function renderTemplate(template: string, data: TemplateData): string { - let result = template - - // Replace simple {{KEY}} placeholders - for (const [key, value] of Object.entries(data)) { - if (typeof value === 'string') { - result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value) - } - } - - return result -} - -// ============================================================================= -// File Templates -// ============================================================================= - -const SKILL_TEMPLATE = `--- -name: ai-context -description: | - Project knowledge base for coding agents. Activate when: (1) starting a new session in this project, (2) encountering unfamiliar code patterns or architecture decisions, (3) user asks about project design or rationale, (4) before making significant structural changes. Contains tiered knowledge from stable design principles to dynamic issues. ---- - -# AI Context — {{PROJECT_NAME}} - -> This skill provides pre-generated project knowledge to help you understand the project faster and work more effectively. - ---- - -## 🎯 When to Activate This Skill - -**Activate immediately when:** -- Starting a new coding session in this project -- You need to understand "why something is designed this way" -- User asks about project architecture, design decisions, or constraints - -**Refer to specific sections when:** -- Encountering unexpected behavior or errors → \`DYNAMICS.md\` -- Planning structural changes → \`references/DECISIONS.md\` -- Need high-level overview → \`references/PROJECT-ESSENCE.md\` -- Need component relationships → \`references/ARCHITECTURE.md\` - -**Do NOT activate when:** -- Simple code edits with clear context -- User requests are purely mechanical (rename, format, etc.) -- You already have sufficient context from recent conversation - ---- - -## 📁 Knowledge Tiers - -| Tier | File | Stability | Update Frequency | -|------|------|-----------|------------------| -| **Tier 0** | \`PROJECT-ESSENCE.md\` | High | Quarterly / Major version | -| **Tier 1** | \`ARCHITECTURE.md\` | Medium | Monthly / Sprint | -| **Tier 2** | \`DECISIONS.md\` | Low | Per decision change | -| **Tier 3** | \`DYNAMICS.md\` | Dynamic | As needed | - -### Reading Order (Recommended) - -\`\`\` -1. PROJECT-ESSENCE.md ← Start here (1-2 min read) -2. ARCHITECTURE.md ← If working across components -3. DECISIONS.md ← If changing established patterns -4. DYNAMICS.md ← If something feels wrong -\`\`\` - ---- - -## 🔧 How to Use This Knowledge - -### 1. Session Start Protocol -\`\`\` -□ Read PROJECT-ESSENCE.md (always) -□ Scan DYNAMICS.md for active issues -□ Read ARCHITECTURE.md if working across subprojects -□ Proceed with dynamic code exploration -\`\`\` - -### 2. Dynamic Code Exploration -- Use \`grep\` and \`find_path\` to locate actual implementations -- Verify knowledge against current code state -- Update knowledge if you find drift - -### 3. Decision Validation -Before changing established patterns: -\`\`\` -□ Check DECISIONS.md for existing decisions -□ If decision exists: follow it or explicitly propose change -□ If new decision needed: document after implementation -\`\`\` - ---- - -## 🔄 When to Update - -### Update PROJECT-ESSENCE.md when: -- Project purpose or scope fundamentally changes -- New major capability is added - -### Update ARCHITECTURE.md when: -- New component/subproject added -- Component responsibilities shift -- Data flow changes significantly - -### Update DECISIONS.md when: -- A new design decision is made -- An existing decision is revisited/changed - -### Update DYNAMICS.md when: -- New issue discovered -- Issue resolved -- Workaround found - ---- - -## 📚 File Reference - -- [Project Essence](references/PROJECT-ESSENCE.md) -- [Architecture](references/ARCHITECTURE.md) -- [Decisions](references/DECISIONS.md) -- [Dynamics](DYNAMICS.md) -- [Maintenance Guide](meta/MAINTENANCE.md) - ---- - -*Generated by ai-context-generator on {{DATE}}* -` - -const ESSENCE_TEMPLATE = `# Project Essence — {{PROJECT_NAME}} - -> **Stability: HIGH** | Update: Quarterly or major version changes -> -> Last reviewed: {{DATE}} - ---- - -## What Is This Project? - -{{PROJECT_DESCRIPTION}} - ---- - -## Why Does It Exist? - -**Problem:** {{PROJECT_PROBLEM}} - -**Solution:** -{{PROJECT_SOLUTION}} - ---- - -## Who Is This For? - -| User | Use Case | -|------|----------| -{{#TARGET_USERS}} -| {{user}} | {{useCase}} | -{{/TARGET_USERS}} - ---- - -## Key Constraints - -{{PROJECT_CONSTRAINTS}} - ---- - -*This file captures the stable essence of the project. For architecture details, see [ARCHITECTURE.md](ARCHITECTURE.md).* -` - -const ARCHITECTURE_TEMPLATE = `# Architecture — {{PROJECT_NAME}} - -> How components fit together. Last updated: {{DATE}}. -> -> **Update this when:** New component added, responsibilities shift, data flow changes. - ---- - -## System Overview - -\`\`\` -[DIAGRAM_PLACEHOLDER - Replace with your system diagram] - -Example: -┌─────────────────────────────────────────────────────────────────┐ -│ [Main System/Platform] │ -│ │ -│ ┌───────────────────┐ ┌────────────────────────┐ │ -│ │ [Component A] │ │ [Component B] │ │ -│ │ │ │ │ │ -│ │ • [responsibility]│ │ • [responsibility] │ │ -│ └────────┬──────────┘ └───────────┬────────────┘ │ -│ │ │ │ -│ └────────────┬───────────────┘ │ -│ ▼ │ -│ ┌────────────────────────┐ │ -│ │ [Backend Service] │ ← Port XXXX │ -│ └────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -\`\`\` - ---- - -## Component Responsibilities - -{{#COMPONENTS}} -### {{name}} -- **Purpose:** {{purpose}} -{{#entry}} -- **Entry:** \`{{entry}}\` -{{/entry}} - -{{/COMPONENTS}} - ---- - -## Data Flow - -[TBD: Describe key data flows] - ---- - -## Dependencies - -| Package | Purpose | Version | -|---------|---------|---------| -| [package] | [purpose] | [version] | - ---- - -*This file describes component relationships. For implementation details, explore the source code.* -` - -const DECISIONS_TEMPLATE = `# Design Decisions — {{PROJECT_NAME}} - -> Key architectural and design decisions. Update when decisions are made or revisited. -> -> Last reviewed: {{DATE}} - ---- - -## Decision Index - -| ID | Decision | Status | Date | -|----|----------|--------|------| -{{#DECISIONS}} -| ADR-XXX | {{title}} | Active | {{DATE}} | -{{/DECISIONS}} - ---- - -{{#DECISIONS}} -## ADR-XXX: {{title}} - -**Context**: {{context}} - -**Decision**: {{decision}} - -**Rationale**: {{rationale}} - -**Trade-offs**: -- (+) [Benefit] -- (-) [Cost] - ---- - -{{/DECISIONS}} - -## Template for New Decisions - -\`\`\`markdown -## ADR-XXX: [Short Title] - -**Context**: [What is the issue?] - -**Decision**: [What did we decide?] - -**Rationale**: [Why this choice?] - -**Trade-offs**: -- (+) [Benefit] -- (-) [Cost] -\`\`\` - ---- - -*This file captures decisions that aren't obvious from code.* -` - -const DYNAMICS_TEMPLATE = `# Dynamics — Active Issues & Constraints - -> **Last updated:** {{DATE}} -> **Stability:** Dynamic — Update as issues arise/resolve - ---- - -## ⚡ Quick Scan - -| Status | Issue | Impact | Workaround | -|--------|-------|--------|------------| -{{#ISSUES}} -| {{statusIcon}} | {{title}} | {{impact}} | {{workaround}} | -{{/ISSUES}} - ---- - -## 🔴 Active Issues - -{{#ISSUES}} -{{#isActive}} -### {{title}} - -**What:** [Description] - -**Impact:** {{impact}} - -**Workaround:** {{workaround}} - ---- - -{{/isActive}} -{{/ISSUES}} - -## 🟡 Known Constraints - -[TBD: Add known constraints that affect development] - ---- - -## 🟢 Recently Resolved - -| Issue | Resolution | Date | -|-------|------------|------| -| [issue] | [resolution] | [date] | - ---- - -## 📋 Under Consideration - -[TBD: Items being evaluated] - ---- - -*Remember: This file changes frequently. Verify against current code state.* -` - -const MAINTENANCE_TEMPLATE = `# AI Context Maintenance Guide - -> How to keep this knowledge base accurate. Last updated: {{DATE}}. - ---- - -## 🎯 Purpose - -This guide tells you (the coding agent) how to maintain \`.ai-context\`. - ---- - -## 📋 Maintenance Triggers - -| Observation | Action | File | -|-------------|--------|------| -| Code contradicts docs | Fix docs | Relevant file | -| New component | Add entry | ARCHITECTURE.md | -| Major decision | Document | DECISIONS.md | -| Blocking issue | Add entry | DYNAMICS.md | -| Issue resolved | Remove | DYNAMICS.md | - ---- - -## ✍️ Writing Guidelines - -### PROJECT-ESSENCE.md -- Under 100 lines -- "What" and "why", not "how" -- No code snippets -- Update: Quarterly - -### ARCHITECTURE.md -- Diagrams over paragraphs -- Component-level, not file-level -- Show data flow -- Update: Monthly - -### DECISIONS.md -- Non-obvious choices only -- Include rationale -- Update: As decisions made - -### DYNAMICS.md -- Current issues only -- Remove when resolved -- Update: As needed - ---- - -## 🔄 Update Workflow - -1. Identify file needing update -2. Read current content -3. Make minimal changes -4. Update "Last updated" date -5. Continue your task - ---- - -## ❌ Anti-Patterns - -- Don't copy code snippets -- Don't document every file -- Don't keep resolved issues -- Don't duplicate across files - ---- - -## ✅ Quality Checklist - -- [ ] PROJECT-ESSENCE.md readable in 2 min -- [ ] ARCHITECTURE.md shows big picture -- [ ] DECISIONS.md has rationale -- [ ] DYNAMICS.md only current issues -- [ ] All files dated - ---- - -*Update this guide when you discover better maintenance patterns.* -` - -// ============================================================================= -// Project Analysis -// ============================================================================= - -async function analyzeProject(projectPath: string): Promise { - const packageJsonPath = path.join(projectPath, 'package.json') - const readmePath = path.join(projectPath, 'README.md') - const agentsPath = path.join(projectPath, 'AGENTS.md') - - let projectName = path.basename(projectPath) - let description = '' - - // Read package.json if exists - if (fs.existsSync(packageJsonPath)) { - const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) - projectName = pkg.name || projectName - description = pkg.description || '' - } - - // Read README if exists - let readme = '' - if (fs.existsSync(readmePath)) { - readme = fs.readFileSync(readmePath, 'utf-8') - if (!description) { - // Extract first paragraph as description - const match = readme.match(/^#\s.*\n+([^#\n].*?)(?:\n\n|$)/m) - if (match) { - description = match[1].trim() - } - } - } - - // Read AGENTS.md if exists - let agents = '' - if (fs.existsSync(agentsPath)) { - agents = fs.readFileSync(agentsPath, 'utf-8') - } - - // Scan directory structure - const components = scanComponents(projectPath) - - return { - name: projectName, - description: description || 'A software project', - problem: '[TBD: What problem does this project solve?]', - solution: '[TBD: How does this project solve it?]', - targetUsers: [{ user: 'Developer', useCase: 'Building software' }], - components, - decisions: [], - issues: [], - constraints: [] - } -} - -function scanComponents(projectPath: string): ProjectInfo['components'] { - const components: ProjectInfo['components'] = [] - - const entries = fs.readdirSync(projectPath, { withFileTypes: true }) - - for (const entry of entries) { - if (entry.isDirectory() && !entry.name.startsWith('.') && !entry.name.startsWith('node_modules')) { - const subPath = path.join(projectPath, entry.name) - - // Check if it's a subproject (has package.json) - const subPackageJson = path.join(subPath, 'package.json') - if (fs.existsSync(subPackageJson)) { - const pkg = JSON.parse(fs.readFileSync(subPackageJson, 'utf-8')) - components.push({ - name: entry.name, - purpose: pkg.description || 'Subproject', - entry: path.join(entry.name, 'index.ts') - }) - } else { - // Regular directory - check for src - const srcPath = path.join(subPath, 'src') - if (fs.existsSync(srcPath)) { - components.push({ - name: entry.name, - purpose: `${entry.name} module` - }) - } - } - } - } - - return components -} - -// ============================================================================= -// Generator Functions -// ============================================================================= - -function generateSkillMd(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - return renderTemplate(SKILL_TEMPLATE, { - PROJECT_NAME: info.name, - DATE: date - }) -} - -function generateProjectEssence(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - - let targetUsersStr = '' - for (const tu of info.targetUsers) { - targetUsersStr += `| ${tu.user} | ${tu.useCase} |\n` - } - - let constraintsStr = '' - for (let i = 0; i < info.constraints.length; i++) { - constraintsStr += `${i + 1}. **${info.constraints[i]}**\n` - } - if (!constraintsStr) { - constraintsStr = '1. **[TBD: Add key constraints]**\n' - } - - let template = ESSENCE_TEMPLATE - template = template.replace('{{PROJECT_NAME}}', info.name) - template = template.replace('{{DATE}}', date) - template = template.replace('{{PROJECT_DESCRIPTION}}', info.description) - template = template.replace('{{PROJECT_PROBLEM}}', info.problem) - template = template.replace('{{PROJECT_SOLUTION}}', info.solution) - template = template.replace('{{#TARGET_USERS}}\n| {{user}} | {{useCase}} |\n{{/TARGET_USERS}}', targetUsersStr) - template = template.replace('{{PROJECT_CONSTRAINTS}}', constraintsStr) - - return template -} - -function generateArchitecture(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - - let componentsStr = '' - for (const comp of info.components) { - componentsStr += `### ${comp.name}\n` - componentsStr += `- **Purpose:** ${comp.purpose}\n` - if (comp.entry) { - componentsStr += `- **Entry:** \`${comp.entry}\`\n` - } - componentsStr += '\n' - } - - let template = ARCHITECTURE_TEMPLATE - template = template.replace('{{PROJECT_NAME}}', info.name) - template = template.replace('{{DATE}}', date) - - // Handle component block - const compBlockMatch = template.match(/{{#COMPONENTS}}[\s\S]*?{{\/COMPONENTS}}/) - if (compBlockMatch) { - template = template.replace(compBlockMatch[0], componentsStr) - } - - return template -} - -function generateDecisions(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - - let decisionsStr = '' - let decisionBlocks = '' - - for (const dec of info.decisions) { - decisionsStr += `| ADR-XXX | ${dec.title} | Active | ${date} |\n` - decisionBlocks += `## ADR-XXX: ${dec.title}\n\n` - decisionBlocks += `**Context**: ${dec.context}\n\n` - decisionBlocks += `**Decision**: ${dec.decision}\n\n` - decisionBlocks += `**Rationale**: ${dec.rationale}\n\n` - decisionBlocks += '**Trade-offs**:\n- (+) [Benefit]\n- (-) [Cost]\n\n---\n\n' - } - - if (!decisionsStr) { - decisionsStr = '| ADR-001 | [First decision title] | Active | YYYY-MM |\n' - decisionBlocks = `## ADR-001: [Decision Title] - -**Context**: [What is the issue?] - -**Decision**: [What did we decide?] - -**Rationale**: [Why this choice?] - -**Trade-offs**: -- (+) [Benefit] -- (-) [Cost] - ---- - -` - } - - let template = DECISIONS_TEMPLATE - template = template.replace('{{PROJECT_NAME}}', info.name) - template = template.replace('{{DATE}}', date) - - // Handle index block - const indexBlockMatch = template.match(/{{#DECISIONS}}[\s\S]*?{{\/DECISIONS}}/) - if (indexBlockMatch) { - // For index, just use the title - template = template.replace(indexBlockMatch[0], decisionsStr) - } - - // Handle decision blocks - need to re-match after first replacement - const blockMatch = template.match(/{{#DECISIONS}}[\s\S]*?{{\/DECISIONS}}/) - if (blockMatch) { - template = template.replace(blockMatch[0], decisionBlocks) - } - - return template -} - -function generateDynamics(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - - let issuesStr = '' - let activeStr = '' - - for (const issue of info.issues) { - const icon = issue.status === 'active' ? '🔴' : '🟡' - issuesStr += `| ${icon} | ${issue.title} | ${issue.impact} | ${issue.workaround} |\n` - - if (issue.status === 'active') { - activeStr += `### ${issue.title}\n\n` - activeStr += `**What:** [Description]\n\n` - activeStr += `**Impact:** ${issue.impact}\n\n` - activeStr += `**Workaround:** ${issue.workaround}\n\n---\n\n` - } - } - - if (!issuesStr) { - issuesStr = '| 🔴 | [No active issues documented] | — | — |\n' - } - - let template = DYNAMICS_TEMPLATE - template = template.replace('{{DATE}}', date) - - // Handle quick scan block - const scanBlockMatch = template.match(/{{#ISSUES}}[\s\S]*?{{\/ISSUES}}/) - if (scanBlockMatch) { - template = template.replace(scanBlockMatch[0], issuesStr) - } - - return template -} - -function generateMaintenance(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - return renderTemplate(MAINTENANCE_TEMPLATE, { - DATE: date - }) -} - -// ============================================================================= -// Main Generator -// ============================================================================= - -async function generate(projectPath: string): Promise { - console.log(`\n🚀 Generating .ai-context for: ${projectPath}\n`) - - // Analyze project - console.log('📊 Analyzing project...') - const info = await analyzeProject(projectPath) - console.log(` Found: ${info.name}`) - console.log(` Components: ${info.components.length}`) - - // Create directory structure - const aiContextPath = path.join(projectPath, '.ai-context') - const referencesPath = path.join(aiContextPath, 'references') - const metaPath = path.join(aiContextPath, 'meta') - - console.log('\n📁 Creating directory structure...') - fs.mkdirSync(aiContextPath, { recursive: true }) - fs.mkdirSync(referencesPath, { recursive: true }) - fs.mkdirSync(metaPath, { recursive: true }) - - // Generate files - console.log('\n📝 Generating files...') - - // SKILL.md - const skillContent = generateSkillMd(info) - fs.writeFileSync(path.join(aiContextPath, 'SKILL.md'), skillContent) - console.log(' ✓ SKILL.md') - - // PROJECT-ESSENCE.md - const essenceContent = generateProjectEssence(info) - fs.writeFileSync(path.join(referencesPath, 'PROJECT-ESSENCE.md'), essenceContent) - console.log(' ✓ references/PROJECT-ESSENCE.md') - - // ARCHITECTURE.md - const archContent = generateArchitecture(info) - fs.writeFileSync(path.join(referencesPath, 'ARCHITECTURE.md'), archContent) - console.log(' ✓ references/ARCHITECTURE.md') - - // DECISIONS.md - const decisionsContent = generateDecisions(info) - fs.writeFileSync(path.join(referencesPath, 'DECISIONS.md'), decisionsContent) - console.log(' ✓ references/DECISIONS.md') - - // DYNAMICS.md - const dynamicsContent = generateDynamics(info) - fs.writeFileSync(path.join(aiContextPath, 'DYNAMICS.md'), dynamicsContent) - console.log(' ✓ DYNAMICS.md') - - // MAINTENANCE.md - const maintenanceContent = generateMaintenance(info) - fs.writeFileSync(path.join(metaPath, 'MAINTENANCE.md'), maintenanceContent) - console.log(' ✓ meta/MAINTENANCE.md') - - console.log('\n✅ .ai-context generated successfully!') - console.log('\n📌 Next steps:') - console.log(' 1. Review generated files and fill in [TBD] placeholders') - console.log(' 2. Add your project-specific architecture details') - console.log(' 3. Document any non-obvious design decisions') - console.log(' 4. Add current issues to DYNAMICS.md if any\n') -} - -// ============================================================================= -// CLI Entry Point -// ============================================================================= - -const projectPath = process.argv[2] || process.cwd() -generate(projectPath).catch(console.error) diff --git a/.agents/skills/codegraph-skill/SKILL.md b/.agents/skills/codegraph-skill/SKILL.md new file mode 100644 index 0000000..e6cde05 --- /dev/null +++ b/.agents/skills/codegraph-skill/SKILL.md @@ -0,0 +1,113 @@ +--- +name: codegraph-skill +description: Use when a coding agent needs symbol relationships, callers, callees, or change impact. Guides CodeGraph CLI usage (not MCP). +version: 1.3.0 +--- + +# CodeGraph Skill + +[CodeGraph](https://colbymchenry.github.io/codegraph/) provides a pre-indexed **AST code graph** for this project. + +Terrain uses **CLI only** — do not run `codegraph install` (that configures MCP/agent rules separately). + +## Resolve the CodeGraph command (read first) + +Use **conventional paths only** — never machine-specific absolute paths like `/Users/...` or `C:\Users\...`. + +On Windows, the wrapper may be `codegraph.cmd` or `codegraph.exe` under `~/.terrain/bin/`. + +| Priority | Command | When | +|----------|---------|------| +| 1 | `~/.terrain/bin/codegraph` | Terrain env integration (see existence check below) | +| 2 | `bunx codegraph` | No Terrain install; needs network once | +| 3 | `npx codegraph` | Same as bunx if Bun unavailable | + +Optional manifest (local, gitignored): `.terrain/env/agent-tools.json` — same `~/.terrain/bin/…` conventions. + +**Existence check (cross-platform):** + +| Shell | Check | +|-------|-------| +| bash / zsh / Git Bash | `[ -x ~/.terrain/bin/codegraph ] \|\| [ -f ~/.terrain/bin/codegraph.exe ] \|\| [ -f ~/.terrain/bin/codegraph.cmd ]` | +| PowerShell | `Test-Path "$HOME\.terrain\bin\codegraph*"` | + +In examples below, `` means your resolved prefix (`~/.terrain/bin/codegraph` or `bunx codegraph`). + +## Prerequisites + +The **index** is per-repo under `.codegraph/`. If missing, initialize from the repo root: + +```bash +# bash / Git Bash +if [ -x ~/.terrain/bin/codegraph ] || [ -f ~/.terrain/bin/codegraph.exe ] || [ -f ~/.terrain/bin/codegraph.cmd ]; then + ~/.terrain/bin/codegraph init -i +else + bunx codegraph init -i +fi +``` + +Refresh after edits: ` sync` + +Health check: + +```bash + status +``` + +**` status` can lie ("up to date" when it is not).** Observed case: index untouched for 10 days while 24 commits changed source files, `status` still reported fresh, and ` query` on a symbol added days earlier returned nothing. Do not treat `status` as authoritative on its own. + +## CLI commands + +| Intent | Command | +|--------|---------| +| Find symbol by name | ` query ` | +| Who calls X | ` callers ` | +| What X calls | ` callees ` | +| Change blast radius | ` impact ` | +| Tests affected by file changes | ` affected ` | +| Project file tree | ` files` | +| Index health | ` status` | +| Refresh after edits | ` sync` | + +## Recommended workflow + +1. Read `.terrain/agent/context.md` (`terrain-knowledge-skill`) +2. **Always** ` sync` first — cheap, incremental, and removes the need to trust `status`'s own up-to-date claim +3. ` query ` to locate definition +4. `callers` / `callees` / `impact` for relationship questions +5. `repomix-context-skill` for full source text of a specific file +6. Use **`rtk-skill`** for any follow-up shell commands (tests, git) + +## Independent staleness cross-check (mandatory before impact/callers analysis) + +Because ` status` is not reliable, cross-check with Terrain's own git-based drift report before trusting `impact`/`callers`/`callees` results for anything safety-relevant (e.g. "is it safe to remove this function"): + +```bash +~/.terrain/bin/terrain tools codegraph-drift --project +# or: bunx @terrain-ai/cli tools codegraph-drift --project +``` + +This compares `.codegraph/codegraph.db`'s mtime against `git log --since` — independent of CodeGraph's own bookkeeping. If `likely_stale: true`, run ` sync` and re-check before trusting query results. + +## When to use vs other skills + +| Use CodeGraph | Use instead | +|---------------|-------------| +| Symbol lookup, call chains | Architecture → `context.md` | +| Impact before refactor | Business rules → `knowledge/` | +| File/symbol graph | Raw source slice → repomix | +| Verbose test/git output | ` cargo test`, ` git diff` | + +## Do not + +- Run `codegraph install` (Terrain manages AGENTS.md) +- Blind `grep` the whole repo to re-verify CodeGraph AST results +- Chain `query` + manual file reads when `impact` answers the question + +## Staleness + +If ` status` shows pending files after your edits, or `terrain tools codegraph-drift` reports `likely_stale: true`: + +```bash + sync +``` diff --git a/.agents/skills/repomix-context-skill/SKILL.md b/.agents/skills/repomix-context-skill/SKILL.md new file mode 100644 index 0000000..03f4138 --- /dev/null +++ b/.agents/skills/repomix-context-skill/SKILL.md @@ -0,0 +1,84 @@ +--- +name: repomix-context-skill +description: Use when an agent needs source code from the local repomix index under .terrain/agent/repomix.md (not committed; regenerate via Terrain scan). +version: 1.3.0 +--- + +# Repomix Context Skill + +Terrain stores a **local repomix index** at `.terrain/agent/repomix.md` (gitignored, fast to regenerate). + +Read **architecture first** via `terrain-knowledge-skill` → `.terrain/agent/context.md`. + +## When to use + +- Implementation details after reading `context.md` +- Cross-file symbol search within the indexed snapshot +- Locating handlers, types, routes + +## Query strategy (mandatory) + +1. **Read meta** — `.terrain/agent/meta.json` (`total_tokens`, `synced_at`, `top_files_by_tokens`) +2. **Search the pack** — never load the entire file: + ```bash + # = ~/.terrain/bin/rtk or bunx @terrain-ai/rtk (see rtk-skill) + grep "struct ProjectOverview" .terrain/agent/repomix.md + grep "### src/lib/api.ts" .terrain/agent/repomix.md + ``` + Or agent Grep limited to that path with tight patterns. +3. **Read slices** — extract matching `### path/to/file` sections only: + ```bash + read .terrain/agent/repomix.md -l aggressive + ``` + Then read the specific `### file` block (line range), ≤150 lines per read. +4. **Refresh** — if `meta.json.synced_at` is stale, ask user to run Terrain **重建源码索引** / scan + +## Repomix section format + +Sections look like: + +```markdown +### src/lib/foo.ts + +\`\`\`typescript +... file content ... +\`\`\` +``` + +Grep for `### relative/path` to jump to a file. + +## Paths + +| File | Purpose | +|------|---------| +| `.terrain/agent/repomix.md` | Full indexed snapshot (local only) | +| `.terrain/agent/meta.json` | Pack metrics | +| `.terrain/agent/context.md` | Architecture (read first) | + +## Do not + +- Commit or assume `repomix.md` exists in git +- `cat` / Read the entire `repomix.md` (can be 100k+ tokens) +- Read the live repository tree when the pack covers the question +- Skip `context.md` and grep source for architecture questions + +## Regenerate index + +If `repomix.md` is missing: + +```bash +# bash / Git Bash — = ~/.terrain/bin/terrain or bunx @terrain-ai/cli +if [ -x ~/.terrain/bin/terrain ] || [ -x ~/.terrain/bin/terrain.exe ]; then + ~/.terrain/bin/terrain assets pack-agent . +else + bunx @terrain-ai/cli assets pack-agent . +fi +# or from repo root in Terrain UI: 重建源码索引 +``` + +If neither `~/.terrain/bin/terrain` nor `bunx @terrain-ai/cli` works, ask the user to install Terrain or run **重建源码索引** from the desktop app. + +## Related skills + +- **codegraph-skill** — symbol relationships (prefer before wide repomix grep) +- **rtk-skill** — resolve `` prefix for grep/read on the pack file diff --git a/.agents/skills/rtk-skill/SKILL.md b/.agents/skills/rtk-skill/SKILL.md new file mode 100644 index 0000000..c1d0ef9 --- /dev/null +++ b/.agents/skills/rtk-skill/SKILL.md @@ -0,0 +1,213 @@ +--- +name: rtk-skill +description: Use when running shell commands that produce verbose output (git, test, build, lint, package managers, docker). Prefix with rtk to save 60-90% tokens. Terrain projects use explicit rtk prefix (no global hook). +version: 1.2.0 +--- + +# RTK Skill (Rust Token Killer) + +[RTK](https://github.com/rtk-ai/rtk) is a CLI proxy that **filters and compresses command output** before it reaches the LLM (typically **60–90% token savings**). + +## Resolve the RTK command (read first) + +Use **conventional paths only** — never machine-specific absolute paths like `/Users/...` or `C:\Users\...`. + +On Windows, tools deploy to `%USERPROFILE%\.terrain\bin\` (also written as `~/.terrain/bin/` in Git Bash / PowerShell 7+). Binaries use `.exe` extensions; PATHEXT resolves `rtk` → `rtk.exe`. + +| Priority | Command prefix | When | +|----------|----------------|------| +| 1 | `~/.terrain/bin/rtk` | Terrain env integration or desktop app (see existence check below) | +| 2 | `bunx @terrain-ai/rtk` | No Terrain install; needs network once | +| 3 | `npx @terrain-ai/rtk` | Same as bunx if Bun unavailable | + +Optional manifest (local, gitignored): `.terrain/env/agent-tools.json` — same `~/.terrain/bin/…` conventions. + +**Existence check (cross-platform):** + +| Shell | Check | +|-------|-------| +| bash / zsh / Git Bash | `[ -x ~/.terrain/bin/rtk ] \|\| [ -x ~/.terrain/bin/rtk.exe ]` | +| PowerShell | `Test-Path "$HOME\.terrain\bin\rtk.exe"` | +| cmd | `if exist "%USERPROFILE%\.terrain\bin\rtk.exe"` | + +**Shell rules:** + +- Invoke as `~/.terrain/bin/rtk ` — tilde expands at word start in bash/zsh/Git Bash/PowerShell 7+. +- Do **not** `export RTK="$(jq -r .rtk …)"` then `"$RTK"` — quoted variables do not expand `~`. +- Do **not** assume bare `rtk` is on PATH. + +Example (pick one prefix per session after the existence check): + +```bash +# bash / Git Bash +if [ -x ~/.terrain/bin/rtk ] || [ -x ~/.terrain/bin/rtk.exe ]; then + PREFIX=~/.terrain/bin/rtk +else + PREFIX="bunx @terrain-ai/rtk" +fi +$PREFIX git status +``` + +```powershell +# PowerShell +$PREFIX = if (Test-Path "$HOME\.terrain\bin\rtk.exe") { "$HOME\.terrain\bin\rtk.exe" } else { "bunx @terrain-ai/rtk" } +& $PREFIX git status +``` + +Verify: + +```bash +~/.terrain/bin/rtk gain || bunx @terrain-ai/rtk gain +``` + +We do **not** run `rtk init` / global hooks. + +## Golden rule + +> For **any shell command** that prints more than a few lines, run it as **` `** instead of bare ``. + +Applies to: git, tests, builds, linters, package managers, docker/kubectl, `ls`/`grep`/`find`, `gh`, etc. + +In examples below, `` means your resolved prefix (`~/.terrain/bin/rtk` or `bunx @terrain-ai/rtk`). + +## Critical: built-in Read / Grep / Glob + +Claude Code, Cursor, and similar agents often have **native Read/Grep tools that bypass Bash hooks**. + +Those tools **do not** auto-rewrite to RTK. For token-efficient file/search workflows, use: + +| Instead of native tool | Use | +|------------------------|-----| +| Read large source file | ` read path/to/file.rs` | +| Read signatures only | ` read path/to/file.rs -l aggressive` | +| Grep / search repo | ` grep "pattern" .` or ` rg "pattern"` | +| Find files | ` find "*.ts" .` | +| List directory | ` ls .` | + +**Exception:** `.terrain/agent/context.md` and short `knowledge/*.md` — read directly (already dense). + +## Command reference (by category) + +### Git (high savings) + +```bash + git status + git log -n 20 --oneline + git diff + git diff --staged + add -A && git commit -m "msg" + git push + git pull +``` + +### Tests (failures-focused, ~90% savings) + +```bash + cargo test + test cargo test + bun test + vitest + jest + pytest + go test +``` + +### Build & lint + +```bash + cargo build + cargo clippy + tsc + eslint . + ruff check + next build +``` + +### Package managers + +```bash + pnpm list + bun install + pip list +``` + +### Files & search + +```bash + ls src/ + read src/lib/foo.rs + read src/lib/foo.rs -l aggressive + grep "fn handle_" . + find "*.svelte" . + diff file1 file2 +``` + +### Containers / cloud (when used) + +```bash + docker ps + docker logs + kubectl get pods + gh pr list +``` + +### Errors only + +```bash + err npm run build +``` + +## Global flags + +```bash + -u git status + -v cargo test +``` + +## When command fails + +RTK may collapse output but preserves **exit codes**. On failure, look for a tee path in output: + +``` +FAILED: 2/15 tests +[full output: ~/.local/share/rtk/tee/....log] +``` + +Read that log if you need the full unfiltered output — do not blindly re-run the same verbose command. + +## Bypass RTK (rare) + +```bash +RTK_DISABLED=1 git status +``` + +## When NOT to use RTK + +| Use RTK | Use other Terrain skills instead | +|---------|----------------------------------| +| Shell command output | Architecture → `terrain-knowledge-skill` / `context.md` | +| git test build lint | Symbol relations → `codegraph-skill` | +| ` read` for code slices | Structured repomix index → `repomix-context-skill` | + +**Workflow order:** Terrain knowledge → codegraph → repomix slices → **rtk** for remaining shell work. + +## Analytics (optional) + +```bash + gain + gain --history + discover +``` + +## Do not + +- Run `rtk init` or `rtk init -g` — Terrain owns agent guidance via AGENTS.md +- Assume hooks rewrite commands — **you** must type the rtk prefix +- Re-run identical verbose commands after RTK already summarized them +- Use wrong rtk package (verify with ` gain`) + +## Complements + +- **terrain-knowledge-skill** — what project knowledge to read first +- **codegraph-skill** — AST/call-graph queries +- **repomix-context-skill** — grep repomix pack for source sections diff --git a/.agents/skills/terrain-knowledge-skill/SKILL.md b/.agents/skills/terrain-knowledge-skill/SKILL.md new file mode 100644 index 0000000..5406f42 --- /dev/null +++ b/.agents/skills/terrain-knowledge-skill/SKILL.md @@ -0,0 +1,90 @@ +--- +name: terrain-knowledge-skill +description: Use when a coding agent needs project knowledge from Terrain .terrain/ assets. Guides layered reading of context, private knowledge, and repomix index. +version: 1.2.0 +--- + +# Terrain Knowledge Skill + +Terrain stores **AI knowledge assets** under **`.terrain/`** in this repository (not a global `~/.terrain/knowledge` directory). The desktop app registry at `~/.terrain/registry.json` only maps slugs → repo paths. + +Load **`rtk-skill`** when you need to run shell commands during investigation (git, grep repomix file, tests). Resolve tools via **conventional paths** (`~/.terrain/bin/…`) with `bunx`/`npx` fallback — see `rtk-skill` / `codegraph-skill`. + +## Knowledge layers (mandatory order) + +1. **Architecture** — `.terrain/agent/context.md` + - Module map, core flows, system boundaries, tech stack + - Check `.terrain/agent/context-meta.json` or `meta.json` for asset timestamps + - Read directly (short); no RTK needed + +2. **Private domain** — `.terrain/knowledge/**/*.md` + - Business glossary, internal frameworks, APIs, scaffolding guides + - Team-maintained markdown; read in filename sort order when surveying + +3. **Structured meta** — `.terrain/agent/meta-inputs.md` + - Compiled from `terrain-meta.json` and `knowledge/` scans + +4. **Source index** — see `repomix-context-skill` + - Local `.terrain/agent/repomix.md` (gitignored; regenerate via Terrain scan) + +## Knowledge freshness (mandatory before architecture answers) + +Before trusting `context.md` for module/architecture questions: + +```bash +~/.terrain/bin/terrain tools freshness --project +# or: bunx @terrain-ai/cli tools freshness --project +``` + +This recomputes when stale and writes `.terrain/.meta/freshness.json`. **Do not** only read that JSON statically — it is a local cache snapshot. + +| Score | Rule | +|-------|------| +| `< 50` | Do not rely on macro context; use `repomix-context-skill` | +| `50–69` | Cross-check with repomix grep or `codegraph-skill` | +| `≥ 70` | Architecture context is generally reliable | + +On conflict: **repomix source slices > codegraph > agent/context.md > human/** + +For symbol impact/callers, also see `codegraph-skill` (CodeGraph `status` can lie; use `terrain tools codegraph-drift`). + +## Query workflow + +``` +Task received + → freshness check (architecture tasks) + → Read context.md (or relevant section) + → If business/internal terms → read knowledge/*.md + → If symbol / call graph → codegraph-skill + → If implementation / source → repomix-context-skill + → If shell/git/test needed → rtk-skill (prefix with rtk) +``` + +## Rules + +- **Do not** invent module names that contradict `context.md` or `meta-inputs.md` +- **Do not** read the entire live repository tree when indexed assets exist +- **Do not** load full `repomix.md` into context — grep slices only (`rtk grep` on the file is OK) +- Prefer `.terrain/` over guessing project structure + +## Private knowledge directory + +`.terrain/knowledge/` — developers add markdown here; Terrain scans on context generation. + +Example files: +- `00-glossary.md` — domain terms +- `10-internal-framework.md` — internal libs +- `20-api-usage.md` — internal APIs +- `30-scaffolding.md` — project generators + +## Human docs (optional) + +`.terrain/human/` — Litho-generated docs for humans; useful for onboarding context but denser than `context.md`. + +## Related skills + +| Skill | When | +|-------|------| +| `repomix-context-skill` | Source code from repomix index | +| `codegraph-skill` | Callers, callees, impact | +| `rtk-skill` | All verbose shell commands | diff --git a/.ai-context/DYNAMICS.md b/.ai-context/DYNAMICS.md deleted file mode 100644 index 07d0b56..0000000 --- a/.ai-context/DYNAMICS.md +++ /dev/null @@ -1,58 +0,0 @@ -# Dynamics — Active Issues & Constraints - -> **Last updated:** 2026-04-17 -> **Stability:** Dynamic — Update as issues arise/resolve - ---- - -## ⚡ Quick Scan - -| Status | Issue | Impact | Workaround | -|--------|-------|--------|------------| -| 🟡 | LLM serial execution | Throughput limited by concurrency=1 | Intentional; rate limit compliance | -| 🟡 | Keyword-based intent recognition (PM Agent) | Ambiguous requests may be misclassified | PM Agent asks for clarification | -| 🟡 | JSON persistence lacks query capabilities | No efficient search across projects | Use file system tools for search | - ---- - -## 🟡 Known Constraints - -### LLM Serial Execution -- Global semaphore (concurrency=1) means all LLM calls are serialized -- This is intentional for rate limit compliance (30 req/min) -- Pipeline stages cannot parallelize LLM-dependent work - -### PM Agent Intent Recognition -- Uses keyword-based NLP (not ML-based) -- Ambiguous user input triggers clarification requests -- Intent types: bug_fix, requirement_change, new_feature, consultation, ambiguous - -### JSON Storage Limitations -- No built-in indexing or query engine -- Concurrent access not protected (single-user assumption) -- Large projects may experience slower load times - -### Storage Location -- Project data: `.cowork-v2/` in project root -- User config: platform-specific app data directory -- See `crates/cowork-core/src/config.rs` for path resolution - ---- - -## 🟢 Recently Resolved - -| Issue | Resolution | Date | -|-------|------------|------| -| N/A | N/A | — | - ---- - -## 📋 Under Consideration - -- SQLite backend option for team/large-project scenarios -- Local model support (Llama, Mistral) for offline operation -- Multi-user collaboration features (not currently in scope) - ---- - -*Remember: This file changes frequently. Verify against current code state.* diff --git a/.ai-context/SKILL.md b/.ai-context/SKILL.md deleted file mode 100644 index e432007..0000000 --- a/.ai-context/SKILL.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: ai-context -description: | - Project knowledge base for coding agents. Activate when: (1) starting a new session in this project, (2) encountering unfamiliar code patterns or architecture decisions, (3) user asks about project design or rationale, (4) before making significant structural changes. Contains tiered knowledge from stable design principles to dynamic issues. ---- - -# AI Context — Cowork Forge - -> This skill provides pre-generated project knowledge to help you understand the project faster and work more effectively. - ---- - -## 🎯 When to Activate This Skill - -**Activate immediately when:** -- Starting a new coding session in this project -- You need to understand "why something is designed this way" -- User asks about project architecture, design decisions, or constraints - -**Refer to specific sections when:** -- Encountering unexpected behavior or errors → `DYNAMICS.md` -- Planning structural changes → `references/DECISIONS.md` -- Need high-level overview → `references/PROJECT-ESSENCE.md` -- Need component relationships → `references/ARCHITECTURE.md` - -**Do NOT activate when:** -- Simple code edits with clear context -- User requests are purely mechanical (rename, format, etc.) -- You already have sufficient context from recent conversation - ---- - -## 📁 Knowledge Tiers - -| Tier | File | Stability | Update Frequency | -|------|------|-----------|------------------| -| **Tier 0** | `PROJECT-ESSENCE.md` | High | Quarterly / Major version | -| **Tier 1** | `ARCHITECTURE.md` | Medium | Monthly / Sprint | -| **Tier 2** | `DECISIONS.md` | Low | Per decision change | -| **Tier 3** | `DYNAMICS.md` | Dynamic | As needed | - -### Reading Order (Recommended) - -``` -1. PROJECT-ESSENCE.md ← Start here (1-2 min read) -2. ARCHITECTURE.md ← If working across components -3. DECISIONS.md ← If changing established patterns -4. DYNAMICS.md ← If something feels wrong -``` - ---- - -## 🔧 How to Use This Knowledge - -### 1. Session Start Protocol -``` -□ Read PROJECT-ESSENCE.md (always) -□ Scan DYNAMICS.md for active issues -□ Read ARCHITECTURE.md if working across subprojects -□ Proceed with dynamic code exploration -``` - -### 2. Dynamic Code Exploration -- Use `grep` and file search to locate actual implementations -- Verify knowledge against current code state -- Update knowledge if you find drift - -### 3. Decision Validation -Before changing established patterns: -``` -□ Check DECISIONS.md for existing decisions -□ If decision exists: follow it or explicitly propose change -□ If new decision needed: document after implementation -``` - ---- - -## 🔄 When to Update - -### Update PROJECT-ESSENCE.md when: -- Project purpose or scope fundamentally changes -- New major capability is added - -### Update ARCHITECTURE.md when: -- New component/subproject added -- Component responsibilities shift -- Data flow changes significantly - -### Update DECISIONS.md when: -- A new design decision is made -- An existing decision is revisited/changed - -### Update DYNAMICS.md when: -- New issue discovered -- Issue resolved -- Workaround found - ---- - -## 📚 File Reference - -- [Project Essence](references/PROJECT-ESSENCE.md) -- [Architecture](references/ARCHITECTURE.md) -- [Decisions](references/DECISIONS.md) -- [Dynamics](DYNAMICS.md) -- [Maintenance Guide](meta/MAINTENANCE.md) - ---- - -*Generated by ai-context-generator on 2026-04-17* diff --git a/.ai-context/meta/MAINTENANCE.md b/.ai-context/meta/MAINTENANCE.md deleted file mode 100644 index df44f4c..0000000 --- a/.ai-context/meta/MAINTENANCE.md +++ /dev/null @@ -1,98 +0,0 @@ -# AI Context Maintenance Guide - -> How to keep this knowledge base accurate. Last updated: 2026-04-17. - ---- - -## 🎯 Purpose - -This guide tells you (the coding agent) how to maintain `.ai-context`. - ---- - -## 📋 Maintenance Triggers - -| Observation | Action | File | -|-------------|--------|------| -| Code contradicts docs | Fix docs | Relevant file | -| New crate/module | Add entry | ARCHITECTURE.md | -| Major decision | Document | DECISIONS.md | -| Blocking issue | Add entry | DYNAMICS.md | -| Issue resolved | Remove | DYNAMICS.md | -| New stage added | Update pipeline section | ARCHITECTURE.md | -| New tool category | Update tools section | ARCHITECTURE.md | -| New agent type | Update component list | ARCHITECTURE.md | - ---- - -## ✍️ Writing Guidelines - -### PROJECT-ESSENCE.md -- Under 100 lines -- "What" and "why", not "how" -- No code snippets -- Update: Quarterly - -### ARCHITECTURE.md -- Diagrams over paragraphs -- Component-level, not file-level -- Show data flow -- Update: Monthly - -### DECISIONS.md -- Non-obvious choices only -- Include rationale -- Update: As decisions made - -### DYNAMICS.md -- Current issues only -- Remove when resolved -- Update: As needed - ---- - -## 🔄 Update Workflow - -1. Identify file needing update -2. Read current content -3. Make minimal changes -4. Update "Last updated" date -5. Continue your task - ---- - -## ❌ Anti-Patterns - -- Don't copy code snippets — link to files instead -- Don't document every file/function -- Don't keep resolved issues in DYNAMICS.md -- Don't duplicate content across files - ---- - -## ✅ Quality Checklist - -- [ ] PROJECT-ESSENCE.md readable in 2 min -- [ ] ARCHITECTURE.md shows big picture -- [ ] DECISIONS.md has rationale -- [ ] DYNAMICS.md only current issues -- [ ] All files dated - ---- - -## 📎 Project-Specific Notes - -### Key Files for Context Updates - -| What Changed | Where to Look | Which .ai-context File | -|-------------|---------------|----------------------| -| Pipeline stages | `crates/cowork-core/src/pipeline/stages/*.rs` | ARCHITECTURE.md | -| Domain entities | `crates/cowork-core/src/domain/*.rs` | ARCHITECTURE.md | -| Tools | `crates/cowork-core/src/tools/*.rs` | ARCHITECTURE.md | -| Agent configs | `crates/cowork-core/src/config_definition/default_configs/*.json` | ARCHITECTURE.md | -| Instructions | `crates/cowork-core/src/instructions/*.rs` | DECISIONS.md (if pattern changes) | -| Security | `crates/cowork-core/src/runtime_security.rs` | DECISIONS.md | - ---- - -*Update this guide when you discover better maintenance patterns.* diff --git a/.ai-context/references/ARCHITECTURE.md b/.ai-context/references/ARCHITECTURE.md deleted file mode 100644 index ac5468b..0000000 --- a/.ai-context/references/ARCHITECTURE.md +++ /dev/null @@ -1,125 +0,0 @@ -# Architecture — Cowork Forge - -> How components fit together. Last updated: 2026-04-17. -> -> **Update this when:** New component added, responsibilities shift, data flow changes. - ---- - -## System Overview - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Presentation Layer │ -│ ┌──────────────────┐ ┌──────────────────────────┐ │ -│ │ cowork-cli │ │ cowork-gui │ │ -│ │ (clap+dialoguer)│ │ (Tauri + React+AntD) │ │ -│ └────────┬─────────┘ └────────────┬─────────────┘ │ -│ │ implements │ invokes │ -└───────────┼────────────────────────────────────┼───────────────────┘ - ▼ ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Application Layer (cowork-core) │ -│ │ -│ ┌───────────────────────┐ ┌──────────────────────────────┐ │ -│ │ Interaction Domain │ │ Pipeline Domain │ │ -│ │ InteractiveBackend │◄───│ 7-Stage Orchestration │ │ -│ │ (CLI/GUI trait impl) │ │ Stage Executor │ │ -│ └───────────┬───────────┘ │ Flow Config │ │ -│ │ └──────────┬───────────────────┘ │ -│ │ drives │ manages │ -│ ▼ ▼ │ -│ ┌───────────────────────┐ ┌──────────────────────────────┐ │ -│ │ Domain Layer │ │ Supporting Domains │ │ -│ │ Project (Aggregate) │ │ Tools (40+ ADK+MCP) │ │ -│ │ Iteration (Entity) │ │ Agents (adk-rust) │ │ -│ │ Memory (Aggregate) │ │ Instructions (~2000 lines) │ │ -│ └───────────┬───────────┘ │ Skills (agentskills.io) │ │ -│ │ persists │ ACP (External Agent) │ │ -│ ▼ └──────────────────────────────┘ │ -│ ┌───────────────────────────────────────────────────────────┐ │ -│ │ Infrastructure Layer │ │ -│ │ Persistence (JSON) │ LLM (Rate-Limited) │ Security │ │ -│ └───────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ - │ │ - ▼ ▼ - ┌────────────┐ ┌──────────────┐ - │ File System │ │ LLM Provider │ - │ (.cowork-v2)│ │ (OpenAI API) │ - └────────────┘ └──────────────┘ -``` - ---- - -## Component Responsibilities - -### cowork-core -- **Purpose:** Domain logic, pipeline orchestration, tools, agents, persistence -- **Entry:** `crates/cowork-core/src/lib.rs` - -### cowork-cli -- **Purpose:** Command-line interface with HITL terminal interaction -- **Entry:** `crates/cowork-cli/src/main.rs` - -### cowork-gui -- **Purpose:** Desktop GUI (Tauri + React + Ant Design) with real-time streaming -- **Entry:** `crates/cowork-gui/src-tauri/src/main.rs` - -### Pipeline Domain -- **Purpose:** 7-stage workflow orchestration with Actor-Critic pattern -- **Entry:** `crates/cowork-core/src/pipeline/mod.rs` -- Key stages: idea → prd → design → plan → coding → check → delivery - -### Domain Layer -- **Purpose:** Core entities: Project (aggregate root), Iteration, ProjectMemory -- **Entry:** `crates/cowork-core/src/domain/mod.rs` - -### Tools Domain -- **Purpose:** 40+ secure ADK tools + MCP remote tools (file, data, HITL, memory, validation, deployment, legacy analysis) -- **Entry:** `crates/cowork-core/src/tools/mod.rs` - -### Config Definition -- **Purpose:** Data-driven configuration system (agents, stages, flows, skills, integrations as JSON) -- **Entry:** `crates/cowork-core/src/config_definition/mod.rs` - -### Interaction Domain -- **Purpose:** `InteractiveBackend` trait abstracting CLI/GUI interaction -- **Entry:** `crates/cowork-core/src/interaction/mod.rs` - -### Skills Module -- **Purpose:** agentskills.io standard skill discovery, selection, and injection -- **Entry:** `crates/cowork-core/src/skills/` - -### ACP Module -- **Purpose:** Agent Client Protocol for external coding agent integration (OpenCode, Codex, Claude CLI, etc.) -- **Entry:** `crates/cowork-core/src/acp/client.rs` - ---- - -## Data Flow - -1. User provides idea via CLI or GUI -2. Pipeline creates Iteration context (Genesis or Evolution) -3. Each Stage: Agent receives instructions → calls LLM → uses Tools → produces Artifacts -4. Critical stages (idea/prd/design/plan/coding) require HITL confirmation -5. Artifacts persisted as Markdown in `.cowork-v2/iterations/` -6. On completion: knowledge snapshot extracted into ProjectMemory -7. PM Agent available for post-delivery interaction - ---- - -## Key Dependencies - -| Package | Purpose | Version | -|---------|---------|---------| -| adk-rust | Agent framework, tool ecosystem, session management | 0.5.0 | -| tokio | Async runtime | 1 | -| anyhow | Error handling | 1 | -| serde/serde_json | Serialization | 1 | -| agent-client-protocol | ACP for external agents | 0.9 | -| clap | CLI argument parsing | 4 | - ---- - -*This file describes component relationships. For implementation details, explore the source code.* diff --git a/.ai-context/references/DECISIONS.md b/.ai-context/references/DECISIONS.md deleted file mode 100644 index b555624..0000000 --- a/.ai-context/references/DECISIONS.md +++ /dev/null @@ -1,154 +0,0 @@ -# Design Decisions — Cowork Forge - -> Key architectural and design decisions. Update when decisions are made or revisited. -> -> Last reviewed: 2026-04-17 - ---- - -## Decision Index - -| ID | Decision | Status | Date | -|----|----------|--------|------| -| ADR-001 | Multi-Crate Workspace Structure | Active | 2025 | -| ADR-002 | Trait-Based Backend Abstraction | Active | 2025 | -| ADR-003 | JSON-First Persistence | Active | 2025 | -| ADR-004 | Rate Limiting at Infrastructure Layer | Active | 2025 | -| ADR-005 | Event-Driven GUI with Asymmetric Communication | Active | 2025 | -| ADR-006 | Post-Delivery PM Agent | Active | 2025 | -| ADR-007 | agentskills.io Standard for Skills | Active | 2025 | -| ADR-008 | MCP Integration for External Tools | Active | 2025 | - ---- - -## ADR-001: Multi-Crate Workspace Structure - -**Context**: Core domain logic, CLI, and GUI have different deployment and dependency profiles. GUI requires Tauri/WebView; CLI should remain lightweight. - -**Decision**: Separate CLI, GUI, and Core into distinct crates within a Cargo workspace. - -**Rationale**: Enables independent deployment while sharing domain logic. Prevents GUI dependencies from bloating CLI binary. - -**Trade-offs**: -- (+) Clean dependency boundaries, independent versioning -- (-) More complex workspace management, cross-crate refactoring - ---- - -## ADR-002: Trait-Based Backend Abstraction - -**Context**: Pipeline needs to interact with users through both CLI and GUI, which have fundamentally different interaction models (blocking vs event-driven). - -**Decision**: `InteractiveBackend` trait to unify CLI and GUI interactions. - -**Rationale**: Single pipeline code path supports both automation and interactive modes without conditional logic throughout the domain layer. - -**Trade-offs**: -- (+) Clean hexagonal architecture, testable pipeline in isolation -- (-) Trait must accommodate both synchronous (CLI) and asynchronous (GUI) patterns - ---- - -## ADR-003: JSON-First Persistence - -**Context**: Need portable, inspectable, version-control-friendly project storage for a local-first desktop application. - -**Decision**: File-based JSON storage instead of a database. - -**Rationale**: Portability, version control compatibility, and local-first architecture. Users can inspect and modify project state with standard tools. - -**Trade-offs**: -- (+) Zero dependencies, human-readable, git-friendly -- (-) No query capabilities, potential consistency issues with concurrent access - ---- - -## ADR-004: Rate Limiting at Infrastructure Layer - -**Context**: LLM API calls are expensive and quota-limited (typically 30 req/min). Uncontrolled parallelism could exhaust API quotas. - -**Decision**: Decorator pattern with global semaphore (concurrency=1) and 2-second delay for LLM rate limiting. - -**Rationale**: API quota protection and cost control. Global rate limiter ensures compliance regardless of pipeline stage parallelism. - -**Trade-offs**: -- (+) Simple, reliable quota compliance -- (-) Serial LLM calls reduce throughput potential - ---- - -## ADR-005: Event-Driven GUI with Asymmetric Communication - -**Context**: GUI needs both request-response operations and real-time streaming of LLM tokens, tool calls, and process logs. - -**Decision**: Tauri commands for requests, events for streaming responses. - -**Rationale**: Commands provide request-response semantics; events enable server-push for streaming without polling overhead. - -**Trade-offs**: -- (+) Efficient streaming, responsive UI -- (-) Two communication patterns to maintain, oneshot channels for HITL add complexity - ---- - -## ADR-006: Post-Delivery PM Agent - -**Context**: After an iteration completes, users need to continue interacting with the project — fix bugs, add features, ask questions — without understanding pipeline internals. - -**Decision**: Dedicated PM Agent with intent recognition for post-delivery interaction. - -**Rationale**: Natural language bridge from completed pipeline to ongoing project maintenance. Supports bug fixes (goto_stage), new features (create_iteration), and consultation (respond). - -**Trade-offs**: -- (+) User-friendly post-delivery experience, no pipeline knowledge required -- (-) Keyword-based intent recognition can be imprecise; may misclassify ambiguous requests - ---- - -## ADR-007: agentskills.io Standard for Skills - -**Context**: Need a mechanism to inject domain-specific tools, prompts, and context into agents without code modifications. - -**Decision**: Implement agentskills.io standard with SKILL.md markdown format. - -**Rationale**: Industry standard ensures compatibility with external skill packages. Simple markdown format lowers barrier for skill authoring. Auto-discovery from `.skills/` directory. - -**Trade-offs**: -- (+) Community-compatible, zero-code extensibility -- (-) Semantic skill matching quality depends on skill metadata - ---- - -## ADR-008: MCP Integration for External Tools - -**Context**: Agents need access to external capabilities (web search, code documentation) without complex local API integration. - -**Decision**: Model Context Protocol integration for external tool server connectivity. - -**Rationale**: Standardized protocol enables seamless third-party AI service integration (Tavily, DeepWiki). Config-driven auto-initialization at startup with automatic injection into all agents. - -**Trade-offs**: -- (+) Extensible without code changes, standardized protocol -- (-) External service dependency, HTTP transport latency - ---- - -## Template for New Decisions - -```markdown -## ADR-XXX: [Short Title] - -**Context**: [What is the issue?] - -**Decision**: [What did we decide?] - -**Rationale**: [Why this choice?] - -**Trade-offs**: -- (+) [Benefit] -- (-) [Cost] -``` - ---- - -*This file captures decisions that aren't obvious from code.* diff --git a/.ai-context/references/PROJECT-ESSENCE.md b/.ai-context/references/PROJECT-ESSENCE.md deleted file mode 100644 index 76037c5..0000000 --- a/.ai-context/references/PROJECT-ESSENCE.md +++ /dev/null @@ -1,50 +0,0 @@ -# Project Essence — Cowork Forge - -> **Stability: HIGH** | Update: Quarterly or major version changes -> -> Last reviewed: 2026-04-17 - ---- - -## What Is This Project? - -Cowork Forge is an AI-native multi-agent software development platform that simulates a complete virtual development team — Product Manager, Architect, Project Manager, and Engineer — working collaboratively through a 7-stage pipeline to transform ideas into production-ready software. - ---- - -## Why Does It Exist? - -**Problem:** Traditional AI coding assistants only generate code snippets. Real software development requires requirements analysis, architecture design, task planning, quality verification, and delivery — a multi-role collaborative process that single-model tools cannot cover. - -**Solution:** -- Orchestrate specialized AI agents through a structured 7-stage pipeline (Idea→PRD→Design→Plan→Coding→Check→Delivery) -- Apply Actor-Critic pattern for iterative self-refinement at critical stages -- Insert Human-in-the-Loop validation gates at key decision points -- Preserve institutional knowledge across iterations via a Memory system -- Support evolution iterations with intelligent change scope analysis - ---- - -## Who Is This For? - -| User | Use Case | -|------|----------| -| Individual Developers | Rapid prototyping via CLI automation | -| Development Teams | Standardized workflows with cross-iteration memory | -| AI-Augmented Developers | Interactive GUI with real-time streaming and HITL | -| Existing Project Owners | Import and reverse-engineer documentation | - ---- - -## Key Constraints - -1. **Local-First**: All computation and storage is local; no cloud dependency beyond LLM API calls -2. **LLM Rate Limiting**: 30 req/min with concurrency=1 via global semaphore -3. **Workspace Containment**: File operations are validated against project boundaries to prevent path traversal -4. **Security-First**: Command sanitization, build tool whitelisting, and watchdog monitoring -5. **anyhow::Result**: All error handling uses `anyhow::Result`; no `unwrap()` in production code -6. **Rust Edition 2024**: Workspace uses Rust edition 2024 with `adk-rust` 0.5.0 framework - ---- - -*This file captures the stable essence of the project. For architecture details, see [ARCHITECTURE.md](ARCHITECTURE.md).* diff --git a/.claude/skills/codegraph-skill/SKILL.md b/.claude/skills/codegraph-skill/SKILL.md new file mode 100644 index 0000000..e6cde05 --- /dev/null +++ b/.claude/skills/codegraph-skill/SKILL.md @@ -0,0 +1,113 @@ +--- +name: codegraph-skill +description: Use when a coding agent needs symbol relationships, callers, callees, or change impact. Guides CodeGraph CLI usage (not MCP). +version: 1.3.0 +--- + +# CodeGraph Skill + +[CodeGraph](https://colbymchenry.github.io/codegraph/) provides a pre-indexed **AST code graph** for this project. + +Terrain uses **CLI only** — do not run `codegraph install` (that configures MCP/agent rules separately). + +## Resolve the CodeGraph command (read first) + +Use **conventional paths only** — never machine-specific absolute paths like `/Users/...` or `C:\Users\...`. + +On Windows, the wrapper may be `codegraph.cmd` or `codegraph.exe` under `~/.terrain/bin/`. + +| Priority | Command | When | +|----------|---------|------| +| 1 | `~/.terrain/bin/codegraph` | Terrain env integration (see existence check below) | +| 2 | `bunx codegraph` | No Terrain install; needs network once | +| 3 | `npx codegraph` | Same as bunx if Bun unavailable | + +Optional manifest (local, gitignored): `.terrain/env/agent-tools.json` — same `~/.terrain/bin/…` conventions. + +**Existence check (cross-platform):** + +| Shell | Check | +|-------|-------| +| bash / zsh / Git Bash | `[ -x ~/.terrain/bin/codegraph ] \|\| [ -f ~/.terrain/bin/codegraph.exe ] \|\| [ -f ~/.terrain/bin/codegraph.cmd ]` | +| PowerShell | `Test-Path "$HOME\.terrain\bin\codegraph*"` | + +In examples below, `` means your resolved prefix (`~/.terrain/bin/codegraph` or `bunx codegraph`). + +## Prerequisites + +The **index** is per-repo under `.codegraph/`. If missing, initialize from the repo root: + +```bash +# bash / Git Bash +if [ -x ~/.terrain/bin/codegraph ] || [ -f ~/.terrain/bin/codegraph.exe ] || [ -f ~/.terrain/bin/codegraph.cmd ]; then + ~/.terrain/bin/codegraph init -i +else + bunx codegraph init -i +fi +``` + +Refresh after edits: ` sync` + +Health check: + +```bash + status +``` + +**` status` can lie ("up to date" when it is not).** Observed case: index untouched for 10 days while 24 commits changed source files, `status` still reported fresh, and ` query` on a symbol added days earlier returned nothing. Do not treat `status` as authoritative on its own. + +## CLI commands + +| Intent | Command | +|--------|---------| +| Find symbol by name | ` query ` | +| Who calls X | ` callers ` | +| What X calls | ` callees ` | +| Change blast radius | ` impact ` | +| Tests affected by file changes | ` affected ` | +| Project file tree | ` files` | +| Index health | ` status` | +| Refresh after edits | ` sync` | + +## Recommended workflow + +1. Read `.terrain/agent/context.md` (`terrain-knowledge-skill`) +2. **Always** ` sync` first — cheap, incremental, and removes the need to trust `status`'s own up-to-date claim +3. ` query ` to locate definition +4. `callers` / `callees` / `impact` for relationship questions +5. `repomix-context-skill` for full source text of a specific file +6. Use **`rtk-skill`** for any follow-up shell commands (tests, git) + +## Independent staleness cross-check (mandatory before impact/callers analysis) + +Because ` status` is not reliable, cross-check with Terrain's own git-based drift report before trusting `impact`/`callers`/`callees` results for anything safety-relevant (e.g. "is it safe to remove this function"): + +```bash +~/.terrain/bin/terrain tools codegraph-drift --project +# or: bunx @terrain-ai/cli tools codegraph-drift --project +``` + +This compares `.codegraph/codegraph.db`'s mtime against `git log --since` — independent of CodeGraph's own bookkeeping. If `likely_stale: true`, run ` sync` and re-check before trusting query results. + +## When to use vs other skills + +| Use CodeGraph | Use instead | +|---------------|-------------| +| Symbol lookup, call chains | Architecture → `context.md` | +| Impact before refactor | Business rules → `knowledge/` | +| File/symbol graph | Raw source slice → repomix | +| Verbose test/git output | ` cargo test`, ` git diff` | + +## Do not + +- Run `codegraph install` (Terrain manages AGENTS.md) +- Blind `grep` the whole repo to re-verify CodeGraph AST results +- Chain `query` + manual file reads when `impact` answers the question + +## Staleness + +If ` status` shows pending files after your edits, or `terrain tools codegraph-drift` reports `likely_stale: true`: + +```bash + sync +``` diff --git a/.claude/skills/repomix-context-skill/SKILL.md b/.claude/skills/repomix-context-skill/SKILL.md new file mode 100644 index 0000000..03f4138 --- /dev/null +++ b/.claude/skills/repomix-context-skill/SKILL.md @@ -0,0 +1,84 @@ +--- +name: repomix-context-skill +description: Use when an agent needs source code from the local repomix index under .terrain/agent/repomix.md (not committed; regenerate via Terrain scan). +version: 1.3.0 +--- + +# Repomix Context Skill + +Terrain stores a **local repomix index** at `.terrain/agent/repomix.md` (gitignored, fast to regenerate). + +Read **architecture first** via `terrain-knowledge-skill` → `.terrain/agent/context.md`. + +## When to use + +- Implementation details after reading `context.md` +- Cross-file symbol search within the indexed snapshot +- Locating handlers, types, routes + +## Query strategy (mandatory) + +1. **Read meta** — `.terrain/agent/meta.json` (`total_tokens`, `synced_at`, `top_files_by_tokens`) +2. **Search the pack** — never load the entire file: + ```bash + # = ~/.terrain/bin/rtk or bunx @terrain-ai/rtk (see rtk-skill) + grep "struct ProjectOverview" .terrain/agent/repomix.md + grep "### src/lib/api.ts" .terrain/agent/repomix.md + ``` + Or agent Grep limited to that path with tight patterns. +3. **Read slices** — extract matching `### path/to/file` sections only: + ```bash + read .terrain/agent/repomix.md -l aggressive + ``` + Then read the specific `### file` block (line range), ≤150 lines per read. +4. **Refresh** — if `meta.json.synced_at` is stale, ask user to run Terrain **重建源码索引** / scan + +## Repomix section format + +Sections look like: + +```markdown +### src/lib/foo.ts + +\`\`\`typescript +... file content ... +\`\`\` +``` + +Grep for `### relative/path` to jump to a file. + +## Paths + +| File | Purpose | +|------|---------| +| `.terrain/agent/repomix.md` | Full indexed snapshot (local only) | +| `.terrain/agent/meta.json` | Pack metrics | +| `.terrain/agent/context.md` | Architecture (read first) | + +## Do not + +- Commit or assume `repomix.md` exists in git +- `cat` / Read the entire `repomix.md` (can be 100k+ tokens) +- Read the live repository tree when the pack covers the question +- Skip `context.md` and grep source for architecture questions + +## Regenerate index + +If `repomix.md` is missing: + +```bash +# bash / Git Bash — = ~/.terrain/bin/terrain or bunx @terrain-ai/cli +if [ -x ~/.terrain/bin/terrain ] || [ -x ~/.terrain/bin/terrain.exe ]; then + ~/.terrain/bin/terrain assets pack-agent . +else + bunx @terrain-ai/cli assets pack-agent . +fi +# or from repo root in Terrain UI: 重建源码索引 +``` + +If neither `~/.terrain/bin/terrain` nor `bunx @terrain-ai/cli` works, ask the user to install Terrain or run **重建源码索引** from the desktop app. + +## Related skills + +- **codegraph-skill** — symbol relationships (prefer before wide repomix grep) +- **rtk-skill** — resolve `` prefix for grep/read on the pack file diff --git a/.claude/skills/rtk-skill/SKILL.md b/.claude/skills/rtk-skill/SKILL.md new file mode 100644 index 0000000..c1d0ef9 --- /dev/null +++ b/.claude/skills/rtk-skill/SKILL.md @@ -0,0 +1,213 @@ +--- +name: rtk-skill +description: Use when running shell commands that produce verbose output (git, test, build, lint, package managers, docker). Prefix with rtk to save 60-90% tokens. Terrain projects use explicit rtk prefix (no global hook). +version: 1.2.0 +--- + +# RTK Skill (Rust Token Killer) + +[RTK](https://github.com/rtk-ai/rtk) is a CLI proxy that **filters and compresses command output** before it reaches the LLM (typically **60–90% token savings**). + +## Resolve the RTK command (read first) + +Use **conventional paths only** — never machine-specific absolute paths like `/Users/...` or `C:\Users\...`. + +On Windows, tools deploy to `%USERPROFILE%\.terrain\bin\` (also written as `~/.terrain/bin/` in Git Bash / PowerShell 7+). Binaries use `.exe` extensions; PATHEXT resolves `rtk` → `rtk.exe`. + +| Priority | Command prefix | When | +|----------|----------------|------| +| 1 | `~/.terrain/bin/rtk` | Terrain env integration or desktop app (see existence check below) | +| 2 | `bunx @terrain-ai/rtk` | No Terrain install; needs network once | +| 3 | `npx @terrain-ai/rtk` | Same as bunx if Bun unavailable | + +Optional manifest (local, gitignored): `.terrain/env/agent-tools.json` — same `~/.terrain/bin/…` conventions. + +**Existence check (cross-platform):** + +| Shell | Check | +|-------|-------| +| bash / zsh / Git Bash | `[ -x ~/.terrain/bin/rtk ] \|\| [ -x ~/.terrain/bin/rtk.exe ]` | +| PowerShell | `Test-Path "$HOME\.terrain\bin\rtk.exe"` | +| cmd | `if exist "%USERPROFILE%\.terrain\bin\rtk.exe"` | + +**Shell rules:** + +- Invoke as `~/.terrain/bin/rtk ` — tilde expands at word start in bash/zsh/Git Bash/PowerShell 7+. +- Do **not** `export RTK="$(jq -r .rtk …)"` then `"$RTK"` — quoted variables do not expand `~`. +- Do **not** assume bare `rtk` is on PATH. + +Example (pick one prefix per session after the existence check): + +```bash +# bash / Git Bash +if [ -x ~/.terrain/bin/rtk ] || [ -x ~/.terrain/bin/rtk.exe ]; then + PREFIX=~/.terrain/bin/rtk +else + PREFIX="bunx @terrain-ai/rtk" +fi +$PREFIX git status +``` + +```powershell +# PowerShell +$PREFIX = if (Test-Path "$HOME\.terrain\bin\rtk.exe") { "$HOME\.terrain\bin\rtk.exe" } else { "bunx @terrain-ai/rtk" } +& $PREFIX git status +``` + +Verify: + +```bash +~/.terrain/bin/rtk gain || bunx @terrain-ai/rtk gain +``` + +We do **not** run `rtk init` / global hooks. + +## Golden rule + +> For **any shell command** that prints more than a few lines, run it as **` `** instead of bare ``. + +Applies to: git, tests, builds, linters, package managers, docker/kubectl, `ls`/`grep`/`find`, `gh`, etc. + +In examples below, `` means your resolved prefix (`~/.terrain/bin/rtk` or `bunx @terrain-ai/rtk`). + +## Critical: built-in Read / Grep / Glob + +Claude Code, Cursor, and similar agents often have **native Read/Grep tools that bypass Bash hooks**. + +Those tools **do not** auto-rewrite to RTK. For token-efficient file/search workflows, use: + +| Instead of native tool | Use | +|------------------------|-----| +| Read large source file | ` read path/to/file.rs` | +| Read signatures only | ` read path/to/file.rs -l aggressive` | +| Grep / search repo | ` grep "pattern" .` or ` rg "pattern"` | +| Find files | ` find "*.ts" .` | +| List directory | ` ls .` | + +**Exception:** `.terrain/agent/context.md` and short `knowledge/*.md` — read directly (already dense). + +## Command reference (by category) + +### Git (high savings) + +```bash + git status + git log -n 20 --oneline + git diff + git diff --staged + add -A && git commit -m "msg" + git push + git pull +``` + +### Tests (failures-focused, ~90% savings) + +```bash + cargo test + test cargo test + bun test + vitest + jest + pytest + go test +``` + +### Build & lint + +```bash + cargo build + cargo clippy + tsc + eslint . + ruff check + next build +``` + +### Package managers + +```bash + pnpm list + bun install + pip list +``` + +### Files & search + +```bash + ls src/ + read src/lib/foo.rs + read src/lib/foo.rs -l aggressive + grep "fn handle_" . + find "*.svelte" . + diff file1 file2 +``` + +### Containers / cloud (when used) + +```bash + docker ps + docker logs + kubectl get pods + gh pr list +``` + +### Errors only + +```bash + err npm run build +``` + +## Global flags + +```bash + -u git status + -v cargo test +``` + +## When command fails + +RTK may collapse output but preserves **exit codes**. On failure, look for a tee path in output: + +``` +FAILED: 2/15 tests +[full output: ~/.local/share/rtk/tee/....log] +``` + +Read that log if you need the full unfiltered output — do not blindly re-run the same verbose command. + +## Bypass RTK (rare) + +```bash +RTK_DISABLED=1 git status +``` + +## When NOT to use RTK + +| Use RTK | Use other Terrain skills instead | +|---------|----------------------------------| +| Shell command output | Architecture → `terrain-knowledge-skill` / `context.md` | +| git test build lint | Symbol relations → `codegraph-skill` | +| ` read` for code slices | Structured repomix index → `repomix-context-skill` | + +**Workflow order:** Terrain knowledge → codegraph → repomix slices → **rtk** for remaining shell work. + +## Analytics (optional) + +```bash + gain + gain --history + discover +``` + +## Do not + +- Run `rtk init` or `rtk init -g` — Terrain owns agent guidance via AGENTS.md +- Assume hooks rewrite commands — **you** must type the rtk prefix +- Re-run identical verbose commands after RTK already summarized them +- Use wrong rtk package (verify with ` gain`) + +## Complements + +- **terrain-knowledge-skill** — what project knowledge to read first +- **codegraph-skill** — AST/call-graph queries +- **repomix-context-skill** — grep repomix pack for source sections diff --git a/.claude/skills/terrain-knowledge-skill/SKILL.md b/.claude/skills/terrain-knowledge-skill/SKILL.md new file mode 100644 index 0000000..5406f42 --- /dev/null +++ b/.claude/skills/terrain-knowledge-skill/SKILL.md @@ -0,0 +1,90 @@ +--- +name: terrain-knowledge-skill +description: Use when a coding agent needs project knowledge from Terrain .terrain/ assets. Guides layered reading of context, private knowledge, and repomix index. +version: 1.2.0 +--- + +# Terrain Knowledge Skill + +Terrain stores **AI knowledge assets** under **`.terrain/`** in this repository (not a global `~/.terrain/knowledge` directory). The desktop app registry at `~/.terrain/registry.json` only maps slugs → repo paths. + +Load **`rtk-skill`** when you need to run shell commands during investigation (git, grep repomix file, tests). Resolve tools via **conventional paths** (`~/.terrain/bin/…`) with `bunx`/`npx` fallback — see `rtk-skill` / `codegraph-skill`. + +## Knowledge layers (mandatory order) + +1. **Architecture** — `.terrain/agent/context.md` + - Module map, core flows, system boundaries, tech stack + - Check `.terrain/agent/context-meta.json` or `meta.json` for asset timestamps + - Read directly (short); no RTK needed + +2. **Private domain** — `.terrain/knowledge/**/*.md` + - Business glossary, internal frameworks, APIs, scaffolding guides + - Team-maintained markdown; read in filename sort order when surveying + +3. **Structured meta** — `.terrain/agent/meta-inputs.md` + - Compiled from `terrain-meta.json` and `knowledge/` scans + +4. **Source index** — see `repomix-context-skill` + - Local `.terrain/agent/repomix.md` (gitignored; regenerate via Terrain scan) + +## Knowledge freshness (mandatory before architecture answers) + +Before trusting `context.md` for module/architecture questions: + +```bash +~/.terrain/bin/terrain tools freshness --project +# or: bunx @terrain-ai/cli tools freshness --project +``` + +This recomputes when stale and writes `.terrain/.meta/freshness.json`. **Do not** only read that JSON statically — it is a local cache snapshot. + +| Score | Rule | +|-------|------| +| `< 50` | Do not rely on macro context; use `repomix-context-skill` | +| `50–69` | Cross-check with repomix grep or `codegraph-skill` | +| `≥ 70` | Architecture context is generally reliable | + +On conflict: **repomix source slices > codegraph > agent/context.md > human/** + +For symbol impact/callers, also see `codegraph-skill` (CodeGraph `status` can lie; use `terrain tools codegraph-drift`). + +## Query workflow + +``` +Task received + → freshness check (architecture tasks) + → Read context.md (or relevant section) + → If business/internal terms → read knowledge/*.md + → If symbol / call graph → codegraph-skill + → If implementation / source → repomix-context-skill + → If shell/git/test needed → rtk-skill (prefix with rtk) +``` + +## Rules + +- **Do not** invent module names that contradict `context.md` or `meta-inputs.md` +- **Do not** read the entire live repository tree when indexed assets exist +- **Do not** load full `repomix.md` into context — grep slices only (`rtk grep` on the file is OK) +- Prefer `.terrain/` over guessing project structure + +## Private knowledge directory + +`.terrain/knowledge/` — developers add markdown here; Terrain scans on context generation. + +Example files: +- `00-glossary.md` — domain terms +- `10-internal-framework.md` — internal libs +- `20-api-usage.md` — internal APIs +- `30-scaffolding.md` — project generators + +## Human docs (optional) + +`.terrain/human/` — Litho-generated docs for humans; useful for onboarding context but denser than `context.md`. + +## Related skills + +| Skill | When | +|-------|------| +| `repomix-context-skill` | Source code from repomix index | +| `codegraph-skill` | Callers, callees, impact | +| `rtk-skill` | All verbose shell commands | diff --git a/.gitignore b/.gitignore index e88c298..02b4d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,10 @@ target/ .skills config.toml resurvey +.codegraph + +# Terrain — repomix index (regenerated locally, not versioned) +.terrain/agent/repomix.md + +# Terrain — repomix index (regenerated locally, not versioned) +**/.sdd-agent diff --git a/.terrain/.litho-agent/architecture.md b/.terrain/.litho-agent/architecture.md new file mode 100644 index 0000000..0bc5b50 --- /dev/null +++ b/.terrain/.litho-agent/architecture.md @@ -0,0 +1,64 @@ +# 架构研究报告 + +## 架构模式 + +Cowork Forge 的核心架构可以理解为一个"AI 驱动的流水线工厂"——它的整体骨架是**流水线-过滤器(Pipeline-Filter)模式**,7 个开发阶段像流水线上的工位一样串联执行。但每个工位内部又采用了**Actor-Critic 自优化模式**(Agent 先干活、再自我审查、根据反馈迭代改进),使得每个阶段的输出质量在自我博弈中不断提升。 + +这种架构设计解决了一个核心矛盾:AI 生成的内容往往质量不稳定,单一模型的一次输出很难达到专业水准。通过 Actor-Critic 循环,系统让一个 Agent 当"生产者"、另一个当"评审者",模拟人类团队中的"写代码→Code Review"工作流。 + +此外,系统采用**六边形架构(Hexagonal Architecture)**作为模块组织原则——核心领域逻辑(domain)零外部依赖,所有基础设施适配器(持久化、LLM、交互界面)都通过 trait 接口与核心解耦。这使得替换任何一个基础设施组件(比如从 CLI 切换到 GUI)都不会影响业务逻辑。 + +架构模式的第三个关键选择是**策略模式(Strategy)用于阶段行为**——每个开发阶段都实现统一的 `Stage` trait,使得流水线可以在运行时动态组合和替换阶段,而不需要修改流水线引擎本身。 + +## 核心设计原则 + +1. **领域驱动设计(DDD)**——核心领域实体(Project、Iteration、Memory)封装了所有业务规则和生命周期逻辑,外部模块只能通过这些实体提供的公共方法与之交互。这解决了"业务逻辑散落在各处"的问题,使得系统行为可预测、可测试。 + +2. **交互后端抽象(InteractiveBackend trait)**——所有用户交互都通过 `InteractiveBackend` trait 进行,CLI 和 GUI 各自实现这个 trait。这一设计解决了"核心引擎不能同时服务于命令行和图形界面"的问题——Tauri 后端通过 `TauriBackend` 实现,CLI 通过 `CliBackend` 实现,核心代码完全不需要知道用户是用什么界面在交互。 + +3. **数据驱动配置(ConfigRegistry)**——Agent、Stage、Flow、Integration 的定义全部从硬编码迁移到可配置的 JSON 格式。这使得用户可以不修改代码就定义新的 Agent 角色、自定义工作流、或者集成外部系统。这是一个从"写死的 SDK"到"可配置的平台"的关键架构跃迁。 + +4. **记忆即基础设施(Memory as Infrastructure)**——项目记忆(Decisions、Patterns、Context)被设计为一级基础设施,跨迭代自动累积。每次迭代完成后的 Knowledge Generation Agent 自动提取关键决策和模式,使得系统"越用越聪明"——后续迭代可以查询前序迭代的知识。 + +## 技术栈详情 + +| 层次/领域 | 技术选型 | 选择理由 | +|---------|---------|---------| +| 语言与运行时 | Rust (edition 2024) + Tokio | 内存安全、高性能、零成本抽象,适合 IO 密集型多 Agent 并发 | +| Agent 框架 | adk-rust (adk-core, adk-agent, adk-model) | 提供标准化的 Agent 构建 API,支持 LoopAgent 和流式输出 | +| CLI 框架 | clap (v4, derive) + dialoguer + console | 声明式参数解析,跨平台中文支持良好 | +| GUI 框架 | Tauri 2 + React + TypeScript + Ant Design | 跨平台原生应用,Rust 后端安全,React 前端灵活 | +| 序列化 | serde + serde_json | Rust 生态标准序列化方案,零成本抽象 | +| 持久化 | JSON 文件存储 | 无需数据库,简单可靠,适合桌面工具场景 | +| 速率限制 | TokenBucket 算法 | 允许突发请求同时保证长期速率,比固定延迟更高效 | +| 外部 Agent 集成 | Agent Client Protocol (ACP) | 开放标准协议,支持多种外部编码 Agent | + +## 关键数据结构 + +| 类型名 | 文件路径 | 用途 | +|-------|---------|------| +| `Project` | `crates/cowork-core/src/domain/project.rs:6` | 项目根实体,管理名称、迭代列表、元数据 | +| `Iteration` | `crates/cowork-core/src/domain/iteration.rs:8` | 迭代实体,一个开发周期,包含状态、制品、阶段记录 | +| `Artifacts` | `crates/cowork-core/src/domain/iteration.rs` | 迭代制品集合(idea.md, prd.md, design.md, plan.md 等) | +| `InheritanceMode` | `crates/cowork-core/src/domain/iteration.rs` | 迭代继承模式(None/Full/Partial) | +| `ProjectMemory` | `crates/cowork-core/src/domain/memory.rs:7` | 项目级记忆,跨迭代积累决策、模式、上下文 | +| `PipelineContext` | `crates/cowork-core/src/pipeline/mod.rs:29` | 管道执行上下文,携带项目和迭代信息 | +| `StageResult` | `crates/cowork-core/src/pipeline/mod.rs:19` | 阶段执行结果枚举(Success/Failed/Paused/NeedsRevision/GotoStage) | +| `ConfigRegistry` | `crates/cowork-core/src/config_definition/registry.rs:41` | 全局配置注册表,管理 Agent/Stage/Flow/Integration 定义 | + +## 核心接口/Trait/协议 + +| 名称 | 实现数量 | 核心职责 | +|-----|---------|---------| +| `Stage` trait | 7(Idea/PRD/Design/Plan/Coding/Check/Delivery) | 定义开发阶段的统一接口——每个阶段必须实现 execute 方法 | +| `InteractiveBackend` trait | 2(CliBackend, TauriBackend) | 抽象用户交互方式——消息展示、用户输入、进度通知 | +| `Llm` trait | 2(真实 LLM + TokenBucketRateLimiter 装饰器) | LLM 调用抽象,RateLimiter 作为装饰器透明添加速率限制 | +| `Agent` trait (adk_core) | 多个(LoopAgent, LlmAgentBuilder 产物) | 统一的 Agent 执行接口,支持流式输出 | + +## 架构决策记录 + +- **决策1**:选择了 Actor-Critic 自优化循环,放弃了单次 Agent 输出,因为 AI 单次生成内容质量不稳定,自我博弈能显著提升输出质量。观察依据:`crates/cowork-core/src/agents/mod.rs:68-99`(PRD Loop 的 Actor+Cirtic 构建) +- **决策2**:选择了 JSON 文件持久化,放弃了关系型数据库,因为桌面工具场景不需要多用户并发访问,JSON 文件简单可靠、无需运维。观察依据:`crates/cowork-core/src/persistence/mod.rs`(基于文件的 ProjectStore/IterationStore/MemoryStore) +- **决策3**:选择了 TokenBucket 速率限制,放弃了固定延迟方案,因为允许突发请求可以在需要时快速响应,同时保证长期平均速率不超过限制。观察依据:`crates/cowork-core/src/llm/rate_limiter.rs:32-60` +- **决策4**:选择了 InteractiveBackend trait 抽象交互,放弃了 CLI 和 GUI 直接调用核心 API,因为核心引擎需要同时支持 CLI 和 GUI 而不感知具体交互方式。观察依据:`crates/cowork-core/src/interaction/mod.rs:108-160` +- **决策5**:选择了 LoopAgent + max_iterations=1 解决 SequentialAgent 终止 bug,放弃了修复 adk-rust 框架本身,因为框架边界决策更稳妥。观察依据:`crates/cowork-core/src/agents/mod.rs:4-10`(Bug 注释) diff --git a/.terrain/.litho-agent/boundary.md b/.terrain/.litho-agent/boundary.md new file mode 100644 index 0000000..639733e --- /dev/null +++ b/.terrain/.litho-agent/boundary.md @@ -0,0 +1,161 @@ +# 边界接口报告 + +## CLI 接口 + +Cowork Forge 通过 `cowork` CLI 二进制提供完整的命令行界面。所有的子命令和参数在 `crates/cowork-cli/src/main.rs:12-117` 使用 clap derive 宏定义。 + +### 命令:cowork init +- **描述**:初始化一个新项目,创建 `.cowork-v2/` 目录结构 +- **参数**: + | 参数 | 类型 | 是否必须 | 默认值 | 说明 | + |------|------|---------|-------|------| + | `-n, --name` | String | 否 | - | 项目名称,如果不指定则交互式输入 | +- **使用示例**: + ```bash + cowork init --name "My Project" + ``` + +### 命令:cowork iter +- **描述**:创建并执行一个新的迭代——这是最核心的命令 +- **参数**: + | 参数 | 类型 | 是否必须 | 默认值 | 含义解读 | + |------|------|---------|-------|---------| + | `title` | String | 是 | - | 迭代标题/想法描述——这就是你的"需求",用自然语言描述你想要的 | + | `-d, --description` | String | 否 | - | 更详细的迭代描述,如果想法很复杂可以在这里展开说明 | + | `-b, --base` | String | 否 | - | 基础迭代 ID,用于创建演化迭代(继承前一个迭代的代码或制品) | + | `-i, --inherit` | String | 否 | `"full"` | 继承模式:`none`(全新开始)、`full`(复制代码+制品)、`partial`(只复制制品,重新生成代码) | +- **使用示例**: + ```bash + cowork iter --project "my-project" "Build a REST API for task management" + cowork iter --project "my-project" --base iter-2 --inherit partial "Add user authentication" + ``` + +### 命令:cowork list +- **描述**:列出项目的所有迭代 +- **参数**: + | 参数 | 类型 | 是否必须 | 默认值 | 说明 | + |------|------|---------|-------|------| + | `-a, --all` | bool | 否 | false | 显示所有迭代,包括已完成的 | +- **使用示例**: + ```bash + cowork list + cowork list --all + ``` + +### 命令:cowork show +- **描述**:显示指定迭代的详细信息 +- **参数**: + | 参数 | 类型 | 是否必须 | 默认值 | 说明 | + |------|------|---------|-------|------| + | `iteration_id` | String | 否 | 当前迭代 | 要查看的迭代 ID | +- **使用示例**: + ```bash + cowork show + cowork show iter-1-1234567890 + ``` + +### 命令:cowork continue +- **描述**:继续执行已暂停的迭代 +- **参数**: + | 参数 | 类型 | 是否必须 | 默认值 | 说明 | + |------|------|---------|-------|------| + | `iteration_id` | String | 否 | 当前迭代 | 要继续的迭代 ID | +- **使用示例**: + ```bash + cowork continue + ``` + +### 命令:cowork status +- **描述**:显示项目当前状态 +- **参数**:无 +- **使用示例**: + ```bash + cowork status + ``` + +### 命令:cowork delete +- **描述**:删除指定迭代 +- **参数**: + | 参数 | 类型 | 是否必须 | 默认值 | 说明 | + |------|------|---------|-------|------| + | `iteration_id` | String | 是 | - | 要删除的迭代 ID | +- **使用示例**: + ```bash + cowork delete iter-1-1234567890 + ``` + +### 命令:cowork import +- **描述**:导入已有项目——分析现有代码结构并反向工程生成文档 +- **参数**: + | 参数 | 类型 | 是否必须 | 默认值 | 含义解读 | + |------|------|---------|-------|---------| + | `path` | String | 是 | - | 已有项目的目录路径 | + | `-n, --name` | String | 否 | 目录名 | 在 Cowork Forge 中使用的项目名称 | + | `--idea` | bool | 否 | true | 是否生成 idea.md | + | `--prd` | bool | 否 | true | 是否生成 prd.md | + | `--design` | bool | 否 | true | 是否生成 design.md | + | `--plan` | bool | 否 | true | 是否生成 plan.md | + | `--template-only` | bool | 否 | false | 仅使用模板生成(不使用 LLM) | +- **使用示例**: + ```bash + cowork import /path/to/existing/project + cowork import /path/to/project --template-only + ``` + +### 命令:cowork config +- **描述**:配置 LLM 设置(API 地址、密钥、模型等) +- **参数**:无(交互式配置) +- **使用示例**: + ```bash + cowork config + ``` + +## 配置结构 + +配置文件位于系统应用数据目录的 `config.toml`: + +| 配置项 | 类型 | 含义解读 | +|-------|------|---------| +| `[llm].api_base_url` | String | LLM API 地址——决定用哪家服务商,默认是 OpenAI | +| `[llm].api_key` | String | API 密钥——敏感信息,建议用环境变量替代硬编码 | +| `[llm].model_name` | String | 使用的模型名——如 `gpt-5` | +| `[embedding].api_base_url` | String | 嵌入模型 API 地址(可选) | +| `[coding_agent].enabled` | bool | 是否启用外部编码 Agent | +| `[coding_agent].agent_type` | String | 外部 Agent 类型:opencode/iflow/codex/gemini/claude | +| `[coding_agent].command` | String | 启动外部 Agent 的命令 | +| `[coding_agent].transport` | String | 通信方式:stdio 或 websocket | + +**示例配置**: +```toml +[llm] +api_base_url = "https://api.openai.com/v1" +api_key = "sk-your-openai-api-key" +model_name = "gpt-5" + +[embedding] +api_base_url = "https://your-embedding-api.com/v1" +api_key = "your-embedding-api-key" +model_name = "text-embedding-3-small" + +[coding_agent] +enabled = true +agent_type = "opencode" +command = "bun" +args = ["x", "opencode-ai", "acp"] +transport = "stdio" +``` + +## 集成建议 + +**通过 CLI 集成到 CI/CD**: +```bash +# 在 CI 流水线中初始化项目和创建迭代 +cowork init --name "My Project" +cowork iter --project "my-project" "Auto-generated feature from CI" +``` + +**通过外部 Agent 协议集成**: +Cowork Forge 支持通过 ACP(Agent Client Protocol)集成外部编码 Agent。配置 `[coding_agent]` 部分后,Coding 阶段将自动调用外部 Agent 而非内置 Agent。 + +**通过 Integration Hook 集成**: +配置 Integration 定义后,可以在特定阶段完成后触发 webhook 调用外部系统(如部署平台、需求管理系统)。 diff --git a/.terrain/.litho-agent/c1-system-context.md b/.terrain/.litho-agent/c1-system-context.md new file mode 100644 index 0000000..8a7f824 --- /dev/null +++ b/.terrain/.litho-agent/c1-system-context.md @@ -0,0 +1,40 @@ +# C1 系统上下文报告 + +## 系统定义 +Cowork Forge 是一个"AI 原生多 Agent 软件开发平台"。它不是普通的代码生成器,而是一个完整的虚拟开发团队——内部有扮演产品经理、架构师、项目经理、工程师等角色的 AI Agent,通过七阶段流水线协作,把用户的原始想法逐步演化为可交付的软件产品。用户只需要描述想要什么,剩下的流程由 AI 团队自动完成。 + +## 用户角色 +| 角色 | 描述 | 主要使用场景 | +|------|------|------------| +| 独立开发者/创业者 | 一个人就是一个团队,没有预算雇佣完整开发团队 | 快速验证产品想法、构建 MVP、小规模项目 | +| 技术团队负责人 | 需要快速原型或探索技术方案的资深开发者 | 技术预研、架构评估、新功能快速原型 | +| 非技术产品经理 | 需要将产品需求转化为可实现的技术方案 | 自动生成 PRD、设计文档、实施计划 | +| 开源项目维护者 | 需要自动化文档生成和代码重构支持 | 遗留项目导入、增量更新、知识提取 | + +## 外部系统依赖 +| 外部系统 | 交互类型 | 数据流向 | 关键性 | +|---------|---------|---------|-------| +| LLM API(OpenAI 兼容) | HTTP API | 双向(请求→推理→响应) | 高(系统运行核心) | +| 文件系统 | 文件读写 | 双向 | 高(所有持久化依赖文件) | +| 外部 ACP Agent(OpenCode/Gemini CLI 等) | stdio/WebSocket | 双向 | 低(可选外部编码 Agent) | +| Git | 命令行调用 | 单向(仅执行 git 命令) | 低(仅当用户使用版本控制) | +| 操作系统 Shell | 命令行调用 | 双向 | 中(用于构建和测试) | + +## 业务价值 +- **解决的核心问题**:软件开发需要多角色协作,小型团队缺乏专业资源;Cowork Forge 用 AI 模拟完整团队,一个人即可完成从想法到交付的全流程。 +- **主要输入**:用户用自然语言描述的项目想法或需求 +- **主要输出**:完整软件项目(包括 PRD 文档、架构设计、实施计划、源代码、质量报告) + +## 系统边界(范围内) +- 从想法到交付的完整开发流程自动化 +- 多 Agent 角色协作与自优化(Actor-Critic 模式) +- 人类在环关键决策验证 +- 增量代码更新(仅修改受影响文件) +- 遗留项目导入与反向工程 +- 项目级知识提取与跨迭代记忆 + +## 系统边界(范围外) +- 不提供 IDE 功能(代码由 Agent 通过文件工具修改) +- 不直接部署到生产环境(但可通过集成 Hook 触发部署) +- 不提供实时协作编辑(聚焦 AI-Agent 协作,非人-人协作) +- 不提供数据库服务(持久化通过 JSON 文件) diff --git a/.terrain/.litho-agent/c2-domain-modules.md b/.terrain/.litho-agent/c2-domain-modules.md new file mode 100644 index 0000000..36b3402 --- /dev/null +++ b/.terrain/.litho-agent/c2-domain-modules.md @@ -0,0 +1,161 @@ +# 领域模块报告 + +## 识别到的领域模块(完整列表) + +| 模块名称 | 路径 | 核心职责 | DDD 分类 | 重要性(1-10) | 复杂度(1-10) | +|---------|------|---------|---------|:----------:|:----------:| +| pipeline | `crates/cowork-core/src/pipeline/` | 7 阶段开发流水线编排与执行 | 核心域 | 10 | 9 | +| agents | `crates/cowork-core/src/agents/` | AI Agent 构建与 Actor-Critic 循环 | 核心域 | 10 | 8 | +| tools | `crates/cowork-core/src/tools/` | 30+ ADK 工具(文件、命令、验证、Memory) | 支撑域 | 8 | 7 | +| instructions | `crates/cowork-core/src/instructions/` | Agent 提示词库(每个阶段的 Actor/Critic 指令) | 支撑域 | 8 | 4 | +| domain | `crates/cowork-core/src/domain/` | 核心领域实体模型(Project/Iteration/Memory) | 通用域 | 7 | 5 | +| persistence | `crates/cowork-core/src/persistence/` | JSON 文件持久化存储 | 支撑域 | 7 | 5 | +| llm | `crates/cowork-core/src/llm/` | LLM 客户端集成与速率限制 | 支撑域 | 8 | 5 | +| config_definition | `crates/cowork-core/src/config_definition/` | 数据驱动配置系统 | 支撑域 | 7 | 7 | +| interaction | `crates/cowork-core/src/interaction/` | InteractiveBackend 交互抽象层 | 通用域 | 6 | 4 | +| acp | `crates/cowork-core/src/acp/` | 外部 Agent 协议客户端 | 支撑域 | 5 | 4 | +| skills | `crates/cowork-core/src/skills/` | agentskills.io 技能系统 | 通用域 | 4 | 4 | +| integration | `crates/cowork-core/src/integration/` | 外部集成 Hook 管理器 | 支撑域 | 4 | 5 | +| importer | `crates/cowork-core/src/importer/` | 遗留项目导入与反向工程 | 核心域 | 7 | 6 | +| project_runtime | `crates/cowork-core/src/project_runtime.rs` | 项目运行时配置与安全 | 支撑域 | 4 | 3 | +| cowork-cli | `crates/cowork-cli/src/` | CLI 命令行接口 | 核心域 | 8 | 4 | +| cowork-gui | `crates/cowork-gui/` | Tauri + React 图形界面 | 核心域 | 8 | 7 | + +## 领域间关系 + +```mermaid +graph TD + Pipeline["pipeline
流水线编排"] --> Agents["agents
AI Agent"] + Pipeline --> Interaction["interaction
交互后端"] + Pipeline --> LLM["llm
LLM 集成"] + Pipeline --> Domain["domain
领域实体"] + Pipeline --> Persistence["persistence
持久化"] + + Agents --> Instructions["instructions
提示词库"] + Agents --> Tools["tools
工具集"] + Agents --> Domain + + Tools --> Persistence + Tools --> LLM + + Config["config_definition
配置定义"] --> Agents + Config --> Pipeline + + Importer["importer
项目导入"] --> Domain + Importer --> Persistence + + ACP["acp
外部 Agent"] --> Tools + + Integration["integration
集成系统"] --> Pipeline + + Skills["skills
技能系统"] --> Agents + + CLI["cowork-cli
命令行"] --> Pipeline + CLI --> Interaction + + GUI["cowork-gui
图形界面"] --> Pipeline + GUI --> Interaction +``` + +## 业务流程(Business Flows) + +| 流程名称 | 描述 | 涉及领域 | 入口点 | +|---------|------|---------|-------| +| 7-Stage 开发流水线 | 从想法到交付的完整开发流程 | pipeline, agents, domain, llm, tools, interaction, persistence | `crates/cowork-cli/src/main.rs` | +| 遗留项目导入 | 分析现有项目并反向生成文档 | importer, domain, persistence | `crates/cowork-cli/src/commands/import.rs` | +| 外部 Agent 编码 | 通过 ACP 协议调用外部编码 Agent | acp, pipeline, tools | `crates/cowork-core/src/acp/client.rs` | +| 项目知识生成 | 每次迭代完成后自动提取项目知识 | agents, domain, persistence, instructions | `crates/cowork-core/src/pipeline/executor/knowledge.rs` | +| PM Agent 交互 | 交付后通过 PM Agent 继续与项目互动 | agents, domain, persistence, tools | `crates/cowork-core/src/agents/mod.rs` | + +## 各模块详情 + +### pipeline 模块 +- **路径**:`crates/cowork-core/src/pipeline/` +- **职责**:7 阶段开发流水线的编排和执行,管理迭代生命周期 +- **核心抽象**:`Stage` trait(`crates/cowork-core/src/pipeline/mod.rs:47`)、`PipelineContext`(`crates/cowork-core/src/pipeline/mod.rs:29`)、`StageResult`(`crates/cowork-core/src/pipeline/mod.rs:19`) +- **子模块**: + - `executor/`:迭代执行器(IterationExecutor)——统一入口点 + - `stages/`:各阶段实现(idea/prd/design/plan/coding/check/delivery) + - `stage_executor.rs`:配置驱动的阶段执行框架 +- **依赖的模块**:domain, agents, llm, config_definition, interaction, persistence +- **重要性评分**:10 + +### agents 模块 +- **路径**:`crates/cowork-core/src/agents/` +- **职责**:使用 adk-rust 框架构建 AI Agent,实现 Actor-Critic 自优化循环 +- **核心抽象**:LoopAgent(Actor+Cirtic 循环)、LlmAgentBuilder +- **子模块**: + - `external_coding_agent.rs`:外部 ACP 编码 Agent 集成 + - `legacy_project_analyzer.rs`:遗留项目分析 Agent +- **依赖的模块**:instructions, tools, domain +- **被依赖的模块**:pipeline +- **重要性评分**:10 + +### tools 模块 +- **路径**:`crates/cowork-core/src/tools/` +- **职责**:提供 30+ ADK 工具,涵盖文件操作、命令执行、数据 CRUD、验证、Memory 等 +- **核心抽象**:ToolNotifyFn(工具通知回调,用于 GUI 实时显示) +- **子模块**:file_tools, hitl_tools, test_lint_tools, data_tools, validation_tools, control_tools, artifact_tools, memory_tools, pm_tools, mcp_tools 等 +- **依赖的模块**:domain, persistence +- **重要性评分**:8 + +### llm 模块 +- **路径**:`crates/cowork-core/src/llm/` +- **职责**:LLM 客户端创建与 TokenBucket 速率限制 +- **核心抽象**:`TokenBucketRateLimiter`(`crates/cowork-core/src/llm/rate_limiter.rs:32`) +- **子模块**:config.rs(配置加载)、rate_limiter.rs(速率限制) +- **重要性评分**:8 + +### persistence 模块 +- **路径**:`crates/cowork-core/src/persistence/` +- **职责**:JSON 文件持久化,管理 Project、Iteration、Memory 的存储 +- **核心抽象**:ProjectStore, IterationStore, MemoryStore +- **子模块**:project_store.rs, iteration_store.rs, memory_store.rs, iteration_data.rs +- **重要性评分**:7 + +### domain 模块 +- **路径**:`crates/cowork-core/src/domain/` +- **职责**:核心领域实体定义,采用 DDD 聚合模式 +- **核心抽象**:`Project`(`crates/cowork-core/src/domain/project.rs:6`)、`Iteration`(`crates/cowork-core/src/domain/iteration.rs:8`)、`ProjectMemory`(`crates/cowork-core/src/domain/memory.rs:7`) +- **重要性评分**:7 + +### config_definition 模块 +- **路径**:`crates/cowork-core/src/config_definition/` +- **职责**:数据驱动配置系统,将硬编码的 Agent/Stage/Flow/Integration 定义变为可配置 +- **核心抽象**:`ConfigRegistry`(`crates/cowork-core/src/config_definition/registry.rs:41`)、`AgentDefinition`、`StageDefinition`、`FlowDefinition` +- **子模块**:agent_definition, stage_definition, flow_definition, integration_definition, registry, validator, builtin, agent_factory +- **重要性评分**:7 + +### interaction 模块 +- **路径**:`crates/cowork-core/src/interaction/` +- **职责**:定义 InteractiveBackend trait,抽象 CLI 和 GUI 的用户交互方式 +- **核心抽象**:`InteractiveBackend` trait(`crates/cowork-core/src/interaction/mod.rs:109`) +- **子模块**:cli.rs(CLI 实现)、tauri.rs(Tauri GUI 实现) +- **重要性评分**:6 + +### acp 模块 +- **路径**:`crates/cowork-core/src/acp/` +- **职责**:Agent Client Protocol 客户端,支持外部 Agent(OpenCode/Gemini CLI 等)集成 +- **核心抽象**:`AcpClient`(`crates/cowork-core/src/acp/client.rs`) +- **重要性评分**:5 + +### importer 模块 +- **路径**:`crates/cowork-core/src/importer/` +- **职责**:遗留项目导入与反向工程——分析已有项目结构并生成文档产出 +- **核心抽象**:`ImportConfig`, `ImportResult`, `ImportPreview` +- **子模块**:import_config.rs, project_analyzer.rs, artifact_generator.rs +- **重要性评分**:7 + +### cowork-cli 模块 +- **路径**:`crates/cowork-cli/src/` +- **职责**:CLI 命令行接口(clap + dialoguer),提供项目初始化和迭代管理 +- **核心抽象**:Cli Parser(`crates/cowork-cli/src/main.rs:12`) +- **子模块**:commands/(11 个命令处理模块) +- **重要性评分**:8 + +### cowork-gui 模块 +- **路径**:`crates/cowork-gui/` +- **职责**:Tauri + React + Ant Design 图形界面,提供可视化项目管理 +- **核心文件**:`src-tauri/src/lib.rs`、`src/App.tsx` +- **子模块**:Tauri 后端(commands, project_manager, project_runner)和 React 前端(components, hooks, stores) +- **重要性评分**:8 diff --git a/.terrain/.litho-agent/database.md b/.terrain/.litho-agent/database.md new file mode 100644 index 0000000..1ccf41e --- /dev/null +++ b/.terrain/.litho-agent/database.md @@ -0,0 +1,27 @@ +# 数据库概览报告 + +## 数据库状况 + +**本项目未使用关系型数据库。** + +Cowork Forge 采用 JSON 文件持久化方案,而非传统的关系型数据库。所有数据存储在项目根目录的 `.cowork-v2/` 目录中: + +``` +.cowork-v2/ +├── project.json # 项目信息 +├── iterations/ +│ └── {iteration_id}.json # 每次迭代的快照数据 +├── memory/ +│ ├── project/ +│ │ └── project_memory.json # 项目级记忆决策/模式 +│ └── iterations/ +│ └── {iteration_id}.json # 迭代级知识快照 +└── workspace/ + └── {iteration_id}/ # 迭代的工作区代码文件 +``` + +这种设计决策的原因: +1. **无需数据库运维**——桌面工具追求开箱即用,不依赖数据库服务 +2. **数据结构动态变化**——JSON 文件天然支持 schema 演化,适合迭代式开发 +3. **文件即备份**——整个项目状态就是一组文件,可以直接用 Git 管理 +4. **低并发场景**——单用户桌面工具不需要数据库的并发控制能力 diff --git a/.terrain/.litho-agent/modules/acp.md b/.terrain/.litho-agent/modules/acp.md new file mode 100644 index 0000000..e8c08d4 --- /dev/null +++ b/.terrain/.litho-agent/modules/acp.md @@ -0,0 +1,28 @@ +# acp 模块深度报告 + +## 这个模块在做什么 + +ACP 模块是 Cowork Forge 的"外交部门"——它负责与系统外部的 AI Agent 通信。当内置的 Coding Agent 不够用时,可以通过 ACP(Agent Client Protocol)协议调用外部的编码 Agent(如 OpenCode、Gemini CLI、Claude CLI 等)来完成编码任务。 + +## 核心功能点 + +1. **ACP 客户端**——实现 Agent Client Protocol,支持 stdio 和 WebSocket 两种传输方式。代码位置:`crates/cowork-core/src/acp/client.rs` +2. **外部编码 Agent 集成**——支持 OpenCode、iFlow、Codex、Gemini CLI、Claude CLI 等多种外部 Agent。代码位置:`crates/cowork-core/src/agents/external_coding_agent.rs` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `AcpClient` | `crates/cowork-core/src/acp/client.rs` | ACP 协议客户端,管理外部 Agent 连接 | +| `AcpTaskResult` | `crates/cowork-core/src/acp/mod.rs` | ACP 任务执行结果 | +| `ExternalCodingAgent` | `crates/cowork-core/src/agents/external_coding_agent.rs` | 外部编码 Agent 适配器 | + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| tools | 被依赖 | ACP 工具调用外部 Agent | + +## 跨模块协作场景 + +**在编码阶段配置了外部 Agent 时**:Coding Stage 检查配置 → 如果有 `[coding_agent]` 配置 → 使用 `ExternalCodingAgent` 替代内置 Coding Loop → 通过 `AcpClient` 与外部 Agent 通信 → 外部 Agent 执行编码任务 → 结果返回给 Coding Stage。 diff --git a/.terrain/.litho-agent/modules/agents.md b/.terrain/.litho-agent/modules/agents.md new file mode 100644 index 0000000..adad9fa --- /dev/null +++ b/.terrain/.litho-agent/modules/agents.md @@ -0,0 +1,79 @@ +# agents 模块深度报告 + +## 这个模块在做什么 + +Agents 模块是 Cowork Forge 的"人力资源部"——它负责创建和管理系统中所有的 AI Agent。每个 Agent 就像工厂里的一个"工人",有的当产品经理(PRD Agent),有的当架构师(Design Agent),有的写代码(Coding Agent)。但和人类团队不同,这里的每个"关键岗位"实际上是一对工人:一个负责干活(Actor),一个负责审查(Critic),通过这种"互相监督"的机制来保证输出质量。 + +## 核心功能点 + +1. **Agent 工厂**——提供一系列 `create_*_agent()` 和 `create_*_agent_with_id()` 函数,为 7 个开发阶段创建对应的 Agent 实例。每个 Agent 使用 `LlmAgentBuilder` 构建,注入特定的指令和工具集。代码位置:`crates/cowork-core/src/agents/mod.rs:32-507` +2. **Actor-Critic 循环**——PRD、Design、Plan、Coding 四个阶段使用 `LoopAgent` 组合 Actor 和 Critic 两个 Agent。Actor 先生成内容,Critic 再评审并给出反馈,循环迭代直到输出令人满意。代码位置:`crates/cowork-core/src/agents/mod.rs:68-378` +3. **PM Agent(项目经理 Agent)**——迭代完成后激活的聊天式 Agent,支持查询项目状态、跳转到已有阶段、创建新迭代。通过流式 API 支持 GUI 的实时消息推送。代码位置:`crates/cowork-core/src/agents/mod.rs:548-928` +4. **外部编码 Agent 集成**——通过 ACP 协议调用外部 Agent(如 OpenCode、Gemini CLI)替代内置的 Coding Agent。代码位置:`crates/cowork-core/src/agents/external_coding_agent.rs` +5. **遗留项目分析 Agent**——专门用于分析已有项目结构并反向工程生成文档。代码位置:`crates/cowork-core/src/agents/legacy_project_analyzer.rs` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `create_idea_agent()` | `crates/cowork-core/src/agents/mod.rs:32` | 创建 Idea Agent,捕捉用户需求生成 idea.md | +| `create_prd_loop()` | `crates/cowork-core/src/agents/mod.rs:68` | 创建 PRD Actor+Cirtic LoopAgent,生成并自优化 PRD | +| `create_design_loop()` | `crates/cowork-core/src/agents/mod.rs:149` | 创建 Design Actor+Cirtic LoopAgent,设计技术架构 | +| `create_plan_loop()` | `crates/cowork-core/src/agents/mod.rs:223` | 创建 Plan Actor+Cirtic LoopAgent,分解任务和依赖 | +| `create_coding_loop()` | `crates/cowork-core/src/agents/mod.rs:301` | 创建 Coding Actor+Cirtic LoopAgent(5 次迭代),编写代码 | +| `create_check_agent()` | `crates/cowork-core/src/agents/mod.rs:384` | 创建 Check Agent,验证质量和完整性 | +| `create_delivery_agent()` | `crates/cowork-core/src/agents/mod.rs:444` | 创建 Delivery Agent,生成交付报告 | +| `create_project_manager_agent()` | `crates/cowork-core/src/agents/mod.rs:548` | 创建 PM Agent,交付后聊天交互 | +| `PMAgentResult` | `crates/cowork-core/src/agents/mod.rs:621` | PM Agent 执行结果,包含响应消息和检测到的动作 | +| `PMAgentStreamCallback` trait | `crates/cowork-core/src/agents/mod.rs:652` | 流式回调接口,支持 GUI 实时显示 Agent 输出 | + +## 内部数据流 + +```mermaid +flowchart TD + A["Pipeline 请求
创建阶段 Agent"] --> B{"阶段类型?"} + B -->|Idea/Check/Delivery| C["单 Agent
LlmAgentBuilder"] + B -->|PRD/Design/Plan| D["LoopAgent
Actor + Critic"] + B -->|Coding| E["LoopAgent
Actor + Critic
max_iterations=5"] + + C --> F["注入指令
(instructions 模块)"] + C --> G["注入工具
(tools 模块)"] + C --> H["绑定模型
(llm 模块)"] + C --> I["执行 + 输出"] + + D --> J["Actor: 生成内容
调用数据/文件工具"] + J --> K["Critic: 评审反馈
调用验证工具"] + K --> L{"满意?"} + L -->|否| J + L -->|是| I + + I --> M["保存制品"] +``` + +## 关键接口与扩展点 + +Agent 创建通过 `LlmAgentBuilder` 模式进行,可以灵活组合不同的指令、工具和模型参数。`config_definition/agent_factory.rs` 中的 `create_agent_for_stage()` 和 `create_agent_from_config()` 提供了基于配置文件的 Agent 创建方式,使得用户可以不修改代码就定义新的 Agent 角色。 + +PM Agent 可以通过 MCP 工具集扩展能力(`crates/cowork-core/src/agents/mod.rs:563`)。 + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| instructions | 依赖 | Agent 使用 instructions 模块中的提示词常量构建指令 | +| tools | 依赖 | Agent 需要注入各种 ADK 工具来执行文件/数据/验证操作 | +| domain | 依赖 | 需要访问 Iteration 和 Project 数据来构建上下文 | +| llm | 依赖 | Agent 需要绑定 LLM 模型进行推理 | +| config_definition | 依赖 | 通过 agent_factory 从配置创建 Agent | + +## 跨模块协作场景 + +**在 7-Stage 开发流水线中**:每个阶段执行时,Pipeline 的 `StageExecutor` 调用 agents 模块创建对应的 Agent,注入当前迭代的上下文。Agent 执行过程中调用 tools 模块提供的文件、数据和验证工具,通过 llm 模块的速率限制器与 LLM API 通信。完成后将结果保存到 persistence 模块。 + +## 性能考量 + +所有 Agent 执行都是异步的(基于 Tokio),但 LLM 调用通过 TokenBucketRateLimiter 串行化。Coding Loop 的 max_iterations=5 比其他 Loop(max_iterations=1)更多,因为编码任务通常需要多次迭代才能完成。 + +## 实现亮点 + +**SequentialAgent 终止 Bug 的解决**(`crates/cowork-core/src/agents/mod.rs:4-10`):这是一个值得注意的架构决策。adk-rust 的 LoopAgent 在子 Agent 调用 `exit_loop()` 时会终止整个 SequentialAgent(而非仅终止当前 LoopAgent)。解决方案是将 max_iterations 设为 1,让 LoopAgent 自然完成而非通过 exit_loop 终止。这个妥协保证了 SequentialAgent 中的后续 Agent 能继续执行。 diff --git a/.terrain/.litho-agent/modules/config_definition.md b/.terrain/.litho-agent/modules/config_definition.md new file mode 100644 index 0000000..f79692e --- /dev/null +++ b/.terrain/.litho-agent/modules/config_definition.md @@ -0,0 +1,36 @@ +# config_definition 模块深度报告 + +## 这个模块在做什么 + +ConfigDefinition 是 Cowork Forge 的"规章制度手册"——它把原来写在代码里的 Agent 定义、阶段流程、集成配置等"规定",变成了可以随时修改的配置文件。以前想加一个新的 Agent 角色需要改 Rust 代码重新编译,现在只需要写一个 JSON 配置文件就能注册。 + +## 核心功能点 + +1. **Agent 定义**——`AgentDefinition` 结构体定义了 Agent 的角色、指令、工具集和模型参数,支持内置指令和文件指令两种方式。代码位置:`crates/cowork-core/src/config_definition/agent_definition.rs` +2. **Stage 定义**——`StageDefinition` 配置每个阶段的执行方式(Simple/ActorCritic)、Hook、制品模板和质量标准。代码位置:`crates/cowork-core/src/config_definition/stage_definition.rs` +3. **Flow 定义**——`FlowDefinition` 定义自定义开发流程的阶段组合和执行顺序,支持继承模式配置。代码位置:`crates/cowork-core/src/config_definition/flow_definition.rs` +4. **ConfigRegistry**——全局配置注册表,集中管理所有定义,提供查询、验证和生命周期管理。代码位置:`crates/cowork-core/src/config_definition/registry.rs:41-60` +5. **Agent Factory**——`create_agent_for_stage()` 和 `create_agent_from_config()` 根据配置定义动态创建 Agent 实例。代码位置:`crates/cowork-core/src/config_definition/agent_factory.rs` +6. **MCP 工具集初始化**——支持通过 Model Context Protocol 集成远程 MCP 服务器工具。代码位置:`crates/cowork-core/src/config_definition/agent_factory.rs` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `AgentDefinition` | `crates/cowork-core/src/config_definition/agent_definition.rs` | 定义 Agent 角色、指令、工具和模型参数 | +| `StageDefinition` | `crates/cowork-core/src/config_definition/stage_definition.rs` | 定义阶段的执行方式和 Hook | +| `FlowDefinition` | `crates/cowork-core/src/config_definition/flow_definition.rs` | 定义自定义开发流程的阶段序列 | +| `ConfigRegistry` | `crates/cowork-core/src/config_definition/registry.rs:41` | 全局配置注册表,管理的所有定义集合 | +| `ConfigValidator` | `crates/cowork-core/src/config_definition/validator.rs` | 验证配置定义的完整性和正确性 | + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| pipeline | 被依赖 | Pipeline 查询 Flow 和 Stage 配置 | +| agents | 被依赖 | Agent Factory 根据 AgentDefinition 构建 Agent | +| tools | 被依赖 | MCP 工具集工具注册 | + +## 跨模块协作场景 + +**在流水线执行中**:`IterationExecutor` 调用 `get_stages_from_flow()` 从 `ConfigRegistry` 获取当前流程的阶段定义 → 对每个阶段调用 `create_agent_for_stage()` 根据 `AgentDefinition` 创建 Agent → Agent 执行过程中使用注册的工具集 → 执行结束后检查是否有 Hook 配置需要触发外部集成。 diff --git a/.terrain/.litho-agent/modules/domain.md b/.terrain/.litho-agent/modules/domain.md new file mode 100644 index 0000000..bdbd72d --- /dev/null +++ b/.terrain/.litho-agent/modules/domain.md @@ -0,0 +1,39 @@ +# domain 模块深度报告 + +## 这个模块在做什么 + +Domain 是 Cowork Forge 的"骨架"——它定义了系统里最重要的几个概念:项目(Project)、迭代(Iteration)、记忆(Memory)。就像盖房子先要画蓝图,这些核心实体是所有其他模块的操作基础。特别值得一提的是"迭代"(Iteration)的概念——它不是普通意义上的"重复",而是 Cowork Forge 的核心创新:每个迭代都是一个独立的开发周期,可以"继承"前一个迭代的代码或知识。 + +## 核心功能点 + +1. **Project 实体**——根聚合,管理项目名称、迭代列表、当前迭代 ID。支持添加迭代、设置当前迭代、获取最新完成的迭代。代码位置:`crates/cowork-core/src/domain/project.rs:6-50` +2. **Iteration 实体**——核心聚合,代表一个完整的开发周期。支持 Genesis(首次)和 Evolution(演化)两种创建方式,管理状态流转(Draft→Running→Paused→Completed→Failed)和阶段进度。代码位置:`crates/cowork-core/src/domain/iteration.rs:8-100` +3. **InheritanceMode 继承模式**——定义了迭代间的三种继承策略:None(全新开始)、Full(完整复制代码+制品)、Partial(只复制制品,代码重新生成)。代码位置:`crates/cowork-core/src/domain/iteration.rs` +4. **ProjectMemory 记忆系统**——跨迭代的知识累积,包括决策(Decisions)、模式(Patterns)、项目上下文(Context)。支持按关键词查询和历史知识快照管理。代码位置:`crates/cowork-core/src/domain/memory.rs:7-80` +5. **Artifacts 制品集合**——每次迭代的产出物容器,包括 idea.md、prd.md、design.md、plan.md、delivery_report.md 等。代码位置:`crates/cowork-core/src/domain/iteration.rs` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `Project` | `crates/cowork-core/src/domain/project.rs:6` | 项目根实体,管理名称、迭代清单和状态 | +| `Iteration` | `crates/cowork-core/src/domain/iteration.rs:8` | 开发周期实体,管理状态、阶段进度、制品 | +| `InheritanceMode` | `crates/cowork-core/src/domain/iteration.rs` | 迭代继承策略枚举(None/Full/Partial) | +| `Artifacts` | `crates/cowork-core/src/domain/iteration.rs` | 迭代产出物容器 | +| `ProjectMemory` | `crates/cowork-core/src/domain/memory.rs:7` | 项目级跨迭代记忆系统 | +| `IterationKnowledge` | `crates/cowork-core/src/domain/memory.rs:83` | 单次迭代的知识快照 | +| `Decision` | `crates/cowork-core/src/domain/memory.rs` | 项目的关键决策记录 | +| `Pattern` | `crates/cowork-core/src/domain/memory.rs` | 项目中发现的可复用模式 | + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| persistence | 被依赖 | 持久化模块负责保存和加载 Project/Iteration/Memory | +| pipeline | 被依赖 | Pipeline 读取和更新 Iteration 状态 | +| agents | 被依赖 | Agent 需要访问 Project 和 Iteration 上下文数据 | +| tools | 间接 | 数据工具通过 domain 类型操作迭代数据 | + +## 跨模块协作场景 + +**在迭代执行过程中**:Pipeline 读取 Project 获取当前迭代 ID → 从 Persistence 加载 Iteration → 执行阶段并更新 Iteration 状态 → 保存 Iteration 到 Persistence → 每次迭代完成后 Knowledge Generation Agent 提取关键决策和模式 → 保存到 ProjectMemory。 diff --git a/.terrain/.litho-agent/modules/importer.md b/.terrain/.litho-agent/modules/importer.md new file mode 100644 index 0000000..aab7ae8 --- /dev/null +++ b/.terrain/.litho-agent/modules/importer.md @@ -0,0 +1,30 @@ +# importer 模块深度报告 + +## 这个模块在做什么 + +Importer 是 Cowork Forge 的"考古学家"——当你想把一个已有的项目纳入 Cowork Forge 管理时,它会自动分析项目结构、检测技术栈、读取关键配置和代码,然后用 LLM 把所有这些信息综合成结构化的文档产出(idea.md、prd.md、design.md、plan.md)。它让用户不需要从头开始,而是可以把现有项目"带进"Cowork Forge 的迭代体系。 + +## 核心功能点 + +1. **项目导入配置**——定义导入参数和选项(`ImportConfig`),包括哪些文档需要生成、是否使用 LLM、项目名称。代码位置:`crates/cowork-core/src/importer/import_config.rs` +2. **项目分析器**——`ProjectAnalyzer` 扫描目录结构、检测技术栈、提取关键信息。代码位置:`crates/cowork-core/src/importer/project_analyzer.rs` +3. **制品生成器**——`ArtifactGenerator` 使用 LLM 将分析结果综合成标准文档产出。代码位置:`crates/cowork-core/src/importer/artifact_generator.rs` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `ImportConfig` | `crates/cowork-core/src/importer/import_config.rs` | 导入参数配置 | +| `ImportResult` | `crates/cowork-core/src/importer/mod.rs` | 导入结果,包含生成的制品和导入预览 | +| `ProjectAnalysis` | `crates/cowork-core/src/importer/mod.rs` | 项目分析结果,包含结构和技术栈信息 | + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| domain | 依赖 | 导入器创建 Project 和 Iteration 实体 | +| persistence | 依赖 | 导入器需要保存 Project 和 Iteration | + +## 跨模块协作场景 + +**在导入流程中**:CLI 的 `import` 命令调用 Importer → 分析项目结构(`project_analyzer.rs`)→ 生成文档制品(`artifact_generator.rs`)→ 创建 Project 和 Genesis Iteration → 保存到 persistence。 diff --git a/.terrain/.litho-agent/modules/instructions.md b/.terrain/.litho-agent/modules/instructions.md new file mode 100644 index 0000000..b032015 --- /dev/null +++ b/.terrain/.litho-agent/modules/instructions.md @@ -0,0 +1,33 @@ +# instructions 模块深度报告 + +## 这个模块在做什么 + +Instructions 是 Cowork Forge 的"岗位说明书"集合——它包含了所有 Agent 角色的提示词(Prompt)。每个 Agent 在创建时都会加载对应角色的指令,这些指令定义了 Agent 的角色定位、行为规则、可用工具和目标输出。 + +## 核心功能点 + +1. **阶段 Actor 指令**——为 Idea、PRD、Design、Plan、Coding 阶段的 Actor Agent 提供生成指令。代码位置:`crates/cowork-core/src/instructions/idea.rs` 到 `coding.rs` +2. **阶段 Critic 指令**——为 PRD、Design、Plan、Coding 阶段的 Critic Agent 提供评审和反馈指令。 +3. **Check/Delivery 指令**——验证和交付阶段的专属指令。 +4. **PM Agent 指令**——项目交付后 PM Agent 的交互指令。代码位置:`crates/cowork-core/src/instructions/project_manager.rs` +5. **Summary/KG 指令**——迭代摘要和知识生成 Agent 的指令。代码位置:`crates/cowork-core/src/instructions/summary.rs`、`knowledge_gen.rs` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `IDEA_AGENT_INSTRUCTION` | `crates/cowork-core/src/instructions/idea.rs` | Idea Agent 的提示词 | +| `PRD_ACTOR_INSTRUCTION` | `crates/cowork-core/src/instructions/prd.rs` | PRD Actor 的生成指令 | +| `PRD_CRITIC_INSTRUCTION` | `crates/cowork-core/src/instructions/prd.rs` | PRD Critic 的评审指令 | +| `CODING_ACTOR_INSTRUCTION` | `crates/cowork-core/src/instructions/coding.rs` | Coding Actor 的编码指令 | +| `PROJECT_MANAGER_AGENT_INSTRUCTION` | `crates/cowork-core/src/instructions/project_manager.rs` | PM Agent 的交互指令 | + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| agents | 被依赖 | Agent 工厂引用指令常量构建 Agent | + +## 跨模块协作场景 + +**在 Agent 创建时**:Agent 工厂调用 `create_prd_loop()` → 函数内引用 `PRD_ACTOR_INSTRUCTION` 和 `PRD_CRITIC_INSTRUCTION` → 将指令注入到 `LlmAgentBuilder` → Agent 执行时按指令规定的方式工作。 diff --git a/.terrain/.litho-agent/modules/integration.md b/.terrain/.litho-agent/modules/integration.md new file mode 100644 index 0000000..8de69da --- /dev/null +++ b/.terrain/.litho-agent/modules/integration.md @@ -0,0 +1,30 @@ +# integration 模块深度报告 + +## 这个模块在做什么 + +Integration 是 Cowork Forge 的"API 网关"——它允许在流水线执行过程中触发外部系统调用。比如,当 Delivery 阶段完成后,自动触发一个 Webhook 通知部署平台开始部署,或者将 PRD 内容同步到需求管理工具。 + +## 核心功能点 + +1. **Hook 管理器**——`HookManager` 在特定执行点(阶段完成、失败等)触发预配置的回调。代码位置:`crates/cowork-core/src/integration/hooks.rs` +2. **REST Adapter**——`RestAdapter` 实现标准的 REST API 调用,支持 POST 请求和认证配置。代码位置:`crates/cowork-core/src/integration/rest_adapter.rs` +3. **Integration 定义**——配置集成类型、连接参数、认证方式和触发事件。代码位置:`crates/cowork-core/src/integration_definition.rs`(config_definition 模块) + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `HookManager` | `crates/cowork-core/src/integration/hooks.rs` | 管理执行钩子,在特定事件点触发外部操作 | +| `RestAdapter` | `crates/cowork-core/src/integration/rest_adapter.rs` | REST API 调用适配器 | +| `IntegrationAdapter` trait | `crates/cowork-core/src/integration/adapters.rs` | 各种集成适配器的统一接口 | + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| pipeline | 被依赖 | Pipeline 在阶段执行前后触发 Hook | +| config_definition | 依赖 | 从 ConfigRegistry 获取集成配置 | + +## 跨模块协作场景 + +**在阶段完成时**:Pipeline 执行完一个阶段后 → 检查 ConfigRegistry 中是否有对应阶段的 Hook 配置 → 如果有,调用 HookManager 执行 → HookManager 根据配置调用 RestAdapter(或其他适配器)→ 外部系统收到通知 → Pipeline 继续执行。 diff --git a/.terrain/.litho-agent/modules/interaction.md b/.terrain/.litho-agent/modules/interaction.md new file mode 100644 index 0000000..e29b527 --- /dev/null +++ b/.terrain/.litho-agent/modules/interaction.md @@ -0,0 +1,33 @@ +# interaction 模块深度报告 + +## 这个模块在做什么 + +Interaction 是 Cowork Forge 的"翻译官"——它定义了系统内核与用户之间的沟通协议。内核只需要调用 `show_message()`、`request_input()` 等方法,具体的展示形式(命令行打印还是图形弹窗)由实现者决定。这种设计让同一套内核代码既能服务 CLI,也能服务 GUI 界面。 + +## 核心功能点 + +1. **InteractiveBackend trait**——定义了所有用户交互的接口:消息展示、输入请求、进度通知、流式推送。代码位置:`crates/cowork-core/src/interaction/mod.rs:108-160` +2. **CliBackend 实现**——基于 dialoguer 和 console 的 CLI 交互实现。代码位置:`crates/cowork-core/src/interaction/cli.rs` +3. **TauriBackend 实现**——基于 Tauri 事件系统的 GUI 交互实现。代码位置:`crates/cowork-core/src/interaction/tauri.rs` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `InteractiveBackend` trait | `crates/cowork-core/src/interaction/mod.rs:109` | 定义用户交互的统一接口 | +| `CliBackend` | `crates/cowork-core/src/interaction/cli.rs` | CLI 模式的交互实现 | +| `TauriBackend` | `crates/cowork-core/src/interaction/tauri.rs` | Tauri GUI 模式的交互实现 | +| `MessageContext` | `crates/cowork-core/src/interaction/mod.rs:51` | 消息上下文,包含 Agent 名称、消息类型和阶段信息 | +| `ProgressInfo` | `crates/cowork-core/src/interaction/mod.rs:101` | 长时间运行任务的进度信息 | + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| pipeline | 被依赖 | Pipeline 通过 InteractiveBackend 与用户交互 | +| cowork-cli | 实现 | CLI 创建 CliBackend 实例注入 Pipeline | +| cowork-gui | 实现 | GUI 创建 TauriBackend 实例注入 Pipeline | + +## 跨模块协作场景 + +**在阶段执行过程中**:`IterationExecutor` 调用 `interaction.show_message()` 显示阶段开始 → Agent 执行过程中通过 `interaction.send_streaming()` 推送实时输出 → 关键决策点通过 `interaction.request_input()` 请求用户确认 → "确认通过"后继续执行。 diff --git a/.terrain/.litho-agent/modules/llm.md b/.terrain/.litho-agent/modules/llm.md new file mode 100644 index 0000000..2f5d89b --- /dev/null +++ b/.terrain/.litho-agent/modules/llm.md @@ -0,0 +1,49 @@ +# llm 模块深度报告 + +## 这个模块在做什么 + +LLM 模块是 Cowork Forge 的"大脑接口"——它负责与外部大语言模型 API 通信,并确保通信过程不会因过于频繁的请求而被服务商限流。简单来说,它就像工厂的"电力系统":没有它所有机器都转不起来,但如果电压不稳(API 限流),整个工厂都会瘫痪。 + +## 核心功能点 + +1. **LLM 客户端创建**——从 config.toml 加载配置创建 OpenAI 兼容的 LLM 客户端。代码位置:`crates/cowork-core/src/llm/config.rs` +2. **TokenBucket 速率限制**——实现 TokenBucket 算法,允许 5 个突发请求同时进行,长期平均速率为 30 req/min。代码位置:`crates/cowork-core/src/llm/rate_limiter.rs:32-60` +3. **限流器装饰器模式**——`TokenBucketRateLimiter` 实现了 `Llm` trait,作为装饰器包裹真实的 LLM 客户端,对上层调用者完全透明。代码位置:`crates/cowork-core/src/llm/rate_limiter.rs` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `TokenBucketRateLimiter` | `crates/cowork-core/src/llm/rate_limiter.rs:32` | TokenBucket 速率限制装饰器,透明控制 LLM 请求频率 | +| `create_llm_client()` | `crates/cowork-core/src/llm/config.rs` | 从配置文件创建 LLM 客户端 | +| `load_config()` | `crates/cowork-core/src/llm/config.rs` | 加载 LLM 配置(API 地址、密钥、模型名) | + +## 内部数据流 + +```mermaid +flowchart TD + A["Agent 请求 LLM"] --> B["TokenBucketRateLimiter
检查令牌"] + B --> C{"有可用令牌?"} + C -->|是| D["消耗令牌
转发请求"] + C -->|否| E["等待令牌补充
(60s/rate 间隔)"] + E --> B + D --> F["真实 LLM API"] + F --> G["返回响应"] + G --> H["释放/补充令牌"] + H --> A +``` + +## 关键接口与扩展点 + +`TokenBucketRateLimiter` 实现了 `adk_core::Llm` trait,可以透明地替换或包装任何其他 `Llm` 实现。参数 `max_burst` 和 `rate_limit_per_minute` 可配置。 + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| agents | 被依赖 | Agent 构建时需要绑定 LLM 模型 | +| pipeline | 被依赖 | StageExecutor 创建 LLM 客户端供所有阶段使用 | + +## 跨模块协作场景 + +**在每个 Agent 执行过程中**:Agent 每次调用 LLM 推理时 → 经过 `TokenBucketRateLimiter` 获取令牌 → 如果没令牌就等待 → 获取令牌后调用真实 LLM API → 返回结果 → 补充令牌。整个过程对 Agent 完全透明。 diff --git a/.terrain/.litho-agent/modules/persistence.md b/.terrain/.litho-agent/modules/persistence.md new file mode 100644 index 0000000..74e9fd3 --- /dev/null +++ b/.terrain/.litho-agent/modules/persistence.md @@ -0,0 +1,33 @@ +# persistence 模块深度报告 + +## 这个模块在做什么 + +Persistence 是 Cowork Forge 的"文件柜"——它负责把系统中的所有数据(项目信息、迭代快照、项目记忆)写到硬盘上的 JSON 文件中。之所以不用数据库而用 JSON 文件,是因为桌面工具追求的是"开箱即用"——用户不需要安装和配置数据库就能用。 + +## 核心功能点 + +1. **ProjectStore**——保存和加载项目根信息(Project 实体)。代码位置:`crates/cowork-core/src/persistence/project_store.rs` +2. **IterationStore**——保存和加载每次迭代的完整快照(Iteration 实体),包括迭代历史、阶段进度、制品。代码位置:`crates/cowork-core/src/persistence/iteration_store.rs` +3. **MemoryStore**——保存和加载项目级记忆(ProjectMemory),包括决策、模式、迭代知识快照。代码位置:`crates/cowork-core/src/persistence/memory_store.rs` +4. **IterationData**——迭代数据的结构和序列化。代码位置:`crates/cowork-core/src/persistence/iteration_data.rs` +5. **工作区路径管理**——支持 GUI 模式下设置全局工作区路径,解决 macOS 应用包启动时的路径问题。代码位置:`crates/cowork-core/src/persistence/mod.rs:20-35` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `ProjectStore` | `crates/cowork-core/src/persistence/project_store.rs` | 项目根信息的 JSON 文件读写 | +| `IterationStore` | `crates/cowork-core/src/persistence/iteration_store.rs` | 迭代快照的 JSON 文件管理 | +| `MemoryStore` | `crates/cowork-core/src/persistence/memory_store.rs` | 项目记忆的 JSON 文件持久化 | + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| domain | 依赖 | 持久化 domain 类型(Project/Iteration/ProjectMemory) | +| pipeline | 被依赖 | Pipeline 通过 Store 保存和加载项目/迭代数据 | +| tools | 被依赖 | 数据工具通过 Store 操作迭代数据 | + +## 跨模块协作场景 + +**在迭代创建和保存过程中**:`IterationExecutor.create_genesis_iteration()` 创建 Iteration 实体 → 调用 `IterationStore.save()` 写 JSON 文件 → 调用 `ProjectStore.add_iteration()` 更新项目信息。整个流程是"内存中构建对象 → 序列化 JSON → 写入文件"的三步模式。 diff --git a/.terrain/.litho-agent/modules/pipeline.md b/.terrain/.litho-agent/modules/pipeline.md new file mode 100644 index 0000000..2ba56bc --- /dev/null +++ b/.terrain/.litho-agent/modules/pipeline.md @@ -0,0 +1,73 @@ +# pipeline 模块深度报告 + +## 这个模块在做什么 + +Pipeline 是 Cowork Forge 的"流水线传送带"——它负责把 7 个开发阶段按正确的顺序串联起来,管理每个阶段的执行、暂停、重试和跳转。如果把整个系统比作一个工厂,Pipeline 就是那个决定"什么零件什么时候送到哪个工位"的生产调度中心。 + +## 核心功能点 + +1. **7 阶段流水线编排**——定义了从 Idea 到 Delivery 的 7 个开发阶段的执行顺序,支持从任意阶段开始执行(`get_stages_from()`)和根据流程配置动态创建阶段(`get_stages_from_flow()`)。代码位置:`crates/cowork-core/src/pipeline/mod.rs:77-94` +2. **Flow 配置驱动**——通过 ConfigRegistry 可以定义自定义流程(自定义阶段组合和顺序),系统会自动根据流程配置创建对应的阶段实例。代码位置:`crates/cowork-core/src/pipeline/mod.rs:116-143` +3. **迭代执行器**——`IterationExecutor` 是统一的迭代生命周期管理器,负责创建迭代(Genesis/Evolution)、保存状态、执行所有阶段。代码位置:`crates/cowork-core/src/pipeline/executor/mod.rs:17-80` +4. **Actor-Critic 阶段执行**——`stage_executor.rs` 实现了配置驱动的阶段执行框架,根据阶段类型(Simple 或 Actor-Critic)选择合适的执行策略,处理反馈和迭代。代码位置:`crates/cowork-core/src/pipeline/stage_executor.rs:1-80` +5. **知识生成**——迭代完成后自动触发知识生成,提取关键决策、模式和学习心得。代码位置:`crates/cowork-core/src/pipeline/executor/knowledge.rs` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `Stage` trait | `crates/cowork-core/src/pipeline/mod.rs:47` | 定义所有开发阶段的统一接口(execute + execute_with_feedback) | +| `PipelineContext` | `crates/cowork-core/src/pipeline/mod.rs:29` | 保存流水线执行的上下文信息(项目、迭代、工作区路径) | +| `StageResult` | `crates/cowork-core/src/pipeline/mod.rs:19` | 阶段执行结果枚举(Success/Failed/Paused/NeedsRevision/GotoStage) | +| `IterationExecutor` | `crates/cowork-core/src/pipeline/executor/mod.rs:17` | 迭代执行器,统一管理迭代生命周期的创建和执行 | +| `StageExecutor` | `crates/cowork-core/src/pipeline/stage_executor.rs` | 配置驱动的阶段执行框架,处理 Agent 创建、执行和反馈循环 | + +## 内部数据流 + +```mermaid +flowchart TD + A["用户请求
cowork iter"] --> B["IterationExecutor
create + execute"] + B --> C{"阶段类型?"} + C -->|Simple| D["单 Agent 执行
Idea/Check/Delivery"] + C -->|Loop| E["Actor-Critic 循环
PRD/Design/Plan/Coding"] + D --> F{"结果?"} + E --> F + F -->|Success| G["保存制品
+ 人类验证"] + F -->|NeedsRevision| E + F -->|Failed| H["暂停 + 报错"] + G -->|通过| I["下一步段"] + G -->|拒绝| E + I --> J{还有阶段?} + J -->|是| C + J -->|否| K["知识生成
+ 交付"] +``` + +关键步骤: +1. 用户请求到达 `crates/cowork-cli/src/main.rs`,通过 CLI 命令路由到 `commands/iter.rs` +2. `IterationExecutor` 创建迭代并开始执行阶段序列(`crates/cowork-core/src/pipeline/executor/mod.rs:79`) +3. 每个阶段通过 `Stage` trait 的 `execute()` 方法执行,结果返回 `StageResult` +4. 对于 Loop 类型阶段,Actor-Critic 循环可能因 `NeedsRevision` 多次迭代 +5. 人类验证关键阶段的输出后才能继续 + +## 扩展点 + +ConfigRegistry 允许定义自定义 Flow(`crates/cowork-core/src/config_definition/flow_definition.rs`),可以重新排列阶段顺序、跳过某些阶段、或者添加自定义 Hook。Integration 系统(`crates/cowork-core/src/integration/`)允许在阶段完成后触发外部 Webhook。 + +## 与其他模块的交互 + +| 交互模块 | 方向 | 接口/协议 | 说明 | +|---------|------|---------|------| +| agents | 依赖 | `create_prd_loop()`, `create_idea_agent()` 等 | Pipeline 调用 Agent 工厂创建各阶段 Agent | +| domain | 依赖 | `Project`, `Iteration` | Pipeline 读取项目/迭代状态并更新 | +| llm | 依赖 | `create_llm_client()`, `TokenBucketRateLimiter` | Pipeline 创建 LLM 客户端供 Agent 使用 | +| interaction | 依赖 | `InteractiveBackend` trait | Pipeline 通过交互后端与用户通信 | +| persistence | 依赖 | `ProjectStore`, `IterationStore` | Pipeline 保存和加载项目/迭代数据 | +| config_definition | 依赖 | `ConfigRegistry` | Pipeline 查询流程配置和 Agent 定义 | + +## 跨模块协作场景 + +**在 7-Stage 开发流水线中**:Pipeline 是主调度器,它按顺序实例化每个阶段的 Agent,提供执行上下文,处理结果流转。具体来说:Pipeline 从 ConfigRegistry 获取流程定义 → 创建 LLM 客户端(llm 模块)→ 构建 Agent(agents 模块)→ 注册交互后端(interaction 模块)→ 执行阶段 → 保存结果(persistence 模块)→ 重复直到所有阶段完成。 + +## 性能考量 + +Pipeline 的执行是串行化的——每个阶段必须等待前一个阶段完成后才能开始。这是设计使然,因为开发流程本质上是有序的(不能在设计完成之前就开始编码)。LLM 调用通过 TokenBucketRateLimiter 串行化,确保不会触发 API 速率限制。文件操作和命令执行异步执行,不会阻塞主流程。 diff --git a/.terrain/.litho-agent/modules/skills.md b/.terrain/.litho-agent/modules/skills.md new file mode 100644 index 0000000..99fbf2e --- /dev/null +++ b/.terrain/.litho-agent/modules/skills.md @@ -0,0 +1,30 @@ +# skills 模块深度报告 + +## 这个模块在做什么 + +Skills 模块是 Cowork Forge 的"技能培训中心"——它实现了 agentskills.io 标准,允许通过 Skill 包向 Agent 注入特定领域的知识、工具和提示词。比如,如果项目需要构建 React 前端,可以加载一个"React 开发技能包",让 Coding Agent 自动获得 React 相关的最佳实践和工具。 + +## 核心功能点 + +1. **Skill 管理**——`SkillManager` 管理 Skill 文档的加载、索引和查询。代码位置:`crates/cowork-core/src/skills/manager.rs` +2. **Skill 注入**——`SkillInjector` 负责将匹配的 Skill 注入到 Agent 的指令中。代码位置:`crates/cowork-core/src/skills/mod.rs` +3. **Skill 发现**——支持从 `.skills/` 目录自动发现 Skill 文件。代码位置:`crates/cowork-core/src/skills/mod.rs` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `SkillManager` | `crates/cowork-core/src/skills/manager.rs` | Skill 生命周期管理(加载、索引、查询) | +| `SkillInjector` | `crates/cowork-core/src/skills/mod.rs` | Skill 注入器,将匹配的 Skill 插入 Agent 指令 | +| `SkillDocument` | `crates/cowork-core/src/skills/mod.rs` | Skill 文档的标准化表示 | + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| agents | 被依赖 | Agent 创建时可以注入匹配的 Skill | +| config_definition | 被依赖 | Skill 配置可以在 ConfigRegistry 中注册 | + +## 跨模块协作场景 + +**在 Agent 创建过程中**:Agent Factory 创建 Agent 时 → 调用 SkillManager 查询匹配的 Skill → 调用 SkillInjector 将 Skill 内容注入 Agent 指令 → Agent 执行时使用注入的知识和工具。 diff --git a/.terrain/.litho-agent/modules/tools.md b/.terrain/.litho-agent/modules/tools.md new file mode 100644 index 0000000..8c536e3 --- /dev/null +++ b/.terrain/.litho-agent/modules/tools.md @@ -0,0 +1,44 @@ +# tools 模块深度报告 + +## 这个模块在做什么 + +Tools 模块是 Cowork Forge 的"工具箱"——它提供了 30 多个 ADK 标准工具,Agent 在"工作"时就用这些工具来操作文件、执行命令、读写数据、验证结果、与用户交互。没有这些工具,Agent 就像工人没有扳手和螺丝刀,只能看不能干。 + +## 核心功能点 + +1. **文件操作工具**——`ReadFileTool`、`WriteFileTool`、`ListFilesTool` 等,支持安全的工作区边界文件操作。代码位置:`crates/cowork-core/src/tools/file_tools.rs` +2. **人类在环工具**——`ReviewWithFeedbackContentTool`、`ProvideFeedbackTool` 等,在关键决策点暂停流水线请求人类确认。代码位置:`crates/cowork-core/src/tools/hitl_tools.rs`、`hitl_content_tools.rs` +3. **数据 CRUD 工具**——`CreateRequirementTool`、`CreateTaskTool`、`GetRequirementsTool`、`GetDesignTool`、`GetPlanTool` 等,管理迭代数据。代码位置:`crates/cowork-core/src/tools/data_tools.rs` +4. **验证工具**——`CheckFeatureCoverageTool`、`CheckTaskDependenciesTool`、`CheckDataFormatTool`、`CheckTestsTool`、`CheckLintTool`,质量把关。代码位置:`crates/cowork-core/src/tools/validation_tools.rs` +5. **Memory 工具**——`QueryMemoryTool`、`SaveInsightTool`、`SaveIssueTool`、`SaveLearningTool`、`PromoteToDecisionTool`、`PromoteToPatternTool`,跨迭代知识管理。代码位置:`crates/cowork-core/src/tools/memory_tools.rs` +6. **PM 工具**——`PMGotoStageTool`、`PMCreateIterationTool`、`PMRespondTool`、`PMSaveDecisionTool`,PM Agent 专用。代码位置:`crates/cowork-core/src/tools/pm_tools.rs` +7. **工具通知系统**——全局回调机制,实时广播工具调用到 GUI。代码位置:`crates/cowork-core/src/tools/mod.rs:36-104` + +## 关键组件 + +| 组件/类型 | 文件路径 | 一句话职责 | +|---------|---------|----------| +| `ReadFileTool` | `crates/cowork-core/src/tools/file_tools.rs` | 读取工作区文件内容 | +| `WriteFileTool` | `crates/cowork-core/src/tools/file_tools.rs` | 写入文件到工作区 | +| `ExecuteShellCommandTool` | `crates/cowork-core/src/tools/test_lint_tools.rs` | 执行 Shell 命令(构建/测试等) | +| `QueryMemoryTool` | `crates/cowork-core/src/tools/memory_tools.rs` | 查询项目记忆 | +| `ReviewWithFeedbackContentTool` | `crates/cowork-core/src/tools/hitl_content_tools.rs` | 请求人类对内容给出反馈 | +| `CheckTestsTool` | `crates/cowork-core/src/tools/validation_tools.rs` | 检查测试是否通过 | +| `PMGotoStageTool` | `crates/cowork-core/src/tools/pm_tools.rs` | PM Agent 跳转到指定阶段 | +| `ToolNotifyFn` | `crates/cowork-core/src/tools/mod.rs:36` | 工具通知回调类型,GUI 实时显示工具调用 | + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| domain | 依赖 | 操作 Iteration 的制品和任务数据 | +| persistence | 依赖 | 保存和加载数据 | +| llm | 间接依赖 | 部分工具间接需要 LLM 功能 | + +## 跨模块协作场景 + +**在 Coding 阶段**:Coding Actor 调用 `ReadFileTool` 读取现有代码 → `WriteFileTool` 写入新代码 → `ExecuteShellCommandTool` 运行构建和测试 → Coding Critic 调用 `ReadFileTool` 审查代码质量 → `ProvideFeedbackTool` 给出改进建议。 + +## 性能考量 + +文件操作使用 Tokio 异步封装,不会阻塞主线程。命令执行有超时控制,防止长时间运行的构建任务耗尽资源。工具通知系统设计为回调模式而非轮询,对性能影响极小。 diff --git a/.terrain/.litho-agent/preprocessing.md b/.terrain/.litho-agent/preprocessing.md new file mode 100644 index 0000000..49822b2 --- /dev/null +++ b/.terrain/.litho-agent/preprocessing.md @@ -0,0 +1,93 @@ +# 预处理报告 + +## 项目基本信息 +- **项目名称**:Cowork Forge +- **版本**:2.5.2 +- **项目类型**:AI-native 多 Agent 软件开发平台(CLI + GUI) +- **主要编程语言**:Rust(主要), TypeScript + React(GUI 前端) +- **核心框架/运行时**:adk-rust(Agent Development Kit), Tokio 异步运行时, Tauri 桌面框架 + +## 技术栈 +- **运行时**:Rust (edition 2024), Node.js (GUI 前端构建) +- **Web 框架**:无(Tauri 内置 WebView) +- **数据库**:无关系型数据库,采用 JSON 文件持久化 +- **LLM/AI 集成**:OpenAI-compatible API, TokenBucketRateLimiter(30 req/min, concurrency=1) +- **主要依赖库**: + - `adk-rust / adk-core / adk-agent / adk-model` — ADK Agent 框架 + - `tokio`(full features)— 异步运行时 + - `clap` + `dialoguer` — CLI 交互 + - `serde / serde_json` — 序列化 + - `tracing` — 日志 + - `agent-client-protocol` — 外部 Agent 集成(ACP) + +## 目录结构摘要 +``` +cowork-forge/ +├── crates/ +│ ├── cowork-core/src/ # 核心逻辑(领域、管道、工具、Agent) +│ │ ├── pipeline/ # 7 阶段开发流水线编排 +│ │ ├── domain/ # 核心领域实体(Project, Iteration, Memory) +│ │ ├── tools/ # 30+ ADK 工具(文件、命令、验证等) +│ │ ├── agents/ # Agent 构建器(LoopAgent + Actor-Critic) +│ │ ├── llm/ # LLM 集成 + 速率限制 +│ │ ├── config_definition/ # 数据驱动配置(Agent/Stage/Flow/Integration) +│ │ ├── interaction/ # InteractiveBackend trait(CLI/GUI 抽象) +│ │ ├── persistence/ # JSON 文件存储(Project/Iteration/Memory) +│ │ ├── instructions/ # Agent 提示词库 +│ │ ├── acp/ # Agent Client Protocol 外部 Agent 集成 +│ │ ├── skills/ # agentskills.io 标准技能系统 +│ │ ├── integration/ # 外部集成 Hook 管理器 +│ │ ├── importer/ # 遗留项目导入分析器 +│ │ └── data/ # 数据模型 +│ ├── cowork-cli/src/ # CLI 适配器(clap + dialoguer) +│ │ └── commands/ # CLI 命令实现(11 个命令) +│ └── cowork-gui/ # Tauri + React GUI +│ ├── src-tauri/src/ # Rust 后端(Tauri 命令 + 事件) +│ └── src/ # 前端(TypeScript + Ant Design) +├── .terrain/ # Terrain AI 工程环境 +├── litho.docs/ # 已有的 Litho 文档 +└── assets/ # 资源文件 +``` + +## 识别到的核心模块 +| 模块名称 | 路径 | DDD 分类 | 职责描述 | +|---------|------|---------|---------| +| pipeline | `crates/cowork-core/src/pipeline/` | 核心域 | 7 阶段开发流水线编排(Idea→PRD→Design→Plan→Coding→Check→Delivery) | +| domain | `crates/cowork-core/src/domain/` | 通用域 | 核心领域实体:Project、Iteration、Memory 聚合 | +| agents | `crates/cowork-core/src/agents/` | 核心域 | AI Agent 构建器,包含 Idea/PRD/Design/Plan/Coding/Check/Delivery Agent | +| tools | `crates/cowork-core/src/tools/` | 支撑域 | 30+ ADK 工具实现(文件操作、命令执行、验证、Memory 等) | +| llm | `crates/cowork-core/src/llm/` | 支撑域 | LLM 客户端集成与 TokenBucket 速率限制 | +| config_definition | `crates/cowork-core/src/config_definition/` | 支撑域 | 数据驱动配置系统(Agent/Stage/Flow/Integration 定义) | +| interaction | `crates/cowork-core/src/interaction/` | 通用域 | InteractiveBackend trait(CLI/GUI 抽象) | +| persistence | `crates/cowork-core/src/persistence/` | 支撑域 | JSON 文件持久化(Project/Iteration/Memory 存储) | +| instructions | `crates/cowork-core/src/instructions/` | 支撑域 | Agent 提示词库(每个阶段的 Actor/Critic 指令) | +| acp | `crates/cowork-core/src/acp/` | 支撑域 | Agent Client Protocol 外部 Agent 集成 | +| skills | `crates/cowork-core/src/skills/` | 通用域 | agentskills.io 标准技能系统(SkillManager、SkillInjector) | +| integration | `crates/cowork-core/src/integration/` | 支撑域 | 外部集成 Hook 管理器(REST Adapter + Webhook) | +| importer | `crates/cowork-core/src/importer/` | 核心域 | 遗留项目导入分析器(反向工程生成文档) | +| data | `crates/cowork-core/src/data/` | 通用域 | 数据模型定义 | +| project_runtime | `crates/cowork-core/src/project_runtime.rs` | 支撑域 | 项目运行时配置(GUI Preview/Run) | +| cowork-cli | `crates/cowork-cli/src/` | 核心域 | CLI 命令行接口 | +| cowork-gui | `crates/cowork-gui/` | 核心域 | Tauri + React 图形界面 | + +## 关键文件清单 +- **入口文件**:`crates/cowork-cli/src/main.rs`、`crates/cowork-core/src/lib.rs`、`crates/cowork-gui/src-tauri/src/main.rs` +- **核心抽象**:`crates/cowork-core/src/interaction/mod.rs`(InteractiveBackend trait)、`crates/cowork-core/src/pipeline/mod.rs`(Stage trait) +- **数据类型**:`crates/cowork-core/src/domain/project.rs`(Project)、`crates/cowork-core/src/domain/iteration.rs`(Iteration)、`crates/cowork-core/src/domain/memory.rs`(ProjectMemory) + +## 依赖关系摘要 +- **pipeline → domain, agents, llm, config_definition, interaction, persistence**(管道编排需要所有模块) +- **agents → instructions, tools, domain**(Agent 构建需要指令、工具、数据) +- **config_definition → (无核心依赖,自成体系)**(配置定义模块) +- **tools → domain, persistence**(工具需要访问领域数据和持久化) +- **acp → (无核心依赖)**(外部 Agent 协议客户端) +- **importer → domain, persistence**(导入器需要创建项目和迭代) + +## README 核心内容 +Cowork Forge 是一个完整的 AI 驱动虚拟开发团队系统。AI Agent 扮演产品经理、架构师、项目经理和工程师的角色,通过 7 阶段流水线协作完成从构思到交付的全流程。核心特性包括:Actor-Critic 自优化模式、人类在环验证、增量代码更新、多语言项目支持、外部 ACP Agent 集成、PVS(Project Version System)迭代架构。 + +## 注意事项 +- 采用六边形(Hexagonal)架构,domain 层零外部依赖 +- 所有用户交互通过 `InteractiveBackend` trait 抽象,CLI 和 GUI 各自实现 +- 速率限制使用 TokenBucket 算法(5 突发令牌,30 req/min) +- 无关系型数据库,使用 JSON 文件持久化 diff --git a/.terrain/.litho-agent/workflow.md b/.terrain/.litho-agent/workflow.md new file mode 100644 index 0000000..3b88f1a --- /dev/null +++ b/.terrain/.litho-agent/workflow.md @@ -0,0 +1,98 @@ +# 工作流研究报告 + +## 主要工作流 + +Cowork Forge 的核心工作流可以想象成一条"汽车生产线"——用户的原始想法就像原材料,经过 7 个工位(阶段)的接力加工,最终变成一辆完整的汽车(可交付的软件项目)。但和传统生产线不同,这里的每个工位都有"自检"环节:Actor 负责加工,Critic 负责质检,不合格就返工。 + +### 工作流1:7 阶段开发流水线 + +这是 Cowork Forge 最核心的工作流。它从用户的一个想法开始,经过 7 个阶段的顺序执行,最终产出完整的软件项目。 + +**触发方式**:用户在 CLI 执行 `cowork iter --project "my-project" "想法描述"` 或通过 GUI 创建新迭代 +**入口**:`crates/cowork-cli/src/main.rs:119` → `commands/iter.rs` → `IterationExecutor::execute()`(`crates/cowork-core/src/pipeline/executor/mod.rs:79`) +**执行步骤**: + +1. **Idea 阶段**(`IdeaStage` in `crates/cowork-core/src/pipeline/stages/idea.rs`)——Idea Agent 与用户对话,捕捉和结构化需求,生成 idea.md +2. **PRD 阶段**(`PrdStage` in `crates/cowork-core/src/pipeline/stages/prd.rs`)——PRD Actor 生成产品需求文档,PRD Critic 评审并反馈,循环迭代直到满意 +3. **Design 阶段**(`DesignStage` in `crates/cowork-core/src/pipeline/stages/design.rs`)——Design Actor 设计技术架构,Design Critic 评审覆盖度,输出 design.md +4. **Plan 阶段**(`PlanStage` in `crates/cowork-core/src/pipeline/stages/plan.rs`)——Plan Actor 分解任务和依赖,Plan Critic 检查任务完整性,输出 plan.md +5. **Coding 阶段**(`CodingStage` in `crates/cowork-core/src/pipeline/stages/coding.rs`)——Coding Actor 编写代码、运行构建和测试,Coding Critic 审查代码质量 +6. **Check 阶段**(`CheckStage` in `crates/cowork-core/src/pipeline/stages/check.rs`)——Check Agent 验证需求覆盖度、数据格式、任务完整性 +7. **Delivery 阶段**(`DeliveryStage` in `crates/cowork-core/src/pipeline/stages/delivery.rs`)——Delivery Agent 生成交付报告,将代码复制到项目根目录 + +**关键设计**:PRD、Design、Plan、Coding 四个阶段使用 LoopAgent(Actor-Critic 循环),Idea、Check、Delivery 使用单 Agent 模式。这反映了设计理念——生成式任务需要自优化循环,验证式任务单次执行即可。 + +### 工作流2:遗留项目导入 + +**触发方式**:用户执行 `cowork import /path/to/project` +**入口**:`crates/cowork-cli/src/commands/import.rs` → `crates/cowork-core/src/importer/` +**执行步骤**: +1. 扫描项目目录结构、配置文件和依赖 +2. 自动检测技术栈(语言、框架、工具) +3. 读取 README 和关键源文件 +4. 使用 LLM 合成信息生成 idea.md、prd.md、design.md、plan.md +5. 创建初始迭代并保存所有制品 + +### 工作流3:PM Agent 交付后交互 + +**触发方式**:迭代完成后的用户消息 +**入口**:`crates/cowork-core/src/agents/mod.rs:659`(`execute_pm_agent_message_streaming`) +**执行步骤**: +1. 加载当前迭代的制品摘要和项目记忆 +2. 构建对话上下文(包含项目信息、历史决策) +3. PM Agent 分析用户意图,决定执行什么动作 +4. 可能触发的动作:`pm_goto_stage`(跳转到某个阶段)、`pm_create_iteration`(创建新迭代)、`pm_respond`(直接回答) + +## 并发/异步模型 + +Cowork Forge 采用 Tokio 异步运行时全异步架构。但 LLM 调用是串行化的——全局 TokenBucket 速率限制器确保同时只有一个 LLM 请求在进行(concurrency=1)。这看起来是"性能瓶颈",但实际上是刻意的设计选择:LLM API 调用是系统的"最慢环节",并行多个请求不仅容易触发 API 速率限制、增加成本,还会让系统行为变得难以预测。串行化的 LLM 调用使得行为可预测、调试简单,对于桌面工具场景来说,用户体验反而是更好的(响应有序而非乱序)。 + +文件操作(读写、列表)和命令执行(编译、测试)使用 Tokio 异步任务,不会阻塞主循环。 + +## 错误处理策略 + +系统的错误处理核心理念是:"局部失败不应导致全局中断"。整个系统采用 `anyhow::Result` 作为统一的错误返回类型(`crates/cowork-core/src/agents/mod.rs:16`),错误在整个调用栈中逐层传播,最终由流水线执行器或 CLI 入口统一处理。 + +关键错误处理模式: +- **阶段执行失败**:返回 `StageResult::Failed(message)`,流水线暂停,用户可以查看错误信息后选择重试或放弃 +- **LLM 调用失败**:由 TokenBucketRateLimiter 内部处理重试(最多 3 次),逐层传播最终错误 +- **文件操作失败**:使用 `?` 操作符向上传播,由调用方决定如何处理 +- **人类验证失败**:返回 `StageResult::NeedsRevision(feedback)`,触发 Actor-Critic 循环的再次迭代 + +## 关键时序交互 + +```mermaid +sequenceDiagram + participant User as 用户 + participant CLI as CLI + participant Executor as IterationExecutor + participant Stage as 阶段执行器 + participant Agent as AI Agent + participant LLM as LLM API + + User->>CLI: cowork iter "my idea" + CLI->>Executor: execute() + Executor->>Stage: run_stage("idea") + Stage->>Agent: create_idea_agent() + Agent->>LLM: 生成需求文档 + LLM-->>Agent: 输出内容 + Agent-->>Stage: save_idea + Stage-->>User: 人类验证请求 + User->>Stage: 确认通过 + Stage->>Executor: StageResult::Success + Executor->>Stage: run_stage("prd") + Stage->>Agent: create_prd_loop() + loop Actor-Critic 循环 + Agent->>LLM: Actor 生成 PRD + LLM-->>Agent: PRD 草案 + Agent->>LLM: Critic 评审 + LLM-->>Agent: 反馈意见 + end + Agent-->>Stage: save_prd_doc + Stage-->>User: 人类验证请求 + User->>Stage: 确认通过 + Stage->>Executor: StageResult::Success + Note over Executor,Stage: ...继续后续阶段... + Executor-->>CLI: 迭代完成 + CLI-->>User: 交付报告 +``` diff --git a/.terrain/.meta/freshness.json b/.terrain/.meta/freshness.json new file mode 100644 index 0000000..0187760 --- /dev/null +++ b/.terrain/.meta/freshness.json @@ -0,0 +1,82 @@ +{ + "version": 1, + "project": "cowork-forge", + "repo_path": ".", + "baseline": { + "git_head": "9c82bf7333ace6ae9508bf844063558e357d9192", + "git_head_at": "2026-07-06T02:48:28.420847+00:00", + "dirty": true + }, + "assets": { + "agent_pack": { + "path": "agent/repomix.md", + "synced_at": "2026-07-06T02:05:39.083865+00:00", + "baseline_git_head": "9c82bf7333ace6ae9508bf844063558e357d9192", + "stale": false, + "freshness_score": 95 + }, + "agent_context": { + "path": "agent/context.md", + "synced_at": "2026-07-06T02:06:45.225154+00:00", + "baseline_git_head": "9c82bf7333ace6ae9508bf844063558e357d9192", + "stale": false, + "freshness_score": 85 + }, + "human_docs": { + "path": "human/", + "synced_at": "2026-07-06T02:05:39.084445+00:00", + "stale": false, + "freshness_score": 95 + } + }, + "drift": { + "commits_since_baseline": 0, + "changed_files_since_baseline": 0, + "sample_changed_files": [] + }, + "summary": { + "overall_score": 85, + "overall_stale": false, + "commits_since_baseline": 0, + "changed_files_count": 0, + "current_git_head": "9c82bf7", + "working_tree_dirty": true, + "is_git_repo": true, + "last_computed_at": "2026-07-06T02:48:28.420847+00:00", + "agent_pack_score": 95, + "agent_context_score": 85, + "human_docs_score": 95, + "macro_preload_allowed": true, + "drift_factors": [ + { + "id": "baseline_match", + "severity": "info", + "title": "与 baseline 提交一致", + "detail": "源码索引与 Agent 上下文均基于提交 9c82bf7333ace6ae9508bf844063558e357d9192 生成,相对 HEAD 无文件漂移。" + }, + { + "id": "dirty_tree", + "severity": "medium", + "title": "工作区有未提交修改", + "detail": "Git 工作区在源码路径上有未提交改动(已排除 `.terrain/` 等知识产出目录)。知识资产基于某次提交快照,与磁盘上的未提交源码改动不一致,扣 5 分。", + "points_lost": 5 + }, + { + "id": "context_lineage", + "severity": "info", + "title": "Agent 上下文受源码索引牵连", + "detail": "架构上下文原始分 95/100,按规则不超过源码索引分数的 90%,现为 85/100。", + "points_lost": 10 + }, + { + "id": "overall_driver", + "severity": "info", + "title": "总分由 Agent 架构上下文决定", + "detail": "综合分取三层最低值:源码索引 95、Agent 上下文 85、人类文档 95。" + } + ], + "pack_baseline_short": "9c82bf7", + "context_baseline_short": "9c82bf7" + }, + "last_computed_at": "2026-07-06T02:48:28.420847+00:00" +} \ No newline at end of file diff --git a/.terrain/.meta/sync.json b/.terrain/.meta/sync.json new file mode 100644 index 0000000..a2e9121 --- /dev/null +++ b/.terrain/.meta/sync.json @@ -0,0 +1,9 @@ +{ + "project": "cowork-forge", + "repo_path": ".", + "synced_at": "2026-07-06T02:05:39.084445+00:00", + "collectors": [ + "git", + "repomix" + ] +} \ No newline at end of file diff --git a/.terrain/agent/context-meta.json b/.terrain/agent/context-meta.json new file mode 100644 index 0000000..f051e45 --- /dev/null +++ b/.terrain/agent/context-meta.json @@ -0,0 +1,9 @@ +{ + "project": "cowork-forge", + "repo_path": ".", + "output_file": "context.md", + "generated_at": "2026-07-06T02:06:45.225154+00:00", + "section_count": 6, + "char_count": 7427, + "baseline_git_head": "9c82bf7333ace6ae9508bf844063558e357d9192" +} \ No newline at end of file diff --git a/.terrain/agent/context.md b/.terrain/agent/context.md new file mode 100644 index 0000000..516d26d --- /dev/null +++ b/.terrain/agent/context.md @@ -0,0 +1,127 @@ +--- +type: agent_context +project: cowork-forge +title: Agent Architecture Context +source: . +--- + +## 项目概览 + +Cowork Forge 是 AI 原生的多 Agent 软件开发平台:将产品经理、架构师、项目经理、工程师等角色编排为虚拟开发团队,通过七阶段流水线(Idea→PRD→Design→Plan→Coding→Check→Delivery)把自然语言想法转化为可交付软件。面向独立开发者、技术负责人与产品团队;提供 CLI(自动化)与 Tauri 桌面 GUI(交互式)双入口。核心约束:本地优先、工作区沙箱、LLM 限流(30 req/min)、关键阶段 HITL 确认门、Actor-Critic 自优化、迭代继承(Genesis/Evolution)。 + +## 架构设计 + +| 容器/层 | 职责 | 主要路径 | +|---------|------|----------| +| **cowork-core** | 域逻辑、流水线、Agent/Tool、持久化、安全 | `crates/cowork-core/src/` | +| **cowork-cli** | CLI 适配器(clap + dialoguer) | `crates/cowork-cli/` | +| **cowork-gui** | React 前端 + Tauri 后端 | `crates/cowork-gui/src/`, `src-tauri/` | +| **配置驱动层** | Agent/Stage/Flow JSON 定义与注册 | `config_definition/` | +| **知识资产** | Terrain 私域与 Litho 人类文档 | `.terrain/`, `litho.docs/` | + +**架构模式**:六边形(`InteractiveBackend` 入站端口;Store/LLM 出站端口)、DDD 聚合(Project/Iteration/Memory)、Actor-Critic(adk-rust `LoopAgent`)、事件驱动 GUI(Tauri invoke + emit)。 + +**依赖关系**:`cowork-cli` / `cowork-gui` → `cowork-core` → adk-rust 生态(`adk-core`, `adk-agent`, `adk-model`, `adk-tool`)+ Tokio + OpenAI 兼容 LLM API。 + +## 模块地图 + +| 模块 | 职责 | 主要路径 | +|------|------|----------| +| Pipeline | 七阶段编排、上下文传递、阶段转换 | `pipeline/`, `pipeline/stages/`, `stage_executor.rs` | +| Config Definition | 数据驱动 Agent/Stage/Flow 注册与校验 | `config_definition/`, `default_configs/` | +| Domain | Project、Iteration、Memory 聚合与继承模式 | `domain/` | +| Tools | 40+ ADK 工具(文件/工件/HITL/验证/内存/部署) | `tools/` | +| Agents | PM Agent、迭代助手、外部编码 Agent、遗留分析 | `agents/` | +| Instructions | 各阶段 Actor/Critic 提示词库 | `instructions/` | +| Interaction | `InteractiveBackend` trait;CLI/GUI 抽象 | `interaction/`, `cowork-gui/src-tauri/` | +| Persistence | JSON 项目/迭代/内存存储 | `persistence/` | +| LLM | 模型工厂、全局限流装饰器 | `llm/` | +| ACP | Agent Client Protocol 外部编码工具集成 | `acp/` | +| Importer | 遗留项目导入与反向工程 | `importer/` | +| Skills | agentskills.io 标准技能管理 | `skills/` | + +## 核心流程 + +### 1. Genesis 迭代(想法→交付) + +1. 用户通过 CLI/GUI 创建项目与迭代,输入初始想法 +2. `PipelineExecutor` 按 `default.json` 顺序执行七阶段 +3. 每阶段 `StageExecutor` 构建 adk-rust Agent(Actor→Critic `LoopAgent`),流式调用 LLM 与工具 +4. 产出工件(idea/prd/design/plan 等 markdown)写入 `.cowork-v2/iterations/{id}/` +5. 关键阶段触发 HITL:用户通过/编辑/反馈 → `execute_with_feedback` 重试 +6. 完成后生成知识快照,更新迭代状态 + +### 2. Actor-Critic 自优化(单阶段内) + +1. Actor(`IncludeContents::Default`)生成 artifact 并持久化 +2. Critic(`IncludeContents::None`)经工具加载 artifact 审查(非对话历史) +3. 通过 → `exit_loop`;小问题 → 文字反馈供下轮 Actor;大问题 → `provide_feedback` + escalate 触发 Stage 级重试 + +### 3. Evolution 迭代(增量演进) + +1. 用户选择继承模式(None/Full/Partial)创建 Evolution 迭代 +2. 系统分析变更范围,决定起始阶段与可复用工件 +3. 合并项目级/迭代级 Memory,注入 Pipeline 上下文 +4. 从映射阶段继续流水线,复用或增量修改代码与文档 + +### 4. 遗留项目导入 + +1. CLI/GUI 指定现有代码库路径 +2. `LegacyProjectAnalyzer` 检测技术栈、结构、依赖 +3. 生成初始 Project/Iteration 记录与反向工程文档 +4. 纳入常规范畴管理与后续迭代 + +## 技术选型 + +- **语言/运行时**:Rust 2024 edition、Tokio 全特性异步 +- **AI 编排**:adk-rust 1.0(`LlmAgentBuilder`, `LoopAgent`, `Tool` trait) +- **LLM**:OpenAI 兼容 API(`adk-model` openai feature);信号量+延迟限流 +- **CLI**:clap 4、dialoguer、console +- **GUI**:Tauri 2、React 18、TypeScript、Ant Design、Vite +- **序列化/配置**:serde/serde_json、toml +- **持久化**:本地 JSON 文件(ProjectStore/IterationStore/MemoryStore) +- **外部协议**:agent-client-protocol 0.9(ACP)、MCP(Tavily/DeepWiki 等) +- **错误处理**:anyhow、thiserror +- **可观测**:tracing + tracing-subscriber + +## 系统边界 + +| 边界 | 类型 | 说明 | +|------|------|------| +| LLM 提供商 API | 外部、需密钥 | OpenAI 兼容端点;限流 30 req/min | +| 本地文件系统 | 信任域内 | 项目根、`.cowork-v2/` 工件;路径校验防逃逸 | +| Shell/子进程 | 受限执行 | 构建/测试/开发服务器;命令白名单与沙箱 | +| MCP 服务器 | 可选外部 | Tavily 搜索、DeepWiki 文档;`config.toml [mcp]` 配置 | +| ACP 外部 Agent | 可选外部 | OpenCode/Gemini CLI/Claude CLI 等编码阶段 | +| 外部编辑器 | OS 集成 | HITL 编辑阶段调用系统默认编辑器 | +| 用户配置 | 本地 | `~/Library/Application Support/CoworkCreative/config.toml`(macOS) | +| GUI 配置 | 本地 | `com.cowork-forge.app/config/` 用户 Agent/Flow 覆盖 | + +**范围外**:LLM 训练、Git VCS 集成、包注册表、云 CI/CD、多用户实时协作。 + +**信任边界**:`runtime_security` 校验所有文件/命令操作不越出 workspace;API Key 仅存 config/env,不入库。 + +## 代码映射索引 + +| 概念 | 位置 | 备注 | +|------|------|------| +| 流水线入口 | `pipeline/executor/mod.rs` | PipelineContext 与阶段调度 | +| 阶段执行 | `pipeline/stage_executor.rs` | ADK Agent 生命周期、流式、HITL | +| 七阶段实现 | `pipeline/stages/*.rs` | 各 Stage trait 实现 | +| 默认流程定义 | `config_definition/default_configs/flows/default.json` | idea→delivery 顺序 | +| 内置 Agent 配置 | `config_definition/default_configs/agents/built-in/` | Actor/Critic JSON | +| 交互抽象 | `interaction/mod.rs`, `interaction/cli.rs` | InteractiveBackend trait | +| GUI 后端适配 | `cowork-gui/src-tauri/src/lib.rs` | TauriBackend 实现 | +| GUI 命令层 | `cowork-gui/src-tauri/src/commands/` | runner/pm/import 等 | +| 项目运行时 | `project_runtime.rs`, `runtime_analyzer.rs` | 技术栈检测与预览 | +| 安全层 | `runtime_security.rs` | 路径与命令校验 | +| 领域模型 | `domain/project.rs`, `iteration.rs`, `memory.rs` | 核心聚合 | +| 持久化 | `persistence/*_store.rs` | JSON Repository | +| 工具注册 | `tools/mod.rs` | 40+ Tool 聚合导出 | +| 控制流工具 | `tools/control_tools.rs`, `goto_stage_tool.rs` | exit_loop/provide_feedback/goto_stage | +| PM Agent | `agents/mod.rs`, `instructions/project_manager.rs` | 阶段跳转与迭代创建 | +| 外部编码 Agent | `agents/external_coding_agent.rs`, `acp/client.rs` | ACP 协议 | +| CLI 命令 | `cowork-cli/src/commands/` | init/iter/continue/import 等 | +| 技能系统 | `skills/manager.rs` | agentskills.io 加载 | +| 集成钩子 | `integration/hooks.rs`, `adapters.rs` | 外部集成扩展点 | +| 工作区约定 | `.cowork-v2/iterations/{id}/` | artifacts、session_history | \ No newline at end of file diff --git a/.terrain/agent/meta-inputs-manifest.json b/.terrain/agent/meta-inputs-manifest.json new file mode 100644 index 0000000..6c37b61 --- /dev/null +++ b/.terrain/agent/meta-inputs-manifest.json @@ -0,0 +1,19 @@ +{ + "collected_at": "2026-07-06T02:05:39.111425+00:00", + "meta_files": [], + "input_count": 2, + "sources": [ + { + "label": "Private knowledge (.terrain/knowledge/00-glossary.md)", + "source": ".terrain/knowledge/00-glossary.md", + "chars": 0, + "truncated": false + }, + { + "label": "Private knowledge (.terrain/knowledge/adk-rust.md)", + "source": ".terrain/knowledge/adk-rust.md", + "chars": 3503, + "truncated": true + } + ] +} \ No newline at end of file diff --git a/.terrain/agent/meta-inputs.md b/.terrain/agent/meta-inputs.md new file mode 100644 index 0000000..9718ae3 --- /dev/null +++ b/.terrain/agent/meta-inputs.md @@ -0,0 +1,113 @@ +# Developer Meta Inputs + +Compiled from `terrain-meta.json` before Agent context generation. + +## Private knowledge (.terrain/knowledge/00-glossary.md) + +_Source: `.terrain/knowledge/00-glossary.md`_ + + + +## Private knowledge (.terrain/knowledge/adk-rust.md) + +_Source: `.terrain/knowledge/adk-rust.md` (truncated)_ + +# adk-rust 框架速查 + +> Agent Development Kit for Rust — LLM Agent 编排框架 (github.com/zavora-ai/adk-rust) + +## 核心类型 + +| 类型 | 用途 | +|------|------| +| `Agent` (trait) | 可执行 agent 单元,`run(ctx) -> Result` | +| `LlmAgentBuilder` | 构建 LLM agent:`.instruction()` + `.tool()` + `.model()` | +| `LoopAgent` | 循环编排器,按顺序执行 agents 数组,支持多轮迭代 | +| `Tool` (trait) | 工具 trait:`name/description/parameters_schema/execute(ctx, args)` | +| `ToolContext` (Arc) | 工具执行上下文(LLM 调用、session 操作、action 设置) | +| `EventActions` | 工具返回后的控制指令:`escalate` / `exit_loop` / `goto_stage` | +| `IncludeContents` | 子 agent 可见会话历史模式 | +| `Session` | 对话会话,存储 messages/state,agent 间共享 | +| `ExitLoopTool` | 内置工具:调用后设置 `actions.escalate = true`(注意:是 escalate 不是 exit_loop 字段!) | + +## IncludeContents 模式 + +```rust +IncludeContents::None // 子 agent 只看到自己的 instruction + 当前用户 turn(看不到前序/历史消息) +IncludeContents::Default // 子 agent 看到共享 Session 的完整对话历史 +``` + +**Actor-Critic 正确配置(易错!)**: +- **Actor** → `IncludeContents::Default`:Actor 需要看到前一轮 Critic 的文字反馈来修正产出 +- **Critic** → `IncludeContents::None`:Critic **不需要**看 Actor 的对话历史!Critic 通过工具(`load_prd_doc`/`get_plan`/`list_files`等)从磁盘/persistence 加载 Actor 的 artifact 进行审查。设为 None 可避免将 Actor 的 system prompt + 完整工具调用链(可能 50K+ tokens)传给 Critic,节省约一半 token 成本。 + +**误区纠正**:旧说法"Critic 必须用 Default 才能看到 Actor 产出"是错误的。Critic 通过工具加载 artifact,不依赖对话历史。Default 仅用于 Actor 需要跨轮看到 Critic 反馈的场景。 + +## LoopAgent 工作流 (Actor-Critic) + +``` +LoopAgent::new("name", vec![actor_agent, critic_agent]) + .with_max_iterations(N) +``` + +执行流程: +1. LoopAgent 创建一个 `HistoryTrackingSession` 包裹父上下文 +2. 每轮迭代依次执行 Actor → Critic,每个子 agent 的输出 event 都写入 HistoryTrackingSession +3. **Actor**(Default):在第 2+ 轮迭代时能看到前一轮 Critic 的反馈文字,据此修正产出并保存 artifact +4. **Critic**(None):每轮只看到自己的 instruction + 初始用户 prompt,通过工具加载最新 artifact 审查: + - 通过 → Critic 调用 `exit_loop` 工具,循环终止(整个 LoopAgent 成功结束) + - 小问题 → Critic 直接在文字回复中描述问题(不调用 provide_feedback),Actor 下轮可见 + - 大问题 → Critic 调用 `provide_feedback` 持久化反馈 + 退出循环,触发 Stage 级别重试 +5. 达到 max_iterations 仍未 exit_loop → LoopAgent 正常结束,Stage executor 根据历史决定重试 + +**EventActions.escalate 的作用**:子 agent 工具中设置 `escalate=true` 会立即中断 LoopAgent 循环。`provide_feedback` 和 `exit_loop` 都会设置 escalate=true。区别是 provide_feedback 额外持久化了结构化反馈供 Stage executor 使用。 + +## EventActions 使用 + +```rust +// 在 Tool::execute 中设置 action +let mut actions = EventActions::default(); +actions.escalate = true; // 中断当前 LoopAgent/agent,回到上层 +ctx.set_actions(actions); // 必须调用 set_actions 才生效! +``` + +| 字段 | 作用 | +|------|------| +| `escalate` | 设置为 true 时立即退出 LoopAgent(provide_feedback/exit_loop 都用这个) | + +**易错**: +- 创建 `EventActions` 后**必须调用 `ctx.set_actions(actions)`** +- `exit_loop` 字段在新版 adk-rust 中不是独立字段——ExitLoopTool 实际设置的是 `escalate=true` + +## LlmAgentBuilder 构建 + +```rust +LlmAgentBuilder::new("agent_id") + .instruction("你是...") // 不是 system_prompt()! + .model(model) // Arc + .tool(Arc::new(MyTool)) // 不是 with_tool()!可多次调用 + .include_contents(IncludeContents::Default) + .temperature(0.3) + .build() +``` + +**易错**: +- 方法名是 `.instruction()` 不是 `.system_prompt()`,是 `.tool()` 不是 `.with_tool()` +- 忘记添加工具 → LLM 无法调用该工具 +- `include_contents` 默认是 `None` +- instruction 中必须列出可用工具名和用途,否则 LLM 不知道何时调用 + +## Tool trait 实现 + +```rust +#[async_trait] +impl Tool for MyTool { + fn name(&self) -> &str { "my_tool" } + fn description(&self) -> &str { "做什么用" } + fn parameters_schema(&self) -> Option { + Some(json!({ + "type": "object", + "properties": { "param": + +… + diff --git a/.terrain/agent/meta.json b/.terrain/agent/meta.json new file mode 100644 index 0000000..c2dddbf --- /dev/null +++ b/.terrain/agent/meta.json @@ -0,0 +1,95 @@ +{ + "project": "cowork-forge", + "repo_path": ".", + "generator": "repomix-core", + "pack_strategy": "architecture-context", + "output_file": "repomix.md", + "total_files": 257, + "total_tokens": 398391, + "total_characters": 1496430, + "top_files_by_tokens": [ + { + "path": "litho.docs/zh/2、架构概览.md", + "tokens": 15541 + }, + { + "path": "litho.docs/en/2.Architecture.md", + "tokens": 13666 + }, + { + "path": "litho.docs/zh/4、深入探索/4.8、Cowork GUI前端.md", + "tokens": 8452 + }, + { + "path": "crates/cowork-core/src/tools/data_tools.rs", + "tokens": 8207 + }, + { + "path": "litho.docs/zh/4、深入探索/4.7、Cowork CLI.md", + "tokens": 7396 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/GUI Frontend Domain.md", + "tokens": 7255 + }, + { + "path": "litho.docs/zh/3、工作流程.md", + "tokens": 7061 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/CLI Domain.md", + "tokens": 6985 + }, + { + "path": "crates/cowork-core/src/tools/file_tools.rs", + "tokens": 6550 + }, + { + "path": "litho.docs/zh/4、深入探索/4.2、流程调度.md", + "tokens": 6384 + }, + { + "path": "litho.docs/en/3.Workflow.md", + "tokens": 6287 + }, + { + "path": "crates/cowork-core/src/runtime_analyzer.rs", + "tokens": 6108 + }, + { + "path": "litho.docs/zh/4、深入探索/4.4、Agent工具系统.md", + "tokens": 6089 + }, + { + "path": "crates/cowork-gui/src/components/config/AgentConfigForm.tsx", + "tokens": 6072 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/Tools Domain.md", + "tokens": 5965 + }, + { + "path": "crates/cowork-core/src/config_definition/registry.rs", + "tokens": 5960 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/Pipeline Domain.md", + "tokens": 5935 + }, + { + "path": "litho.docs/zh/4、深入探索/4.9、Cowork GUI后端.md", + "tokens": 5696 + }, + { + "path": "crates/cowork-core/src/pipeline/executor/mod.rs", + "tokens": 5572 + }, + { + "path": "litho.docs/zh/4、深入探索/4.1、领域实体.md", + "tokens": 5567 + } + ], + "directory_structure": "crates/\n cowork-cli/\n src/\n commands/\n config.rs\n continue_cmd.rs\n delete.rs\n import.rs\n init.rs\n iter.rs\n knowledge.rs\n list.rs\n mod.rs\n show.rs\n status.rs\n main.rs\n utils.rs\n Cargo.toml\n cowork-core/\n src/\n acp/\n client.rs\n mod.rs\n agents/\n external_coding_agent.rs\n iterative_assistant.rs\n legacy_project_analyzer.rs\n mod.rs\n config_definition/\n default_configs/\n agents/\n built-in/\n check_agent.json\n coding_actor.json\n coding_critic.json\n delivery_agent.json\n design_actor.json\n design_critic.json\n idea_agent.json\n knowledge_gen_agent.json\n plan_actor.json\n plan_critic.json\n pm_agent.json\n prd_actor.json\n prd_critic.json\n summary_agent.json\n flows/\n default.json\n stages/\n check.json\n coding.json\n delivery.json\n design.json\n idea.json\n plan.json\n prd.json\n agent_definition.rs\n builtin.rs\n flow_definition.rs\n integration_definition.rs\n mod.rs\n registry.rs\n stage_definition.rs\n validator.rs\n data/\n mod.rs\n models.rs\n domain/\n iteration.rs\n memory.rs\n mod.rs\n project.rs\n importer/\n artifact_generator.rs\n import_config.rs\n mod.rs\n project_analyzer.rs\n instructions/\n check.rs\n coding.rs\n delivery.rs\n design.rs\n idea.rs\n knowledge_gen.rs\n legacy_project_analyzer.rs\n mod.rs\n plan.rs\n prd.rs\n project_manager.rs\n summary.rs\n integration/\n USAGE_EXAMPLE.md\n adapters.rs\n hooks.rs\n mod.rs\n interaction/\n cli.rs\n mod.rs\n llm/\n mod.rs\n rate_limiter.rs\n persistence/\n iteration_data.rs\n iteration_store.rs\n memory_store.rs\n mod.rs\n project_store.rs\n pipeline/\n executor/\n interaction_ext.rs\n knowledge.rs\n mod.rs\n workspace.rs\n stages/\n check.rs\n coding.rs\n delivery.rs\n design.rs\n idea.rs\n mod.rs\n plan.rs\n prd.rs\n mod.rs\n stage_executor.rs\n skills/\n manager.rs\n mod.rs\n tools/\n artifact_tools.rs\n control_tools.rs\n data_tools.rs\n deployment_tools.rs\n file_tools.rs\n goto_stage_tool.rs\n hitl_content_tools.rs\n knowledge_tools.rs\n legacy_project_analyzer_tools.rs\n load_artifacts.rs\n memory_tools.rs\n mod.rs\n pm_tools.rs\n test_lint_tools.rs\n validation_tools.rs\n config.rs\n lib.rs\n project_runtime.rs\n runtime_analyzer.rs\n runtime_security.rs\n tech_stack.rs\n Cargo.toml\n cowork-gui/\n src/\n components/\n chat/\n ChatPanel.tsx\n InputArea.tsx\n MessageList.tsx\n index.ts\n common/\n LoadingScreen.tsx\n MarkdownMessage.tsx\n StatusBadge.tsx\n index.ts\n config/\n AgentConfigForm.tsx\n AgentsSetupPanel.tsx\n FlowConfigPanel.tsx\n IntegrationConfig.tsx\n SkillManager.tsx\n index.ts\n iterations/\n CreateIterationModal.tsx\n InitProjectModal.tsx\n IterationDetailsModal.tsx\n index.ts\n onboarding/\n index.ts\n projects/\n CreateProjectModal.tsx\n EditProjectModal.tsx\n ImportProjectModal.tsx\n index.ts\n ArtifactsViewer.tsx\n CodeEditor.tsx\n CommandPalette.tsx\n IterationsPanel.tsx\n KnowledgePanel.tsx\n MemoryPanel.tsx\n PreviewPanel.tsx\n ProjectsPanel.tsx\n RunnerPanel.tsx\n constants/\n events.ts\n index.ts\n stages.ts\n status.ts\n hooks/\n index.ts\n useAppEvents.ts\n useAutoScroll.ts\n useChatInput.ts\n useIterationActions.ts\n useIterationEvents.ts\n useIterationsData.ts\n useLoading.ts\n useModal.ts\n usePMAgent.ts\n useProjectEvents.ts\n useProjectsData.ts\n useRefreshTrigger.ts\n useTauriEvent.ts\n stores/\n agentStore.ts\n configStore.ts\n index.ts\n projectStore.ts\n uiStore.ts\n styles/\n antd-overrides.css\n chat.css\n components.css\n global.css\n layout.css\n markdown.css\n theme.css\n types/\n agent.ts\n artifacts.ts\n chat.ts\n config.ts\n index.ts\n iteration.ts\n knowledge.ts\n project.ts\n registry.ts\n utils/\n errorHandler.ts\n index.ts\n markdown.ts\n App.tsx\n assets.d.ts\n main.tsx\n styles.css\n src-tauri/\n capabilities/\n default.json\n src/\n commands/\n file.rs\n import_cmd.rs\n memory.rs\n mod.rs\n path_utils.rs\n pm.rs\n preview.rs\n runner.rs\n system.rs\n template.rs\n gui_types.rs\n iteration_commands.rs\n lib.rs\n main.rs\n project_manager.rs\n project_runner.rs\n static_server.rs\n Cargo.toml\n build.rs\n tauri.conf.json\n README.md\n index.html\n package.json\n tsconfig.json\n tsconfig.node.json\n vite.config.js\nlitho.docs/\n en/\n 4.Deep-Exploration/\n CLI Domain.md\n Domain Logic.md\n GUI Backend Domain.md\n GUI Frontend Domain.md\n Interaction Domain.md\n LLM Integration Domain.md\n Memory Domain.md\n Persistence Domain.md\n Pipeline Domain.md\n Tools Domain.md\n 1.Overview.md\n 2.Architecture.md\n 3.Workflow.md\n zh/\n 4、深入探索/\n 4.10 、LLM集成.md\n 4.1、领域实体.md\n 4.2、流程调度.md\n 4.3、HITL人机协同.md\n 4.4、Agent工具系统.md\n 4.5、Artifacts存储.md\n 4.6、自迭代记忆系统.md\n 4.7、Cowork CLI.md\n 4.8、Cowork GUI前端.md\n 4.9、Cowork GUI后端.md\n 1、项目概述.md\n 2、架构概览.md\n 3、工作流程.md\nAGENTS.md\nCargo.toml\nLICENSE", + "synced_at": "2026-07-06T02:05:39.083865+00:00", + "baseline_git_head": "9c82bf7333ace6ae9508bf844063558e357d9192" +} \ No newline at end of file diff --git a/.terrain/agent/repomix.index.json b/.terrain/agent/repomix.index.json new file mode 100644 index 0000000..8975e3f --- /dev/null +++ b/.terrain/agent/repomix.index.json @@ -0,0 +1,1032 @@ +{ + "files": [ + { + "path": "crates/cowork-core/src/pipeline/stage_executor.rs", + "header_line": 341 + }, + { + "path": "crates/cowork-gui/src-tauri/src/lib.rs", + "header_line": 937 + }, + { + "path": "AGENTS.md", + "header_line": 1227 + }, + { + "path": "crates/cowork-core/src/agents/mod.rs", + "header_line": 1494 + }, + { + "path": "crates/cowork-core/src/tools/mod.rs", + "header_line": 1577 + }, + { + "path": "crates/cowork-gui/src/components/chat/MessageList.tsx", + "header_line": 1613 + }, + { + "path": "crates/cowork-core/src/acp/client.rs", + "header_line": 1652 + }, + { + "path": "crates/cowork-core/src/pipeline/executor/mod.rs", + "header_line": 1966 + }, + { + "path": "crates/cowork-core/src/pipeline/stages/coding.rs", + "header_line": 2614 + }, + { + "path": "crates/cowork-gui/src/components/chat/ChatPanel.tsx", + "header_line": 2977 + }, + { + "path": "crates/cowork-gui/src/components/chat/InputArea.tsx", + "header_line": 3006 + }, + { + "path": "crates/cowork-gui/src/hooks/useAppEvents.ts", + "header_line": 3029 + }, + { + "path": "crates/cowork-core/src/lib.rs", + "header_line": 3494 + }, + { + "path": "crates/cowork-core/src/tools/control_tools.rs", + "header_line": 3608 + }, + { + "path": "crates/cowork-gui/src/components/ArtifactsViewer.tsx", + "header_line": 3876 + }, + { + "path": "crates/cowork-gui/src/components/MemoryPanel.tsx", + "header_line": 3915 + }, + { + "path": "litho.docs/en/1.Overview.md", + "header_line": 3968 + }, + { + "path": "crates/cowork-core/src/instructions/check.rs", + "header_line": 4269 + }, + { + "path": "crates/cowork-core/src/tools/goto_stage_tool.rs", + "header_line": 4367 + }, + { + "path": "crates/cowork-gui/src/components/common/MarkdownMessage.tsx", + "header_line": 4462 + }, + { + "path": "crates/cowork-gui/src/styles/chat.css", + "header_line": 4474 + }, + { + "path": "Cargo.toml", + "header_line": 4911 + }, + { + "path": "crates/cowork-core/src/agents/external_coding_agent.rs", + "header_line": 4969 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_critic.json", + "header_line": 5183 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_critic.json", + "header_line": 5239 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_critic.json", + "header_line": 5292 + }, + { + "path": "crates/cowork-core/src/instructions/plan.rs", + "header_line": 5345 + }, + { + "path": "crates/cowork-core/src/instructions/prd.rs", + "header_line": 5751 + }, + { + "path": "crates/cowork-core/src/tools/artifact_tools.rs", + "header_line": 6041 + }, + { + "path": "crates/cowork-core/src/tools/file_tools.rs", + "header_line": 6350 + }, + { + "path": "crates/cowork-core/src/tools/pm_tools.rs", + "header_line": 7088 + }, + { + "path": "crates/cowork-gui/package.json", + "header_line": 7417 + }, + { + "path": "crates/cowork-gui/src/components/KnowledgePanel.tsx", + "header_line": 7465 + }, + { + "path": "crates/cowork-gui/src/components/config/AgentConfigForm.tsx", + "header_line": 7511 + }, + { + "path": "crates/cowork-gui/src/styles/components.css", + "header_line": 8228 + }, + { + "path": "crates/cowork-gui/src/styles/markdown.css", + "header_line": 8601 + }, + { + "path": "crates/cowork-gui/src-tauri/Cargo.toml", + "header_line": 8818 + }, + { + "path": "crates/cowork-gui/src-tauri/src/commands/import_cmd.rs", + "header_line": 8873 + }, + { + "path": "crates/cowork-gui/vite.config.js", + "header_line": 8966 + }, + { + "path": "litho.docs/en/2.Architecture.md", + "header_line": 8974 + }, + { + "path": "crates/cowork-core/src/config_definition/agent_definition.rs", + "header_line": 10203 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/check_agent.json", + "header_line": 10385 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/coding_actor.json", + "header_line": 10441 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/coding_critic.json", + "header_line": 10515 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_actor.json", + "header_line": 10571 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_actor.json", + "header_line": 10633 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_actor.json", + "header_line": 10701 + }, + { + "path": "crates/cowork-core/src/config_definition/mod.rs", + "header_line": 10766 + }, + { + "path": "crates/cowork-core/src/domain/iteration.rs", + "header_line": 10788 + }, + { + "path": "crates/cowork-core/src/instructions/coding.rs", + "header_line": 11112 + }, + { + "path": "crates/cowork-core/src/instructions/design.rs", + "header_line": 11476 + }, + { + "path": "crates/cowork-core/src/persistence/iteration_data.rs", + "header_line": 11891 + }, + { + "path": "crates/cowork-core/src/pipeline/executor/knowledge.rs", + "header_line": 12031 + }, + { + "path": "crates/cowork-core/src/pipeline/mod.rs", + "header_line": 12070 + }, + { + "path": "crates/cowork-core/src/tools/data_tools.rs", + "header_line": 12211 + }, + { + "path": "crates/cowork-core/src/tools/deployment_tools.rs", + "header_line": 13055 + }, + { + "path": "crates/cowork-core/src/tools/hitl_content_tools.rs", + "header_line": 13363 + }, + { + "path": "crates/cowork-core/src/tools/knowledge_tools.rs", + "header_line": 13573 + }, + { + "path": "crates/cowork-core/src/tools/legacy_project_analyzer_tools.rs", + "header_line": 13899 + }, + { + "path": "crates/cowork-core/src/tools/load_artifacts.rs", + "header_line": 14424 + }, + { + "path": "crates/cowork-core/src/tools/test_lint_tools.rs", + "header_line": 14564 + }, + { + "path": "crates/cowork-core/src/tools/validation_tools.rs", + "header_line": 14746 + }, + { + "path": "crates/cowork-gui/src/App.tsx", + "header_line": 14918 + }, + { + "path": "crates/cowork-gui/src/components/CodeEditor.tsx", + "header_line": 15342 + }, + { + "path": "crates/cowork-gui/src/components/ProjectsPanel.tsx", + "header_line": 15387 + }, + { + "path": "crates/cowork-gui/src/components/RunnerPanel.tsx", + "header_line": 15740 + }, + { + "path": "crates/cowork-gui/src/components/iterations/IterationDetailsModal.tsx", + "header_line": 15781 + }, + { + "path": "crates/cowork-gui/src/hooks/useIterationActions.ts", + "header_line": 15797 + }, + { + "path": "crates/cowork-gui/src/main.tsx", + "header_line": 15900 + }, + { + "path": "crates/cowork-gui/src/stores/agentStore.ts", + "header_line": 15990 + }, + { + "path": "crates/cowork-gui/src/styles/antd-overrides.css", + "header_line": 16112 + }, + { + "path": "crates/cowork-gui/src/types/config.ts", + "header_line": 16384 + }, + { + "path": "crates/cowork-gui/src-tauri/src/commands/mod.rs", + "header_line": 16680 + }, + { + "path": "crates/cowork-gui/src-tauri/src/commands/preview.rs", + "header_line": 16692 + }, + { + "path": "crates/cowork-gui/src-tauri/src/iteration_commands.rs", + "header_line": 16749 + }, + { + "path": "crates/cowork-gui/src-tauri/src/project_runner.rs", + "header_line": 16879 + }, + { + "path": "crates/cowork-gui/src-tauri/tauri.conf.json", + "header_line": 17283 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/CLI Domain.md", + "header_line": 17319 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/Persistence Domain.md", + "header_line": 18058 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/Tools Domain.md", + "header_line": 18499 + }, + { + "path": "crates/cowork-cli/src/commands/config.rs", + "header_line": 18985 + }, + { + "path": "crates/cowork-cli/src/commands/continue_cmd.rs", + "header_line": 18993 + }, + { + "path": "crates/cowork-cli/src/commands/delete.rs", + "header_line": 19001 + }, + { + "path": "crates/cowork-cli/src/commands/import.rs", + "header_line": 19009 + }, + { + "path": "crates/cowork-cli/src/commands/init.rs", + "header_line": 19047 + }, + { + "path": "crates/cowork-cli/src/commands/iter.rs", + "header_line": 19055 + }, + { + "path": "crates/cowork-cli/src/commands/knowledge.rs", + "header_line": 19068 + }, + { + "path": "crates/cowork-cli/src/commands/list.rs", + "header_line": 19076 + }, + { + "path": "crates/cowork-cli/src/commands/mod.rs", + "header_line": 19084 + }, + { + "path": "crates/cowork-cli/src/commands/show.rs", + "header_line": 19111 + }, + { + "path": "crates/cowork-cli/src/commands/status.rs", + "header_line": 19119 + }, + { + "path": "crates/cowork-cli/src/main.rs", + "header_line": 19127 + }, + { + "path": "crates/cowork-cli/src/utils.rs", + "header_line": 19242 + }, + { + "path": "crates/cowork-core/Cargo.toml", + "header_line": 19250 + }, + { + "path": "crates/cowork-core/src/agents/legacy_project_analyzer.rs", + "header_line": 19318 + }, + { + "path": "crates/cowork-core/src/config_definition/builtin.rs", + "header_line": 19341 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/delivery_agent.json", + "header_line": 19365 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/idea_agent.json", + "header_line": 19406 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/pm_agent.json", + "header_line": 19441 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/summary_agent.json", + "header_line": 19485 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/flows/default.json", + "header_line": 19517 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/stages/coding.json", + "header_line": 19587 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/stages/design.json", + "header_line": 19613 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/stages/plan.json", + "header_line": 19644 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/stages/prd.json", + "header_line": 19675 + }, + { + "path": "crates/cowork-core/src/config_definition/flow_definition.rs", + "header_line": 19711 + }, + { + "path": "crates/cowork-core/src/config_definition/registry.rs", + "header_line": 19985 + }, + { + "path": "crates/cowork-core/src/data/mod.rs", + "header_line": 20643 + }, + { + "path": "crates/cowork-core/src/data/models.rs", + "header_line": 20650 + }, + { + "path": "crates/cowork-core/src/importer/artifact_generator.rs", + "header_line": 21065 + }, + { + "path": "crates/cowork-core/src/importer/import_config.rs", + "header_line": 21179 + }, + { + "path": "crates/cowork-core/src/importer/mod.rs", + "header_line": 21393 + }, + { + "path": "crates/cowork-core/src/importer/project_analyzer.rs", + "header_line": 21405 + }, + { + "path": "crates/cowork-core/src/instructions/delivery.rs", + "header_line": 21541 + }, + { + "path": "crates/cowork-core/src/instructions/idea.rs", + "header_line": 21676 + }, + { + "path": "crates/cowork-core/src/instructions/legacy_project_analyzer.rs", + "header_line": 21776 + }, + { + "path": "crates/cowork-core/src/instructions/mod.rs", + "header_line": 21864 + }, + { + "path": "crates/cowork-core/src/instructions/project_manager.rs", + "header_line": 21892 + }, + { + "path": "crates/cowork-core/src/integration/hooks.rs", + "header_line": 22053 + }, + { + "path": "crates/cowork-core/src/interaction/mod.rs", + "header_line": 22309 + }, + { + "path": "crates/cowork-core/src/llm/mod.rs", + "header_line": 22467 + }, + { + "path": "crates/cowork-core/src/persistence/mod.rs", + "header_line": 22483 + }, + { + "path": "crates/cowork-core/src/pipeline/executor/interaction_ext.rs", + "header_line": 22511 + }, + { + "path": "crates/cowork-core/src/pipeline/executor/workspace.rs", + "header_line": 22552 + }, + { + "path": "crates/cowork-core/src/runtime_security.rs", + "header_line": 22618 + }, + { + "path": "crates/cowork-core/src/tools/memory_tools.rs", + "header_line": 22892 + }, + { + "path": "crates/cowork-gui/src/assets.d.ts", + "header_line": 23386 + }, + { + "path": "crates/cowork-gui/src/components/CommandPalette.tsx", + "header_line": 23415 + }, + { + "path": "crates/cowork-gui/src/components/IterationsPanel.tsx", + "header_line": 23438 + }, + { + "path": "crates/cowork-gui/src/components/config/FlowConfigPanel.tsx", + "header_line": 23450 + }, + { + "path": "crates/cowork-gui/src/components/config/IntegrationConfig.tsx", + "header_line": 23458 + }, + { + "path": "crates/cowork-gui/src/components/config/SkillManager.tsx", + "header_line": 23466 + }, + { + "path": "crates/cowork-gui/src/components/projects/ImportProjectModal.tsx", + "header_line": 23708 + }, + { + "path": "crates/cowork-gui/src/components/projects/index.ts", + "header_line": 23750 + }, + { + "path": "crates/cowork-gui/src/hooks/useChatInput.ts", + "header_line": 23758 + }, + { + "path": "crates/cowork-gui/src/hooks/usePMAgent.ts", + "header_line": 23925 + }, + { + "path": "crates/cowork-gui/src/stores/configStore.ts", + "header_line": 24131 + }, + { + "path": "crates/cowork-gui/src/stores/projectStore.ts", + "header_line": 24185 + }, + { + "path": "crates/cowork-gui/src/styles/global.css", + "header_line": 24248 + }, + { + "path": "crates/cowork-gui/src/styles/layout.css", + "header_line": 24287 + }, + { + "path": "crates/cowork-gui/src/styles.css", + "header_line": 24423 + }, + { + "path": "crates/cowork-gui/src/types/index.ts", + "header_line": 24438 + }, + { + "path": "crates/cowork-gui/src/utils/markdown.ts", + "header_line": 24516 + }, + { + "path": "crates/cowork-gui/src-tauri/src/commands/path_utils.rs", + "header_line": 24580 + }, + { + "path": "crates/cowork-gui/src-tauri/src/commands/pm.rs", + "header_line": 24644 + }, + { + "path": "crates/cowork-gui/src-tauri/src/commands/runner.rs", + "header_line": 24703 + }, + { + "path": "crates/cowork-gui/src-tauri/src/commands/system.rs", + "header_line": 24767 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/Domain Logic.md", + "header_line": 24783 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/GUI Backend Domain.md", + "header_line": 25279 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/GUI Frontend Domain.md", + "header_line": 25678 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/LLM Integration Domain.md", + "header_line": 26332 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/Pipeline Domain.md", + "header_line": 26621 + }, + { + "path": "LICENSE", + "header_line": 27167 + }, + { + "path": "crates/cowork-cli/Cargo.toml", + "header_line": 27193 + }, + { + "path": "crates/cowork-core/src/acp/mod.rs", + "header_line": 27242 + }, + { + "path": "crates/cowork-core/src/agents/iterative_assistant.rs", + "header_line": 27249 + }, + { + "path": "crates/cowork-core/src/config.rs", + "header_line": 27570 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/agents/built-in/knowledge_gen_agent.json", + "header_line": 27586 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/stages/check.json", + "header_line": 27621 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/stages/delivery.json", + "header_line": 27643 + }, + { + "path": "crates/cowork-core/src/config_definition/default_configs/stages/idea.json", + "header_line": 27665 + }, + { + "path": "crates/cowork-core/src/config_definition/integration_definition.rs", + "header_line": 27687 + }, + { + "path": "crates/cowork-core/src/config_definition/stage_definition.rs", + "header_line": 27996 + }, + { + "path": "crates/cowork-core/src/config_definition/validator.rs", + "header_line": 28233 + }, + { + "path": "crates/cowork-core/src/domain/memory.rs", + "header_line": 28308 + }, + { + "path": "crates/cowork-core/src/domain/mod.rs", + "header_line": 28690 + }, + { + "path": "crates/cowork-core/src/domain/project.rs", + "header_line": 28702 + }, + { + "path": "crates/cowork-core/src/instructions/knowledge_gen.rs", + "header_line": 28799 + }, + { + "path": "crates/cowork-core/src/instructions/summary.rs", + "header_line": 29071 + }, + { + "path": "crates/cowork-core/src/integration/USAGE_EXAMPLE.md", + "header_line": 29188 + }, + { + "path": "crates/cowork-core/src/integration/adapters.rs", + "header_line": 29425 + }, + { + "path": "crates/cowork-core/src/integration/mod.rs", + "header_line": 29505 + }, + { + "path": "crates/cowork-core/src/interaction/cli.rs", + "header_line": 29524 + }, + { + "path": "crates/cowork-core/src/llm/rate_limiter.rs", + "header_line": 29700 + }, + { + "path": "crates/cowork-core/src/persistence/iteration_store.rs", + "header_line": 29883 + }, + { + "path": "crates/cowork-core/src/persistence/memory_store.rs", + "header_line": 30015 + }, + { + "path": "crates/cowork-core/src/persistence/project_store.rs", + "header_line": 30242 + }, + { + "path": "crates/cowork-core/src/pipeline/stages/check.rs", + "header_line": 30329 + }, + { + "path": "crates/cowork-core/src/pipeline/stages/delivery.rs", + "header_line": 30368 + }, + { + "path": "crates/cowork-core/src/pipeline/stages/design.rs", + "header_line": 30407 + }, + { + "path": "crates/cowork-core/src/pipeline/stages/idea.rs", + "header_line": 30450 + }, + { + "path": "crates/cowork-core/src/pipeline/stages/mod.rs", + "header_line": 30489 + }, + { + "path": "crates/cowork-core/src/pipeline/stages/plan.rs", + "header_line": 30509 + }, + { + "path": "crates/cowork-core/src/pipeline/stages/prd.rs", + "header_line": 30548 + }, + { + "path": "crates/cowork-core/src/project_runtime.rs", + "header_line": 30591 + }, + { + "path": "crates/cowork-core/src/runtime_analyzer.rs", + "header_line": 30998 + }, + { + "path": "crates/cowork-core/src/skills/manager.rs", + "header_line": 31627 + }, + { + "path": "crates/cowork-core/src/skills/mod.rs", + "header_line": 31830 + }, + { + "path": "crates/cowork-core/src/tech_stack.rs", + "header_line": 31855 + }, + { + "path": "crates/cowork-gui/README.md", + "header_line": 31981 + }, + { + "path": "crates/cowork-gui/index.html", + "header_line": 32074 + }, + { + "path": "crates/cowork-gui/src/components/PreviewPanel.tsx", + "header_line": 32092 + }, + { + "path": "crates/cowork-gui/src/components/chat/index.ts", + "header_line": 32109 + }, + { + "path": "crates/cowork-gui/src/components/common/LoadingScreen.tsx", + "header_line": 32117 + }, + { + "path": "crates/cowork-gui/src/components/common/StatusBadge.tsx", + "header_line": 32139 + }, + { + "path": "crates/cowork-gui/src/components/common/index.ts", + "header_line": 32149 + }, + { + "path": "crates/cowork-gui/src/components/config/AgentsSetupPanel.tsx", + "header_line": 32157 + }, + { + "path": "crates/cowork-gui/src/components/config/index.ts", + "header_line": 32165 + }, + { + "path": "crates/cowork-gui/src/components/iterations/CreateIterationModal.tsx", + "header_line": 32175 + }, + { + "path": "crates/cowork-gui/src/components/iterations/InitProjectModal.tsx", + "header_line": 32188 + }, + { + "path": "crates/cowork-gui/src/components/iterations/index.ts", + "header_line": 32200 + }, + { + "path": "crates/cowork-gui/src/components/onboarding/index.ts", + "header_line": 32208 + }, + { + "path": "crates/cowork-gui/src/components/projects/CreateProjectModal.tsx", + "header_line": 32214 + }, + { + "path": "crates/cowork-gui/src/components/projects/EditProjectModal.tsx", + "header_line": 32226 + }, + { + "path": "crates/cowork-gui/src/constants/events.ts", + "header_line": 32239 + }, + { + "path": "crates/cowork-gui/src/constants/index.ts", + "header_line": 32247 + }, + { + "path": "crates/cowork-gui/src/constants/stages.ts", + "header_line": 32257 + }, + { + "path": "crates/cowork-gui/src/constants/status.ts", + "header_line": 32283 + }, + { + "path": "crates/cowork-gui/src/hooks/index.ts", + "header_line": 32295 + }, + { + "path": "crates/cowork-gui/src/hooks/useAutoScroll.ts", + "header_line": 32313 + }, + { + "path": "crates/cowork-gui/src/hooks/useIterationEvents.ts", + "header_line": 32341 + }, + { + "path": "crates/cowork-gui/src/hooks/useIterationsData.ts", + "header_line": 32417 + }, + { + "path": "crates/cowork-gui/src/hooks/useLoading.ts", + "header_line": 32484 + }, + { + "path": "crates/cowork-gui/src/hooks/useModal.ts", + "header_line": 32537 + }, + { + "path": "crates/cowork-gui/src/hooks/useProjectEvents.ts", + "header_line": 32560 + }, + { + "path": "crates/cowork-gui/src/hooks/useProjectsData.ts", + "header_line": 32590 + }, + { + "path": "crates/cowork-gui/src/hooks/useRefreshTrigger.ts", + "header_line": 32642 + }, + { + "path": "crates/cowork-gui/src/hooks/useTauriEvent.ts", + "header_line": 32668 + }, + { + "path": "crates/cowork-gui/src/stores/index.ts", + "header_line": 32715 + }, + { + "path": "crates/cowork-gui/src/stores/uiStore.ts", + "header_line": 32728 + }, + { + "path": "crates/cowork-gui/src/styles/theme.css", + "header_line": 32766 + }, + { + "path": "crates/cowork-gui/src/types/agent.ts", + "header_line": 32805 + }, + { + "path": "crates/cowork-gui/src/types/artifacts.ts", + "header_line": 32922 + }, + { + "path": "crates/cowork-gui/src/types/chat.ts", + "header_line": 33018 + }, + { + "path": "crates/cowork-gui/src/types/iteration.ts", + "header_line": 33148 + }, + { + "path": "crates/cowork-gui/src/types/knowledge.ts", + "header_line": 33193 + }, + { + "path": "crates/cowork-gui/src/types/project.ts", + "header_line": 33219 + }, + { + "path": "crates/cowork-gui/src/types/registry.ts", + "header_line": 33280 + }, + { + "path": "crates/cowork-gui/src/utils/errorHandler.ts", + "header_line": 33305 + }, + { + "path": "crates/cowork-gui/src/utils/index.ts", + "header_line": 33427 + }, + { + "path": "crates/cowork-gui/src-tauri/build.rs", + "header_line": 33444 + }, + { + "path": "crates/cowork-gui/src-tauri/capabilities/default.json", + "header_line": 33452 + }, + { + "path": "crates/cowork-gui/src-tauri/src/commands/file.rs", + "header_line": 33458 + }, + { + "path": "crates/cowork-gui/src-tauri/src/commands/memory.rs", + "header_line": 33526 + }, + { + "path": "crates/cowork-gui/src-tauri/src/commands/template.rs", + "header_line": 33574 + }, + { + "path": "crates/cowork-gui/src-tauri/src/gui_types.rs", + "header_line": 33623 + }, + { + "path": "crates/cowork-gui/src-tauri/src/main.rs", + "header_line": 33806 + }, + { + "path": "crates/cowork-gui/src-tauri/src/project_manager.rs", + "header_line": 33814 + }, + { + "path": "crates/cowork-gui/src-tauri/src/static_server.rs", + "header_line": 34233 + }, + { + "path": "crates/cowork-gui/tsconfig.json", + "header_line": 34324 + }, + { + "path": "crates/cowork-gui/tsconfig.node.json", + "header_line": 34356 + }, + { + "path": "litho.docs/en/3.Workflow.md", + "header_line": 34372 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/Interaction Domain.md", + "header_line": 34927 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/Memory Domain.md", + "header_line": 35262 + }, + { + "path": "litho.docs/zh/1、项目概述.md", + "header_line": 35743 + }, + { + "path": "litho.docs/zh/2、架构概览.md", + "header_line": 36038 + }, + { + "path": "litho.docs/zh/3、工作流程.md", + "header_line": 37285 + }, + { + "path": "litho.docs/zh/4、深入探索/4.10 、LLM集成.md", + "header_line": 37840 + }, + { + "path": "litho.docs/zh/4、深入探索/4.1、领域实体.md", + "header_line": 38129 + }, + { + "path": "litho.docs/zh/4、深入探索/4.2、流程调度.md", + "header_line": 38625 + }, + { + "path": "litho.docs/zh/4、深入探索/4.3、HITL人机协同.md", + "header_line": 39151 + }, + { + "path": "litho.docs/zh/4、深入探索/4.4、Agent工具系统.md", + "header_line": 39486 + }, + { + "path": "litho.docs/zh/4、深入探索/4.5、Artifacts存储.md", + "header_line": 39939 + }, + { + "path": "litho.docs/zh/4、深入探索/4.6、自迭代记忆系统.md", + "header_line": 40380 + }, + { + "path": "litho.docs/zh/4、深入探索/4.7、Cowork CLI.md", + "header_line": 40861 + }, + { + "path": "litho.docs/zh/4、深入探索/4.8、Cowork GUI前端.md", + "header_line": 41572 + }, + { + "path": "litho.docs/zh/4、深入探索/4.9、Cowork GUI后端.md", + "header_line": 42246 + } + ] +} \ No newline at end of file diff --git a/.terrain/env/agent-tools.json b/.terrain/env/agent-tools.json new file mode 100644 index 0000000..7ce085c --- /dev/null +++ b/.terrain/env/agent-tools.json @@ -0,0 +1,17 @@ +{ + "bin_dir": "~/.terrain/bin", + "rtk": "~/.terrain/bin\\rtk.exe", + "codegraph": "~/.terrain/bin\\codegraph.cmd", + "terrain": "~/.terrain/bin\\terrain.exe", + "codegraph_runtime": "~/.terrain/tools\\codegraph-runtime", + "fallback": { + "rtk": "bunx @terrain-ai/rtk", + "codegraph": "bunx codegraph", + "terrain": "bunx @terrain-ai/cli" + }, + "usage": { + "rtk": "Prefer ~/.terrain/bin/rtk if executable; else bunx @terrain-ai/rtk or npx @terrain-ai/rtk", + "codegraph": "Prefer ~/.terrain/bin/codegraph if executable; else bunx codegraph; index under .codegraph/", + "terrain": "Prefer ~/.terrain/bin/terrain if executable; else bunx @terrain-ai/cli or npx @terrain-ai/cli" + } +} \ No newline at end of file diff --git a/.terrain/env/manifest.json b/.terrain/env/manifest.json new file mode 100644 index 0000000..b6cfc16 --- /dev/null +++ b/.terrain/env/manifest.json @@ -0,0 +1,11 @@ +{ + "catalog_version": 1, + "integrated_at": "2026-07-06T02:48:26.200853+00:00", + "applied": [ + "skill-terrain-knowledge: → .agents/skills/terrain-knowledge-skill + .claude/skills/terrain-knowledge-skill", + "skill-repomix: → .agents/skills/repomix-context-skill + .claude/skills/repomix-context-skill", + "skill-codegraph: → .agents/skills/codegraph-skill + .claude/skills/codegraph-skill", + "skill-rtk: → .agents/skills/rtk-skill + .claude/skills/rtk-skill", + "tool-codegraph: CodeGraph 已初始化 .codegraph/(Agent 使用 ~/.terrain/bin/codegraph,见 .terrain/env/agent-tools.json)" + ] +} \ No newline at end of file diff --git "a/.terrain/human/1.\346\246\202\350\277\260.md" "b/.terrain/human/1.\346\246\202\350\277\260.md" new file mode 100644 index 0000000..d258113 --- /dev/null +++ "b/.terrain/human/1.\346\246\202\350\277\260.md" @@ -0,0 +1,88 @@ +# Cowork Forge 项目概述 + +Cowork Forge 是一个"AI 原生的多 Agent 软件开发平台"。它不是普通的代码生成器,也不是简单的 AI 编程助手——它是一个完整的虚拟开发团队,内部有扮演产品经理、架构师、项目经理和工程师角色的 AI Agent,通过七阶段流水线协作,把你的原始想法一步步变成可交付的软件产品。 + +想象一下,你只需要花几分钟描述想要什么,剩下的工作——需求分析、架构设计、任务分解、代码编写、质量验证、交付报告——全部由 AI 团队自动完成。整个流程中,你只在关键决策点参与确认,确保输出符合预期。这就是 Cowork Forge 承诺的核心价值:一个人可以拥有一个完整的开发团队。 + +## 它能做什么 + +Cowork Forge 的核心能力可以概括为"从想法到交付的全自动开发流水线"。简单来说,你描述需求,它交付软件。 + +**全角色 AI 团队协作**——系统中内置了 10+ 个专业 AI Agent,分别模拟软件开发中的不同角色。产品经理 Agent 负责写 PRD,架构师 Agent 负责设计技术方案,项目经理 Agent 负责分解任务,工程师 Agent 负责编写代码。每个关键角色采用 Actor-Critic 自优化模式——先干活、再自我审查、根据反馈改进,确保输出质量。 + +**7 阶段开发流水线**——从 Idea(想法捕捉)到 PRD(需求文档)→ Design(架构设计)→ Plan(实施计划)→ Coding(编码)→ Check(质量验证)→ Delivery(交付),每个阶段都有明确的输入输出和人类验证点。 + +**迭代继承体系**——支持 Genesis(首次)和 Evolution(演化)两种迭代模式。Evolution 迭代可以继承前一次迭代的代码或制品,实现增量开发。三种继承模式(None/Full/Partial)满足不同的演进需求。 + +**遗留项目导入**——可以把现有的项目导入 Cowork Forge,AI 会自动分析项目结构、检测技术栈、反向工程生成文档,让已有项目也能享受迭代管理的好处。 + +**外部 Agent 集成**——支持通过 ACP 协议集成外部的 AI Agent(如 OpenCode、Gemini CLI、Claude CLI),在编码阶段调用更专业的编码工具。 + +**知识累积**——每次迭代完成后自动提取关键决策和模式,跨迭代累积项目记忆,系统"越用越聪明"。 + +## C4 Context 图(系统全景) + +```mermaid +graph TB + subgraph UserRole["用户角色"] + Dev["独立开发者"] + PM["产品经理"] + TLead["技术负责人"] + end + + subgraph CoworkForge["Cowork Forge 系统"] + CLI["CLI 命令行
cowork"] + GUI["图形界面
Tauri + React"] + Core["核心引擎
cowork-core"] + end + + subgraph External["外部依赖"] + LLM["LLM API
OpenAI 兼容"] + Files["文件系统
JSON 持久化"] + Shell["操作系统
Shell"] + ExtAgent["外部 Agent
OpenCode/Gemini"] + end + + Dev -->|"cowork iter 'my idea'"| CLI + PM -->|"GUI 可视化操作"| GUI + TLead -->|"CLI 或 GUI"| CLI + TLead --> GUI + + CLI --> Core + GUI --> Core + + Core -->|"LLM 推理请求"| LLM + Core -->|"读写数据"| Files + Core -->|"执行命令"| Shell + Core -->|"ACP 协议"| ExtAgent +``` + +上图展示了 Cowork Forge 在生态系统中的定位:用户通过 CLI 或 GUI 与系统交互,核心引擎协调 LLM、文件系统和外部 Agent 来完成开发任务。 + +## 技术选型背后的思考 + +Cowork Forge 的技术选型有明确的考量。 + +| 技术领域 | 具体选择 | 为什么这样选 | +|---------|---------|------------| +| 语言与运行时 | Rust (edition 2024) + Tokio | Rust 的内存安全保证和高性能使 IO 密集型的多 Agent 并发成为可能,Tokio 异步运行时则是 Rust 生态的异步标准 | +| Agent 框架 | adk-rust | 提供了标准化的 Agent 构建 API 和 LoopAgent 机制,避免了从头实现 Agent 框架 | +| CLI | clap (v4) + dialoguer | Rust 生态中最成熟的 CLI 框架,derive 宏使参数声明简洁且类型安全 | +| GUI | Tauri 2 + React + Ant Design | Tauri 提供跨平台原生应用能力且体积小,React 前端生态丰富,Ant Design 组件库开箱即用 | +| 持久化 | JSON 文件 | 不依赖外部数据库,开箱即用,文件即备份。适合桌面工具的单用户场景 | +| 速率限制 | TokenBucket 算法 | 允许突发请求的同时保证长期速率,比固定延迟更高效——适合"需要时快速响应,平时不浪费"的场景 | + +## Cowork Forge 能做什么,不能做什么 + +**它能做的事**: +- 从用户的一句话描述,"端到端"完成一个软件项目的开发——生成需求、设计、计划、代码、测试、交付报告 +- 在开发过程中让用户在关键决策点参与确认,保证方向正确 +- 导入已有项目进行迭代管理,增量添加功能或修改 +- 通过外部 Agent 集成,在特定阶段调用更专业的工具 +- 跨迭代累积项目知识和设计决策,越用越聪明 + +**它不做的事**: +- 不替代 IDE——Agent 通过文件工具修改代码,不给用户提供编码 IDE +- 不直接部署到生产——但可以通过集成 Hook 触发部署流程 +- 不提供实时多人协作——聚焦 AI-Agent 协作,非人-人协作 +- 不提供数据库服务——数据以 JSON 文件形式存储在本地 diff --git "a/.terrain/human/2.\346\236\266\346\236\204.md" "b/.terrain/human/2.\346\236\266\346\236\204.md" new file mode 100644 index 0000000..63b7964 --- /dev/null +++ "b/.terrain/human/2.\346\236\266\346\236\204.md" @@ -0,0 +1,290 @@ +# 系统架构文档:Cowork Forge + +**版本:** 1.0 +**分类:** 内部架构文档 +**生成日期:** 2026-07-05 + +--- + +## 1. 架构概述 + +### 1.1 设计理念 + +Cowork Forge 的架构设计围绕几个核心原则展开,它们共同塑造了系统的形态和行为。 + +**1. 流水线驱动的开发流程** + +一切从"开发是有序的"这个观察出发——你不能在设计好之前就编码,也不能在测试之前就交付。因此,系统的核心骨架是一个 7 阶段的流水线,每个阶段有明确的输入、处理和输出。这就像汽车生产线:零件(需求)→ 冲压(设计)→ 焊接(计划)→ 组装(编码)→ 质检(检查)→ 出厂(交付)。 + +**2. 自优化的 Agent 团队** + +AI 单次生成的输出质量不稳定——同一个问题问两次可能得到不同的答案。解决这个问题的方法是让 Agent "自检":采用 Actor-Critic 模式,让一个 Agent 生成内容,另一个 Agent 审查并给出反馈,循环迭代直到满意。这模拟了人类团队中的"写代码→Code Review"工作流。 + +**3. 交互方式无关的核心引擎** + +系统应该同时服务命令行用户和图形界面用户,但核心业务逻辑不应该为任何一种交互方式做出妥协。因此,所有用户交互都通过 `InteractiveBackend` trait 抽象——核心引擎只调用 `show_message()`、`request_input()` 等方法,不知道也不关心这些消息是打印在终端还是显示在 GUI 弹窗中。 + +**4. 可配置而非硬编码** + +Agent 角色、阶段流程、集成规则——这些在早期版本中是写死在代码里的。ConfigDefinition 模块的出现改变了这一点:现在这些东西都可以通过 JSON 配置文件来定义。这意味着用户可以在不修改 Rust 代码、不重新编译的情况下,自定义开发流程、创建新的 Agent 角色、或者配置外部集成。 + +### 1.2 核心架构模式 + +| 模式 | 实现方式 | 为什么这样设计 | +|------|---------|-------------| +| Pipeline-Filter(流水线-过滤器) | 7 个 Stage 实现相同的 `Stage` trait,按序串联执行 | 开发流程本质上是顺序的,流水线模式让每个阶段的职责边界清晰,也便于在任意两个阶段之间插入 Hook | +| Actor-Critic(演员-评论家) | LoopAgent 组合 Actor 和 Critic 两个子 Agent,循环执行 | AI 生成的内容需要质量把关,Actor-Critic 模式让"干活"和"审查"分离,提升输出质量 | +| Hexagonal(六边形架构) | domain 层零外部依赖,基础设施适配器通过 trait 注入 | 核心业务逻辑不受 UI 框架、LLM 服务商等技术细节影响,便于测试和更换 | +| Strategy(策略模式) | Stage trait 定义统一接口,各阶段各自实现 | Pipeline 不需要知道每个阶段的具体实现,只需要按接口调用 | +| Decorator(装饰器模式) | TokenBucketRateLimiter 实现 Llm trait 包裹真实 LLM 客户端 | 速率限制对上层调用者完全透明,不需要 Agent 感知限流逻辑 | + +### 1.3 技术栈概述 + +| 层次/领域 | 技术选型 | 为什么这样选 | +|---------|---------|------------| +| 语言与运行时 | Rust (edition 2024) + Tokio | 内存安全+零成本抽象,适合 IO 密集型并发场景。Tokio 是 Rust 异步运行时的事实标准 | +| Agent 框架 | adk-rust | 成熟的 Agent 构建框架,提供 LoopAgent、LlmAgentBuilder、流式输出等开箱即用的能力 | +| CLI | clap (v4, derive) + dialoguer | clap 的 derive 宏让参数声明简洁,dialoguer 提供交互式选择/输入 | +| GUI | Tauri 2 + React + TypeScript + Ant Design | Tauri 提供安全的后端,React 提供灵活的前端,Ant Design 提供丰富的 UI 组件 | +| 持久化 | JSON 文件存储(`.cowork-v2/`) | 零运维、开箱即用、文件即备份,适合桌面工具的单用户场景 | +| LLM 速率限制 | TokenBucket 算法 | 允许突发请求,长期平均速率可控。max_burst=5,rate_limit=30 req/min | +| 外部 Agent 集成 | ACP(Agent Client Protocol) | 开放标准协议,支持多种外部 Agent 无缝集成 | + +--- + +## 2. 系统上下文(C4 Level 1) + +```mermaid +C4Context + title 系统上下文 - Cowork Forge + + Person(user, "用户", "开发者、产品经理或技术负责人") + + System(cowork, "Cowork Forge", "AI 多 Agent 软件开发平台,从想法到交付的全自动流水线") + + System_Ext(llm, "LLM API", "OpenAI 兼容的大语言模型服务") + System_Ext(fs, "文件系统", "项目文件和 JSON 数据存储") + System_Ext(shell, "操作系统 Shell", "编译、测试等命令行工具") + System_Ext(extAgent, "外部 Agent", "ACP 兼容的外部编码 Agent") + + Rel(user, cowork, "通过 CLI 或 GUI 使用") + Rel(cowork, llm, "调用 LLM 推理", "HTTP API") + Rel(cowork, fs, "读写项目文件和数据", "文件 I/O") + Rel(cowork, shell, "执行构建和测试命令", "子进程") + Rel(cowork, extAgent, "委托编码任务", "ACP stdio/WebSocket") +``` + +--- + +## 3. 容器视图(C4 Level 2) + +```mermaid +C4Container + title 容器架构 - Cowork Forge + + Person(user, "用户", "开发者") + + System_Boundary(coworkSys, "Cowork Forge 应用") { + Container(cli, "CLI 命令行", "Rust (clap)", "提供项目初始化、迭代管理、项目导入等功能") + Container(gui, "GUI 图形界面", "Tauri + React + TypeScript", "提供可视化的项目管理、实时监控和聊天界面") + Container(core, "核心引擎", "Rust (adk-rust)", "7 阶段流水线编排、Agent 管理、工具执行") + ContainerDb(store, "JSON 数据存储", "文件系统 (.cowork-v2/)", "项目、迭代、记忆的持久化") + } + + System_Ext(llm, "LLM API", "OpenAI 兼容") + System_Ext(extAgent, "外部 ACP Agent", "OpenCode/Gemini CLI 等") + + Rel(user, cli, "使用命令行") + Rel(user, gui, "使用图形界面") + Rel(cli, core, "调用核心 API") + Rel(gui, core, "调用核心 API") + Rel(core, store, "读写数据") + Rel(core, llm, "LLM 推理") + Rel(core, extAgent, "ACP 委托任务") +``` + +### 3.1 领域模块职责 + +| 模块/领域 | 路径 | 职责 | 关键抽象 | +|---------|------|------|---------| +| pipeline | `crates/cowork-core/src/pipeline/` | 7 阶段流水线编排与执行 | `Stage trait`, `IterationExecutor` | +| agents | `crates/cowork-core/src/agents/` | AI Agent 构建与管理 | `LoopAgent`, `LlmAgentBuilder` | +| tools | `crates/cowork-core/src/tools/` | 30+ ADK 工具实现 | `ToolNotifyFn`, `ReadFileTool` | +| instructions | `crates/cowork-core/src/instructions/` | Agent 提示词库 | 各 Agent 的指令常量 | +| domain | `crates/cowork-core/src/domain/` | 核心领域实体 | `Project`, `Iteration`, `ProjectMemory` | +| persistence | `crates/cowork-core/src/persistence/` | JSON 文件持久化 | `ProjectStore`, `IterationStore` | +| llm | `crates/cowork-core/src/llm/` | LLM 集成与速率限制 | `TokenBucketRateLimiter` | +| config_definition | `crates/cowork-core/src/config_definition/` | 数据驱动配置系统 | `ConfigRegistry` | +| interaction | `crates/cowork-core/src/interaction/` | CLI/GUI 交互抽象 | `InteractiveBackend trait` | +| acp | `crates/cowork-core/src/acp/` | 外部 Agent 协议 | `AcpClient` | +| importer | `crates/cowork-core/src/importer/` | 遗留项目导入 | `ImportConfig`, `ProjectAnalysis` | + +--- + +## 4. 组件视图(C4 Level 3) + +### 4.1 Pipeline 核心组件 + +```mermaid +graph TD + IE["IterationExecutor"] --> PS["ProjectStore"] + IE --> IS["IterationStore"] + IE --> SE["StageExecutor"] + SE --> SF["Stage 工厂
create_stage_by_id()"] + SE --> AF["Agent 工厂
create_agent_for_stage()"] + SE --> IB["InteractiveBackend"] + AF --> AG["AI Agent
LoopAgent / LlmAgentBuilder"] + AG --> TL["Tools 工具集"] + AG --> LLM["LLM Client
TokenBucketRateLimiter"] +``` + +### 4.2 Agent 组件架构 + +```mermaid +graph LR + subgraph IdeaAgent["Idea Agent(单 Agent)"] + IA["IdeaAgent
LlmAgentBuilder"] + end + + subgraph PRDLoop["PRD Loop(Actor-Critic)"] + PA["PRD Actor
生成需求文档"] + PC["PRD Critic
评审反馈"] + end + + subgraph DesignLoop["Design Loop(Actor-Critic)"] + DA["Design Actor
设计技术架构"] + DC["Design Critic
评审覆盖度"] + end + + subgraph CodingLoop["Coding Loop(Actor-Critic ×5)"] + CA["Coding Actor
编写代码"] + CC["Coding Critic
审查质量"] + end + + PA <--> PC + DA <--> DC + CA <--> CC +``` + +--- + +## 5. 关键流程 + +### 5.1 迭代执行流程 + +```mermaid +sequenceDiagram + participant U as 用户 + participant CLI as CLI + participant IE as IterationExecutor + participant SE as StageExecutor + participant AG as Agent + participant LLM as LLM API + + U->>CLI: cowork iter "my feature" + CLI->>IE: execute() + IE->>IE: create_iteration() + loop 每个阶段 + IE->>SE: run_stage(stage_name) + SE->>AG: create_agent(stage_name) + AG->>LLM: 推理请求 + LLM-->>AG: 结果返回 + AG-->>SE: 保存制品 + SE-->>U: 请求人类确认 + U-->>SE: 确认通过 + SE-->>IE: StageResult::Success + end + IE->>IE: knowledge_generation() + IE-->>CLI: 迭代完成 + CLI-->>U: 交付结果 +``` + +### 5.2 Actor-Critic 迭代循环 + +```mermaid +sequenceDiagram + participant Actor + participant Critic + participant Human + + loop Actor-Critic 循环 + Actor->>Actor: 生成/修改内容 + Actor->>Critic: 请求评审 + Critic->>Critic: 审查内容和质量 + Critic->>Actor: 反馈改进意见 + Actor->>Actor: 根据反馈修改 + alt 达到质量要求 + Actor->>Human: 请求人类验证 + Human-->>Actor: 确认通过 + Note over Actor,Human: 阶段完成 + else 需要修订 + Human->>Actor: 提供反馈 + Actor->>Critic: 继续循环 + end + end +``` + +--- + +## 6. 技术实现 + +### 6.1 关键架构模式 + +**Stage trait 统一接口**(`crates/cowork-core/src/pipeline/mod.rs:47`) +```rust +pub trait Stage: Send + Sync { + fn name(&self) -> &str; + fn description(&self) -> &str; + fn needs_confirmation(&self) -> bool { false } + async fn execute(&self, ctx: &PipelineContext, interaction: Arc) -> StageResult; + async fn execute_with_feedback(&self, ctx: &PipelineContext, interaction: Arc, feedback: &str) -> StageResult { ... } +} +``` + +**TokenBucket 速率限制器**(`crates/cowork-core/src/llm/rate_limiter.rs:32`)— 装饰器模式实现,对上层完全透明。 + +### 6.2 并发和并行策略 + +- LLM 调用串行化(concurrency=1):防止触发 API 速率限制,保证行为可预测 +- 文件操作 Tokio 异步:不阻塞主循环 +- 命令执行异步 + 超时控制:防止长时间运行任务耗尽资源 + +### 6.3 性能优化策略 + +- TokenBucket 速率限制器允许 5 个突发请求,应对初始阶段的多 Agent 启动场景 +- 知识快照数据仅保留最近 N 次迭代,防止记忆文件无限膨胀 +- 路径规范化缓存,避免重复路径解析 + +--- + +## 附录:架构决策记录(ADR) + +**ADR 1:Actor-Critic 自优化循环** + +- **决策**:对 PRD、Design、Plan、Coding 四个关键阶段采用 Actor-Critic 双 Agent 循环 +- **原因**:AI 单次生成的内容质量不稳定。Actor-Critic 模式让"干活"和"审查"分离,通过自我博弈提升输出质量。观察依据:`crates/cowork-core/src/agents/mod.rs:68-99` +- **后果**:增加了阶段执行时间(多次 LLM 调用),但显著提升了输出质量 + +**ADR 2:JSON 文件持久化而非数据库** + +- **决策**:使用 JSON 文件持久化(`.cowork-v2/`),放弃关系型数据库 +- **原因**:桌面工具不需要多用户并发访问,JSON 文件零运维、开箱即用、文件即备份。观察依据:`crates/cowork-core/src/persistence/mod.rs` +- **后果**:无法支持多用户协作场景,但换来了最大的部署便利性 + +**ADR 3:TokenBucket 算法而非固定延迟** + +- **决策**:TokenBucket 速率限制器,允许 max_burst=5 个突发请求,长期速率 30 req/min +- **原因**:固定延迟等待导致每次请求都要等待,TokenBucket 在有空闲配额时可以立即执行,更加高效。观察依据:`crates/cowork-core/src/llm/rate_limiter.rs:32-60` +- **后果**:初始阶段的多 Agent 启动更快,长期运行仍符合 API 速率限制 + +**ADR 4:InteractiveBackend trait 抽象交互方式** + +- **决策**:通过 `InteractiveBackend` trait 抽象所有用户交互,CLI 和 GUI 分别实现 +- **原因**:核心引擎需要同时服务 CLI 和 GUI 用户,但不应该为任何一种界面优化而牺牲通用性。观察依据:`crates/cowork-core/src/interaction/mod.rs:108-160` +- **后果**:新增交互方式只需要实现 trait,无需修改核心逻辑 + +**ADR 5:迭代继承体系(Genesis / Evolution)** + +- **决策**:引入 Genesis(首次)和 Evolution(演化)两种迭代模式,支持三种继承策略 +- **原因**:软件开发很少一次完成,大多数是对已有代码的修改和功能新增。迭代继承让系统天然支持增量开发。观察依据:`crates/cowork-core/src/domain/iteration.rs` +- **后果**:增加了迭代管理的复杂度,但使得增量开发场景非常自然 diff --git "a/.terrain/human/3.\345\267\245\344\275\234\346\265\201.md" "b/.terrain/human/3.\345\267\245\344\275\234\346\265\201.md" new file mode 100644 index 0000000..0e640a6 --- /dev/null +++ "b/.terrain/human/3.\345\267\245\344\275\234\346\265\201.md" @@ -0,0 +1,235 @@ +# 核心工作流 + +## 1. 工作流概述 + +### 1.1 系统架构与工作流理念 + +Cowork Forge 的工作流理念可以概括为"有序的接力赛"——每个阶段完成自己的工作后把"接力棒"交给下一个阶段,但关键赛段有"自我检视"环节来确保接棒不失误。 + +整个系统的工作流分为三个层次: +- **宏观层**:7 阶段开发流水线,从想法到交付的完整路径 +- **中观层**:每个阶段内部的 Actor-Critic 自优化循环 +- **微观层**:Agent 执行时的工具调用和数据操作 + +### 1.2 核心执行路径 + +```mermaid +flowchart LR + subgraph 输入层 + A["用户想法"] --> B["Idea 阶段
需求捕捉"] + end + subgraph 生成层 + C["PRD 阶段
需求文档"] --> D["Design 阶段
架构设计"] + D --> E["Plan 阶段
实施计划"] + E --> F["Coding 阶段
编码实现"] + end + subgraph 验证层 + F --> G["Check 阶段
质量验证"] + G --> H["Delivery 阶段
交付报告"] + end + subgraph 输出层 + H --> I["完整软件项目"] + end + 输入层 --> 生成层 --> 验证层 --> 输出层 +``` + +--- + +## 2. 主要工作流 + +### 2.1 7 阶段开发流水线 + +这是 Cowork Forge 的心脏——从用户的一句话想法,到可交付的软件项目,历经 7 个阶段的接力执行。如果把系统比作一个"AI 工厂",这条流水线就是工厂的生产线。 + +**触发方式**:用户在 CLI 执行 `cowork iter --project "my-project" "想法描述"` 或通过 GUI 创建新迭代 +**入口**:`crates/cowork-cli/src/main.rs:119` → `IterationExecutor::execute()` at `crates/cowork-core/src/pipeline/executor/mod.rs:79` + +#### 流程图 + +```mermaid +flowchart TD + Start(["用户提交想法"]) --> Idea["Idea 阶段
需求捕捉"] + Idea --> IdeaConfirm{"人类确认?"} + IdeaConfirm -->|通过| PRD + IdeaConfirm -->|拒绝| Idea + + PRD["PRD 阶段
Actor 生成 + Critic 评审"] --> PRDConfirm{"人类确认?"} + PRDConfirm -->|通过| Design + PRDConfirm -->|拒绝/反馈| PRD + + Design["Design 阶段
Actor 设计 + Critic 评审"] --> DesignConfirm{"人类确认?"} + DesignConfirm -->|通过| Plan + DesignConfirm -->|拒绝/反馈| Design + + Plan["Plan 阶段
Actor 计划 + Critic 评审"] --> PlanConfirm{"人类确认?"} + PlanConfirm -->|通过| Coding + PlanConfirm -->|拒绝/反馈| Plan + + Coding["Coding 阶段
Actor 编码 + Critic 审查"] --> CodingConfirm{"人类确认?"} + CodingConfirm -->|通过| Check + CodingConfirm -->|拒绝/反馈| Coding + + Check["Check 阶段
质量验证"] --> CheckResult{"检测通过?"} + CheckResult -->|是| Delivery + CheckResult -->|否| Coding + + Delivery["Delivery 阶段
交付报告"] --> End(["迭代完成"]) +``` + +#### 时序图 + +```mermaid +sequenceDiagram + participant U as 用户 + participant Exec as IterationExecutor + participant Stage as StageExecutor + participant Agent as AI Agent + participant LLM as LLM API + participant Store as 持久化 + + U->>Exec: 提交迭代请求 + Exec->>Exec: 创建迭代(Genesis/Evolution) + Exec->>Store: 保存迭代 + Exec->>Stage: run_stage("idea") + Stage->>Agent: create_idea_agent() + Agent->>LLM: 生成 idea + LLM-->>Agent: 输出内容 + Agent->>Store: save_idea + Stage-->>U: 请求确认 + U-->>Stage: 通过 + + Exec->>Stage: run_stage("prd") + Stage->>Agent: create_prd_loop() + loop Actor-Critic 循环 + Agent->>LLM: Actor 生成 PRD + LLM-->>Agent: PRD 草案 + Agent->>LLM: Critic 评审 + LLM-->>Agent: 反馈意见 + end + Agent->>Store: save_prd_doc + Stage-->>U: 请求确认 + U-->>Stage: 通过 + + Exec->>Stage: run_stage("design") + Stage->>Agent: create_design_loop() + loop Actor-Critic 循环 + Agent->>LLM: Actor 设计架构 + LLM-->>Agent: 设计方案 + Agent->>LLM: Critic 评审 + LLM-->>Agent: 反馈意见 + end + Agent->>Store: save_design_doc + Stage-->>U: 请求确认 + U-->>Stage: 通过 + + Exec->>Stage: run_stage("plan") + Stage->>Agent: create_plan_loop() + loop Actor-Critic 循环 + Agent->>LLM: Actor 分解任务 + LLM-->>Agent: 计划方案 + Agent->>LLM: Critic 评审依赖 + LLM-->>Agent: 反馈意见 + end + Agent->>Store: save_plan_doc + Stage-->>U: 请求确认 + U-->>Stage: 通过 + + Exec->>Stage: run_stage("coding") + Stage->>Agent: create_coding_loop() + loop max 5 次迭代 + Agent->>LLM: Actor 编码实现 + LLM-->>Agent: 代码输出 + Agent->>LLM: Critic 审查代码 + LLM-->>Agent: 审查意见 + end + Stage-->>U: 请求确认 + U-->>Stage: 通过 + + Exec->>Stage: run_stage("check") + Stage->>Agent: create_check_agent() + Agent->>LLM: 验证质量和完整 + LLM-->>Agent: 验证结果 + alt 验证失败 + Stage->>Stage: 跳回 coding 阶段 + else 验证通过 + Agent->>Store: save_check_report + end + + Exec->>Stage: run_stage("delivery") + Stage->>Agent: create_delivery_agent() + Agent->>Store: save_delivery_report + Agent->>Store: copy_workspace_to_project + Exec->>Exec: knowledge_generation() + Exec-->>U: 迭代完成报告 +``` + +#### 阶段说明 + +| 阶段 | 执行者 | 输入 | 输出 | 说明 | +|------|-------|------|------|------| +| Idea | Idea Agent | 用户想法描述 | idea.md | 与用户对话捕捉需求,输出结构化的项目概述 | +| PRD | PRD Actor + Critic | idea.md | prd.md | 生成产品需求文档,Actor-Critic 循环自优化 | +| Design | Design Actor + Critic | prd.md | design.md | 设计技术架构和组件,评审覆盖度 | +| Plan | Plan Actor + Critic | design.md | plan.md | 分解任务、规划依赖、设定实施路径 | +| Coding | Coding Actor + Critic | plan.md | 项目代码 | 编码实现,最多 5 次 Actor-Critic 迭代 | +| Check | Check Agent | 所有制品 | check_report.md | 验证需求覆盖、数据格式、任务完成度 | +| Delivery | Delivery Agent | 所有制品 | delivery_report.md | 生成交付报告,将代码复制到输出目录 | + +### 2.2 遗留项目导入工作流 + +**触发方式**:用户执行 `cowork import /path/to/project` +**入口**:`crates/cowork-cli/src/commands/import.rs` → `crates/cowork-core/src/importer/` + +```mermaid +flowchart LR + A["指定项目路径"] --> B["扫描目录结构"] + B --> C["检测技术栈"] + C --> D["读取 README
和关键文件"] + D --> E["AI 反向工程"] + E --> F["生成文档
idea.md/prd.md/design.md/plan.md"] + F --> G["创建初始迭代"] +``` + +**设计意图**:这个工作流解决了一个现实问题——大多数开发者手上都有大量已有项目,这些项目没有 Cowork Forge 需要的结构化文档。Importer 通过分析项目结构、检测技术栈、读取配置和关键文件,然后用 LLM 综合这些信息生成标准文档,让已有项目也能融入迭代体系。 + +### 2.3 PM Agent 交付后交互 + +**触发方式**:迭代完成后用户在 GUI 中与 PM Agent 对话 +**入口**:`crates/cowork-core/src/agents/mod.rs:659`(`execute_pm_agent_message_streaming`) + +```mermaid +flowchart TD + Start(["迭代完成"]) --> PM["PM Agent 激活"] + PM --> UserMsg["用户发送消息"] + UserMsg --> Analyze["PM Agent 分析意图"] + Analyze --> Decision{"意图识别"} + Decision -->|"修 Bug/改功能"| Goto["pm_goto_stage
跳转到编码阶段"] + Decision -->|"新增功能"| Create["pm_create_iteration
创建演化迭代"] + Decision -->|"询问项目信息"| Respond["pm_respond
查询记忆后回答"] + Decision -->|"意图不明确"| Clarify["请求澄清"] + Goto --> End(["继续开发"]) + Create --> End + Respond --> UserMsg + Clarify --> UserMsg +``` + +--- + +## 3. 并发与异步模型 + +Cowork Forge 采用 Tokio 异步运行时,但 LLM 调用是串行化的。这看似矛盾,实则是经过权衡的设计选择: + +- **LLM 串行化(concurrency=1)**:LLM API 是整个系统"最慢的环节"。并行调用不仅容易触发 API 速率限制、增加成本,还会让系统的行为变得不可预测——多个 Agent 同时输出,谁先谁后?串行化让行为有序可追踪,调试起来也更简单。 +- **TokenBucket 突发支持**:虽然串行化,但 TokenBucket 算法允许在配额充足时快速发出多个请求(max_burst=5),应对阶段切换时的"小爆发"。 +- **文件操作异步**:读写文件、列出目录等 IO 操作使用 Tokio 异步,不会阻塞主循环。 +- **命令执行异步 + 超时**:编译、测试等外部命令在异步任务中执行,带有超时控制防止资源耗尽。 + +## 4. 错误处理策略 + +Cowork Forge 的错误处理核心理念是"局部失败不应导致全局中断"。具体策略: + +- **统一错误类型**:全系统使用 `anyhow::Result`,错误通过 `?` 运算符逐层传播,最终由顶层处理。代码位置:`crates/cowork-core/src/agents/mod.rs:16` +- **阶段级容错**:一个阶段执行失败(如 LLM 调用超时)不会导致整个迭代崩溃——返回 `StageResult::Failed`,迭代暂停等待用户处理。代码位置:`crates/cowork-core/src/pipeline/mod.rs:19-25` +- **Actor-Critic 重试**:Actor-Critic 循环中,Critic 的反馈可能要求 Actor 重新生成内容。这是设计预期的"正常错误路径",不是异常。 +- **LLM 调用重试**:TokenBucketRateLimiter 在 LLM 调用失败时自动重试(最多 3 次)。代码位置:`crates/cowork-core/src/llm/rate_limiter.rs` +- **人类验证作为安全网**:关键阶段的输出需要人类确认后才能继续,这防止了 AI 的"自信错误"影响后续流程。 diff --git a/.terrain/human/4.Deep-Exploration/acp.md b/.terrain/human/4.Deep-Exploration/acp.md new file mode 100644 index 0000000..580c898 --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/acp.md @@ -0,0 +1,34 @@ +# ACP 领域 + +**模块路径**:`crates/cowork-core/src/acp/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +ACP 模块是 Cowork Forge 的"外交部门"——它负责通过 Agent Client Protocol 与系统外部的 AI Agent 通信。当内置的 Coding Agent 不够用时,可以调用外部编码 Agent(如 OpenCode、Gemini CLI、Claude CLI)来完成编码任务。 + +--- + +## 核心功能点 + +1. **ACP 客户端**——`AcpClient` 实现 ACP 协议,支持 stdio 和 WebSocket 传输。`crates/cowork-core/src/acp/client.rs` +2. **外部编码 Agent 集成**——`ExternalCodingAgent` 适配 OpenCode、iFlow、Codex、Gemini CLI、Claude CLI。`crates/cowork-core/src/agents/external_coding_agent.rs` + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `AcpClient` | `crates/cowork-core/src/acp/client.rs` | ACP 协议客户端 | +| `ExternalCodingAgent` | `crates/cowork-core/src/agents/external_coding_agent.rs` | 外部编码 Agent 适配器 | + +--- + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| tools | 被依赖 | ACP 工具调用外部 Agent | diff --git a/.terrain/human/4.Deep-Exploration/agents.md b/.terrain/human/4.Deep-Exploration/agents.md new file mode 100644 index 0000000..44df51a --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/agents.md @@ -0,0 +1,90 @@ +# Agents 领域 + +**模块路径**:`crates/cowork-core/src/agents/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +Agents 模块是 Cowork Forge 的"人力资源部"——它负责创建和管理系统中所有的 AI Agent。每个 Agent 就像工厂里的一个"工人",有的当产品经理(PRD Agent),有的当架构师(Design Agent),有的写代码(Coding Agent)。但和人类团队不同,这里的每个关键岗位是一对工人:**Actor 负责干活,Critic 负责审查**,通过这种"互相监督"的机制来保证输出质量。 + +模块的设计智慧藏在它的 Bug 修复注释里。`crates/cowork-core/src/agents/mod.rs:4-10` 有一段重要的注释,解释了 adk-rust 框架中 `SequentialAgent` 在 `LoopAgent` 之后会异常终止的问题。解决方案不是修改框架,而是将 `max_iterations` 设为 1——让 LoopAgent 自然完成而非通过 `exit_loop()` 终止。这是一个"懂得和框架妥协"的务实设计。 + +--- + +## 核心功能点 + +1. **Agent 工厂函数**——`create_idea_agent()`、`create_prd_loop()`、`create_design_loop()`、`create_plan_loop()`、`create_coding_loop()`、`create_check_agent()`、`create_delivery_agent()`,覆盖 7 个阶段。每个函数使用 `LlmAgentBuilder` 注入特定指令和工具集。代码位置:`crates/cowork-core/src/agents/mod.rs:32-507` +2. **Actor-Critic 循环**——PRD/Design/Plan/Coding 使用 `LoopAgent` 组合 Actor 和 Critic,Coding 有 5 次迭代上限。代码位置:`crates/cowork-core/src/agents/mod.rs:68-378` +3. **PM Agent 流式交互**——`execute_pm_agent_message_streaming()` 支持 GUI 实时流式推送,通过回调接口 `PMAgentStreamCallback` 通知文本和工具调用。代码位置:`crates/cowork-core/src/agents/mod.rs:659-917` + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `create_idea_agent()` | `crates/cowork-core/src/agents/mod.rs:32` | 创建 Idea Agent,捕捉需求生成 idea.md | +| `create_prd_loop()` | `crates/cowork-core/src/agents/mod.rs:68` | PRD Actor+Cirtic LoopAgent,生成自优化 PRD | +| `create_design_loop()` | `crates/cowork-core/src/agents/mod.rs:149` | Design Actor+Cirtic LoopAgent,设计技术架构 | +| `create_plan_loop()` | `crates/cowork-core/src/agents/mod.rs:223` | Plan Actor+Cirtic LoopAgent,分解任务和依赖 | +| `create_coding_loop()` | `crates/cowork-core/src/agents/mod.rs:301` | Coding Actor+Cirtic LoopAgent(5 次迭代),编写代码 | +| `create_check_agent()` | `crates/cowork-core/src/agents/mod.rs:384` | Check Agent,验证质量和完整性 | +| `create_delivery_agent()` | `crates/cowork-core/src/agents/mod.rs:444` | Delivery Agent,生成交付报告 | +| `create_project_manager_agent()` | `crates/cowork-core/src/agents/mod.rs:548` | PM Agent,交付后聊天交互 | +| `PMAgentResult` | `crates/cowork-core/src/agents/mod.rs:621` | PM Agent 执行结果,包含响应和动作 | +| `PMAgentStreamCallback` trait | `crates/cowork-core/src/agents/mod.rs:652` | 流式回调接口,GUI 实时显示 | + +--- + +## 内部数据流 + +```mermaid +flowchart TD + A["Pipeline 请求创建 Agent"] --> B{"阶段类型?"} + B -->|Idea/Check/Delivery| C["LlmAgentBuilder
单 Agent"] + B -->|PRD/Design/Plan| D["LoopAgent
Actor + Critic
max_iterations=1"] + B -->|Coding| E["LoopAgent
Actor + Critic
max_iterations=5"] + + C --> F["注入 instructions"] + C --> G["注入 tools"] + C --> H["绑定 llm"] + C --> I["执行 + 输出"] + + D --> J["Actor: 生成内容"] + J --> K["Critic: 评审反馈"] + K --> L{"满意?"} + L -->|否| J + L -->|是| I +``` + +--- + +## 关键接口与扩展点 + +所有 Agent 通过 `LlmAgentBuilder` 创建,可以灵活组合指令、工具和模型。`config_definition/agent_factory.rs` 中的 `create_agent_for_stage()` 和 `create_agent_from_config()` 提供了基于配置的 Agent 创建方式。PM Agent 可以通过 MCP 工具集扩展能力(`crates/cowork-core/src/agents/mod.rs:563`)。 + +--- + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| instructions | 依赖 | 使用提示词常量构建 Agent 指令 | +| tools | 依赖 | 注入各种 ADK 工具执行文件/数据/验证操作 | +| domain | 依赖 | 需要访问 Iteration 和 Project 数据 | +| llm | 依赖 | 绑定 LLM 模型进行推理 | +| config_definition | 依赖 | 通过 agent_factory 从配置创建 Agent | + +--- + +## 性能考量 + +所有 Agent 执行异步(基于 Tokio),但 LLM 调用通过 TokenBucketRateLimiter 串行化。Coding Loop 的 max_iterations=5 比其他 Loop(1)更多,因为编码任务通常需要多次迭代。PM Agent 的流式 API 支持 GUI 实时显示文本输出。 + +--- + +## 实现亮点 + +**SequentialAgent 终止 Bug 的解决方案**(`crates/cowork-core/src/agents/mod.rs:4-10`):adk-rust 的 LoopAgent 在子 Agent 调用 `exit_loop()` 时会终止整个 SequentialAgent。解决方案是 `max_iterations=1`,让 LoopAgent 自然完成而非通过 exit_loop 终止。这个妥协简洁有效地解决了问题,没有改动框架代码。 diff --git a/.terrain/human/4.Deep-Exploration/config_definition.md b/.terrain/human/4.Deep-Exploration/config_definition.md new file mode 100644 index 0000000..0c7db64 --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/config_definition.md @@ -0,0 +1,49 @@ +# ConfigDefinition 领域 + +**模块路径**:`crates/cowork-core/src/config_definition/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +ConfigDefinition 是 Cowork Forge 的"规章制度手册"。它把原来硬编码在 Rust 代码里的 Agent 定义、阶段流程、集成配置等"内部规定",变成了可以通过 JSON 文件随时修改的外部配置。以前想添加一个新 Agent 角色需要修改代码重新编译,现在只需要写一个配置文件就能注册。 + +这是从"固定框架"到"可配置平台"的关键架构转变。ConfigRegistry 就像"公司的规章制度登记处"——所有定义(Agent、Stage、Flow、Integration)都在这里注册、查询和管理。 + +--- + +## 核心功能点 + +1. **Agent 定义**——`AgentDefinition` 定义 Agent 的角色、指令来源、工具集和模型参数 +2. **Stage 定义**——`StageDefinition` 配置阶段的执行方式(Simple/ActorCritic)、Hook 点和制品模板 +3. **Flow 定义**——`FlowDefinition` 定义自定义流程的阶段组合和顺序 +4. **ConfigRegistry**——全局注册表,提供查询、验证和生命周期管理 +5. **Agent Factory**——`create_agent_for_stage()` 根据配置动态创建 Agent + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `AgentDefinition` | `crates/cowork-core/src/config_definition/agent_definition.rs` | 定义 Agent 角色、指令、工具和模型参数 | +| `StageDefinition` | `crates/cowork-core/src/config_definition/stage_definition.rs` | 定义阶段的执行方式和 Hook | +| `FlowDefinition` | `crates/cowork-core/src/config_definition/flow_definition.rs` | 定义自定义流程的阶段序列 | +| `ConfigRegistry` | `crates/cowork-core/src/config_definition/registry.rs:41` | 全局注册表,所有定义的管理中心 | +| `ConfigValidator` | `crates/cowork-core/src/config_definition/validator.rs` | 验证配置完整性 | + +--- + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| pipeline | 被依赖 | Pipeline 查询 Flow 和 Stage 配置 | +| agents | 被依赖 | Agent Factory 根据 AgentDefinition 构建 Agent | + +--- + +## 跨模块协作场景 + +**在流水线执行中**:`IterationExecutor` → `get_stages_from_flow()` 查询 `ConfigRegistry` → `create_agent_for_stage()` 根据 `AgentDefinition` 创建 Agent → Agent 执行 → 检查 Hook 配置触发外部集成。 diff --git a/.terrain/human/4.Deep-Exploration/domain.md b/.terrain/human/4.Deep-Exploration/domain.md new file mode 100644 index 0000000..10fdc09 --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/domain.md @@ -0,0 +1,41 @@ +# Domain 领域 + +**模块路径**:`crates/cowork-core/src/domain/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +Domain 是 Cowork Forge 的"骨架"——它定义了系统中最重要的几个概念:项目(Project)、迭代(Iteration)、记忆(Memory)。这些是所有其他模块操作的基础。特别值得一提的是**迭代继承**(InheritanceMode),这是 Cowork Forge 的核心创新:每个迭代都是一个独立的开发周期,可以"继承"前一个迭代的代码或知识,让增量开发变得自然。 + +--- + +## 核心功能点 + +1. **Project 实体**——根聚合,管理名称、迭代列表、状态。代码位置:`crates/cowork-core/src/domain/project.rs:6` +2. **Iteration 实体**——开发周期实体,Genesis/Evolution 创建、状态流转(Draft→Running→Paused→Completed→Failed)。代码位置:`crates/cowork-core/src/domain/iteration.rs:8` +3. **InheritanceMode**——三种继承策略:None(全新)、Full(完整复制)、Partial(只复制制品)。代码位置:`crates/cowork-core/src/domain/iteration.rs` +4. **ProjectMemory 记忆系统**——跨迭代知识累积(Decisions、Patterns、Context),支持关键词查询和快照管理。代码位置:`crates/cowork-core/src/domain/memory.rs:7` + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `Project` | `crates/cowork-core/src/domain/project.rs:6` | 项目根实体 | +| `Iteration` | `crates/cowork-core/src/domain/iteration.rs:8` | 开发周期实体 | +| `InheritanceMode` | `crates/cowork-core/src/domain/iteration.rs` | 迭代继承策略枚举 | +| `ProjectMemory` | `crates/cowork-core/src/domain/memory.rs:7` | 跨迭代记忆系统 | +| `IterationKnowledge` | `crates/cowork-core/src/domain/memory.rs:83` | 单次迭代知识快照 | + +--- + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| persistence | 被依赖 | 保存和加载 Project/Iteration/Memory | +| pipeline | 被依赖 | 读取和更新 Iteration 状态 | +| agents | 被依赖 | 需要访问 Project/Iteration 上下文 | diff --git a/.terrain/human/4.Deep-Exploration/importer.md b/.terrain/human/4.Deep-Exploration/importer.md new file mode 100644 index 0000000..237ea11 --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/importer.md @@ -0,0 +1,37 @@ +# Importer 领域 + +**模块路径**:`crates/cowork-core/src/importer/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +Importer 是 Cowork Forge 的"考古学家"。它能让已有项目进入 Cowork Forge 的迭代体系——自动分析项目结构、检测技术栈、读取配置和关键文件,然后用 LLM 综合成标准文档(idea.md、prd.md、design.md、plan.md)。 + +--- + +## 核心功能点 + +1. **项目导入配置**——`ImportConfig` 定义导入参数(生成哪些文档、是否使用 LLM)。`crates/cowork-core/src/importer/import_config.rs` +2. **项目分析器**——扫描目录结构、检测技术栈。`crates/cowork-core/src/importer/project_analyzer.rs` +3. **制品生成器**——LLM 将分析结果综合成文档。`crates/cowork-core/src/importer/artifact_generator.rs` + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `ImportConfig` | `crates/cowork-core/src/importer/import_config.rs` | 导入参数配置 | +| `ImportResult` | `crates/cowork-core/src/importer/mod.rs` | 导入结果 | +| `ProjectAnalysis` | `crates/cowork-core/src/importer/mod.rs` | 项目分析结果 | + +--- + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| domain | 依赖 | 创建 Project/Iteration | +| persistence | 依赖 | 保存 Project/Iteration | diff --git a/.terrain/human/4.Deep-Exploration/instructions.md b/.terrain/human/4.Deep-Exploration/instructions.md new file mode 100644 index 0000000..b1b2ca0 --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/instructions.md @@ -0,0 +1,39 @@ +# Instructions 领域 + +**模块路径**:`crates/cowork-core/src/instructions/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +Instructions 是 Cowork Forge 的"岗位说明书"集合——它包含所有 Agent 角色的提示词(Prompt)。每个 Agent 在创建时加载对应的指令,这些指令定义了 Agent 的角色定位、行为规则和产出要求。 + +--- + +## 核心功能点 + +1. **阶段 Actor 指令**——Idea、PRD、Design、Plan、Coding 的生成指令 +2. **阶段 Critic 指令**——PRD、Design、Plan、Coding 的评审和反馈指令 +3. **Check/Delivery 指令**——验证和交付阶段的专属指令 +4. **PM Agent 指令**——交付后 PM Agent 的交互指令 + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `IDEA_AGENT_INSTRUCTION` | `crates/cowork-core/src/instructions/idea.rs` | Idea Agent 提示词 | +| `PRD_ACTOR_INSTRUCTION` | `crates/cowork-core/src/instructions/prd.rs` | PRD Actor 生成指令 | +| `PRD_CRITIC_INSTRUCTION` | `crates/cowork-core/src/instructions/prd.rs` | PRD Critic 评审指令 | +| `CODING_ACTOR_INSTRUCTION` | `crates/cowork-core/src/instructions/coding.rs` | Coding Actor 编码指令 | +| `PROJECT_MANAGER_AGENT_INSTRUCTION` | `crates/cowork-core/src/instructions/project_manager.rs` | PM Agent 指令 | + +--- + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| agents | 被依赖 | Agent 工厂引用指令常量构建 Agent | diff --git a/.terrain/human/4.Deep-Exploration/integration.md b/.terrain/human/4.Deep-Exploration/integration.md new file mode 100644 index 0000000..1e126c2 --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/integration.md @@ -0,0 +1,28 @@ +# Integration 领域 + +**模块路径**:`crates/cowork-core/src/integration/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +Integration 是 Cowork Forge 的"API 网关"——它允许在流水线执行过程中触发外部系统调用。比如 Delivery 阶段完成后自动触发部署 Webhook,或将 PRD 同步到需求管理工具。 + +--- + +## 核心功能点 + +1. **Hook 管理器**——`HookManager` 在阶段完成/失败等点触发回调。`crates/cowork-core/src/integration/hooks.rs` +2. **REST Adapter**——`RestAdapter` 实现 REST API 调用(POST + 认证)。`crates/cowork-core/src/integration/rest_adapter.rs` +3. **Integration 定义**——配置集成类型、连接参数、认证方式和触发事件(在 `config_definition` 模块中) + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `HookManager` | `crates/cowork-core/src/integration/hooks.rs` | 执行钩子管理 | +| `RestAdapter` | `crates/cowork-core/src/integration/rest_adapter.rs` | REST API 调用适配器 | +| `IntegrationAdapter` trait | `crates/cowork-core/src/integration/adapters.rs` | 集成适配器统一接口 | diff --git a/.terrain/human/4.Deep-Exploration/interaction.md b/.terrain/human/4.Deep-Exploration/interaction.md new file mode 100644 index 0000000..19d8e71 --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/interaction.md @@ -0,0 +1,45 @@ +# Interaction 领域 + +**模块路径**:`crates/cowork-core/src/interaction/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +Interaction 是 Cowork Forge 的"翻译官"——它定义了内核与用户的沟通协议。系统内核只需要调用 `show_message()`、`request_input()` 等方法,具体的展示形式(命令行打印还是图形弹窗)由实现者决定。这让同一套内核代码既能服务 CLI 也能服务 GUI。 + +--- + +## 核心功能点 + +1. **InteractiveBackend trait**——用户交互的完整接口定义(消息、输入、进度、流式)。`crates/cowork-core/src/interaction/mod.rs:108-160` +2. **CliBackend 实现**——基于 dialoguer + console。`crates/cowork-core/src/interaction/cli.rs` +3. **TauriBackend 实现**——基于 Tauri 事件系统。`crates/cowork-core/src/interaction/tauri.rs` + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `InteractiveBackend` trait | `crates/cowork-core/src/interaction/mod.rs:109` | 用户交互统一接口 | +| `CliBackend` | `crates/cowork-core/src/interaction/cli.rs` | CLI 模式交互实现 | +| `TauriBackend` | `crates/cowork-core/src/interaction/tauri.rs` | Tauri GUI 模式实现 | +| `MessageContext` | `crates/cowork-core/src/interaction/mod.rs:51` | 消息上下文(Agent 名、消息类型、阶段) | + +--- + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| pipeline | 被依赖 | Pipeline 通过 InteractiveBackend 与用户交互 | +| cowork-cli | 实现 | CLI 创建 CliBackend | +| cowork-gui | 实现 | GUI 创建 TauriBackend | + +--- + +## 跨模块协作场景 + +**在阶段执行中**:`IterationExecutor` → `interaction.show_message()` 显示阶段开始 → Agent 通过 `send_streaming()` 推送实时输出 → 关键决策点通过 `request_input()` 请求确认 → 确认通过后继续。 diff --git a/.terrain/human/4.Deep-Exploration/llm.md b/.terrain/human/4.Deep-Exploration/llm.md new file mode 100644 index 0000000..667c98e --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/llm.md @@ -0,0 +1,59 @@ +# LLM 领域 + +**模块路径**:`crates/cowork-core/src/llm/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +LLM 模块是 Cowork Forge 的"大脑接口"——它负责与外部大语言模型 API 通信,并确保通信过程不会因过于频繁的请求而被限流。如果把系统比作一个 AI 工厂,LLM 模块就是工厂的"电力系统":没有它所有机器都转不起来,但如果电压不稳(API 限流),整个工厂都会瘫痪。 + +--- + +## 核心功能点 + +1. **LLM 客户端创建**——从 config.toml 加载 API 配置创建 OpenAI 兼容的 LLM 客户端。代码位置:`crates/cowork-core/src/llm/config.rs` +2. **TokenBucket 速率限制**——允许 5 个突发请求,长期平均速率 30 req/min。代码位置:`crates/cowork-core/src/llm/rate_limiter.rs:32-60` +3. **装饰器模式**——`TokenBucketRateLimiter` 实现 `Llm` trait 包裹真实 LLM 客户端,对上层完全透明 + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `TokenBucketRateLimiter` | `crates/cowork-core/src/llm/rate_limiter.rs:32` | TokenBucket 速率限制装饰器 | +| `create_llm_client()` | `crates/cowork-core/src/llm/config.rs` | 从配置创建 LLM 客户端 | + +--- + +## 内部数据流 + +```mermaid +flowchart TD + A["Agent 请求 LLM"] --> B["TokenBucketRateLimiter
检查令牌"] + B --> C{"有可用令牌?"} + C -->|是| D["消耗令牌
转发请求"] + C -->|否| E["等待补充"] + E --> B + D --> F["真实 LLM API"] + F --> G["返回响应"] + G --> H["补充令牌"] + H --> A +``` + +--- + +## 关键接口与扩展点 + +`TokenBucketRateLimiter` 实现了 `adk_core::Llm` trait,可透明替换任意 `Llm` 实现。参数 `max_burst` 和 `rate_limit_per_minute` 可配置。默认 max_burst=5,rate_limit=30 req/min。 + +--- + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| agents | 被依赖 | Agent 绑定 LLM 模型进行推理 | +| pipeline | 被依赖 | StageExecutor 创建 LLM 客户端 | diff --git a/.terrain/human/4.Deep-Exploration/persistence.md b/.terrain/human/4.Deep-Exploration/persistence.md new file mode 100644 index 0000000..f1d4ea6 --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/persistence.md @@ -0,0 +1,54 @@ +# Persistence 领域 + +**模块路径**:`crates/cowork-core/src/persistence/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +Persistence 是 Cowork Forge 的"文件柜"。它把系统中的所有数据(项目信息、迭代快照、项目记忆)写到硬盘上的 JSON 文件中。选择 JSON 文件而非数据库,是因为桌面工具追求"开箱即用"——用户不需要安装和配置数据库。数据文件都在 `.cowork-v2/` 目录下,方便 Git 管理和备份。 + +--- + +## 核心功能点 + +1. **ProjectStore**——保存和加载项目根信息。`crates/cowork-core/src/persistence/project_store.rs` +2. **IterationStore**——保存和加载迭代快照(包含进度、制品)。`crates/cowork-core/src/persistence/iteration_store.rs` +3. **MemoryStore**——保存和加载项目记忆(决策、模式、知识)。`crates/cowork-core/src/persistence/memory_store.rs` +4. **工作区路径管理**——支持 GUI 设置全局路径,解决 macOS 路径问题。`crates/cowork-core/src/persistence/mod.rs:20-35` + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `ProjectStore` | `crates/cowork-core/src/persistence/project_store.rs` | 项目根信息 JSON 读写 | +| `IterationStore` | `crates/cowork-core/src/persistence/iteration_store.rs` | 迭代快照 JSON 管理 | +| `MemoryStore` | `crates/cowork-core/src/persistence/memory_store.rs` | 项目记忆 JSON 持久化 | + +--- + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| domain | 依赖 | 持久化 domain 类型(Project/Iteration/Memory) | +| pipeline | 被依赖 | Pipeline 通过 Store 保存/加载数据 | +| tools | 被依赖 | 数据工具通过 Store 操作数据 | + +--- + +## 数据目录结构 + +``` +.cowork-v2/ +├── project.json +├── iterations/ +│ └── {iteration_id}.json +├── memory/ +│ ├── project/project_memory.json +│ └── iterations/{iteration_id}.json +└── workspace/{iteration_id}/ +``` diff --git a/.terrain/human/4.Deep-Exploration/pipeline.md b/.terrain/human/4.Deep-Exploration/pipeline.md new file mode 100644 index 0000000..3334379 --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/pipeline.md @@ -0,0 +1,112 @@ +# Pipeline 领域 + +**模块路径**:`crates/cowork-core/src/pipeline/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +Pipeline 是 Cowork Forge 的"流水线传送带"。它负责把 7 个开发阶段按顺序串联起来,管理每个阶段的执行、暂停、重试和跳转。如果把整个系统比作一个 AI 工厂,Pipeline 就是那个决定"什么零件什么时候送到哪个工位"的生产调度中心。 + +Pipeline 的核心设计思路是:**开发流程本质上是有序的**。你不可能在设计完成之前就写代码,也不可能在测试通过之前就交付。因此,Pipeline 用统一的 `Stage` trait 抽象所有阶段,用流水线模式确保阶段按序执行,同时用 Actor-Critic 循环让每个阶段内部能自优化。 + +--- + +## 核心功能点 + +1. **7 阶段流水线编排**——定义了从 Idea 到 Delivery 的 7 个开发阶段,支持从任意阶段开始执行(`get_stages_from()` at `crates/cowork-core/src/pipeline/mod.rs:90`)和根据流程配置动态创建阶段(`get_stages_from_flow()` at `crates/cowork-core/src/pipeline/mod.rs:116`) +2. **Flow 配置驱动**——通过 `ConfigRegistry` 可以定义自定义流程,系统自动根据流程配置创建对应的阶段实例,而不是硬编码阶段顺序。代码位置:`crates/cowork-core/src/pipeline/mod.rs:147-154` +3. **迭代执行器**——`IterationExecutor` 是统一的迭代生命周期管理器,负责 Genesis/Evolution 迭代的创建、执行、状态保存。代码位置:`crates/cowork-core/src/pipeline/executor/mod.rs:17` +4. **Actor-Critic 阶段执行**——`stage_executor.rs` 实现配置驱动的阶段执行框架,处理 Simple/Loop 两种阶段类型,支持反馈循环。代码位置:`crates/cowork-core/src/pipeline/stage_executor.rs:1-80` +5. **知识生成**——迭代完成后自动触发知识生成,提取关键决策和模式。代码位置:`crates/cowork-core/src/pipeline/executor/knowledge.rs` + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `Stage` trait | `crates/cowork-core/src/pipeline/mod.rs:47` | 定义所有开发阶段的统一接口(execute + execute_with_feedback) | +| `PipelineContext` | `crates/cowork-core/src/pipeline/mod.rs:29` | 保存流水线执行上下文(项目、迭代、工作区路径) | +| `StageResult` | `crates/cowork-core/src/pipeline/mod.rs:19` | 阶段执行结果枚举(Success/Failed/Paused/NeedsRevision/GotoStage) | +| `IterationExecutor` | `crates/cowork-core/src/pipeline/executor/mod.rs:17` | 迭代执行器,统一生命周期管理 | +| `StageExecutor` | `crates/cowork-core/src/pipeline/stage_executor.rs` | 配置驱动的阶段执行框架 | + +--- + +## 内部数据流 + +```mermaid +flowchart TD + A["用户请求
cowork iter"] --> B["IterationExecutor
创建 + 执行"] + B --> C{"阶段类型?"} + C -->|Simple| D["单 Agent 执行
Idea/Check/Delivery"] + C -->|Loop| E["Actor-Critic 循环
PRD/Design/Plan/Coding"] + D --> F{"结果?"} + E --> F + F -->|Success| G["保存制品 + 人类验证"] + F -->|NeedsRevision| E + F -->|Failed| H["暂停 + 报错"] + G -->|通过| I["下一步段"] + G -->|拒绝| E + I --> J{"还有阶段?"} + J -->|是| C + J -->|否| K["知识生成 + 交付"] +``` + +**关键步骤说明**: +1. 用户请求到达 `crates/cowork-cli/src/main.rs:119`,路由到 `commands/iter.rs` +2. `IterationExecutor::execute()` 启动迭代,从 `get_stages_from_flow()` 获取阶段列表(`crates/cowork-core/src/pipeline/executor/mod.rs:79-80`) +3. 每个阶段通过 `Stage::execute()` 执行,结果返回 `StageResult` +4. Loop 类型阶段可能因 `NeedsRevision` 多次迭代 +5. 关键阶段输出需要人类验证后才能继续 + +--- + +## 关键接口与扩展点 + +`Stage` trait 是 Pipeline 的核心接口: + +```rust +pub trait Stage: Send + Sync { + fn name(&self) -> &str; + fn description(&self) -> &str; + fn needs_confirmation(&self) -> bool { false } + async fn execute(&self, ctx: &PipelineContext, interaction: Arc) -> StageResult; + async fn execute_with_feedback(&self, ctx: &PipelineContext, interaction: Arc, feedback: &str) -> StageResult; +} +``` + +扩展点:`ConfigRegistry` 允许定义自定义 Flow(`crates/cowork-core/src/config_definition/flow_definition.rs`),可以重新排列阶段顺序、跳过阶段或添加 Hook。 + +--- + +## 与其他模块的交互 + +| 交互模块 | 方向 | 接口/协议 | 说明 | +|---------|------|---------|------| +| agents | 依赖 | `create_*_agent()` 系列函数 | Pipeline 调用 Agent 工厂创建各阶段 Agent | +| domain | 依赖 | `Project`, `Iteration` | Pipeline 读取项目/迭代状态并更新 | +| llm | 依赖 | `create_llm_client()` | Pipeline 创建 LLM 客户端供 Agent 使用 | +| interaction | 依赖 | `InteractiveBackend` trait | Pipeline 通过交互后端与用户通信 | +| persistence | 依赖 | `ProjectStore`, `IterationStore` | Pipeline 保存和加载项目/迭代数据 | +| config_definition | 依赖 | `ConfigRegistry` | Pipeline 查询流程配置和 Agent 定义 | + +--- + +## 跨模块协作场景 + +**在 7-Stage 开发流水线中**:Pipeline 是主调度器。它从 `ConfigRegistry` 获取流程定义 → 创建 LLM 客户端(llm 模块)→ 构建 Agent(agents 模块)→ 注册交互后端(interaction 模块)→ 执行阶段 → 保存结果(persistence 模块)→ 重复直到所有阶段完成。迭代完成后触发知识生成(knowledge.rs)和持久化更新。 + +--- + +## 性能考量 + +Pipeline 的执行是串行化的——每个阶段必须等待前一个完成。这是设计使然,因为开发流程本质上是有序的。LLM 调用通过 `TokenBucketRateLimiter` 串行化(concurrency=1),确保不会触发 API 速率限制。文件操作和命令执行使用 Tokio 异步,不会阻塞主流程。 + +--- + +## 实现亮点 + +**Flow 配置驱动的动态阶段创建**(`crates/cowork-core/src/pipeline/mod.rs:98-112`):`create_stage_by_id()` 函数使 Pipeline 不再硬编码阶段序列,而是可以根据 Flow 配置灵活组装。这是一个从"固定流水线"到"可配置流水线"的关键设计转变。 diff --git a/.terrain/human/4.Deep-Exploration/skills.md b/.terrain/human/4.Deep-Exploration/skills.md new file mode 100644 index 0000000..cfa9a08 --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/skills.md @@ -0,0 +1,28 @@ +# Skills 领域 + +**模块路径**:`crates/cowork-core/src/skills/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +Skills 模块实现了 agentskills.io 标准,允许通过 Skill 包向 Agent 注入特定领域的知识、工具和提示词。比如加载"React 开发技能包",Coding Agent 就能自动获得 React 最佳实践。 + +--- + +## 核心功能点 + +1. **Skill 管理**——`SkillManager` 加载、索引和查询 Skill 文档。`crates/cowork-core/src/skills/manager.rs` +2. **Skill 注入**——`SkillInjector` 将匹配的 Skill 注入 Agent 指令。`crates/cowork-core/src/skills/mod.rs` +3. **Skill 发现**——从 `.skills/` 目录自动发现 Skill 文件。 + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `SkillManager` | `crates/cowork-core/src/skills/manager.rs` | Skill 生命周期管理 | +| `SkillInjector` | `crates/cowork-core/src/skills/mod.rs` | Skill 注入 Agent 指令 | +| `SkillDocument` | `crates/cowork-core/src/skills/mod.rs` | Skill 标准化表示 | diff --git a/.terrain/human/4.Deep-Exploration/tools.md b/.terrain/human/4.Deep-Exploration/tools.md new file mode 100644 index 0000000..5722b33 --- /dev/null +++ b/.terrain/human/4.Deep-Exploration/tools.md @@ -0,0 +1,60 @@ +# Tools 领域 + +**模块路径**:`crates/cowork-core/src/tools/` +**生成日期**:2026-07-05 + +--- + +## 概述 + +Tools 模块是 Cowork Forge 的"工具箱"。它提供了 30 多个 ADK 标准工具,Agent 在执行任务时通过它们来操作文件、执行命令、管理数据、验证结果、与用户交互。没有这些工具,Agent 就像没有扳手的工人——只能看不能干。 + +Tools 模块的设计精妙之处在于**工具通知系统**(`crates/cowork-core/src/tools/mod.rs:36-104`):它提供了一个全局回调机制,可以在工具调用前后通知 GUI 界面。这意味着 GUI 用户可以看到 Agent "正在做什么"——读哪个文件、执行什么命令——让 Agent 的工作过程变得透明可见。 + +--- + +## 核心功能点 + +1. **文件操作工具**——`ReadFileTool`、`WriteFileTool`、`ListFilesTool`,安全的工作区边界文件操作 +2. **人类在环工具**——`ReviewWithFeedbackContentTool`、`ProvideFeedbackTool`,在关键决策点暂停请求人类确认 +3. **数据 CRUD 工具**——`CreateRequirementTool`、`CreateTaskTool`、`GetRequirementsTool`、`GetDesignTool`、`GetPlanTool` 等,管理迭代数据 +4. **验证工具**——`CheckFeatureCoverageTool`、`CheckTaskDependenciesTool`、`CheckDataFormatTool`、`CheckTestsTool`、`CheckLintTool` +5. **Memory 工具**——`QueryMemoryTool`、`SaveInsightTool`、`SaveIssueTool`、`SaveLearningTool`、`PromoteToDecisionTool`、`PromoteToPatternTool` +6. **PM 工具**——`PMGotoStageTool`、`PMCreateIterationTool`、`PMRespondTool`、`PMSaveDecisionTool` + +--- + +## 关键组件 + +| 组件/类型 | 文件路径 | 核心职责 | +|---------|---------|---------| +| `ReadFileTool` | `crates/cowork-core/src/tools/file_tools.rs` | 读取工作区文件内容 | +| `WriteFileTool` | `crates/cowork-core/src/tools/file_tools.rs` | 写入文件到工作区 | +| `ExecuteShellCommandTool` | `crates/cowork-core/src/tools/test_lint_tools.rs` | 执行 Shell 命令(构建/测试) | +| `QueryMemoryTool` | `crates/cowork-core/src/tools/memory_tools.rs` | 查询项目记忆 | +| `ReviewWithFeedbackContentTool` | `crates/cowork-core/src/tools/hitl_content_tools.rs` | 请求人类对内容给出反馈 | +| `CheckTestsTool` | `crates/cowork-core/src/tools/validation_tools.rs` | 检查测试是否通过 | +| `PMGotoStageTool` | `crates/cowork-core/src/tools/pm_tools.rs` | PM Agent 跳转到指定阶段 | +| `ToolNotifyFn` | `crates/cowork-core/src/tools/mod.rs:36` | 工具通知回调类型,GUI 实时显示 | + +--- + +## 与其他模块的交互 + +| 交互模块 | 方向 | 说明 | +|---------|------|------| +| domain | 依赖 | 操作 Iteration 的制品和任务数据 | +| persistence | 依赖 | 保存和加载数据 | +| llm | 间接依赖 | 部分工具间接需要 LLM 功能 | + +--- + +## 跨模块协作场景 + +**在 Coding 阶段**:Coding Actor 调用 `ReadFileTool` 读取现有代码 → `WriteFileTool` 写入新代码 → `ExecuteShellCommandTool` 运行构建和测试 → Coding Critic 调用 `ReadFileTool` 审查代码质量 → `ProvideFeedbackTool` 给出改进建议。 + +--- + +## 性能考量 + +文件操作使用 Tokio 异步封装,不阻塞主线程。命令执行有超时控制。工具通知系统设计为回调模式,对性能影响极小。 diff --git "a/.terrain/human/5.\350\276\271\347\225\214\346\216\245\345\217\243.md" "b/.terrain/human/5.\350\276\271\347\225\214\346\216\245\345\217\243.md" new file mode 100644 index 0000000..349bda1 --- /dev/null +++ "b/.terrain/human/5.\350\276\271\347\225\214\346\216\245\345\217\243.md" @@ -0,0 +1,196 @@ +# 系统边界接口文档 + +本文档描述了 Cowork Forge 对外提供的接口,包括 CLI 命令、配置文件、外部 Agent 集成协议等。了解这些接口是在自己的项目中使用或集成 Cowork Forge 的起点。 + +--- + +## 命令行接口 (CLI) + +Cowork Forge 通过 `cowork` 二进制文件提供完整的 CLI,所有子命令在 `crates/cowork-cli/src/main.rs:12-117` 使用 clap derive 宏定义。对于只想"开箱即用"的用户,CLI 是最直接的交互方式。 + +### 命令:`cowork init` + +初始化一个新项目,在项目目录下创建 `.cowork-v2/` 目录结构。 + +**参数**: +| 参数 | 类型 | 是否必须 | 默认值 | 含义解读 | +|------|------|---------|-------|---------| +| `-n, --name` | String | 否 | 交互输入 | 项目名称——好记的名字有助于在项目管理界面中快速识别 | + +**使用示例**: +```bash +cowork init --name "My Project" +``` + +--- + +### 命令:`cowork iter` + +这是 Cowork Forge 最核心的命令——创建并执行新的开发迭代。一次 `cowork iter` 调用会触发完整的 7 阶段开发流水线。 + +**参数**: +| 参数 | 类型 | 是否必须 | 默认值 | 含义解读 | +|------|------|---------|-------|---------| +| `title` | String | 是 | - | 迭代标题/想法描述——用自然语言描述你想要构建的功能,越详细越好 | +| `-d, --description` | String | 否 | - | 详细描述——如果想法比较复杂,可以在这里展开说明背景和约束 | +| `-b, --base` | String | 否 | - | 基础迭代 ID——用于创建演化迭代(继承前一次迭代的代码或制品),不指定则从零开始 | +| `-i, --inherit` | String | 否 | `"full"` | 继承模式:`none`(全新开始)、`full`(完整复制代码+制品)、`partial`(只复制制品,重新生成代码) | + +**使用示例**: +```bash +# 创建全新迭代 +cowork iter --project "my-project" "Build a REST API for task management" + +# 在已有迭代基础上演进(仅继承制品,代码重新生成) +cowork iter --project "my-project" --base iter-2 --inherit partial "Add user authentication" +``` + +--- + +### 命令:`cowork list` + +列出项目的所有迭代,适合快速查看项目进展。 + +**参数**: +| 参数 | 类型 | 是否必须 | 默认值 | 说明 | +|------|------|---------|-------|------| +| `-a, --all` | bool | 否 | false | 显示所有迭代(包括已完成的),默认只显示活跃的 | + +--- + +### 命令:`cowork show` + +查看指定迭代的详细信息。 + +| 参数 | 类型 | 是否必须 | 默认值 | 说明 | +|------|------|---------|-------|------| +| `iteration_id` | String | 否 | 当前迭代 | 要查看详情的迭代 ID | + +--- + +### 命令:`cowork continue` + +继续执行一个已暂停的迭代(比如因等待人类确认而暂停的)。 + +| 参数 | 类型 | 是否必须 | 默认值 | 说明 | +|------|------|---------|-------|------| +| `iteration_id` | String | 否 | 当前迭代 | 要继续的迭代 ID | + +--- + +### 命令:`cowork status` + +快速查看当前项目的整体状态——哪些迭代在运行、暂停还是完成。 + +--- + +### 命令:`cowork import` + +**核心功能之一**——将已有的项目导入 Cowork Forge。它会分析项目结构、检测技术栈,然后用 AI 自动生成文档。 + +| 参数 | 类型 | 是否必须 | 默认值 | 含义解读 | +|------|------|---------|-------|---------| +| `path` | String | 是 | - | 已有项目的目录路径——这是要分析的代码库 | +| `-n, --name` | String | 否 | 目录名 | 在 Cowork Forge 中使用的项目名称 | +| `--idea` | bool | 否 | true | 是否生成 idea.md(项目概述) | +| `--prd` | bool | 否 | true | 是否生成 prd.md(产品需求) | +| `--design` | bool | 否 | true | 是否生成 design.md(架构设计) | +| `--plan` | bool | 否 | true | 是否生成 plan.md(实施计划) | +| `--template-only` | bool | 否 | false | 仅使用模板生成(不使用 LLM,适合离线场景) | + +```bash +# 导入现有项目并自动生成全套文档 +cowork import /path/to/existing/project + +# 仅使用模板,不调用 LLM +cowork import /path/to/project --template-only +``` + +--- + +### 命令:`cowork config` + +交互式配置 LLM 设置——第一次使用必须执行此命令配置 API 密钥和模型。 + +--- + +## 配置结构 + +配置文件存储在系统应用数据目录的 `config.toml` 中。这是 Cowork Forge 连接外部 AI 能力的"钥匙"。 + +### LLM 配置 + +| 配置项 | 默认值 | 含义解读 | +|-------|-------|---------| +| `[llm].api_base_url` | `https://api.openai.com/v1` | LLM API 地址——决定了用哪家服务商。默认是 OpenAI,也可以改为任何兼容的 API(如 Azure OpenAI、本地部署的 vLLM) | +| `[llm].api_key` | - | API 密钥——这是敏感信息,建议通过环境变量传入而不是直接写在配置文件中 | +| `[llm].model_name` | - | 使用的模型名——`gpt-5`。模型的选择直接影响输出质量和运行成本 | + +### 嵌入模型配置(可选) + +| 配置项 | 默认值 | 含义解读 | +|-------|-------|---------| +| `[embedding].api_base_url` | - | 嵌入模型的 API 地址——用于语义搜索和知识匹配 | +| `[embedding].model_name` | - | 嵌入模型名——如 `text-embedding-3-small` | + +### 外部编码 Agent 配置(可选) + +| 配置项 | 默认值 | 含义解读 | +|-------|-------|---------| +| `[coding_agent].enabled` | false | 是否启用外部编码 Agent——开启后,Coding 阶段会调用外部 Agent 而不是内置的 | +| `[coding_agent].agent_type` | opencode | 外部 Agent 的类型——支持 opencode、iflow、codex、gemini、claude | +| `[coding_agent].command` | - | 启动命令——如 `bun` | +| `[coding_agent].args` | - | 命令参数——如 `["x", "opencode-ai", "acp"]` | +| `[coding_agent].transport` | stdio | 通信方式——stdio(命令行管道)或 websocket(网络连接) | + +**示例配置**: +```toml +[llm] +api_base_url = "https://api.openai.com/v1" +api_key = "sk-your-openai-api-key" +model_name = "gpt-5" + +[embedding] +api_base_url = "https://your-embedding-api.com/v1" +api_key = "your-embedding-api-key" +model_name = "text-embedding-3-small" + +[coding_agent] +enabled = true +agent_type = "opencode" +command = "bun" +args = ["x", "opencode-ai", "acp"] +transport = "stdio" +``` + +--- + +## 外部 Agent 集成协议 (ACP) + +Cowork Forge 通过 ACP(Agent Client Protocol)与其他 AI Agent 通信。ACP 是一个开放协议,支持的 Agent 包括 OpenCode、iFlow、Codex、Gemini CLI、Claude CLI 等。 + +**配置方式**:在 `config.toml` 中设置 `[coding_agent]` 部分,Coding 阶段将自动调用外部 Agent 替代内置 Agent。 + +**传输方式**: +- `stdio`:通过子进程的标准输入输出通信,适合本地 CLI Agent +- `websocket`:通过网络连接通信,适合远程 Agent 服务 + +--- + +## 集成建议 + +### CI/CD 流水线集成 + +```bash +# 在 CI 中自动化项目开发 +cowork init --name "My Project" +cowork iter --project "my-project" "Auto-generated feature" +``` + +### 多项目并行管理 + +通过 `--project` 参数可以在同一机器上管理多个项目,每个项目的状态存储在各自目录的 `.cowork-v2/` 中。 + +### 外部系统集成 + +通过 Integration 系统(`crates/cowork-core/src/integration/`),可以在流水线执行的关键节点(阶段完成、失败等)触发 Webhook 通知外部系统(如 CI/CD 流水线、消息通知、部署平台等)。 diff --git "a/.terrain/human/6.\346\225\260\346\215\256\345\272\223\346\246\202\350\247\210.md" "b/.terrain/human/6.\346\225\260\346\215\256\345\272\223\346\246\202\350\247\210.md" new file mode 100644 index 0000000..9a7b71c --- /dev/null +++ "b/.terrain/human/6.\346\225\260\346\215\256\345\272\223\346\246\202\350\247\210.md" @@ -0,0 +1,44 @@ +# 数据库概览 + +--- + +## 本项目未使用关系型数据库 + +Cowork Forge 不需要安装和配置任何数据库系统。数据以 JSON 文件形式存储在项目目录下的 `.cowork-v2/` 中。 + +### 为什么没有数据库? + +这是一个刻意的设计决策,而非遗漏。作为桌面工具,Cowork Forge 服务于单用户场景,不需要考虑多用户并发、ACID 事务等高阶数据库特性。选择 JSON 文件持久化带来了几个实际好处: + +- **开箱即用**——用户不需要安装、配置或运维任何数据库服务 +- **文件即备份**——`.cowork-v2/` 目录可以整体用 Git 管理,也可以直接复制备份 +- **Schema 演化**——JSON 文件天然支持数据结构变化,每次迭代的制品可以有不同的字段 +- **轻量**——不需要数据库进程常驻后台,不消耗额外资源 + +### 数据存储结构 + +``` +.cowork-v2/ +├── project.json # 项目信息 +├── iterations/ +│ └── {iteration_id}.json # 每次迭代的完整快照 +├── memory/ +│ ├── project/ +│ │ └── project_memory.json # 项目级记忆(决策、模式) +│ └── iterations/ +│ └── {iteration_id}.json # 迭代级知识快照 +└── workspace/ + └── {iteration_id}/ # 迭代的工作区代码 + ├── ... # 本次迭代生成的代码文件 + └── artifacts/ # 迭代制品(idea.md、prd.md 等) +``` + +### 核心数据文件 + +| 文件 | 内容 | 对应代码 | +|------|------|---------| +| `project.json` | 项目名称、迭代列表、当前迭代 ID | `crates/cowork-core/src/domain/project.rs:6` | +| `iterations/{id}.json` | 迭代状态、阶段进度、制品、继承信息 | `crates/cowork-core/src/domain/iteration.rs:8` | +| `memory/project/project_memory.json` | 跨迭代决策、模式、迭代知识快照 | `crates/cowork-core/src/domain/memory.rs:7` | + +所有数据通过 `ProjectStore`、`IterationStore`、`MemoryStore`(均在 `crates/cowork-core/src/persistence/`)进行读写,使用 serde + serde_json 序列化。 diff --git a/.terrain/index.md b/.terrain/index.md new file mode 100644 index 0000000..5103b43 --- /dev/null +++ b/.terrain/index.md @@ -0,0 +1,48 @@ +--- +type: project +project: cowork-forge +title: cowork-forge +source: . +--- + +# cowork-forge + + + +## Repository + +`.` + +## Tech stack + +- Rust + +## Structure + + - Cargo.toml + - crates/ + - cowork-gui/ + - cowork-cli/ + - cowork-core/ + - LICENSE + - .agents/ + - skills/ + - Cargo.lock + - litho.docs/ + - zh/ + - en/ + - README.md + - .gitignore + - README_zh.md + - .github/ + - workflows/ + - AGENTS.md + - assets/ + - icon_cowork.png + - snapshots/ + - blend_banner.png + - icon_cowork_v2b.jpeg + - clarify_flows.jpg + - icon_cowork_v1.png + - clarify_teams_like_human.jpg + - clarify_snapshot_gui.jpg diff --git a/.terrain/knowledge/.gitkeep b/.terrain/knowledge/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.terrain/knowledge/00-glossary.md b/.terrain/knowledge/00-glossary.md new file mode 100644 index 0000000..e69de29 diff --git a/.terrain/knowledge/adk-rust.md b/.terrain/knowledge/adk-rust.md new file mode 100644 index 0000000..de69428 --- /dev/null +++ b/.terrain/knowledge/adk-rust.md @@ -0,0 +1,141 @@ +# adk-rust 框架速查 + +> Agent Development Kit for Rust — LLM Agent 编排框架 (github.com/zavora-ai/adk-rust) + +## 核心类型 + +| 类型 | 用途 | +|------|------| +| `Agent` (trait) | 可执行 agent 单元,`run(ctx) -> Result` | +| `LlmAgentBuilder` | 构建 LLM agent:`.instruction()` + `.tool()` + `.model()` | +| `LoopAgent` | 循环编排器,按顺序执行 agents 数组,支持多轮迭代 | +| `Tool` (trait) | 工具 trait:`name/description/parameters_schema/execute(ctx, args)` | +| `ToolContext` (Arc) | 工具执行上下文(LLM 调用、session 操作、action 设置) | +| `EventActions` | 工具返回后的控制指令:`escalate` / `exit_loop` / `goto_stage` | +| `IncludeContents` | 子 agent 可见会话历史模式 | +| `Session` | 对话会话,存储 messages/state,agent 间共享 | +| `ExitLoopTool` | 内置工具:调用后设置 `actions.escalate = true`(注意:是 escalate 不是 exit_loop 字段!) | + +## IncludeContents 模式 + +```rust +IncludeContents::None // 子 agent 只看到自己的 instruction + 当前用户 turn(看不到前序/历史消息) +IncludeContents::Default // 子 agent 看到共享 Session 的完整对话历史 +``` + +**Actor-Critic 正确配置(易错!)**: +- **Actor** → `IncludeContents::Default`:Actor 需要看到前一轮 Critic 的文字反馈来修正产出 +- **Critic** → `IncludeContents::None`:Critic **不需要**看 Actor 的对话历史!Critic 通过工具(`load_prd_doc`/`get_plan`/`list_files`等)从磁盘/persistence 加载 Actor 的 artifact 进行审查。设为 None 可避免将 Actor 的 system prompt + 完整工具调用链(可能 50K+ tokens)传给 Critic,节省约一半 token 成本。 + +**误区纠正**:旧说法"Critic 必须用 Default 才能看到 Actor 产出"是错误的。Critic 通过工具加载 artifact,不依赖对话历史。Default 仅用于 Actor 需要跨轮看到 Critic 反馈的场景。 + +## LoopAgent 工作流 (Actor-Critic) + +``` +LoopAgent::new("name", vec![actor_agent, critic_agent]) + .with_max_iterations(N) +``` + +执行流程: +1. LoopAgent 创建一个 `HistoryTrackingSession` 包裹父上下文 +2. 每轮迭代依次执行 Actor → Critic,每个子 agent 的输出 event 都写入 HistoryTrackingSession +3. **Actor**(Default):在第 2+ 轮迭代时能看到前一轮 Critic 的反馈文字,据此修正产出并保存 artifact +4. **Critic**(None):每轮只看到自己的 instruction + 初始用户 prompt,通过工具加载最新 artifact 审查: + - 通过 → Critic 调用 `exit_loop` 工具,循环终止(整个 LoopAgent 成功结束) + - 小问题 → Critic 直接在文字回复中描述问题(不调用 provide_feedback),Actor 下轮可见 + - 大问题 → Critic 调用 `provide_feedback` 持久化反馈 + 退出循环,触发 Stage 级别重试 +5. 达到 max_iterations 仍未 exit_loop → LoopAgent 正常结束,Stage executor 根据历史决定重试 + +**EventActions.escalate 的作用**:子 agent 工具中设置 `escalate=true` 会立即中断 LoopAgent 循环。`provide_feedback` 和 `exit_loop` 都会设置 escalate=true。区别是 provide_feedback 额外持久化了结构化反馈供 Stage executor 使用。 + +## EventActions 使用 + +```rust +// 在 Tool::execute 中设置 action +let mut actions = EventActions::default(); +actions.escalate = true; // 中断当前 LoopAgent/agent,回到上层 +ctx.set_actions(actions); // 必须调用 set_actions 才生效! +``` + +| 字段 | 作用 | +|------|------| +| `escalate` | 设置为 true 时立即退出 LoopAgent(provide_feedback/exit_loop 都用这个) | + +**易错**: +- 创建 `EventActions` 后**必须调用 `ctx.set_actions(actions)`** +- `exit_loop` 字段在新版 adk-rust 中不是独立字段——ExitLoopTool 实际设置的是 `escalate=true` + +## LlmAgentBuilder 构建 + +```rust +LlmAgentBuilder::new("agent_id") + .instruction("你是...") // 不是 system_prompt()! + .model(model) // Arc + .tool(Arc::new(MyTool)) // 不是 with_tool()!可多次调用 + .include_contents(IncludeContents::Default) + .temperature(0.3) + .build() +``` + +**易错**: +- 方法名是 `.instruction()` 不是 `.system_prompt()`,是 `.tool()` 不是 `.with_tool()` +- 忘记添加工具 → LLM 无法调用该工具 +- `include_contents` 默认是 `None` +- instruction 中必须列出可用工具名和用途,否则 LLM 不知道何时调用 + +## Tool trait 实现 + +```rust +#[async_trait] +impl Tool for MyTool { + fn name(&self) -> &str { "my_tool" } + fn description(&self) -> &str { "做什么用" } + fn parameters_schema(&self) -> Option { + Some(json!({ + "type": "object", + "properties": { "param": {"type": "string"} }, + "required": ["param"] + })) + } + async fn execute(&self, ctx: Arc, args: Value) -> adk_core::Result { + let param = args.get("param").and_then(|v| v.as_str()) + .ok_or_else(|| adk_core::AdkError::tool("param required"))?; + Ok(json!({"result": "ok"})) + } +} +``` + +**要点**: +- 参数校验失败返回 `Err(adk_core::AdkError::tool(...))` +- 成功返回 `Ok(json!(...))` +- 不要 panic/unwrap,用 `?` 传播错误 + +## 常见陷阱 (Pitfalls) + +1. **Critic 用 Default 浪费 token**:Critic 应设 `IncludeContents::None`,通过工具加载 artifact;Actor 才需要 Default 看反馈 +2. **exit_loop 不生效**:① Critic 的工具列表必须注册 `ExitLoopTool`(`"exit_loop"`)② ExitLoopTool 设置 escalate=true 而非 exit_loop 字段 +3. **EventActions 不生效**:必须调用 `ctx.set_actions(actions)`,不是仅创建 struct +4. **工具永远不被调用**:检查 ① 是否注册到 agent ② instruction 中是否提及该工具 +5. **Builder 方法名错误**:用 `.instruction()` 不是 `.system_prompt()`,用 `.tool()` 不是 `.with_tool()` +6. **HITL 绕过 InteractiveBackend**:不要直接用 dialoguer/println,必须通过 `InteractiveBackend` trait +7. **跨平台命令**:Windows 用 `cmd /C `,Unix 用 `sh -c ` +8. **非 Actor-Critic 场景用 Default 污染上下文**:普通 agent 用 `IncludeContents::None` +9. **max_iterations 必须设置**:建议 PRD/Design/Plan=2,Coding=3;太小(=1)会导致"一审终局"触发代价更高的 Stage 全量重试;太大浪费 token +10. **provide_feedback vs 文字反馈**:Critic 调用 provide_feedback 会持久化结构化反馈并退出循环(触发 Stage 重试);小问题直接文字描述即可(Actor 下轮可见,不退出循环) + +## Token 用量追踪 + +adk-rust 内置 token 追踪能力: +- `adk_core::UsageMetadata`:每个 LlmResponse 包含 prompt/completion/total token 数 +- `adk_model::usage_tracking::with_usage_tracking()`:包装 LlmResponseStream 自动记录 usage 到当前 tracing span +- `adk_telemetry::record_llm_usage()`:手动记录 token 用量到 OpenTelemetry span + +当前项目未启用 token 追踪,可参考 rate_limiter 的装饰器模式实现 UsageTracking LLM 包装器。 + +## CoworkForge 中的使用模式 + +- 产出阶段使用 `LoopAgent(actor, critic)`:PRD/Design/Plan max_iterations=2,Coding max_iterations=3 +- Actor(Default):生成 artifact 并保存到磁盘 +- Critic(None):通过工具加载 artifact 审查 → exit_loop 通过 / 文字反馈小修 / provide_feedback 大修 +- 用户交互 → `request_human_review` / `ask_user`(通过 InteractiveBackend,escalate=true) +- 跨阶段跳转 → `goto_stage`(设置 meta.goto_stage + escalate) diff --git a/AGENTS.md b/AGENTS.md index fa313c1..631d15e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,6 @@ # AGENTS.md — Cowork Forge > This file provides AI coding agents with the context needed to work effectively on this project. -> For project knowledge (architecture, decisions, issues), see [`.ai-context/SKILL.md`](.ai-context/SKILL.md). --- @@ -9,14 +8,6 @@ **Cowork Forge** is an AI-native multi-agent software development platform. It orchestrates specialized AI agents (Product Manager, Architect, Project Manager, Engineer) through a 7-stage pipeline to transform ideas into production-ready software. -| Aspect | Detail | -|--------|--------| -| Language | Rust (edition 2024) | -| Agent Framework | adk-rust 0.5.0 | -| GUI | Tauri + React 18 + Ant Design | -| Architecture | Hexagonal + DDD | -| License | MIT | - ### Workspace Structure ``` @@ -162,24 +153,6 @@ npm run dev --- -## Key Files - -When working on specific areas, start from these files: - -| Area | Primary File | Related | -|------|-------------|---------| -| Pipeline execution | `crates/cowork-core/src/pipeline/executor/mod.rs` | `stage_executor.rs`, `knowledge.rs` | -| Stage implementations | `crates/cowork-core/src/pipeline/stages/*.rs` | 7 stage files: idea, prd, design, plan, coding, check, delivery | -| Tool implementations | `crates/cowork-core/src/tools/mod.rs` | `file_tools.rs`, `data_tools.rs`, `hitl_tools.rs`, `pm_tools.rs`, etc. | -| Domain entities | `crates/cowork-core/src/domain/mod.rs` | `project.rs`, `iteration.rs`, `memory.rs` | -| HITL interface | `crates/cowork-core/src/interaction/mod.rs` | `cli.rs`, `tauri.rs` | -| Agent configs | `crates/cowork-core/src/config_definition/` | `default_configs/*.json` | -| Agent prompts | `crates/cowork-core/src/instructions/*.rs` | 12 instruction modules | -| External agent | `crates/cowork-core/src/acp/client.rs` | ACP protocol client | -| Skill system | `crates/cowork-core/src/skills/` | agentskills.io standard | - ---- - ## Security Considerations - **Path validation**: All file operations are validated against workspace boundaries. Never bypass `validate_path()` checks. @@ -191,72 +164,99 @@ When working on specific areas, start from these files: --- -## Project Knowledge (.ai-context) +## Common Pitfalls -This project uses a tiered knowledge base in `.ai-context/` for architectural context that code alone cannot convey. **AGENTS.md tells you *how to work*; `.ai-context/` tells you *what the project is*.** +- **Don't bypass `InteractiveBackend`**: Never call CLI-specific functions (e.g., `dialoguer`) from `cowork-core`. All user interaction must go through the `InteractiveBackend` trait. +- **Don't ignore rate limiting**: LLM calls are serialized for a reason. Don't try to parallelize them. +- **Don't access files outside workspace**: The security layer validates all paths. If you need to access a new path, update the validation logic, don't bypass it. +- **Don't hardcode stage IDs**: Use `create_stage_by_id()` or flow configuration instead of string matching. +- **Don't use `unwrap()`**: Use `anyhow::Result` with proper error propagation (`?` operator or `.context()`). -### When to Read `.ai-context` + +## AI 工程环境(Terrain) -| Situation | What to Read | -|-----------|-------------| -| Starting a new session | `.ai-context/references/PROJECT-ESSENCE.md` | -| Working across components | `.ai-context/references/ARCHITECTURE.md` | -| Changing established patterns | `.ai-context/references/DECISIONS.md` | -| Debugging unexpected behavior | `.ai-context/DYNAMICS.md` | -| Unsure *why* something is designed a way | `.ai-context/references/DECISIONS.md` | +本仓库由 Terrain 配置了 AI 工程环境。Coding Agent 请遵循以下约定: -### Session Start Protocol +- **知识资产**位于本仓库 **`.terrain/`**(Agent 友好的知识资产、人类友好的知识库、私域知识、源码索引;可随 Git 协作) +- **项目登记**在本地 `~/.terrain/registry.json`(仅记录仓库路径,不含知识正文) +- **Skills** 位于 `.agents/skills/`(由 Terrain 注入,可按需重新集成) +- **Agent 工具**约定在 `~/.terrain/bin/`(`rtk` / `codegraph` / `terrain`);可选本地清单 `.terrain/env/agent-tools.json`(不入库) +- **无 Terrain 安装**时:RTK / CodeGraph 可降级为 `bunx` / `npx`(见 `rtk-skill`、`codegraph-skill`) +- **工作流**:先读知识 → 再查关系 → 最后读源码;shell 输出优先走 RTK + -``` -1. Read this file (AGENTS.md) -2. Read .ai-context/references/PROJECT-ESSENCE.md -3. Scan .ai-context/DYNAMICS.md for active issues -4. Proceed with code exploration -``` + +## Terrain 知识资产 -### Knowledge Tiers +Coding Agent **必须先加载** `terrain-knowledge-skill`,并按其中分层策略查询 **`.terrain/`**(仓库内路径,非全局目录)。 -| Tier | File | Update Frequency | -|------|------|------------------| -| 0 | `references/PROJECT-ESSENCE.md` | Quarterly / Major version | -| 1 | `references/ARCHITECTURE.md` | Monthly / Sprint | -| 2 | `references/DECISIONS.md` | Per decision change | -| 3 | `DYNAMICS.md` | As needed | +| 层级 | 路径 | 何时使用 | +|------|------|----------| +| Agent 友好 | `.terrain/agent/context.md` | 模块划分、核心流程、系统边界 | +| 私域 | `.terrain/knowledge/` | 业务术语、内部框架/API/脚手架 | +| 人类友好 | `.terrain/human/` | Litho 人类友好的知识库(可选参考) | +| 源码 | `.terrain/agent/repomix.md`(见 `repomix-context-skill`) | 实现细节(本地索引,不入库) | +| 关系 | codegraph CLI(见 `codegraph-skill`) | 调用链、依赖关系、影响分析 | -Full entry point: [`.ai-context/SKILL.md`](.ai-context/SKILL.md) +**原则**:先宏观后微观;优先读已生成文档,再 grep 源码索引。 -### Updating `.ai-context` +## 知识保鲜(必读) -When making significant changes, update the corresponding knowledge file: +1. 回答架构/模块问题前,读取 `.terrain/.meta/freshness.json`(或 `freshness` 工具输出) +2. `freshness_score < 70` 时:不得仅凭 `agent/context.md` 下结论,须用 `grep repomix` 或 `codegraph` 交叉验证 +3. `freshness_score < 50` 时:宏观架构上下文不可信,以 repomix 源码切片为准 +4. 发现矛盾时的优先级:**repomix 源码 > codegraph > agent/context.md > human/** +5. `knowledge/` 私域文档视为人为维护;若 `refs` 指向的源码路径已删除,应降权处理 + -| What Changed | Update | -|-------------|--------| -| New crate or major component | `.ai-context/references/ARCHITECTURE.md` | -| Architecture decision | `.ai-context/references/DECISIONS.md` | -| New active issue / constraint | `.ai-context/DYNAMICS.md` | -| Project scope change | `.ai-context/references/PROJECT-ESSENCE.md` | + +### 可用 Skills -Before updating, read `.ai-context/meta/MAINTENANCE.md` for writing guidelines. +| Skill | 用途 | +|-------|------| +| `terrain-knowledge-skill` | `.terrain/` 知识分层与查询顺序(先读) | +| `repomix-context-skill` | grep/读取 `repomix.md` 源码切片 | +| `codegraph-skill` | 符号关系;`~/.terrain/bin/codegraph` 或 `bunx codegraph` | +| `rtk-skill` | 冗长 shell 加 rtk 前缀;`~/.terrain/bin/rtk` 或 `bunx @terrain-ai/rtk` | -**No update needed for**: struct fields, function signatures, refactoring, bug fixes. +加载顺序建议:knowledge → codegraph / repomix → rtk(执行命令时)。 + ---- + +### 工具链 -## PR Guidelines +| 工具 | 约定路径 | 无 Terrain 时降级 | +|------|----------|-------------------| +| RTK | `~/.terrain/bin/rtk` | `bunx @terrain-ai/rtk` 或 `npx @terrain-ai/rtk` | +| CodeGraph | `~/.terrain/bin/codegraph` | `bunx codegraph` 或 `npx codegraph` | +| Terrain CLI | `~/.terrain/bin/terrain` | `bunx @terrain-ai/cli` 或 `npx @terrain-ai/cli` | +| 知识文件 | `.terrain/` 仓库内路径 | 直接 Read/Grep,无需 CLI | -- Run `cargo test` and `cargo clippy` before committing. -- Ensure no `unwrap()` in production code paths. -- If you added a new tool or stage, update `.ai-context/references/ARCHITECTURE.md`. -- If you made a non-obvious design choice, add an ADR to `.ai-context/references/DECISIONS.md`. -- Commit messages: use conventional commits format (`feat:`, `fix:`, `refactor:`, etc.). +| 场景 | 用法 | +|------|------| +| 架构、私域知识 | 加载 `terrain-knowledge-skill` | +| 源码片段 | `repomix-context-skill`;` grep` 搜索 pack | +| 符号关系 | `codegraph-skill`;检查 `~/.terrain/bin/codegraph` 是否存在(见 codegraph-skill) | +| git/test/build | `rtk-skill`;检查 `~/.terrain/bin/rtk` 是否存在(见 rtk-skill) | +| ACP 知识查询 | `~/.terrain/bin/terrain tools …` | ---- +### Agent 工具解析(必读) -## Common Pitfalls +**一律使用约定路径**(`~/.terrain/bin/…`、`.terrain/…`),**不要**写机器相关的绝对路径(如 `/Users/…` 或 `C:\Users\…`)。 -- **Don't bypass `InteractiveBackend`**: Never call CLI-specific functions (e.g., `dialoguer`) from `cowork-core`. All user interaction must go through the `InteractiveBackend` trait. -- **Don't ignore rate limiting**: LLM calls are serialized for a reason. Don't try to parallelize them. -- **Don't access files outside workspace**: The security layer validates all paths. If you need to access a new path, update the validation logic, don't bypass it. -- **Don't hardcode stage IDs**: Use `create_stage_by_id()` or flow configuration instead of string matching. -- **Don't use `unwrap()`**: Use `anyhow::Result` with proper error propagation (`?` operator or `.context()`). -- **Don't duplicate knowledge**: If information exists in `.ai-context/`, link to it rather than repeating it here. +Windows 上工具部署在 `%USERPROFILE%\.terrain\bin\`(Git Bash / PowerShell 7+ 中可写为 `~/.terrain/bin/`),二进制带 `.exe` 后缀。 + +1. 执行前检查工具是否存在 — 见 `rtk-skill` / `codegraph-skill` 中的跨平台检查表(**不要**在 Windows 上使用 Unix 专用的 `test -x`) +2. 存在 → 用 `~/.terrain/bin/ …`(词首 `~` 在 bash/zsh/Git Bash/PowerShell 7+ 会展开) +3. 不存在 → RTK / CodeGraph 用上表 `bunx` / `npx` 降级;Terrain CLI 请用户通过桌面应用操作 +4. 可选参考:`.terrain/env/agent-tools.json`(本地生成、不入库),内容与约定路径一致 + +**不要**把 manifest 里的 `~` 路径赋给变量再引号调用(`"$VAR"` 不会展开 `~`)。直接写 `~/.terrain/bin/rtk` 或选用 `bunx` 前缀。 + +### RTK 要点(必读 `rtk-skill`) + +- **必须显式**加 rtk 前缀 — Terrain 不启用 `rtk init` 全局 hook +- 内置 Read/Grep 不会自动走 RTK — 大文件用 ` read`,搜索用 ` grep` + +**注意**:不要运行 `codegraph install` 或 `rtk init`(已由 Terrain + Skills 配置)。 + diff --git a/Cargo.lock b/Cargo.lock index 14c9276..add133b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,117 +2,44 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adk-agent" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5efa2eb987552fa32e9de78051a7d7cc14388e1765cea5f45199708d1ee3137" +checksum = "be73cfc954523b114c59c00db7bbe1ade30d37ecd27af4c590127725b0c97f04" dependencies = [ "adk-core", "adk-skill", "adk-telemetry", - "adk-tool", "async-stream", "async-trait", + "chrono", "futures", + "jsonschema", + "schemars 1.2.0", "serde", "serde_json", "tokio", "tracing", ] -[[package]] -name = "adk-anthropic" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e3edc789550d1a71aef872d1362aa3c4e0c5a28b6bc100fe75440d91f9e986" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures", - "regex", - "reqwest 0.12.28", - "serde", - "serde_json", - "time", - "tokio", - "url", -] - [[package]] name = "adk-artifact" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51740eb6a77909b4bfc9b0cfea9dae9fbf668cffbe1f340c4b047e8c7b0bf818" -dependencies = [ - "adk-core", - "async-trait", - "serde_json", - "tokio", -] - -[[package]] -name = "adk-auth" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01971c1a399c19a80cf8cdba941f06cb9ce886db6ab3ec95eeecaeb2eb408beb" +checksum = "3359a550dd7e5f53ffad890c4653238234964a2664243d398e056adf8fe100b3" dependencies = [ "adk-core", "async-trait", - "chrono", - "serde", "serde_json", - "thiserror 2.0.18", "tokio", - "tracing", -] - -[[package]] -name = "adk-cli" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "892eb583f3833d6376fe8a9f554c9583bfbafdf6dc4b6a32a7df3bcbf46a615e" -dependencies = [ - "adk-agent", - "adk-artifact", - "adk-core", - "adk-deploy", - "adk-model", - "adk-runner", - "adk-server", - "adk-session", - "adk-skill", - "adk-telemetry", - "adk-tool", - "anyhow", - "axum", - "clap", - "dirs 6.0.0", - "futures", - "keyring", - "rustyline", - "serde", - "serde_json", - "tokio", - "tokio-util", - "toml 0.8.2", - "tracing", ] [[package]] name = "adk-core" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219e406012be3573a3c2949936300a6b02bccda0f49243fec3438246f9e555c7" +checksum = "177227011b93af832e592bbe8577b8284104a2d095fda3040bb27ed8cfc9814d" dependencies = [ "async-trait", "chrono", @@ -120,38 +47,18 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.18", - "uuid", -] - -[[package]] -name = "adk-deploy" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ae8782aa2125441f2713f90228b7f86c209e2367b956b0800cdce57f47799e" -dependencies = [ - "anyhow", - "chrono", - "dirs 6.0.0", - "flate2", - "hex", - "reqwest 0.12.28", - "serde", - "serde_json", - "sha2", - "tar", - "thiserror 2.0.18", - "toml 0.8.2", + "tokio", "tracing", - "url", "uuid", ] [[package]] name = "adk-gemini" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1830198d7267080936b961e837d83f7c77e5f23fe8f6820e2d8bb07cb221546" +checksum = "1a0a343f7cb43da47ae7060a88d74e2ef1e6cb68f4b9e599fda0c0db6403fcc7" dependencies = [ + "adk-core", "async-stream", "async-trait", "base64 0.22.1", @@ -173,44 +80,12 @@ dependencies = [ "url", ] -[[package]] -name = "adk-guardrail" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2d4a5e59f388d413de579c6c28a8545e6da367bce834f7a42ca81512f9678b" -dependencies = [ - "adk-core", - "async-trait", - "futures", - "jsonschema", - "regex", - "serde", - "serde_json", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "adk-memory" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598d89ab5eb484db7c9291583d3a1b9b0d00c0deff57ed30449d8f57b656ae53" -dependencies = [ - "adk-core", - "async-trait", - "chrono", - "serde", - "serde_json", - "tracing", -] - [[package]] name = "adk-model" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69df5f91666a2b3f3b92da22fccac105138ca3db884c9c504b16b52ce626a9f2" +checksum = "f79e38948bcec656a66db7a71c2efebafb20c8f196110278b13d105e6c7eb105" dependencies = [ - "adk-anthropic", "adk-core", "adk-gemini", "adk-telemetry", @@ -218,15 +93,10 @@ dependencies = [ "async-openai", "async-stream", "async-trait", - "aws-config", - "aws-sdk-bedrockruntime", - "aws-smithy-types", - "base64 0.21.7", + "base64 0.22.1", "chrono", "futures", - "ollama-rs", "reqwest 0.12.28", - "schemars 1.2.0", "serde", "serde_json", "tokio", @@ -235,9 +105,9 @@ dependencies = [ [[package]] name = "adk-plugin" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa96d52f4b13b8e888f1eb92d321b0299a2a580ecd79fed24da6b49b381e03f" +checksum = "53bb86de42bb670e3258eca11b1c6fa0bc6f441f2b1cba53f3455d8eb3498a34" dependencies = [ "adk-core", "serde_json", @@ -247,9 +117,9 @@ dependencies = [ [[package]] name = "adk-runner" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f931416dc586d03630731d9d5d43235f69c4b8b009f8e0d6ca143ddd79104ea" +checksum = "59e7d5e8526343f2d4b9e0d4bf30a4012f9fbff18990a2e147f9b3ce8831d4ea" dependencies = [ "adk-artifact", "adk-core", @@ -261,6 +131,7 @@ dependencies = [ "futures", "serde", "serde_json", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -269,26 +140,15 @@ dependencies = [ [[package]] name = "adk-rust" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5651471d57aed253dbe23ad1e7fd94bbf551afa6e9922932ad15acedac476f7e" +checksum = "b571459ef1d9d216ad4308963545b333be870e2bc34765c28c38a49c3a2c4bdb" dependencies = [ "adk-agent", - "adk-anthropic", - "adk-artifact", - "adk-auth", - "adk-cli", "adk-core", - "adk-guardrail", - "adk-memory", "adk-model", - "adk-plugin", "adk-runner", - "adk-server", "adk-session", - "adk-skill", - "adk-telemetry", - "adk-tool", "anyhow", "async-trait", "futures", @@ -299,54 +159,20 @@ dependencies = [ [[package]] name = "adk-rust-macros" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2782699bb794593903945fcbf54d8379940b5333c6bf12776eb1ba9fad4d5e6c" +checksum = "33bf0c5663b71f9f32185e9684fc2b6e30a43a35846bf7c2529395e88f7503b5" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] -[[package]] -name = "adk-server" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fab54a8bf6b4b39298b8f8b830b36e6e0f2154725050398497356a57a21016f" -dependencies = [ - "adk-agent", - "adk-artifact", - "adk-core", - "adk-runner", - "adk-session", - "adk-telemetry", - "anyhow", - "async-stream", - "async-trait", - "axum", - "base64 0.22.1", - "chrono", - "futures", - "mime_guess", - "reqwest 0.12.28", - "rust-embed", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tower 0.4.13", - "tower-http", - "tracing", - "uuid", -] - [[package]] name = "adk-session" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bde3882167412a98e1b00b2a2ece1a4d705947c4518c805c389cc9f3561c359" +checksum = "a38d1da6013f8627eac516d5be6afad3837bf093d80dd074dc812fbf6a0b8a2d" dependencies = [ "adk-core", "async-trait", @@ -359,9 +185,9 @@ dependencies = [ [[package]] name = "adk-skill" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad7de3492d401f4621309fc848056836a02585bedc093468dd71cf3e0ce90bd4" +checksum = "3222d4e3faf1ddde1da9215912449afe9137dce614575ab370745ff3f22dc90a" dependencies = [ "adk-core", "adk-plugin", @@ -375,39 +201,34 @@ dependencies = [ [[package]] name = "adk-telemetry" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6eb04e8f53cb0474803e51cb6bc78a23e35d4df3095ce96e55b31dd6f32361" +checksum = "a2d2be5505a6d41f335f4c9292070117432761714d959bef26d9780529244f0e" dependencies = [ - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry_sdk", - "serde", - "serde_json", "thiserror 2.0.18", "tracing", - "tracing-opentelemetry", "tracing-subscriber", ] [[package]] name = "adk-tool" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e7d3c451da3f36048dbe423ac7f77a3713f0760a47f2723e07dd6cd9119f7" +checksum = "9d9bec020b7161c160f398679fdb9a9e964aa18d278860723b6bc4f64fd6e416" dependencies = [ "adk-core", "adk-rust-macros", "adk-telemetry", "async-trait", - "base64 0.21.7", + "base64 0.22.1", "futures", "reqwest 0.12.28", "rmcp", - "schemars 0.8.22", + "schemars 1.2.0", "serde", "serde_json", "tokio", + "tokio-util", "tracing", "uuid", ] @@ -418,17 +239,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - [[package]] name = "agent-client-protocol" version = "0.9.4" @@ -498,12 +308,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -656,8 +460,6 @@ dependencies = [ "eventsource-stream", "futures", "getrandom 0.3.4", - "hex", - "hmac", "rand 0.9.2", "reqwest 0.12.28", "reqwest-eventsource", @@ -665,11 +467,9 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sha2", "thiserror 2.0.18", "tokio", "tokio-stream", - "tokio-tungstenite", "tokio-util", "tracing", "url", @@ -705,584 +505,142 @@ dependencies = [ ] [[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "attohttpc" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" -dependencies = [ - "base64 0.22.1", - "flate2", - "http 1.4.0", - "log", - "native-tls", - "url", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-config" -version = "1.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sso", - "aws-sdk-ssooidc", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "hex", - "http 1.4.0", - "sha1", - "time", - "tokio", - "tracing", - "url", - "zeroize", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - -[[package]] -name = "aws-lc-rs" -version = "1.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.37.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "aws-runtime" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "bytes-utils", - "fastrand", - "http 1.4.0", - "http-body 1.0.1", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-bedrockruntime" -version = "1.129.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c710f0b7dbd906047724ec892afc0de0b92c7484ba25f499a91563e0417a96d6" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "http-body-util", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sso" -version = "1.96.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64a6eded248c6b453966e915d32aeddb48ea63ad17932682774eb026fbef5b1" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ssooidc" -version = "1.98.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db96d720d3c622fcbe08bae1c4b04a72ce6257d8b0584cb5418da00ae20a344f" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.100.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fafbdda43b93f57f699c5dfe8328db590b967b8a820a13ccdd6687355dfcc7ca" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.4.0", - "percent-encoding", - "sha2", - "time", - "tracing", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" -dependencies = [ - "aws-smithy-types", - "bytes", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.63.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http-client" -version = "1.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2", - "http 1.4.0", - "hyper", - "hyper-rustls", - "hyper-util", - "pin-project-lite", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower 0.5.3", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.62.5" +name = "async-recursion" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ - "aws-smithy-types", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "aws-smithy-observability" -version = "0.2.6" +name = "async-signal" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ - "aws-smithy-runtime-api", + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", ] [[package]] -name = "aws-smithy-query" -version = "0.60.15" +name = "async-stream" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ - "aws-smithy-types", - "urlencoding", + "async-stream-impl", + "futures-core", + "pin-project-lite", ] [[package]] -name = "aws-smithy-runtime" -version = "1.10.3" +name = "async-stream-impl" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-http-client", - "aws-smithy-observability", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "pin-project-lite", - "pin-utils", - "tokio", - "tracing", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "aws-smithy-runtime-api" -version = "1.11.6" +name = "async-task" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.12", - "http 1.4.0", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] -name = "aws-smithy-types" -version = "1.4.7" +name = "async-trait" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "aws-smithy-xml" -version = "0.60.15" +name = "atk" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" dependencies = [ - "xmlparser", + "atk-sys", + "glib", + "libc", ] [[package]] -name = "aws-types" -version = "1.3.14" +name = "atk-sys" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", ] [[package]] -name = "axum" -version = "0.8.8" +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ - "axum-core", - "axum-macros", - "bytes", - "form_urlencoded", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower 0.5.3", - "tower-layer", - "tower-service", - "tracing", + "base64 0.22.1", + "flate2", + "http", + "log", + "native-tls", + "url", ] [[package]] -name = "axum-core" -version = "0.5.6" +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" dependencies = [ - "bytes", - "futures-core", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", + "aws-lc-sys", + "zeroize", ] [[package]] -name = "axum-macros" -version = "0.5.0" +name = "aws-lc-sys" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "cc", + "cmake", + "dunce", + "fs_extra", ] [[package]] @@ -1299,21 +657,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.1", -] - [[package]] name = "base64" version = "0.21.7" @@ -1326,16 +669,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -1375,15 +708,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - [[package]] name = "block2" version = "0.6.2" @@ -1476,16 +800,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - [[package]] name = "cairo-rs" version = "0.18.5" @@ -1553,15 +867,6 @@ dependencies = [ "toml 0.9.11+spec-1.1.0", ] -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - [[package]] name = "cc" version = "1.2.53" @@ -1633,16 +938,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "clap" version = "4.6.1" @@ -1683,15 +978,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - [[package]] name = "cmake" version = "0.1.57" @@ -1792,9 +1078,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.1", "core-foundation 0.10.1", @@ -1816,7 +1102,7 @@ dependencies = [ [[package]] name = "cowork-cli" -version = "2.5.1" +version = "2.5.2" dependencies = [ "adk-core", "adk-runner", @@ -1842,7 +1128,7 @@ dependencies = [ [[package]] name = "cowork-core" -version = "2.5.1" +version = "2.5.2" dependencies = [ "adk-agent", "adk-core", @@ -1881,7 +1167,7 @@ dependencies = [ [[package]] name = "cowork-gui" -version = "2.5.1" +version = "2.5.2" dependencies = [ "adk-core", "adk-runner", @@ -1992,6 +1278,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -2004,14 +1303,20 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.117", + "ctor-proc-macro", + "dtor", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "darling" version = "0.20.11" @@ -2116,12 +1421,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - [[package]] name = "dbus" version = "0.9.10" @@ -2133,24 +1432,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "dbus-secret-service" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" -dependencies = [ - "aes", - "block-padding", - "cbc", - "dbus", - "fastrand", - "hkdf", - "num", - "once_cell", - "sha2", - "zeroize", -] - [[package]] name = "deranged" version = "0.5.5" @@ -2248,7 +1529,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -2293,12 +1573,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "dispatch2" version = "0.3.0" @@ -2345,6 +1619,21 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", +] + [[package]] name = "dpi" version = "0.1.2" @@ -2366,9 +1655,24 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" dependencies = [ - "dtoa", + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", ] +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -2381,12 +1685,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - [[package]] name = "email_address" version = "0.2.9" @@ -2437,12 +1735,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" -[[package]] -name = "endian-type" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" - [[package]] name = "enumflags2" version = "0.7.12" @@ -2497,12 +1789,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - [[package]] name = "event-listener" version = "5.4.1" @@ -2537,9 +1823,9 @@ dependencies = [ [[package]] name = "fancy-regex" -version = "0.17.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" dependencies = [ "bit-set", "regex-automata", @@ -2571,17 +1857,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - [[package]] name = "find-msvc-tools" version = "0.1.8" @@ -2600,9 +1875,9 @@ dependencies = [ [[package]] name = "fluent-uri" -version = "0.4.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" dependencies = [ "borrow-or-share", "ref-cast", @@ -2981,12 +2256,6 @@ dependencies = [ "wasip3", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "gio" version = "0.18.4" @@ -3159,7 +2428,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http", "indexmap 2.13.0", "slab", "tokio", @@ -3187,11 +2456,6 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] [[package]] name = "heck" @@ -3217,33 +2481,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "html5ever" version = "0.29.1" @@ -3252,19 +2489,18 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] [[package]] -name = "http" -version = "0.2.12" +name = "html5ever" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ - "bytes", - "fnv", - "itoa", + "log", + "markup5ever 0.38.0", ] [[package]] @@ -3277,17 +2513,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -3295,7 +2520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http", ] [[package]] @@ -3306,8 +2531,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -3334,10 +2559,9 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -3352,7 +2576,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", + "http", "hyper", "hyper-util", "rustls", @@ -3364,35 +2588,6 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.19" @@ -3404,8 +2599,8 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "hyper", "ipnet", "libc", @@ -3450,7 +2645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -3634,16 +2829,6 @@ dependencies = [ "cfb", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding", - "generic-array", -] - [[package]] name = "instant" version = "0.1.13" @@ -3694,15 +2879,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.17" @@ -3798,28 +2974,25 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.45.1" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "257eb0e588b76827bbddc9e73945a9743693dd2adeaee9da26420f93cfedb798" +checksum = "4b8f66fe41fa46a5c83ed1c717b7e0b4635988f427083108c8cf0a882cc13441" dependencies = [ "ahash", + "base64 0.22.1", "bytecount", - "data-encoding", "email_address", "fancy-regex", "fraction", - "getrandom 0.3.4", "idna", "itoa", "num-cmp", - "num-traits", + "once_cell", "percent-encoding", "referencing", - "regex", "regex-syntax", "serde", "serde_json", - "unicode-general-category", "uuid-simd", ] @@ -3834,32 +3007,16 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "keyring" -version = "3.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" -dependencies = [ - "byteorder", - "dbus-secret-service", - "log", - "secret-service", - "security-framework 2.11.1", - "security-framework 3.5.1", - "windows-sys 0.60.2", - "zeroize", -] - [[package]] name = "kuchikiki" version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ - "cssparser", - "html5ever", + "cssparser 0.29.6", + "html5ever 0.29.1", "indexmap 2.13.0", - "selectors", + "selectors 0.24.0", ] [[package]] @@ -3931,7 +3088,6 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.11.1", "libc", - "redox_syscall 0.7.3", ] [[package]] @@ -3982,9 +3138,20 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", ] [[package]] @@ -4013,12 +3180,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - [[package]] name = "memchr" version = "2.8.0" @@ -4079,9 +3240,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.17.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" dependencies = [ "crossbeam-channel", "dpi", @@ -4092,10 +3253,10 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4130,12 +3291,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -4151,28 +3306,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nix" version = "0.31.2" @@ -4319,9 +3452,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -4335,17 +3468,9 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.11.1", "block2", - "libc", "objc2", - "objc2-cloud-kit", - "objc2-core-data", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", "objc2-foundation", - "objc2-quartz-core", ] [[package]] @@ -4365,7 +3490,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.11.1", "objc2", "objc2-foundation", ] @@ -4405,28 +3529,25 @@ dependencies = [ ] [[package]] -name = "objc2-core-text" +name = "objc2-core-location" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "bitflags 2.11.1", "objc2", - "objc2-core-foundation", - "objc2-core-graphics", + "objc2-foundation", ] [[package]] -name = "objc2-core-video" +name = "objc2-core-text" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.11.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", - "objc2-io-surface", ] [[package]] @@ -4468,16 +3589,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-javascript-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" -dependencies = [ - "objc2", - "objc2-core-foundation", -] - [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -4491,25 +3602,33 @@ dependencies = [ ] [[package]] -name = "objc2-security" +name = "objc2-ui-kit" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.11.1", + "block2", "objc2", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", ] [[package]] -name = "objc2-ui-kit" +name = "objc2-user-notifications" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "bitflags 2.11.1", "objc2", - "objc2-core-foundation", "objc2-foundation", ] @@ -4525,36 +3644,6 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", - "objc2-javascript-core", - "objc2-security", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "ollama-rs" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f647d8676b95a6b6205e11453c9fac338d73c9cdcc011c94d1ba9c9bfea582cd" -dependencies = [ - "async-stream", - "log", - "reqwest 0.12.28", - "schemars 1.2.0", - "serde", - "serde_json", - "static_assertions", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "url", ] [[package]] @@ -4608,103 +3697,27 @@ dependencies = [ ] [[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-probe" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "opentelemetry" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "opentelemetry-http" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" -dependencies = [ - "async-trait", - "bytes", - "http 1.4.0", - "opentelemetry", - "reqwest 0.12.28", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.31.1" +name = "openssl-probe" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" -dependencies = [ - "http 1.4.0", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost", - "reqwest 0.12.28", - "thiserror 2.0.18", - "tokio", - "tonic", - "tracing", -] +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] -name = "opentelemetry-proto" -version = "0.31.0" +name = "openssl-probe" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" -dependencies = [ - "opentelemetry", - "opentelemetry_sdk", - "prost", - "tonic", - "tonic-prost", -] +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" [[package]] -name = "opentelemetry_sdk" -version = "0.31.0" +name = "openssl-sys" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ - "futures-channel", - "futures-executor", - "futures-util", - "opentelemetry", - "percent-encoding", - "rand 0.9.2", - "thiserror 2.0.18", - "tokio", - "tokio-stream", + "cc", + "libc", + "pkg-config", + "vcpkg", ] [[package]] @@ -4778,7 +3791,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link 0.2.1", ] @@ -4827,10 +3840,20 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.3", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -4851,6 +3874,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -4881,6 +3914,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.10.0" @@ -4897,12 +3940,12 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn 2.0.117", @@ -4936,23 +3979,12 @@ dependencies = [ ] [[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" +name = "phf_shared" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "siphasher 1.0.2", ] [[package]] @@ -5010,6 +4042,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -5133,26 +4178,17 @@ dependencies = [ ] [[package]] -name = "prost" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.14.3" +name = "process-wrap" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn 2.0.117", + "futures", + "indexmap 2.13.0", + "nix", + "tokio", + "tracing", + "windows 0.62.2", ] [[package]] @@ -5241,16 +4277,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "radix_trie" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a" -dependencies = [ - "endian-type", - "nibble_vec", -] - [[package]] name = "rand" version = "0.7.3" @@ -5376,15 +4402,6 @@ dependencies = [ "bitflags 2.11.1", ] -[[package]] -name = "redox_syscall" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" -dependencies = [ - "bitflags 2.11.1", -] - [[package]] name = "redox_users" version = "0.4.6" @@ -5429,15 +4446,13 @@ dependencies = [ [[package]] name = "referencing" -version = "0.45.1" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2f38748ceca8d0b0013e60f534d94a6e23dfd89fd2a88318fc5a2d04fda1010" +checksum = "d0dcb5ab28989ad7c91eb1b9531a37a1a137cc69a0499aee4117cae4a107c464" dependencies = [ "ahash", "fluent-uri", - "getrandom 0.3.4", - "hashbrown 0.16.1", - "parking_lot", + "once_cell", "percent-encoding", "serde_json", ] @@ -5465,12 +4480,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - [[package]] name = "regex-syntax" version = "0.8.8" @@ -5486,22 +4495,19 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", - "futures-channel", "futures-core", "futures-util", "h2", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", "mime", "mime_guess", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -5513,10 +4519,9 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", "tokio-util", - "tower 0.5.3", + "tower", "tower-http", "tower-service", "url", @@ -5539,8 +4544,8 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "hyper", "hyper-rustls", @@ -5560,7 +4565,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "tower 0.5.3", + "tower", "tower-http", "tower-service", "url", @@ -5626,17 +4631,18 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.5.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67d69668de0b0ccd9cc435f700f3b39a7861863cf37a15e1f304ea78688a4826" +checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59" dependencies = [ "async-trait", "base64 0.22.1", "chrono", "futures", - "http 1.4.0", + "http", "pastey", "pin-project-lite", + "process-wrap", "reqwest 0.13.2", "rmcp-macros", "schemars 1.2.0", @@ -5652,9 +4658,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.5.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fdc01c81097b0aed18633e676e269fefa3a78ec1df56b4fe597c1241b92025" +checksum = "1aad0035b69380782d78ea95b508327e6deaa2235909053e596eea8f27b5e1d5" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -5663,46 +4669,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "rust-embed" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn 2.0.117", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" -dependencies = [ - "sha2", - "walkdir", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -5813,27 +4779,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "rustyline" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "clipboard-win", - "home", - "libc", - "log", - "memchr", - "nix 0.31.2", - "radix_trie", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "windows-sys 0.61.2", -] - [[package]] name = "ryu" version = "1.0.22" @@ -5939,25 +4884,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secret-service" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" -dependencies = [ - "aes", - "cbc", - "futures-util", - "generic-array", - "hkdf", - "num", - "once_cell", - "rand 0.8.5", - "serde", - "sha2", - "zbus 4.4.0", -] - [[package]] name = "security-framework" version = "2.11.1" @@ -6001,14 +4927,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", + "cssparser 0.29.6", "derive_more 0.99.20", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", "smallvec", ] @@ -6081,7 +5026,6 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.13.0", "itoa", "memchr", "serde", @@ -6089,17 +5033,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - [[package]] name = "serde_repr" version = "0.1.20" @@ -6218,14 +5151,12 @@ dependencies = [ ] [[package]] -name = "sha1" -version = "0.10.6" +name = "servo_arc" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "stable_deref_trait", ] [[package]] @@ -6306,7 +5237,6 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" dependencies = [ - "backtrace", "snafu-derive", ] @@ -6347,7 +5277,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall 0.5.18", + "redox_syscall", "tracing", "wasm-bindgen", "web-sys", @@ -6388,7 +5318,7 @@ checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" dependencies = [ "bytes", "futures-util", - "http-body 1.0.1", + "http-body", "http-body-util", "pin-project-lite", ] @@ -6399,12 +5329,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "string_cache" version = "0.8.9" @@ -6418,6 +5342,18 @@ dependencies = [ "serde", ] +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + [[package]] name = "string_cache_codegen" version = "0.5.4" @@ -6430,6 +5366,18 @@ dependencies = [ "quote", ] +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -6582,39 +5530,39 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.5" +version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ "bitflags 2.11.1", "block2", "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", - "dispatch", + "dbus", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static", "libc", "log", "ndk", - "ndk-context", "ndk-sys", "objc2", "objc2-app-kit", "objc2-foundation", + "objc2-ui-kit", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -6631,17 +5579,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "tar" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "target-lexicon" version = "0.12.16" @@ -6650,9 +5587,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.10.2" +version = "2.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" dependencies = [ "anyhow", "bytes", @@ -6664,7 +5601,7 @@ dependencies = [ "glob", "gtk", "heck 0.5.0", - "http 1.4.0", + "http", "jni", "libc", "log", @@ -6696,14 +5633,14 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] name = "tauri-build" -version = "2.5.5" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", @@ -6717,22 +5654,21 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.11+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.5.4" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -6750,9 +5686,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.4" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -6781,9 +5717,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" dependencies = [ "log", "raw-window-handle", @@ -6799,13 +5735,15 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.4.5" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" dependencies = [ "anyhow", "dunce", "glob", + "log", + "objc2-foundation", "percent-encoding", "schemars 0.8.22", "serde", @@ -6815,15 +5753,15 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.18", - "toml 0.9.11+spec-1.1.0", + "toml 1.0.1+spec-1.1.0", "url", ] [[package]] name = "tauri-plugin-opener" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" dependencies = [ "dunce", "glob", @@ -6837,20 +5775,20 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.18", "url", - "windows", - "zbus 5.13.2", + "windows 0.61.3", + "zbus", ] [[package]] name = "tauri-runtime" -version = "2.10.0" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" dependencies = [ "cookie", "dpi", "gtk", - "http 1.4.0", + "http", "jni", "objc2", "objc2-ui-kit", @@ -6863,22 +5801,21 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] name = "tauri-runtime-wry" -version = "2.10.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" dependencies = [ "gtk", - "http 1.4.0", + "http", "jni", "log", "objc2", "objc2-app-kit", - "objc2-foundation", "once_cell", "percent-encoding", "raw-window-handle", @@ -6889,30 +5826,32 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] [[package]] name = "tauri-utils" -version = "2.8.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "anyhow", "brotli", "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "html5ever", - "http 1.4.0", + "html5ever 0.29.1", + "http", "infer", "json-patch", "kuchikiki", "log", "memchr", - "phf 0.11.3", + "phf 0.13.1", + "plist", "proc-macro2", "quote", "regex", @@ -6924,7 +5863,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.18", - "toml 0.9.11+spec-1.1.0", + "toml 1.0.1+spec-1.1.0", "url", "urlpattern", "uuid", @@ -6957,12 +5896,22 @@ dependencies = [ [[package]] name = "tendril" -version = "0.4.3" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tendril" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" dependencies = [ - "futf", - "mac", + "new_debug_unreachable", "utf-8", ] @@ -7111,16 +6060,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -7142,18 +6081,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -7289,58 +6216,6 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" -[[package]] -name = "tonic" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "sync_wrapper", - "tokio", - "tokio-stream", - "tower 0.5.3", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-prost" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" -dependencies = [ - "bytes", - "prost", - "tonic", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.3" @@ -7349,15 +6224,11 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.13.0", "pin-project-lite", - "slab", "sync_wrapper", "tokio", - "tokio-util", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -7369,16 +6240,13 @@ dependencies = [ "bitflags 2.11.1", "bytes", "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", + "http", + "http-body", "iri-string", "pin-project-lite", - "tokio", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -7399,7 +6267,6 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -7437,32 +6304,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-opentelemetry" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" -dependencies = [ - "js-sys", - "opentelemetry", - "smallvec", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", - "web-time", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - [[package]] name = "tracing-subscriber" version = "0.3.22" @@ -7473,22 +6314,19 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", - "serde", - "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", - "tracing-serde", ] [[package]] name = "tray-icon" -version = "0.21.3" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", "dirs 6.0.0", @@ -7500,10 +6338,10 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7512,19 +6350,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" -dependencies = [ - "bytes", - "log", - "rand 0.9.2", - "thiserror 2.0.18", - "utf-8", -] - [[package]] name = "typeid" version = "1.0.3" @@ -7595,12 +6420,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" -[[package]] -name = "unicode-general-category" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" - [[package]] name = "unicode-ident" version = "1.0.22" @@ -7705,6 +6524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" dependencies = [ "outref", + "uuid", "vsimd", ] @@ -7946,6 +6766,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + [[package]] name = "webkit2gtk" version = "2.0.2" @@ -8016,7 +6848,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -8040,7 +6872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] @@ -8107,11 +6939,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -8123,6 +6967,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -8157,7 +7010,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -8204,6 +7068,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -8377,6 +7251,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -8696,24 +7579,23 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" -version = "0.54.2" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", "block2", "cookie", "crossbeam-channel", "dirs 6.0.0", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", - "http 1.4.0", + "http", "javascriptcore-rs", "jni", - "kuchikiki", "libc", "ndk", "objc2", @@ -8733,7 +7615,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -8760,32 +7642,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - [[package]] name = "yoke" version = "0.8.1" @@ -8809,38 +7665,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zbus" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" -dependencies = [ - "async-broadcast", - "async-process", - "async-recursion", - "async-trait", - "enumflags2", - "event-listener", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix 0.29.0", - "ordered-stream", - "rand 0.8.5", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "windows-sys 0.52.0", - "xdg-home", - "zbus_macros 4.4.0", - "zbus_names 3.0.0", - "zvariant 4.2.0", -] - [[package]] name = "zbus" version = "5.13.2" @@ -8871,22 +7695,9 @@ dependencies = [ "uuid", "windows-sys 0.61.2", "winnow 0.7.14", - "zbus_macros 5.13.2", - "zbus_names 4.3.1", - "zvariant 5.9.2", -] - -[[package]] -name = "zbus_macros" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.117", - "zvariant_utils 2.1.0", + "zbus_macros", + "zbus_names", + "zvariant", ] [[package]] @@ -8899,20 +7710,9 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "zbus_names 4.3.1", - "zvariant 5.9.2", - "zvariant_utils 3.3.0", -] - -[[package]] -name = "zbus_names" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" -dependencies = [ - "serde", - "static_assertions", - "zvariant 4.2.0", + "zbus_names", + "zvariant", + "zvariant_utils", ] [[package]] @@ -8923,7 +7723,7 @@ checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", "winnow 0.7.14", - "zvariant 5.9.2", + "zvariant", ] [[package]] @@ -8972,20 +7772,6 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] [[package]] name = "zerotrie" @@ -9026,19 +7812,6 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" -[[package]] -name = "zvariant" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" -dependencies = [ - "endi", - "enumflags2", - "serde", - "static_assertions", - "zvariant_derive 4.2.0", -] - [[package]] name = "zvariant" version = "5.9.2" @@ -9049,21 +7822,8 @@ dependencies = [ "enumflags2", "serde", "winnow 0.7.14", - "zvariant_derive 5.9.2", - "zvariant_utils 3.3.0", -] - -[[package]] -name = "zvariant_derive" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.117", - "zvariant_utils 2.1.0", + "zvariant_derive", + "zvariant_utils", ] [[package]] @@ -9076,18 +7836,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "zvariant_utils 3.3.0", -] - -[[package]] -name = "zvariant_utils" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "zvariant_utils", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 80de6ec..eec2290 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ ] [workspace.package] -version = "2.5.1" +version = "2.5.2" edition = "2024" authors = ["Sopaco"] license = "MIT" @@ -15,14 +15,14 @@ repository = "https://github.com/sopaco/cowork-forge" [workspace.dependencies] # ADK Rust framework -adk-rust = "0.5.0" -adk-core = "0.5.0" -adk-agent = "0.5.0" -adk-model = { version = "0.5.0", features = ["openai"] } -adk-tool = "0.5.0" -adk-runner = "0.5.0" -adk-session = "0.5.0" -adk-skill = "0.5.0" +adk-rust = "1.0.0" +adk-core = "1.0.0" +adk-agent = "1.0.0" +adk-model = { version = "1.0.0", features = ["openai"] } +adk-tool = "1.0.0" +adk-runner = "1.0.0" +adk-session = "1.0.0" +adk-skill = "1.0.0" # Core dependencies tokio = { version = "1", features = ["full"] } diff --git a/README.md b/README.md index ba59985..e9c98b8 100644 --- a/README.md +++ b/README.md @@ -404,7 +404,7 @@ Cowork Forge uses a `config.toml` file stored in your system's application data [llm] api_base_url = "https://api.openai.com/v1" api_key = "sk-your-openai-api-key" -model_name = "gpt-4" +model_name = "gpt-5" # Optional: Embedding Configuration [embedding] diff --git a/README_zh.md b/README_zh.md index 19833d9..e0b8931 100644 --- a/README_zh.md +++ b/README_zh.md @@ -550,7 +550,7 @@ Cowork Forge 使用 `config.toml` 文件进行配置。在项目目录中创建 [llm] api_base_url = "https://api.openai.com/v1" api_key = "sk-your-openai-api-key" -model_name = "gpt-4" +model_name = "gpt-5" # 可选:嵌入配置 [embedding] diff --git a/adk-rust-learning.md b/adk-rust-learning.md deleted file mode 100644 index 3cf48e8..0000000 --- a/adk-rust-learning.md +++ /dev/null @@ -1,1802 +0,0 @@ -# ADK-Rust 学习指南 - -本指南基于 deepwiki 提供的 adk-rust 实际使用方法和源码,重点介绍 tools、loop、典型 agent 模式等高级抽象的 API 和示例代码。 - -## 目录 - -1. [概述](#概述) -2. [Tools(工具)](#tools工具) -3. [Agent 模式](#agent-模式) -4. [Loop(循环)](#loop循环) -5. [会话状态管理](#会话状态管理) -6. [高级功能和配置](#高级功能和配置) -7. [错误处理和回调](#错误处理和回调) -8. [流式处理](#流式处理) - -## 概述 - -ADK-Rust 是一个生产就绪的 Rust 实现的 Google Agent Development Kit (ADK),用于构建高性能、内存安全的 AI 代理系统,支持流式响应、工作流编排和可扩展的工具集成。 - -主要特点: -- 高性能、内存安全的 AI 代理系统 -- 流式响应和工作流编排 -- 可扩展的工具集成 -- 支持多种 agent 模式(顺序、并行、循环) - -## Tools(工具) - -### 1. 实现 Tool trait - -最基础的创建工具方式是实现 `Tool` trait: - -```rust -use adk_core::{Tool, ToolContext, Result}; -use async_trait::async_trait; -use serde_json::{json, Value}; -use std::sync::Arc; - -struct WeatherTool { - api_key: String, -} - -#[async_trait] -impl Tool for WeatherTool { - fn name(&self) -> &str { - "get_weather" - } - - fn description(&self) -> &str { - "Get current weather for a city. Use this when the user asks about weather conditions." - } - - fn parameters_schema(&self) -> Option { - Some(json!({ - "type": "object", - "properties": { - "city": { - "type": "string", - "description": "City name (e.g., 'London', 'New York')" - }, - "units": { - "type": "string", - "enum": ["celsius", "fahrenheit"], - "description": "Temperature units" - } - }, - "required": ["city"] - })) - } - - async fn execute(&self, _ctx: Arc, args: Value) -> Result { - let city = args["city"].as_str().unwrap_or("Unknown"); - let units = args["units"].as_str().unwrap_or("celsius"); - - // 调用天气 API... - - Ok(json!({ - "city": city, - "temperature": 22, - "units": units, - "condition": "sunny" - })) - } -} -``` - -### 2. 使用 FunctionTool - -更简单的方式是使用 `FunctionTool`,它允许您将异步函数包装为工具: - -```rust -let weather_tool = FunctionTool::new( - "get_weather", // 工具名称(LLM 使用) - "Get the current weather for a city", // 描述(帮助 LLM 决定何时使用) - |_ctx, args| async move { // 处理函数 - let city = args.get("city") // 从 JSON 中提取参数 - .and_then(|v| v.as_str()) - .unwrap_or("unknown"); - Ok(json!({ "city": city, "temperature": "22°C" })) // 返回 JSON 结果 - }, -); -``` - -### 3. 将工具添加到 Agent - -创建工具后,可以通过 `LlmAgentBuilder` 将其添加到 agent: - -```rust -let agent = LlmAgentBuilder::new("weather_assistant") - .description("A helpful assistant with weather abilities") - .instruction("You are a helpful assistant. Use the weather tool for weather questions.") - .model(Arc::new(model)) - .tool(Arc::new(weather_tool)) - .build()?; -``` - -### 4. 工具遥测 - -可以在工具实现中添加自定义遥测: - -```rust -use adk_rust::prelude::*; -use adk_telemetry::{info, instrument, tool_execute_span}; -use serde_json::{json, Value}; - -#[instrument(skip(ctx))] -async fn weather_tool_impl( - ctx: Arc, - args: Value, -) -> Result { - let span = tool_execute_span("weather_tool"); - let _enter = span.enter(); - - let location = args.get("location").and_then(|v| v.as_str()).unwrap_or("unknown"); - info!(location = location, "Fetching weather data"); - - // 工具逻辑 - let result = json!({ - "temperature": 72, - "condition": "sunny" - }); - - info!(location = location, "Weather data retrieved"); - Ok(result) -} -``` - -### 5. 安全参数提取(Cowork Forge 最佳实践) - -#### 问题:unwrap() 导致 panic - -传统的工具实现使用 `unwrap()` 提取参数,当参数缺失时会导致整个应用崩溃: - -```rust -// ❌ 危险:使用 unwrap() -async fn execute(&self, _ctx: Arc, args: Value) -> Result { - let title = args["title"].as_str().unwrap(); // 如果参数缺失,panic! - let content = args["content"].as_str().unwrap(); // 如果参数缺失,panic! - // ... -} -``` - -**运行时错误**: -``` -thread 'tokio-runtime-worker' panicked at crates\cowork-core\src\tools\hitl_content_tools.rs:156:48: -called `Option::unwrap()` on a `None` value -``` - -#### 解决方案:安全的参数提取函数 - -提供辅助函数来安全地提取参数: - -```rust -// tools/mod.rs - -/// 安全提取必需的字符串参数 -pub fn get_required_string_param<'a>( - args: &'a Value, - key: &str, -) -> Result<&'a str, AdkError> { - args.get(key) - .and_then(|v| v.as_str()) - .ok_or_else(|| AdkError::Tool(format!("Missing required parameter: {}", key))) -} - -/// 安全提取可选的字符串参数 -pub fn get_optional_string_param(args: &Value, key: &str) -> Option { - args.get(key) - .and_then(|v| v.as_str()) - .map(String::from) -} - -/// 安全提取必需的数组参数 -pub fn get_required_array_param<'a>( - args: &'a Value, - key: &str, -) -> Result<&'a Vec, AdkError> { - args.get(key) - .and_then(|v| v.as_array()) - .ok_or_else(|| AdkError::Tool(format!("Missing required parameter: {}", key))) -} -``` - -#### 正确的工具实现 - -```rust -// ✅ 安全:使用参数提取函数 -pub struct SaveIdeaTool; - -#[async_trait] -impl Tool for SaveIdeaTool { - async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { - // 安全提取必需参数 - let content = get_required_string_param(&args, "content")?; - - save_idea(content) - .map_err(|e| adk_core::AdkError::Tool(e.to_string()))?; - - Ok(json!({ - "status": "success", - "message": "Idea document saved successfully", - "file_path": "artifacts/idea.md" - })) - } -} -``` - -**运行时错误**(当参数缺失时): -``` -Error: Missing required parameter: content -``` - -#### 参数提取函数对比 - -| 函数 | 用途 | 缺失时行为 | 返回类型 | -|------|------|------------|----------| -| `get_required_string_param` | 提取必需字符串 | 返回错误 | `Result<&str, AdkError>` | -| `get_optional_string_param` | 提取可选字符串 | 返回 `None` | `Option` | -| `get_required_array_param` | 提取必需数组 | 返回错误 | `Result<&Vec, AdkError>` | -| `get_optional_array_param` | 提取可选数组 | 返回空数组 | `Vec` | - -#### 实际应用:修复 unwrap() 统计 - -在 Cowork Forge 项目中,我们修复了 33 个危险的 unwrap() 调用: - -| 文件 | 修复数量 | 示例 | -|------|----------|------| -| hitl_content_tools.rs | 2 | `title`, `content` | -| validation_tools.rs | 1 | `data_type` | -| artifact_tools.rs | 5 | `content` (多个 Save 工具) | -| goto_stage_tool.rs | 2 | `stage`, `reason` | -| file_tools.rs | 3 | `path`, `content`, `command` | -| hitl_tools.rs | 4 | `file_path`, `title`, `path` | -| control_tools.rs | 5 | `feedback_type`, `severity`, `details` | -| data_tools.rs | 11 | `priority`, `category`, `title`, `description`, `feature_id`, `component_id` | - -**结果**: -- ✅ 从 39 个潜在 panic 点减少到 6 个相对安全的点 -- ✅ 所有缺失参数现在返回清晰的错误信息 -- ✅ 系统稳定性显著提升 - -#### 工具实现检查清单 - -实现新工具时,请检查以下项目: - -```rust -pub struct MyNewTool; - -#[async_trait] -impl Tool for MyNewTool { - fn name(&self) -> &str { - "my_new_tool" - } - - fn description(&self) -> &str { - "Describe what this tool does in detail." - } - - fn parameters_schema(&self) -> Option { - Some(json!({ - "type": "object", - "properties": { - "required_param": { - "type": "string", - "description": "Description of required parameter" - }, - "optional_param": { - "type": "string", - "description": "Description of optional parameter" - } - }, - "required": ["required_param"] // 声明必需参数 - })) - } - - async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { - // ✅ 使用安全的参数提取 - let required = get_required_string_param(&args, "required_param")?; - - // ✅ 使用安全的可选参数提取 - let optional = get_optional_string_param(&args, "optional_param"); - - // ✅ 处理逻辑 - // ... - - // ✅ 返回结构化结果 - Ok(json!({ - "status": "success", - "result": "..." - })) - } -} -``` - -**检查清单**: -- [ ] 必需参数使用 `get_required_string_param` 或 `get_required_array_param` -- [ ] 可选参数使用 `get_optional_string_param` 或 `get_optional_array_param` -- [ ] 不使用 `unwrap()` 提取参数 -- [ ] `parameters_schema` 中声明所有必需参数 -- [ ] 返回清晰的错误信息(不要返回空错误) -- [ ] 返回结构化的 JSON 结果 - -#### 生命周期标注注意事项 - -当引用 `Value` 中的字符串时,需要添加生命周期标注: - -```rust -// ❌ 错误:缺少生命周期标注 -pub fn get_required_string_param(args: &Value, key: &str) -> Result<&str, AdkError> { - // 编译错误:missing lifetime specifier -} - -// ✅ 正确:添加生命周期标注 -pub fn get_required_string_param<'a>( - args: &'a Value, - key: &str, -) -> Result<&'a str, AdkError> { - args.get(key) - .and_then(|v| v.as_str()) - .ok_or_else(|| AdkError::Tool(format!("Missing required parameter: {}", key))) -} -``` - -生命周期 `'a` 告诉编译器:返回的字符串引用的生命周期与输入 `args` 相同。 - -## Agent 模式 - -### 1. 基础 LlmAgent - -使用 `LlmAgentBuilder` 创建基础 agent: - -```rust -use adk_agent::LlmAgentBuilder; -use adk_model::GeminiModel; -use std::sync::Arc; - -#[tokio::main] -async fn main() -> std::result::Result<(), Box> { - let api_key = std::env::var("GOOGLE_API_KEY")?; - let model = GeminiModel::new(&api_key, "gemini-2.5-flash")?; - - let agent = LlmAgentBuilder::new("assistant") - .description("A helpful AI assistant") - .instruction("You are a friendly assistant. Be helpful and concise.") - .model(Arc::new(model)) - .build()?; - - println!("Agent '{}' ready!", agent.name()); - Ok(()) -} -``` - -### 2. Actor-Critic Loop 模式(Cowork Forge 核心模式) - -使用 LoopAgent 实现经典的 Actor-Critic 协作模式: - -```rust -// 创建 Actor - 负责创建内容 -let actor = LlmAgentBuilder::new("prd_actor") - .instruction(PRD_ACTOR_INSTRUCTION) - .model(model.clone()) - .tool(Arc::new(CreateRequirementTool)) - .tool(Arc::new(AddFeatureTool)) - .tool(Arc::new(GetRequirementsTool)) - .build()?; - -// 创建 Critic - 负责审查质量 -let critic = LlmAgentBuilder::new("prd_critic") - .instruction(PRD_CRITIC_INSTRUCTION) - .model(model) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(CheckFeatureCoverageTool)) - .build()?; - -// 创建 LoopAgent - Actor 和 Critic 交替执行 -let mut loop_agent = LoopAgent::new( - "prd_loop", - vec![Arc::new(actor), Arc::new(critic)], -); -// 使用 max_iterations=1 避免过度优化,保持节奏 -loop_agent = loop_agent.with_max_iterations(1); - -Ok(Arc::new(loop_agent)) -``` - -**设计理念**: -- Actor:创建内容(需求、设计、计划、代码) -- Critic:审查质量、发现问题 -- max_iterations=1:避免过度优化,让流程继续 -- 每个阶段独立迭代,而非无限循环 - -### 3. 多工具 Agent - -创建具有多个工具的 agent: - -```rust -// Weather tool -let weather = FunctionTool::new( - "get_weather", - "Get weather for a city. Parameters: city (string)", - |_ctx, args| async move { - let city = args.get("city").and_then(|v| v.as_str()).unwrap_or("unknown"); - Ok(json!({ - "city": city, - "temperature": "22°C", - "humidity": "65%", - "condition": "partly cloudy" - })) - }, -); - -// Calculator tool -let calc = FunctionTool::new( - "calculate", - "Math operations. Parameters: expression (string like '2 + 2')", - |_ctx, args| async move { - let expr = args.get("expression").and_then(|v| v.as_str()).unwrap_or("0"); - Ok(json!({ "expression": expr, "result": "computed" })) - }, -); - -// Build the agent with tools -let agent = LlmAgentBuilder::new("assistant") - .description("A helpful assistant with weather and calculation abilities") - .instruction("You are a helpful assistant. Use the weather tool for weather questions. Use the calculator for math. Be concise and friendly.") - .model(Arc::new(model)) - .tool(Arc::new(weather)) - .tool(Arc::new(calc)) - .output_key("last_response") // 保存响应到会话状态 - .max_iterations(10) // 限制 LLM 轮次 - .build()?; -``` - -### 3. 全局指令 - -可以使用 `global_instruction` 设置适用于所有 agents 的全局指令: - -```rust -let agent = LlmAgentBuilder::new("assistant") - .description("A helpful assistant") - .global_instruction( - "You are a professional assistant for Acme Corp. - Always maintain a friendly but professional tone. - Our company values are: customer-first, innovation, and integrity." - ) - .instruction("Help users with their questions and tasks.") - .model(model.clone()) - .build()?; -``` - -### 3. 顺序和并行工作流 - -创建多 agent 组合: - -```rust -use adk_agent::{SequentialAgent, ParallelAgent, LoopAgent}; -use std::sync::Arc; - -// Sequential: A -> B -> C -let pipeline = SequentialAgent::new("pipeline", vec![ - agent_a.clone(), - agent_b.clone(), - agent_c.clone(), -]); - -// Parallel: A, B, C simultaneously -let team = ParallelAgent::new("team", vec![ - analyst_a.clone(), - analyst_b.clone(), -]); - -// Loop: repeat until exit or max iterations -let iterator = LoopAgent::new("iterator", vec![worker.clone()]) - .with_max_iterations(10); -``` - -### 4. 并行 Agent 的潜在应用 - -虽然 Cowork Forge 当前使用顺序执行,但可以考虑在以下场景使用 ParallelAgent: - -**场景 1:并行分析** -```rust -// 从不同角度并行分析需求 -let technical_analyst = create_agent("technical", ...); -let business_analyst = create_agent("business", ...); -let ux_analyst = create_agent("ux", ...); - -let parallel_analysis = ParallelAgent::new( - "parallel_requirements_analysis", - vec![ - Arc::new(technical_analyst), - Arc::new(business_analyst), - Arc::new(ux_analyst), - ], -); - -let synthesizer = create_agent("synthesizer", ...); -let pipeline = SequentialAgent::new( - "full_requirements_flow", - vec![Arc::new(parallel_analysis), Arc::new(synthesizer)], -); -``` - -**场景 2:并行测试** -```rust -// 并行运行多种测试 -let unit_tests = create_agent("unit_tests", ...); -let integration_tests = create_agent("integration_tests", ...); -let e2e_tests = create_agent("e2e_tests", ...); - -let parallel_testing = ParallelAgent::new( - "parallel_test_suite", - vec![Arc::new(unit_tests), Arc::new(integration_tests), Arc::new(e2e_tests)], -); -``` - -### 5. 研究管道示例 - -创建一个研究管道:研究 → 分析 → 总结: - -```rust -// Step 1: Research agent gathers information -let researcher = LlmAgentBuilder::new("researcher") - .instruction("Research the given topic. List 3-5 key facts or points. Be factual and concise.") - .model(model.clone()) - .output_key("research") // 保存输出到状态 - .build()?; - -// Step 2: Analyzer agent identifies patterns -let analyzer = LlmAgentBuilder::new("analyzer") - .instruction("Based on the research above, identify 2-3 key insights or patterns. What's the bigger picture?") - .model(model.clone()) - .output_key("analysis") - .build()?; - -// Step 3: Summarizer creates final output -let summarizer = LlmAgentBuilder::new("summarizer") - .instruction("Create a brief executive summary combining the research and analysis. Keep it under 100 words.") - .model(model.clone()) - .build()?; - -// Create the sequential pipeline -let pipeline = SequentialAgent::new( - "research_pipeline", - vec![Arc::new(researcher), Arc::new(analyzer), Arc::new(summarizer)], -).with_description("Research → Analyze → Summarize"); -``` - -## ⚠️ 重要:LoopAgent 与 SequentialAgent 的交互问题 - -### 问题 -当 LoopAgent 作为 SequentialAgent 的子节点时,如果 LoopAgent 中的任何 agent 调用 `exit_loop()` 工具,**整个 SequentialAgent 都会终止**,而不仅仅是 LoopAgent。这是 adk-rust 的设计限制。 - -### 解决方案 -不要使用 `exit_loop()` 工具。改用 `max_iterations` 控制循环: - -```rust -// ❌ 错误:使用 exit_loop 会导致整个 pipeline 终止 -let refiner = LlmAgentBuilder::new("refiner") - .tool(Arc::new(ExitLoopTool::new())) - .build()?; - -// ✅ 正确:使用 max_iterations 控制循环 -let loop_agent = LoopAgent::new("loop", vec![critic, refiner]) - .with_max_iterations(1); // 让 LoopAgent 自然完成 -``` - -### 适用场景 -- **适用**:当 LoopAgent 是 SequentialAgent 的一部分时 -- **适用**:当 LoopAgent 与其他 agent 需要顺序执行时 -- **不适用**:如果 LoopAgent 是顶层 agent(不嵌套在 SequentialAgent 中),可以安全使用 exit_loop - -### Cowork Forge 的实践 -在 Cowork Forge 中,所有 LoopAgent 都使用 `max_iterations=1` 避免这个问题: - -```rust -// PRD Loop - 只迭代一次 -let mut loop_agent = LoopAgent::new("prd_loop", vec![prd_actor, prd_critic]); -loop_agent = loop_agent.with_max_iterations(1); - -// Coding Loop - 更多迭代,但仍然不使用 exit_loop -let mut loop_agent = LoopAgent::new("coding_loop", vec![coding_actor, coding_critic]); -loop_agent = loop_agent.with_max_iterations(5); -``` - -## Loop(循环) - -### 1. LoopAgent 基本用法 - -`LoopAgent` 重复执行一组 agents,直到满足退出条件或达到最大迭代次数: - -```rust -let loop_agent = LoopAgent::new("name", vec![agent1, agent2]) - .with_max_iterations(5) // 安全限制(推荐) - .with_description("Optional description") - .before_callback(callback) - .after_callback(callback) -``` - -### 2. 迭代改进循环 - -创建一个迭代改进循环,使用批评者和改进者不断优化内容: - -```rust -// Critic agent evaluates content -let critic = LlmAgentBuilder::new("critic") - .instruction("Review the content for quality. Score it 1-10 and list specific improvements needed. Be constructive but critical.") - .model(model.clone()) - .build()?; - -// Refiner agent improves based on critique -let refiner = LlmAgentBuilder::new("refiner") - .instruction("Apply the critique to improve the content. If the score is 8 or higher, call exit_loop to finish. Otherwise, provide an improved version.") - .model(model.clone()) - .tool(Arc::new(ExitLoopTool::new())) // 可以退出循环 - .build()?; - -// Create inner sequential: critic → refiner -let critique_refine = SequentialAgent::new( - "critique_refine_step", - vec![Arc::new(critic), Arc::new(refiner)], -); - -// Wrap in loop with max 3 iterations -let iterative_improver = LoopAgent::new( - "iterative_improver", - vec![Arc::new(critique_refine)], -).with_max_iterations(3) - .with_description("Critique-refine loop (max 3 passes)"); -``` - -### 3. 复杂工作流组合 - -创建一个复杂工作流:并行分析 → 合成 → 质量循环: - -```rust -// 1. Parallel analysis from multiple perspectives -let parallel_analysis = ParallelAgent::new( - "multi_analysis", - vec![Arc::new(tech_analyst), Arc::new(biz_analyst)], -); - -// 2. Synthesize the parallel results -let synthesizer = LlmAgentBuilder::new("synthesizer") - .instruction("Combine all analyses into a unified recommendation.") - .model(model.clone()) - .build()?; - -// 3. Quality loop: critique and refine -let quality_loop = LoopAgent::new( - "quality_check", - vec![Arc::new(critic), Arc::new(refiner)], -).with_max_iterations(2); - -// Final pipeline: parallel → synthesize → quality loop -let full_pipeline = SequentialAgent::new( - "full_analysis_pipeline", - vec![ - Arc::new(parallel_analysis), - Arc::new(synthesizer), - Arc::new(quality_loop), - ], -); -``` - -## 会话状态管理 - -### 1. 状态范围和前缀 - -状态键使用前缀来控制范围和持久性: - -| 前缀 | 范围 | 持久性 | 使用场景 | -|------|------|--------|----------| -| `user:` | 用户级别 | 跨所有会话 | 用户偏好、设置 | -| `app:` | 应用程序级别 | 应用程序范围 | 共享配置 | -| `temp:` | 轮次级别 | 每轮清空 | 临时计算数据 | -| (无) | 会话级别 | 仅此会话 | 对话上下文 | - -### 2. 状态管理示例 - -```rust -// In a callback or tool -let state = ctx.session().state(); - -// User preference (persists across sessions) -state.set("user:theme".into(), json!("dark")); - -// Session-specific data -state.set("current_topic".into(), json!("weather")); - -// Temporary data (cleared after this turn) -state.set("temp:step_count".into(), json!(1)); - -// Read values -if let Some(theme) = state.get("user:theme") { - println!("Theme: {}", theme); -} -``` - -### 3. 在 Agent 中保存输出 - -使用 `output_key` 方法保存 agent 的响应到会话状态: - -```rust -let agent = LlmAgentBuilder::new("summarizer") - .instruction("Summarize the provided text.") - .model(model.clone()) - .output_key("summary") // 响应保存到 state["summary"] - .build()?; -``` - -## 高级功能和配置 - -### 1. 限制 LLM 轮次 - -使用 `max_iterations` 控制 agent 可以进行的最大 LLM 调用次数: - -```rust -let agent = LlmAgentBuilder::new("bounded_agent") - .model(Arc::new(model)) - .instruction("You are a helpful assistant.") - .tool(Arc::new(my_tool)) - .max_iterations(10) // 10 次 LLM 调用后停止 - .build()?; -``` - -### 2. 动态指令提供者 - -使用 `instruction_provider` 根据上下文提供动态指令: - -```rust -let agent = LlmAgentBuilder::new("contextual_agent") - .model(Arc::new(model)) - .instruction_provider(Box::new(|ctx| { - Box::pin(async move { - // 根据上下文动态生成指令 - let user_preference = ctx.session().state() - .get("user:communication_style") - .and_then(|v| v.as_str()) - .unwrap_or("professional"); - - Ok(format!("You are a helpful assistant. Communicate in a {} style.", user_preference)) - }) - })) - .build()?; -``` - -### 3. 子代理注册 - -使用 `sub_agent` 注册子代理来处理特定任务或代理交接: - -```rust -let specialized = LlmAgentBuilder::new("specialized") - .instruction("Handle specialized tasks.") - .model(model.clone()) - .build()?; - -let coordinator = LlmAgentBuilder::new("coordinator") - .description("Coordinates tasks and delegates when necessary") - .instruction("You are a coordinator. Delegate specialized tasks to the specialized agent.") - .model(model.clone()) - .sub_agent(Arc::new(specialized)) - .build()?; -``` - -## 错误处理和回调 - -### 1. 添加前后回调 - -添加 `before_agent` 和 `after_agent` 回调: - -```rust -let agent = LlmAgentBuilder::new("my_agent") - .model(model) - .instruction("You are a helpful assistant.") - // Add before_agent callback - .before_callback(Box::new(|ctx| { - Box::pin(async move { - println!("Agent starting: {}", ctx.agent_name()); - Ok(None) // 继续执行 - }) - })) - // Add after_agent callback - .after_callback(Box::new(|ctx| { - Box::pin(async move { - println!("Agent completed: {}", ctx.agent_name()); - Ok(None) // 保持原始结果 - }) - })) - .build()?; -``` - -### 2. 错误处理 - -使用回调进行验证和错误处理: - -```rust -let agent = LlmAgentBuilder::new("error_handling_agent") - .model(model) - .before_callback(Box::new(|ctx| { - Box::pin(async move { - // 验证关键条件 - if ctx.user_id().is_empty() { - return Err(AdkError::Agent("User ID is required".to_string())); - } - Ok(None) - }) - })) - .build()?; -``` - -### 3. 工具回调 - -添加 `before_tool` 和 `after_tool` 回调: - -```rust -let agent = LlmAgentBuilder::new("tool_monitoring_agent") - .model(model) - .before_tool_callback(Box::new(|ctx, tool_name| { - Box::pin(async move { - println!("Starting tool: {}", tool_name); - Ok(()) - }) - })) - .after_tool_callback(Box::new(|ctx, tool_name, result| { - Box::pin(async move { - match result { - Ok(_) => println!("Tool {} completed successfully", tool_name), - Err(e) => println!("Tool {} failed: {}", tool_name, e), - } - Ok(()) - }) - })) - .build()?; -``` - -## 流式处理 - -### 1. 使用 SSE 运行 Agent - -使用 Server-Sent Events (SSE) 流式执行 agent: - -```http -POST /api/run_sse - -{ - "appName": "my_agent", - "userId": "user123", - "sessionId": "session456", - "newMessage": { - "role": "user", - "parts": [ - { - "text": "What is the capital of France?" - } - ] - }, - "streaming": true -} -``` - -### 2. SSE 响应示例 - -```json -{ - "id": "evt_123", - "timestamp": 1234567890, - "author": "my_agent", - "content": { - "role": "model", - "parts": [ - { - "text": "The capital of France is Paris." - } - ] - }, - "actions": {}, - "llm_response": { - "content": { - "role": "model", - "parts": [ - { - "text": "The capital of France is Paris." - } - ] - } - } -} -``` - -### 3. 使用 Python 请求客户端 - -```python -import requests -import json - -def run_agent(message): - url = 'http://localhost:8080/api/run_sse' - payload = { - 'appName': 'my_agent', - 'userId': 'user123', - 'sessionId': 'session456', - 'newMessage': { - 'role': 'user', - 'parts': [{'text': message}] - }, - 'streaming': True - } - - response = requests.post(url, json=payload, stream=True) - - for line in response.iter_lines(): - if line: - line_str = line.decode('utf-8') - if line_str.startswith('data: '): - event = json.loads(line_str[6:]) - print('Event:', event) - -run_agent('What is the capital of France?') -``` - -### 4. 使用 JavaScript/TypeScript Fetch API - -```javascript -async function runAgent(message) { - const response = await fetch('http://localhost:8080/api/run_sse', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - appName: 'my_agent', - userId: 'user123', - sessionId: 'session456', - newMessage: { - role: 'user', - parts: [{ text: message }] - }, - streaming: true - }) - }); - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value); - const lines = chunk.split('\n'); - - for (const line of lines) { - if (line.startsWith('data: ')) { - const event = JSON.parse(line.slice(6)); - console.log('Event:', event); - } - } - } -} -``` - -## 数据传递模式对比 - -### 模式 1:工具主动获取(Cowork Forge 使用) - -**方式**:通过工具获取结构化数据 - -```rust -// Agent 通过工具获取数据 -let critic = LlmAgentBuilder::new("critic") - .instruction("Review the requirements and design...") - .tool(Arc::new(GetRequirementsTool)) // 主动获取需求 - .tool(Arc::new(GetDesignTool)) // 主动获取设计 - .tool(Arc::new(CheckFeatureCoverageTool)) // 验证覆盖范围 - .build()?; -``` - -**优点**: -- 数据结构化、类型安全 -- 数据可追溯、可审计 -- 支持跨 session 的数据访问 - -**缺点**: -- 需要额外的工具调用开销 -- 需要定义数据结构 - -### 模式 2:output_key(状态传递) - -**方式**:通过 output_key 保存输出,后续 agent 从状态读取 - -```rust -// Research agent 保存输出 -let researcher = LlmAgentBuilder::new("researcher") - .instruction("Research the given topic...") - .model(model.clone()) - .output_key("research") // 保存到 state["research"] - .build()?; - -// Analyzer agent 从状态读取 -let analyzer = LlmAgentBuilder::new("analyzer") - .instruction("Based on the research in state['research'], analyze...") - .model(model.clone()) - .build()?; -``` - -**优点**: -- 简单直接 -- 自动传递 -- 无需额外工具 - -**缺点**: -- 数据非结构化(纯文本) -- 依赖 LLM 解析 -- 难以追踪数据来源 - -**选择建议**: -- **结构化数据**(需求、设计、计划)→ 使用工具模式 -- **简单文本内容**(摘要、反馈)→ 使用 output_key 模式 - -Cowork Forge 采用工具模式,因为需要: -1. 数据独立于对话历史 -2. 支持跨 session 的数据访问 -3. 结构化的数据验证和管理 - -## HITL(Human-in-the-Loop)工具 - -### 概念 -HITL 工具允许 agent 在执行过程中请求人类反馈或编辑内容,实现"人在回路"的质量保证。 - -### Cowork Forge 的 HITL 工具演进 - -#### 两种 HITL 工具类型 - -**1. Content-based HITL 工具(推荐)** - -基于内容的交互,不暴露文件路径,更安全、更灵活: - -```rust -// ReviewAndEditContentTool - 让用户审查和编辑内容 -let idea_agent = LlmAgentBuilder::new("idea_agent") - .instruction(IDEA_AGENT_INSTRUCTION) - .tool(Arc::new(SaveIdeaTool)) - .tool(Arc::new(ReviewAndEditContentTool)) // Content-based HITL - .build()?; - -// ReviewWithFeedbackContentTool - 支持三种反馈模式 -let prd_actor = LlmAgentBuilder::new("prd_actor") - .instruction(PRD_ACTOR_INSTRUCTION) - .tool(Arc::new(SavePrdDocTool)) - .tool(Arc::new(ReviewWithFeedbackContentTool)) // Content-based HITL - .tool(Arc::new(CreateRequirementTool)) - .build()?; -``` - -**三种反馈模式**: - -1. **Edit 模式** - 输入 "edit" 或粘贴多行内容 - - 用户直接提供编辑后的内容 - - Agent 使用编辑后的内容 - -2. **Pass 模式** - 输入 "pass" 或选择"通过" - - 跳过当前阶段 - - 使用原始内容 - -3. **Feedback 模式** - 输入文本建议 - - 提供具体的修改建议 - - Agent 根据建议修订内容 - -**使用流程示例**: -``` -Agent: 生成需求大纲内容 → 调用 review_with_feedback_content(title="Review PRD", content=) -User: "需求太多,减少到5个核心需求" -Agent: 识别 action="feedback" → 修订内容 → 再次调用 review_with_feedback_content -User: "pass" -Agent: 使用修订后的内容创建正式需求 → 调用 save_prd_doc(content=) -``` - -**优点**: -- 安全:不暴露文件路径 -- 灵活:可以在内存中操作,无需实际文件 -- 解耦:Agent 只关心内容,不关心存储位置 - -**2. File-based HITL 工具(已废弃)** - -基于文件的交互,需要文件路径: - -```rust -// 已废弃 - 不推荐使用 -let idea_agent = LlmAgentBuilder::new("idea_agent") - .instruction(IDEA_AGENT_INSTRUCTION) - .tool(Arc::new(WriteFileTool)) - .tool(Arc::new(ReviewAndEditFileTool)) // File-based(已废弃) - .build()?; -``` - -**问题**: -- 需要通用的 WriteFileTool 权限 -- 暴露文件路径给 Agent -- 违反权限最小化原则 - -#### Cowork Forge 的最佳实践 - -**Idea 阶段**: -```rust -// ✅ 正确:使用 Content-based HITL -let idea_agent = LlmAgentBuilder::new("idea_agent") - .tool(Arc::new(SaveIdeaTool)) - .tool(Arc::new(ReviewAndEditContentTool)) - .build()?; - -// ❌ 错误:使用 File-based HITL -let idea_agent = LlmAgentBuilder::new("idea_agent") - .tool(Arc::new(WriteFileTool)) - .tool(Arc::new(ReviewAndEditFileTool)) - .build()?; -``` - -**PRD/Design/Plan 阶段**: -```rust -// ✅ 正确:使用 Content-based HITL -let prd_actor = LlmAgentBuilder::new("prd_actor") - .tool(Arc::new(SavePrdDocTool)) - .tool(Arc::new(ReviewWithFeedbackContentTool)) - .tool(Arc::new(CreateRequirementTool)) - .build()?; - -// ❌ 错误:使用 File-based HITL + WriteFileTool -let prd_actor = LlmAgentBuilder::new("prd_actor") - .tool(Arc::new(WriteFileTool)) // 不应该有通用写权限 - .tool(Arc::new(ReviewWithFeedbackTool)) - .build()?; -``` - -**Coding 阶段**: -```rust -// Coding 阶段需要 ReadFileTool/WriteFileTool,但不使用 HITL -let coding_actor = LlmAgentBuilder::new("coding_actor") - .tool(Arc::new(ReadFileTool)) // 需要:读取代码文件 - .tool(Arc::new(WriteFileTool)) // 需要:写入代码文件 - .tool(Arc::new(ListFilesTool)) - .tool(Arc::new(RunCommandTool)) - // 不使用 HITL 工具 - .build()?; -``` - -### HITL 的最佳实践 - -1. **时机选择**:在关键决策点使用 HITL - - Idea 阶段:确认项目方向 - - PRD 阶段:审查需求完整性 - - Design 阶段:审查架构合理性 - - Plan 阶段:审查任务可行性 - -2. **频率控制**:避免过度打扰用户 - - 每个阶段最多 1-2 次 HITL 交互 - - Critic 评审后不再 HITL - -3. **上下文提供**:给用户充分的信息 - - 预览内容(前 15 行) - - 清晰的操作提示 - - 已知问题列表 - -4. **安全原则**: - - 非编码阶段:使用 Content-based HITL - - 不暴露文件路径 - - 使用专用的 Save 工具 - -## Event 流处理 - -### Event 流结构 - -`Agent::run()` 返回的是 `Stream>`: - -```rust -let mut stream = agent.run(invocation_ctx).await?; - -while let Some(result) = stream.next().await { - match result { - Ok(event) => { - // 处理不同类型的事件 - } - Err(e) => { - return Err(format!("流错误: {}", e)); - } - } -} -``` - -### Event 类型处理 - -```rust -match &event { - Event::Content(content) => { - // 提取文本内容 - for part in &content.parts { - if let Some(text) = part.text() { - generated_text.push_str(text); - } - } - } - Event::ToolCall(call) => { - println!("工具调用: {}({:?})", call.name, call.args); - } - Event::ToolResult(result) => { - println!("工具结果: {:?}", result); - } - Event::Error(e) => { - println!("错误: {}", e); - } -} -``` - -### 完整示例 - -```rust -async fn execute_agent_with_context( - agent: Arc, - ctx: Arc, -) -> Result { - let mut stream = agent.run(ctx).await?; - let mut generated_text = String::new(); - let mut tool_calls = Vec::new(); - - while let Some(result) = stream.next().await { - match result { - Ok(event) => { - match &event { - Event::Content(content) => { - for part in &content.parts { - if let Some(text) = part.text() { - generated_text.push_str(text); - } - } - } - Event::ToolCall(call) => { - tool_calls.push(call.clone()); - println!("工具调用: {}", call.name); - } - Event::ToolResult(result) => { - println!("工具结果: {:?}", result); - } - _ => {} - } - } - Err(e) => { - return Err(format!("流错误: {}", e)); - } - } - } - - Ok(generated_text) -} -``` - -## IncludeContents 控制上下文包含 - -### 概念 -`IncludeContents` 控制 agent 执行时包含哪些历史对话内容,影响 token 消耗和上下文质量。 - -### 选项说明 - -| 值 | 说明 | 适用场景 | Token 消耗 | -|---|---|---|---| -| `IncludeContents::None` | 不包含历史 | 每次都是全新的上下文 | 最少 | -| `IncludeContents::LastN(n)` | 包含最近 n 条 | 需要短期上下文时 | 中等 | -| `IncludeContents::All` | 包含所有历史 | 需要完整对话历史 | 最多 | - -### Cowork Forge 的选择 - -```rust -let agent = LlmAgentBuilder::new("prd_actor") - .instruction(PRD_ACTOR_INSTRUCTION) - .model(model) - .tool(Arc::new(GetRequirementsTool)) - .include_contents(IncludeContents::None) // 不包含历史 - .build()?; -``` - -**选择 IncludeContents::None 的原因**: -1. **独立任务**:每个阶段都是独立的任务(需求、设计、计划等) -2. **工具获取数据**:通过工具(GetRequirementsTool 等)获取数据,而非依赖对话历史 -3. **节省 token**:不传递历史对话,显著降低 token 消耗 -4. **可预测性**:每次执行都是干净的上下文,结果更可预测 - -### 何时使用 IncludeContents::All - -```rust -// 需要完整对话历史的场景 -let chat_agent = LlmAgentBuilder::new("chat_agent") - .instruction("Continue our conversation...") - .model(model) - .include_contents(IncludeContents::All) // 包含所有历史 - .build()?; -``` - -适用场景: -- 对话式 agent(需要记住之前的对话) -- 需要跨轮次的上下文积累 -- 需要引用之前的回复 - -### 何时使用 IncludeContents::LastN - -```rust -// 需要短期上下文的场景 -let refinement_agent = LlmAgentBuilder::new("refinement_agent") - .instruction("Refine based on recent discussion...") - .model(model) - .include_contents(IncludeContents::LastN(3)) // 最近 3 条 - .build()?; -``` - -适用场景: -- 需要短期上下文但不需要完整历史 -- 节省 token 的同时保持一定的上下文 -- 多轮对话的中间阶段 - -## 结构化数据持久化 - -### 概念 -将数据独立于对话历史存储到文件系统,支持跨 session 访问和数据追踪。 - -### Cowork Forge 的实现 - -#### 1. 数据结构定义 - -```rust -// requirements.json -pub struct Requirements { - pub schema_version: String, - pub created_at: DateTime, - pub updated_at: DateTime, - pub requirements: Vec, -} - -// feature_list.json -pub struct FeatureList { - pub schema_version: String, - pub features: Vec, -} - -// design_spec.json -pub struct DesignSpec { - pub schema_version: String, - pub architecture: Architecture, - pub technology_stack: TechnologyStack, -} - -// implementation_plan.json -pub struct ImplementationPlan { - pub schema_version: String, - pub milestones: Vec, - pub tasks: Vec, -} -``` - -#### 2. 持久化函数 - -```rust -// storage/mod.rs -pub fn load_requirements() -> Result; -pub fn save_requirements(reqs: &Requirements) -> Result<()>; -pub fn load_feature_list() -> Result; -pub fn save_feature_list(features: &FeatureList) -> Result<()>; -pub fn load_design_spec() -> Result; -pub fn save_design_spec(design: &DesignSpec) -> Result<()>; -pub fn load_implementation_plan() -> Result; -pub fn save_implementation_plan(plan: &ImplementationPlan) -> Result<()>; -``` - -#### 3. 工具与持久化的集成 - -```rust -// CreateRequirementTool - 创建并保存需求 -pub struct CreateRequirementTool; - -#[async_trait] -impl Tool for CreateRequirementTool { - async fn execute(&self, _ctx: Arc, args: Value) -> Result { - let mut reqs = load_requirements()?; - - let priority = match get_required_string_param(&args, "priority")?.as_str() { - "high" => Priority::High, - "medium" => Priority::Medium, - "low" => Priority::Low, - _ => Priority::Medium, - }; - - let category = match get_required_string_param(&args, "category")?.as_str() { - "functional" => RequirementCategory::Functional, - "non_functional" => RequirementCategory::NonFunctional, - _ => RequirementCategory::Functional, - }; - - let requirement = Requirement { - id: generate_id("REQ", reqs.requirements.len()), - title: get_required_string_param(&args, "title")?.to_string(), - description: get_required_string_param(&args, "description")?.to_string(), - priority, - category, - acceptance_criteria: get_required_array_param(&args, "acceptance_criteria")? - .iter() - .map(|v| v.as_str().unwrap_or("").to_string()) - .collect(), - related_features: vec![], - }; - - reqs.requirements.push(requirement); - save_requirements(&reqs)?; // 持久化到文件 - - Ok(json!({"status": "success", "requirement_id": requirement.id})) - } -} - -// GetRequirementsTool - 从文件读取需求 -pub struct GetRequirementsTool; - -#[async_trait] -impl Tool for GetRequirementsTool { - async fn execute(&self, _ctx: Arc, _args: Value) -> Result { - let requirements = load_requirements()?; - let features = load_feature_list()?; - - Ok(json!({ - "requirements": requirements.requirements, - "features": features.features - })) - } -} -``` - -### 优点 - -1. **数据独立性**:数据不依赖对话历史,可独立访问 -2. **可追溯性**:每次修改都有时间戳和版本信息 -3. **跨 session 访问**:不同 session 可以访问同一项目的数据 -4. **类型安全**:使用强类型结构,避免解析错误 -5. **版本管理**:支持 schema_version 进行数据迁移 - -### 使用场景 - -```rust -// Stage 1: 创建需求 -create_idea_agent() → 创建 idea.md -create_prd_loop() → 创建 requirements.json, feature_list.json - -// Stage 2: 创建设计 -create_design_loop() → 读取 requirements.json → 创建 design_spec.json - -// Stage 3: 创建计划 -create_plan_loop() → 读取 requirements.json, design_spec.json → 创建 implementation_plan.json - -// Stage 4: 编码 -create_coding_loop() → 读取 implementation_plan.json → 执行任务 → 更新 task status - -// Stage 5: 检查 -create_check_agent() → 读取所有数据 → 验证完整性 - -// Stage 6: 交付 -create_delivery_agent() → 读取所有数据 → 生成报告 -``` - -## 工具权限管理 - -### 核心原则 - -Cowork Forge 遵循**权限最小化**原则:每个 agent 只分配完成任务所需的最小工具集,避免不必要的权限。 - -### 工具权限分类 - -#### 1. 编码阶段(Coding & Check) - -拥有完整的文件操作和命令执行权限: - -```rust -// Coding Actor - 需要读写代码文件 -let coding_actor = LlmAgentBuilder::new("coding_actor") - .tool(Arc::new(ReadFileTool)) // 读取代码文件 - .tool(Arc::new(WriteFileTool)) // 写入代码文件 - .tool(Arc::new(ListFilesTool)) // 列出文件 - .tool(Arc::new(RunCommandTool)) // 运行测试/构建命令 - .tool(Arc::new(CheckTestsTool)) // 检查测试 - .tool(Arc::new(UpdateTaskStatusTool)) // 更新任务状态 - .build()?; - -// Check Agent - 需要读取代码文件和运行命令 -let check_agent = LlmAgentBuilder::new("check_agent") - .tool(Arc::new(ReadFileTool)) // 读取代码文件 - .tool(Arc::new(ListFilesTool)) // 列出文件 - .tool(Arc::new(RunCommandTool)) // 运行测试/构建 - .tool(Arc::new(CheckTestsTool)) // 检查测试 - .tool(Arc::new(CheckLintTool)) // 检查代码质量 - .tool(Arc::new(GotoStageTool)) // 回退到之前阶段 - .build()?; -``` - -#### 2. 设计阶段(Idea、PRD、Design、Plan) - -使用专用的 Artifact 工具,不提供通用文件操作权限: - -```rust -// Idea Agent - 只能保存 idea.md -let idea_agent = LlmAgentBuilder::new("idea_agent") - .tool(Arc::new(SaveIdeaTool)) // 保存 idea.md - .tool(Arc::new(ReviewAndEditContentTool)) // 用户审查 - .build()?; - -// PRD Actor - 只能保存 prd.md 和操作结构化数据 -let prd_actor = LlmAgentBuilder::new("prd_actor") - .tool(Arc::new(SavePrdDocTool)) // 保存 prd.md - .tool(Arc::new(ReviewWithFeedbackContentTool)) // 用户审查 - .tool(Arc::new(CreateRequirementTool)) // 创建需求 - .tool(Arc::new(AddFeatureTool)) // 添加功能 - .tool(Arc::new(GetRequirementsTool)) // 读取需求数据 - .build()?; - -// Design Actor - 只能保存 design.md -let design_actor = LlmAgentBuilder::new("design_actor") - .tool(Arc::new(SaveDesignDocTool)) // 保存 design.md - .tool(Arc::new(ReviewWithFeedbackContentTool)) // 用户审查 - .tool(Arc::new(CreateDesignComponentTool)) // 创建组件 - .tool(Arc::new(GetRequirementsTool)) // 读取需求 - .tool(Arc::new(GetDesignTool)) // 读取设计 - .build()?; - -// Plan Actor - 只能保存 plan.md -let plan_actor = LlmAgentBuilder::new("plan_actor") - .tool(Arc::new(SavePlanDocTool)) // 保存 plan.md - .tool(Arc::new(ReviewWithFeedbackContentTool)) // 用户审查 - .tool(Arc::new(CreateTaskTool)) // 创建任务 - .tool(Arc::new(GetRequirementsTool)) // 读取需求 - .tool(Arc::new(GetDesignTool)) // 读取设计 - .tool(Arc::new(GetPlanTool)) // 读取计划 - .build()?; -``` - -#### 3. 交付阶段(Delivery) - -使用 Load 工具读取 artifacts,不提供通用读取权限: - -```rust -// Delivery Agent - 只能加载 artifacts 和保存报告 -let delivery_agent = LlmAgentBuilder::new("delivery_agent") - .tool(Arc::new(LoadIdeaTool)) // 加载 idea.md - .tool(Arc::new(LoadPrdDocTool)) // 加载 prd.md - .tool(Arc::new(LoadDesignDocTool)) // 加载 design.md - .tool(Arc::new(SaveDeliveryReportTool)) // 保存报告 - .tool(Arc::new(ListFilesTool)) // 验证项目文件 - .build()?; -``` - -### 专用工具实现 - -#### Save 工具(保存 Artifacts) - -```rust -pub struct SavePrdDocTool; - -#[async_trait] -impl Tool for SavePrdDocTool { - fn name(&self) -> &str { - "save_prd_doc" - } - - fn description(&self) -> &str { - "Save the PRD (Product Requirements Document) markdown file." - } - - async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { - let content = get_required_string_param(&args, "content")?; - - // 使用 artifact_path 确保路径限制在 artifacts 目录 - let path = artifact_path("prd.md")?; - fs::write(&path, content)?; - - Ok(json!({ - "status": "success", - "message": "PRD document saved successfully", - "file_path": "artifacts/prd.md" // 返回保存的路径 - })) - } -} -``` - -#### Load 工具(加载 Artifacts) - -```rust -pub struct LoadPrdDocTool; - -#[async_trait] -impl Tool for LoadPrdDocTool { - fn name(&self) -> &str { - "load_prd_doc" - } - - fn description(&self) -> &str { - "Load the PRD (Product Requirements Document) markdown from the artifacts directory." - } - - async fn execute(&self, _ctx: Arc, _args: Value) -> adk_core::Result { - // 使用 artifact_path 确保路径限制在 artifacts 目录 - let path = artifact_path("prd.md")?; - let content = fs::read_to_string(&path)?; - - Ok(json!({ - "status": "success", - "content": content, - "file_path": "artifacts/prd.md" - })) - } -} -``` - -### 权限矩阵 - -| 阶段 | ReadFile | WriteFile | LoadIdea | LoadPrd | LoadDesign | LoadPlan | SaveXxx | ListFiles | RunCommand | HITL | -|------|----------|-----------|----------|---------|------------|---------|---------|-----------|------------|------| -| Idea | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ save_idea | ❌ | ❌ | ✅ review_and_edit_content | -| PRD Actor | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ save_prd_doc | ❌ | ❌ | ✅ review_with_feedback_content | -| PRD Critic | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | -| Design Actor | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ save_design_doc | ❌ | ❌ | ✅ review_with_feedback_content | -| Design Critic | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| Plan Actor | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ save_plan_doc | ❌ | ❌ | ✅ review_with_feedback_content | -| Plan Critic | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | -| Coding Actor | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | -| Coding Critic | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | -| Check Agent | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | -| Delivery Agent | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ save_delivery_report | ✅ | ❌ | ❌ | - -### 安全特性 - -#### 1. 路径限制 - -```rust -// storage/mod.rs -pub fn artifact_path(filename: &str) -> Result { - let iteration_dir = get_iteration_dir()?; - let artifacts_dir = iteration_dir.join("artifacts"); - - // 只允许在 artifacts 目录下操作 - let path = artifacts_dir.join(filename); - - // 验证路径没有遍历攻击 - let canonical = path.canonicalize()?; - let artifacts_canonical = artifacts_dir.canonicalize()?; - - if !canonical.starts_with(&artifacts_canonical) { - return Err(anyhow!("Path traversal detected")); - } - - Ok(path) -} -``` - -#### 2. 命令白名单 - -```rust -// file_tools.rs -pub async fn execute(&self, _ctx: Arc, args: Value) -> Result { - let command = get_required_string_param(&args, "command")?; - - // 拒绝危险命令 - let dangerous_patterns = ["rm -rf", "sudo", "format", "del /f", "format c:"]; - for pattern in dangerous_patterns { - if command.to_lowercase().contains(pattern) { - return Err(AdkError::Tool(format!("Dangerous command blocked: {}", command))); - } - } - - // 执行命令 - let output = tokio::process::Command::new("sh") - .arg("-c") - .arg(command) - .timeout(Duration::from_secs(30)) // 超时控制 - .output() - .await?; - - Ok(json!({ - "status": "success", - "output": String::from_utf8_lossy(&output.stdout) - })) -} -``` - -#### 3. Content-based HITL - -使用基于内容的 HITL 工具,避免文件路径暴露: - -```rust -// hitl_content_tools.rs -pub struct ReviewWithFeedbackContentTool; - -#[async_trait] -impl Tool for ReviewWithFeedbackContentTool { - fn name(&self) -> &str { - "review_with_feedback_content" - } - - fn description(&self) -> &str { - "Review content and allow user to: edit, pass, or provide feedback." - } - - async fn execute(&self, _ctx: Arc, args: Value) -> Result { - let title = get_required_string_param(&args, "title")?; - let content = get_required_string_param(&args, "content")?; - - // 显示内容给用户 - interaction.show_message(MessageLevel::Info, format!("\n📝 {}\n{}", title, content)).await; - - // 获取用户输入 - let response = interaction.request_input( - "Type 'edit' to open editor, 'pass' to continue, or provide feedback:", - options, - Some(content.to_string()) - ).await?; - - match response { - InputResponse::Text(text) => { - // 判断是编辑内容还是反馈 - if text.contains('\n') || text.len() > 100 { - Ok(json!({ - "action": "edit", - "content": text - })) - } else { - Ok(json!({ - "action": "feedback", - "feedback": text, - "content": content - })) - } - } - _ => Ok(json!({"action": "pass", "content": content})) - } - } -} -``` - -## 最佳实践 - -1. **工具设计**: - - 提供清晰的名称和描述,帮助 LLM 理解何时使用工具 - - 定义明确的 JSON 模式和必需参数 - - 在工具实现中添加遥测和日志记录 - - **添加安全约束**:验证路径、拒绝绝对路径、防止路径遍历 - - **添加执行限制**:检测并拒绝阻塞命令 - -2. **Agent 构建**: - - 使用 `max_iterations` 设置合理的迭代限制,防止无限循环 - - 提供清晰具体的指令,帮助 agent 理解何时调用 `exit_loop` - - **避免在 SequentialAgent 中使用 exit_loop**:使用 max_iterations 替代 - - **使用 IncludeContents::None** 节省 token(除非需要对话历史) - - 使用 `output_key` 保存关键的中间结果到会话状态 - -3. **工作流组合**: - - 使用 `SequentialAgent` 构建线性处理流程 - - **考虑使用 ParallelAgent**:适用于并行分析和测试 - - 使用 `LoopAgent` 实现迭代改进或质量控制循环 - - **Actor-Critic Loop**:使用 max_iterations=1 避免过度优化 - -4. **状态管理**: - - 使用适当的状态前缀(`user:`、`app:`、`temp:`)管理不同范围的持久性 - - **结构化数据持久化**:使用工具+文件系统,而非依赖对话历史 - - 在状态间传递关键信息和中间结果 - - 定期清理不再需要的临时数据 - -5. **错误处理**: - - 在关键路径上使用前后回调进行验证 - - 实现适当的错误恢复机制 - - 添加足够的监控和日志记录 - -6. **HITL 设计**: - - 在关键决策点使用 HITL 工具 - - 控制交互频率,避免过度打扰用户 - - 提供充分的上下文和清晰的提示 - - 支持多种反馈模式(edit、pass、text) - -7. **数据传递**: - - **结构化数据** → 使用工具模式(Tool trait) - - **简单文本** → 使用 output_key 模式 - - **跨 session 数据** → 使用文件持久化 - -ADK-Rust 提供了一个强大且灵活的框架,可用于构建各种复杂的 AI 代理系统,从简单的任务助手到多阶段的工作流程引擎。Cowork Forge 的实践展示了如何将这些高级特性组合成一个完整的、生产就绪的开发系统。 \ No newline at end of file diff --git a/crates/cowork-core/src/acp/client.rs b/crates/cowork-core/src/acp/client.rs index ee75463..fb9a871 100644 --- a/crates/cowork-core/src/acp/client.rs +++ b/crates/cowork-core/src/acp/client.rs @@ -6,7 +6,7 @@ //! The agent-client-protocol SDK uses ?Send futures, which we handle by running //! in a dedicated thread with its own LocalSet and communicating via channels. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::Arc; @@ -17,6 +17,12 @@ use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; use crate::llm::config::CodingAgentConfig; +/// Timeout for a single ACP prompt turn. +const PROMPT_TIMEOUT_SECONDS: u64 = 3000; + +/// Max stderr line length to forward as status. +const MAX_STATUS_LINE_LEN: usize = 500; + /// Message types from the agent #[derive(Debug, Clone)] pub enum AgentMessage { @@ -36,29 +42,137 @@ pub enum AgentMessage { struct CoworkClient { output: Arc>, message_tx: mpsc::UnboundedSender, + workspace: PathBuf, +} + +impl CoworkClient { + /// Validate that an absolute path is within the workspace directory. + /// Returns the path relative to the workspace if valid, or an error. + fn validate_workspace_path(&self, abs_path: &Path) -> Result { + let workspace_abs = self + .workspace + .canonicalize() + .map_err(acp::Error::into_internal_error)?; + let target_abs = abs_path + .canonicalize() + .or_else(|_| { + // File may not exist yet (write); canonicalize the parent instead. + let parent = abs_path.parent().unwrap_or(abs_path); + let file_name = abs_path.file_name().ok_or_else(|| { + acp::Error::invalid_params().data("path has no file name") + })?; + let parent_abs = parent.canonicalize().map_err(acp::Error::into_internal_error)?; + Ok::<_, acp::Error>(parent_abs.join(file_name)) + }) + .map_err(acp::Error::into_internal_error)?; + + let workspace_stripped = strip_unc_prefix(&workspace_abs); + let target_stripped = strip_unc_prefix(&target_abs); + + if target_stripped.starts_with(&workspace_stripped) { + Ok(target_stripped) + } else { + Err(acp::Error::invalid_params().data(format!( + "path '{}' is outside workspace '{}'", + target_stripped.display(), + workspace_stripped.display() + ))) + } + } +} + +/// Strip the Windows UNC prefix (`\\?\`) so paths can be compared consistently. +fn strip_unc_prefix(path: &Path) -> PathBuf { + let s = path.display().to_string(); + if let Some(stripped) = s.strip_prefix(r"\\?\") { + PathBuf::from(stripped) + } else { + path.to_path_buf() + } } #[async_trait::async_trait(?Send)] impl acp::Client for CoworkClient { async fn request_permission( &self, - _args: acp::RequestPermissionRequest, + args: acp::RequestPermissionRequest, ) -> acp::Result { - Err(acp::Error::method_not_found()) + tracing::info!( + session_id = %args.session_id, + "ACP permission request for tool call: {:?}", + args.tool_call + ); + + // Auto-approve the first "allow" option so the agent can keep working. + // In the future this can be wired to the InteractiveBackend for user consent. + let allow_option = args + .options + .into_iter() + .find(|o| matches!(o.kind, acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways)); + + match allow_option { + Some(option) => { + tracing::info!(option_id = %option.option_id.0, "Auto-approving ACP permission request"); + Ok(acp::RequestPermissionResponse::new( + acp::RequestPermissionOutcome::Selected( + acp::SelectedPermissionOutcome::new(option.option_id), + ), + )) + } + None => { + tracing::warn!("ACP permission request had no allow option; cancelling"); + Ok(acp::RequestPermissionResponse::new( + acp::RequestPermissionOutcome::Cancelled, + )) + } + } } async fn write_text_file( &self, - _args: acp::WriteTextFileRequest, + args: acp::WriteTextFileRequest, ) -> acp::Result { - Err(acp::Error::method_not_found()) + tracing::debug!(session_id = %args.session_id, path = %args.path.display(), "ACP write_text_file"); + + let rel_path = self.validate_workspace_path(&args.path)?; + let full_path = self.workspace.join(&rel_path); + + if let Some(parent) = full_path.parent() { + std::fs::create_dir_all(parent).map_err(acp::Error::into_internal_error)?; + } + std::fs::write(&full_path, args.content).map_err(|e| { + tracing::error!(path = %full_path.display(), error = %e, "ACP write_text_file failed"); + acp::Error::into_internal_error(e) + })?; + + tracing::info!(path = %full_path.display(), bytes = ?std::fs::metadata(&full_path).map(|m| m.len()).unwrap_or(0), "ACP write_text_file succeeded"); + let _ = self.message_tx.send(AgentMessage::Status(format!( + "Wrote file {}", + rel_path.display() + ))); + Ok(acp::WriteTextFileResponse::new()) } async fn read_text_file( &self, - _args: acp::ReadTextFileRequest, + args: acp::ReadTextFileRequest, ) -> acp::Result { - Err(acp::Error::method_not_found()) + tracing::debug!(session_id = %args.session_id, path = %args.path.display(), "ACP read_text_file"); + + let rel_path = self.validate_workspace_path(&args.path)?; + let full_path = self.workspace.join(&rel_path); + + let content = std::fs::read_to_string(&full_path).map_err(|e| { + tracing::error!(path = %full_path.display(), error = %e, "ACP read_text_file failed"); + if e.kind() == std::io::ErrorKind::NotFound { + acp::Error::resource_not_found(Some(full_path.display().to_string())) + } else { + acp::Error::into_internal_error(e) + } + })?; + + tracing::info!(path = %full_path.display(), len = content.len(), "ACP read_text_file succeeded"); + Ok(acp::ReadTextFileResponse::new(content)) } async fn create_terminal( @@ -101,25 +215,24 @@ impl acp::Client for CoworkClient { args: acp::SessionNotification, ) -> acp::Result<(), acp::Error> { match args.update { - acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk { content, .. }) => { - if let acp::ContentBlock::Text(text_content) = content { - let text = text_content.text.clone(); - eprintln!("AGENT: {}", text); - // Send to GUI - let _ = self.message_tx.send(AgentMessage::Output(text)); - // Also store in output - if let Ok(mut out) = self.output.lock() { - out.push_str(&text_content.text); - } + acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk { + content: acp::ContentBlock::Text(text_content), + .. + }) => { + let text = text_content.text.clone(); + tracing::debug!(len = text.len(), "ACP agent message chunk"); + let _ = self.message_tx.send(AgentMessage::Output(text)); + if let Ok(mut out) = self.output.lock() { + out.push_str(&text_content.text); } } - acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk { content, .. }) => { - if let acp::ContentBlock::Text(text_content) = content { - let text = text_content.text.clone(); - eprintln!("AGENT THINKING: {}", text); - // Send thinking to GUI - let _ = self.message_tx.send(AgentMessage::Thinking(text)); - } + acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk { + content: acp::ContentBlock::Text(text_content), + .. + }) => { + let text = text_content.text.clone(); + tracing::debug!(len = text.len(), "ACP agent thought chunk"); + let _ = self.message_tx.send(AgentMessage::Thinking(text)); } // Ignore other updates (different protocol versions may have different variants) _ => {} @@ -176,7 +289,12 @@ pub fn execute_with_external_agent( workspace: PathBuf, task: String, ) -> (mpsc::UnboundedReceiver, impl std::future::Future>>) { - eprintln!("DEBUG: Starting external agent with {} {:?}", config.command, config.args); + tracing::info!( + command = %config.command, + args = ?config.args, + workspace = %workspace.display(), + "Starting external ACP agent" + ); // Create channel for real-time messages let (message_tx, message_rx) = mpsc::unbounded_channel(); @@ -197,6 +315,9 @@ pub fn execute_with_external_agent( }) } +/// Maximum stderr lines to retain for error diagnostics. +const MAX_STDERR_LINES: usize = 50; + /// Run ACP operations in a dedicated thread with its own runtime fn run_acp_in_thread( config: CodingAgentConfig, @@ -213,19 +334,26 @@ fn run_acp_in_thread( rt.block_on(async { use tokio::process::Command; - // Send status update let _ = message_tx.send(AgentMessage::Status("Starting agent process...".to_string())); // Spawn the agent process let mut cmd = Command::new(&config.command); cmd.args(&config.args) - .env("PATH", std::env::var("PATH").unwrap_or_default()) .current_dir(&workspace) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .kill_on_drop(true); + // Preserve PATH and inject any user-configured environment variables. + cmd.env("PATH", std::env::var("PATH").unwrap_or_default()); + if let Some(ref env_vars) = config.env { + for (key, value) in env_vars { + tracing::debug!(key, value, "Injecting env var into ACP agent"); + cmd.env(key, value); + } + } + // On Windows, use CREATE_NO_WINDOW to prevent console window from appearing #[cfg(target_os = "windows")] { @@ -239,86 +367,242 @@ fn run_acp_in_thread( .spawn() .with_context(|| format!("Failed to spawn agent: {}", config.command))?; - eprintln!("DEBUG: Agent process spawned, pid: {:?}", child.id()); - - let outgoing = child.stdin.take().unwrap().compat_write(); - let incoming = child.stdout.take().unwrap().compat(); - - // Handle stderr for debugging - if let Some(stderr) = child.stderr.take() { + tracing::info!(pid = ?child.id(), "ACP agent process spawned"); + let _ = message_tx.send(AgentMessage::Status(format!( + "ACP command: {} {}", + config.command, + config.args.join(" ") + ))); + let _ = message_tx.send(AgentMessage::Status(format!( + "Agent process started (pid: {:?})", + child.id() + ))); + + let stdin = child.stdin.take().context("Failed to open agent stdin")?; + let stdout = child.stdout.take().context("Failed to open agent stdout")?; + let outgoing = stdin.compat_write(); + let incoming = stdout.compat(); + + // Capture stderr for diagnostics. + let stderr_buffer: Arc>> = + Arc::new(std::sync::Mutex::new(Vec::new())); + let stderr_buffer_clone = stderr_buffer.clone(); + + let stderr_handle: Option> = if let Some(stderr) = child.stderr.take() { let tx = message_tx.clone(); - tokio::spawn(async move { + let buf = stderr_buffer_clone.clone(); + Some(tokio::spawn(async move { use tokio::io::{AsyncBufReadExt, BufReader}; let mut stderr = BufReader::new(stderr).lines(); while let Ok(Some(line)) = stderr.next_line().await { - eprintln!("AGENT STDERR: {}", line); - // Send stderr as status (for debugging info) - let _ = tx.send(AgentMessage::Status(format!("[stderr] {}", line))); + tracing::debug!(stderr = %line, "ACP agent stderr"); + if let Ok(mut b) = buf.lock() { + if b.len() >= MAX_STDERR_LINES { + b.remove(0); + } + b.push(line.clone()); + } + let status = if line.len() > MAX_STATUS_LINE_LEN { + format!("[stderr] {}...", &line[..MAX_STATUS_LINE_LEN]) + } else { + format!("[stderr] {}", line) + }; + let _ = tx.send(AgentMessage::Status(status)); } - }); - } + })) + } else { + None + }; + + // Helper to build an error context that includes recent stderr. + let with_stderr = |msg: &str| { + let recent = stderr_buffer + .lock() + .map(|b| b.join("\n")) + .unwrap_or_default(); + if recent.is_empty() { + msg.to_string() + } else { + format!("{}\nRecent agent stderr:\n{}", msg, recent) + } + }; // Use LocalSet for non-Send futures let local_set = tokio::task::LocalSet::new(); let output = Arc::new(std::sync::Mutex::new(String::new())); let output_clone = output.clone(); + let workspace_clone = workspace.clone(); + let tx_err = message_tx.clone(); + let status_tx = message_tx.clone(); + // Clone for the completion notification sent after the local_set returns. + // The original message_tx is moved into CoworkClient inside the local_set block. + let completion_tx = message_tx.clone(); + + let result: Result = local_set + .run_until(async move { + let (conn, handle_io) = acp::ClientSideConnection::new( + CoworkClient { + output: output_clone, + message_tx, + workspace: workspace_clone, + }, + outgoing, + incoming, + |fut| { + tokio::task::spawn_local(fut); + }, + ); + + // Handle I/O in the background + tokio::task::spawn_local(handle_io); + + tracing::info!("Initializing ACP connection"); + let _ = status_tx.send(AgentMessage::Status( + "Initializing ACP connection...".to_string(), + )); + + let init_response = conn + .initialize( + acp::InitializeRequest::new(acp::ProtocolVersion::V1) + .client_info( + acp::Implementation::new( + "cowork-forge".to_string(), + env!("CARGO_PKG_VERSION").to_string(), + ) + .title("Cowork Forge".to_string()), + ) + .client_capabilities( + acp::ClientCapabilities::new().fs( + acp::FileSystemCapability::new() + .read_text_file(true) + .write_text_file(true), + ), + ), + ) + .await + .context("Failed to initialize ACP connection")?; + + let agent_name = init_response + .agent_info + .as_ref() + .map(|i| i.name.clone()) + .unwrap_or_else(|| "unknown".to_string()); + let agent_version = init_response + .agent_info + .as_ref() + .map(|i| i.version.clone()) + .unwrap_or_default(); + tracing::info!( + protocol_version = ?init_response.protocol_version, + agent = ?init_response.agent_info, + auth_methods = ?init_response.auth_methods, + "ACP initialized" + ); + let _ = status_tx.send(AgentMessage::Status(format!( + "ACP initialized with {} {}", + agent_name, agent_version + ))); + + // Handle authentication if the agent requires it. + if let Some(first_method) = init_response.auth_methods.first() { + tracing::info!(method_id = %first_method.id.0, "ACP agent requires authentication"); + let _ = status_tx.send(AgentMessage::Status( + "ACP authenticating...".to_string(), + )); + conn.authenticate(acp::AuthenticateRequest::new(first_method.id.clone())) + .await + .context("Failed to authenticate with ACP agent")?; + tracing::info!("ACP authentication completed"); + let _ = status_tx.send(AgentMessage::Status( + "ACP authentication completed".to_string(), + )); + } - local_set.run_until(async move { - let (conn, handle_io) = acp::ClientSideConnection::new( - CoworkClient { - output: output_clone, - message_tx, - }, - outgoing, - incoming, - |fut| { - tokio::task::spawn_local(fut); - }, - ); - - // Handle I/O in the background - tokio::task::spawn_local(handle_io); - - eprintln!("DEBUG: Initializing ACP connection..."); - - // Initialize - conn.initialize( - acp::InitializeRequest::new(acp::ProtocolVersion::V1) - .client_info(acp::Implementation::new( - "cowork-forge".to_string(), - "2.5.1".to_string(), - ).title("Cowork Forge".to_string())) - ) - .await - .context("Failed to initialize ACP connection")?; - - eprintln!("DEBUG: ACP initialized, creating session..."); - - // Create session - let session_response = conn - .new_session(acp::NewSessionRequest::new(workspace)) - .await - .context("Failed to create ACP session")?; - - eprintln!("DEBUG: Session created: {:?}", session_response.session_id); - - // Send prompt - eprintln!("DEBUG: Sending prompt to agent..."); - - let result = conn - .prompt(acp::PromptRequest::new( - session_response.session_id, - vec![task.into()], - )) + tracing::info!("Creating ACP session"); + let _ = status_tx.send(AgentMessage::Status(format!( + "Creating ACP session in {}...", + workspace.display() + ))); + let session_response = conn + .new_session(acp::NewSessionRequest::new(workspace)) + .await + .context("Failed to create ACP session")?; + + tracing::info!(session_id = %session_response.session_id, "ACP session created"); + let _ = status_tx.send(AgentMessage::Status(format!( + "ACP session created: {}", + session_response.session_id + ))); + + tracing::info!("Sending prompt to ACP agent"); + let _ = status_tx.send(AgentMessage::Status("Sending prompt...".to_string())); + let prompt = + acp::PromptRequest::new(session_response.session_id, vec![task.into()]); + let prompt_result = tokio::time::timeout( + tokio::time::Duration::from_secs(PROMPT_TIMEOUT_SECONDS), + conn.prompt(prompt), + ) .await + .context(format!( + "ACP prompt timed out after {} seconds", + PROMPT_TIMEOUT_SECONDS + ))? .context("Failed to send prompt to agent")?; - eprintln!("DEBUG: Prompt completed, stop reason: {:?}", result.stop_reason); + tracing::info!( + stop_reason = ?prompt_result.stop_reason, + "ACP prompt completed" + ); + let _ = status_tx.send(AgentMessage::Status(format!( + "Prompt completed: {:?}", + prompt_result.stop_reason + ))); + + // Get accumulated output + let output = output + .lock() + .map_err(|e| anyhow::anyhow!("Failed to lock output buffer: {}", e))? + .clone(); + Ok::<_, anyhow::Error>(output) + }) + .await; + + // Cleanup: kill the child process and wait for stderr task to complete. + // This ensures all clones of message_tx are dropped before we return, + // which closes the message channel and unblocks the message loop in the + // pipeline stage. Without this, the stderr task (spawned via tokio::spawn) + // keeps message_tx alive and the channel never closes. + tracing::info!("Cleaning up ACP agent process"); + let _ = child.start_kill(); + // Wait for the child to exit (with a timeout to avoid hanging) + let _ = tokio::time::timeout( + tokio::time::Duration::from_secs(5), + child.wait(), + ).await; + // Wait for the stderr task to complete (it should exit once the child's + // stderr pipe closes, which happens when the child is killed) + if let Some(handle) = stderr_handle { + let _ = tokio::time::timeout( + tokio::time::Duration::from_secs(5), + handle, + ).await; + } - // Get accumulated output - let output = output.lock().unwrap().clone(); - Ok::<_, anyhow::Error>(output) - }).await + if let Err(ref e) = result { + let diagnostic = with_stderr(&e.to_string()); + tracing::error!(error = %diagnostic, "ACP agent failed"); + let _ = tx_err.send(AgentMessage::Error(format!( + "ACP agent failed: {}", + diagnostic + ))); + // Signal the message loop to exit even on error + let _ = completion_tx.send(AgentMessage::Completed); + return Err(anyhow::anyhow!(diagnostic)); + } + // Notify the message loop that the task is complete so it can exit. + // This is the primary signal that unblocks the pipeline stage. + let _ = completion_tx.send(AgentMessage::Completed); + result }) } @@ -330,10 +614,10 @@ pub struct AcpClient { impl AcpClient { /// Create from CodingAgentConfig - pub async fn from_config(config: &CodingAgentConfig, workspace: &PathBuf) -> Result { + pub async fn from_config(config: &CodingAgentConfig, workspace: &Path) -> Result { Ok(Self { config: config.clone(), - workspace: workspace.clone(), + workspace: workspace.to_path_buf(), }) } diff --git a/crates/cowork-core/src/agents/external_coding_agent.rs b/crates/cowork-core/src/agents/external_coding_agent.rs index d6c5e15..a72d7a6 100644 --- a/crates/cowork-core/src/agents/external_coding_agent.rs +++ b/crates/cowork-core/src/agents/external_coding_agent.rs @@ -12,10 +12,10 @@ use crate::instructions::coding::CODING_ACTOR_INSTRUCTION; use crate::llm::config::{load_config, CodingAgentConfig}; /// External Coding Agent Adapter -/// -/// This adapter allows Cowork to use external coding CLI tools (like iFlow, Gemini CLI, Codex) +/// +/// This adapter allows Cowork to use external coding CLI tools (like Codex, Claude Code, Gemini) /// as the underlying coding agent instead of the built-in adk-rust agent. -/// +/// /// The adapter communicates with the external agent via ACP (Agent Client Protocol), /// either through stdio or WebSocket. pub struct ExternalCodingAgent { @@ -45,16 +45,15 @@ impl ExternalCodingAgent { /// Create a new External Coding Agent with iteration context pub async fn new_with_iteration(workspace: &PathBuf, iteration: Option) -> Result { - eprintln!("DEBUG: ExternalCodingAgent::new_with_iteration called with workspace: {}", workspace.display()); + tracing::debug!(workspace = %workspace.display(), "creating ExternalCodingAgent"); if let Some(ref iter) = iteration { - eprintln!("DEBUG: Iteration context: id={}, base_id={:?}, inheritance={:?}", - iter.id, iter.base_iteration_id, iter.inheritance); + tracing::debug!(iteration_id = %iter.id, base_id = ?iter.base_iteration_id, inheritance = ?iter.inheritance, "iteration context"); } - + let config = load_config() .context("Failed to load config")?; - - eprintln!("DEBUG: Config loaded, coding_agent.enabled: {}", config.coding_agent.enabled); + + tracing::debug!(enabled = config.coding_agent.enabled, "external coding agent config loaded"); if !config.coding_agent.enabled { anyhow::bail!("External coding agent is not enabled in config"); @@ -76,7 +75,7 @@ impl ExternalCodingAgent { } /// Execute a coding task with streaming messages - /// + /// /// Returns a StreamingTask with a message receiver for real-time updates /// and a result future for the final output. pub fn execute_task_stream( @@ -85,7 +84,7 @@ impl ExternalCodingAgent { project_context: &str, ) -> StreamingTask { let prompt = self.build_prompt(task_description, project_context); - + // Use the execute_with_external_agent directly to avoid async issues let (messages, result) = crate::acp::execute_with_external_agent( self.config, @@ -100,7 +99,7 @@ impl ExternalCodingAgent { } /// Execute a coding task (simpler API) - /// + /// /// This method sends the task to the external agent and returns the result. /// It builds a comprehensive prompt including: /// - The base instruction @@ -118,7 +117,7 @@ impl ExternalCodingAgent { // Create client and execute let mut client = AcpClient::from_config(&self.config, &self.workspace).await?; - + // Execute the task match client.execute_task(&prompt).await { Ok(result) => { @@ -140,7 +139,7 @@ impl ExternalCodingAgent { let is_evolution = self.iteration.as_ref() .map(|i| i.base_iteration_id.is_some()) .unwrap_or(false); - + let inheritance_mode = self.iteration.as_ref() .map(|i| i.inheritance) .unwrap_or(InheritanceMode::None); @@ -155,7 +154,7 @@ impl ExternalCodingAgent { prompt.push_str("This iteration builds upon an EXISTING project.\n"); prompt.push_str("The workspace directory already contains code from a previous iteration.\n"); prompt.push_str("\n"); - + match inheritance_mode { InheritanceMode::Partial => { prompt.push_str("📋 INHERITANCE MODE: PARTIAL\n"); @@ -170,7 +169,7 @@ impl ExternalCodingAgent { } InheritanceMode::None => {} } - + prompt.push_str("\n"); prompt.push_str("🎯 YOUR TASK:\n"); prompt.push_str("1. FIRST, list the existing files in the workspace to understand the current structure\n"); diff --git a/crates/cowork-core/src/agents/mod.rs b/crates/cowork-core/src/agents/mod.rs index 6bc7105..567ecf1 100644 --- a/crates/cowork-core/src/agents/mod.rs +++ b/crates/cowork-core/src/agents/mod.rs @@ -1,18 +1,31 @@ // Agents module - Agent builders using adk-rust // -// IMPORTANT: This file solves a CRITICAL bug where SequentialAgent stops after -// the first LoopAgent completes. +// Actor-Critic Loop Design (config-driven, see `config_definition/agent_factory.rs`): +// - LoopAgent runs Actor then Critic for up to `max_iterations` iterations. +// - Each iteration: Actor generates/updates content → Critic reviews. +// - Actor sees prior turns (including Critic feedback) via `IncludeContents::Default`, +// which the LoopAgent's HistoryTrackingSession preserves across iterations. +// - Critic has two exit signals (both set `EventActions.escalate = true`): +// * `exit_loop` — satisfied with the work; loop exits, stage succeeds. +// * `provide_feedback` — records structured feedback and exits the loop so +// the executor can retry the stage with the feedback (StageResult::NeedsRevision). +// - For minor issues the Critic can also just describe them in its response +// WITHOUT calling any tool; the Actor will pick them up from conversation +// history in the next loop iteration. +// - If the loop exhausts `max_iterations` without an early exit, the +// stage_executor falls back to checking pending feedback to decide +// Success vs NeedsRevision. // -// PROBLEM: When a sub-agent in LoopAgent calls exit_loop(), it terminates the -// ENTIRE SequentialAgent, not just the LoopAgent. This is adk-rust's design. +// Anti-loop protection: MAX_STAGE_RETRIES=3 (executor level) + Critic anti-loop rules. // -// SOLUTION: Remove exit_loop tools and use max_iterations=1 to let LoopAgent -// complete naturally, allowing SequentialAgent to continue to next agent. +// NOTE: Stage-specific agent construction is config-driven. See +// `config_definition/agent_factory.rs::create_agent_for_stage` and the JSON +// definitions under `config_definition/default_configs/agents/built-in/`. use crate::instructions::*; use crate::tools::*; use crate::IterationStore; -use adk_agent::{LlmAgentBuilder, LoopAgent}; +use adk_agent::LlmAgentBuilder; use adk_core::{Llm, IncludeContents}; use anyhow::Result; use std::sync::Arc; @@ -25,469 +38,6 @@ pub use external_coding_agent::{ExternalCodingAgent, StreamingTask}; pub mod legacy_project_analyzer; pub use legacy_project_analyzer::{create_legacy_project_analyzer, create_legacy_project_analyzer_with_id, create_legacy_project_analyzer_with_context}; -// ============================================================================ -// IdeaAgent - Simple agent to capture initial idea -// ============================================================================ - -pub fn create_idea_agent(model: Arc) -> Result> { - let agent = LlmAgentBuilder::new("idea_agent") - .instruction(IDEA_AGENT_INSTRUCTION) - .model(model) - .tool(Arc::new(SaveIdeaTool)) - .tool(Arc::new(ReviewAndEditContentTool)) - .include_contents(IncludeContents::None) - .build()?; - - Ok(Arc::new(agent)) -} - -pub fn create_idea_agent_with_id(model: Arc, iteration_id: String) -> Result> { - // Replace {ITERATION_ID} placeholder in instruction - let instruction = IDEA_AGENT_INSTRUCTION.replace("{ITERATION_ID}", &iteration_id); - - let save_idea_tool = Arc::new(SaveIdeaTool); - eprintln!("[DEBUG] Created SaveIdeaTool"); - - let agent = LlmAgentBuilder::new("idea_agent") - .instruction(&instruction) - .model(model) - .tool(save_idea_tool) - .tool(Arc::new(QueryMemoryTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveInsightTool::new(iteration_id.clone()))) - .include_contents(IncludeContents::None) - .build()?; - - eprintln!("[DEBUG] Created idea_agent successfully"); - Ok(Arc::new(agent)) -} - -// ============================================================================ -// PRD Loop - Actor + Critic with LoopAgent -// ============================================================================ - -pub fn create_prd_loop(model: Arc) -> Result> { - let prd_actor = LlmAgentBuilder::new("prd_actor") - .instruction(PRD_ACTOR_INSTRUCTION) - .model(model.clone()) - .tool(Arc::new(LoadIdeaTool)) // Load idea document - .tool(Arc::new(ReviewWithFeedbackContentTool)) // HITL tool (content-based) - .tool(Arc::new(CreateRequirementTool)) - .tool(Arc::new(AddFeatureTool)) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(SavePrdDocTool)) // Save final PRD document - .include_contents(IncludeContents::None) - .build()?; - - let prd_critic = LlmAgentBuilder::new("prd_critic") - .instruction(PRD_CRITIC_INSTRUCTION) - .model(model) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(LoadIdeaTool)) // Load idea for context - .tool(Arc::new(ProvideFeedbackTool)) - .include_contents(IncludeContents::None) - .build()?; - - // Create LoopAgent with agents vector - let mut loop_agent = LoopAgent::new( - "prd_loop", - vec![Arc::new(prd_actor), Arc::new(prd_critic)], - ); - // Use max_iterations=1 to avoid SequentialAgent termination bug - loop_agent = loop_agent.with_max_iterations(1); - - Ok(Arc::new(loop_agent)) -} - -pub fn create_prd_loop_with_id(model: Arc, iteration_id: String) -> Result> { - // Replace {ITERATION_ID} placeholder in instructions - let actor_instruction = PRD_ACTOR_INSTRUCTION.replace("{ITERATION_ID}", &iteration_id); - let critic_instruction = PRD_CRITIC_INSTRUCTION.replace("{ITERATION_ID}", &iteration_id); - - let prd_actor = LlmAgentBuilder::new("prd_actor") - .instruction(&actor_instruction) - .model(model.clone()) - .tool(Arc::new(LoadFeedbackHistoryTool)) // For incremental update support - .tool(Arc::new(LoadIdeaTool)) // Load idea document - .tool(Arc::new(CreateRequirementTool)) - .tool(Arc::new(AddFeatureTool)) - .tool(Arc::new(UpdateRequirementTool)) // For incremental updates - .tool(Arc::new(UpdateFeatureTool)) // For incremental updates - .tool(Arc::new(DeleteRequirementTool)) // For incremental updates - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(SavePrdDocTool)) // Save final PRD document - .tool(Arc::new(QueryMemoryTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveInsightTool::new(iteration_id.clone()))) - .include_contents(IncludeContents::None) - .build()?; - - let prd_critic = LlmAgentBuilder::new("prd_critic") - .instruction(&critic_instruction) - .model(model) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(LoadIdeaTool)) // Load idea for context - .tool(Arc::new(ProvideFeedbackTool)) - .tool(Arc::new(QueryMemoryTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveIssueTool::new(iteration_id.clone()))) - .include_contents(IncludeContents::None) - .build()?; - - // Create LoopAgent with agents vector - let mut loop_agent = LoopAgent::new( - "prd_loop", - vec![Arc::new(prd_actor), Arc::new(prd_critic)], - ); - // Use max_iterations=1 to avoid SequentialAgent termination bug - loop_agent = loop_agent.with_max_iterations(1); - - Ok(Arc::new(loop_agent)) -} - -// ============================================================================ -// Design Loop - Actor + Critic -// ============================================================================ - -pub fn create_design_loop(model: Arc) -> Result> { - let design_actor = LlmAgentBuilder::new("design_actor") - .instruction(DESIGN_ACTOR_INSTRUCTION) - .model(model.clone()) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(GetDesignTool)) - .tool(Arc::new(LoadPrdDocTool)) // Load PRD document - .tool(Arc::new(ReviewWithFeedbackContentTool)) // HITL tool (content-based) - .tool(Arc::new(CreateDesignComponentTool)) - .tool(Arc::new(SaveDesignDocTool)) // Save final design document - .include_contents(IncludeContents::None) - .build()?; - - let design_critic = LlmAgentBuilder::new("design_critic") - .instruction(DESIGN_CRITIC_INSTRUCTION) - .model(model) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(GetDesignTool)) - .tool(Arc::new(LoadDesignDocTool)) // Verify design markdown - .tool(Arc::new(CheckFeatureCoverageTool)) - .tool(Arc::new(ProvideFeedbackTool)) - .include_contents(IncludeContents::None) - .build()?; - - let mut loop_agent = LoopAgent::new("design_loop", vec![Arc::new(design_actor), Arc::new(design_critic)]); - loop_agent = loop_agent.with_max_iterations(1); - - Ok(Arc::new(loop_agent)) -} - -pub fn create_design_loop_with_id(model: Arc, iteration_id: String) -> Result> { - // Replace {ITERATION_ID} placeholder in instructions - let actor_instruction = DESIGN_ACTOR_INSTRUCTION.replace("{ITERATION_ID}", &iteration_id); - let critic_instruction = DESIGN_CRITIC_INSTRUCTION.replace("{ITERATION_ID}", &iteration_id); - - let design_actor = LlmAgentBuilder::new("design_actor") - .instruction(&actor_instruction) - .model(model.clone()) - .tool(Arc::new(LoadFeedbackHistoryTool)) // For incremental update support - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(GetDesignTool)) - .tool(Arc::new(LoadPrdDocTool)) // Load PRD document - .tool(Arc::new(CreateDesignComponentTool)) - .tool(Arc::new(SaveDesignDocTool)) // Save final design document - .tool(Arc::new(QueryMemoryTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveInsightTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveIssueTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveLearningTool::new(iteration_id.clone()))) - .include_contents(IncludeContents::None) - .build()?; - - let design_critic = LlmAgentBuilder::new("design_critic") - .instruction(&critic_instruction) - .model(model) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(GetDesignTool)) - .tool(Arc::new(LoadDesignDocTool)) // Verify design markdown - .tool(Arc::new(CheckFeatureCoverageTool)) - .tool(Arc::new(ProvideFeedbackTool)) - .tool(Arc::new(QueryMemoryTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveIssueTool::new(iteration_id.clone()))) - .include_contents(IncludeContents::None) - .build()?; - - let mut loop_agent = LoopAgent::new("design_loop", vec![Arc::new(design_actor), Arc::new(design_critic)]); - loop_agent = loop_agent.with_max_iterations(1); - - Ok(Arc::new(loop_agent)) -} - -// ============================================================================ -// Plan Loop - Actor + Critic -// ============================================================================ - -pub fn create_plan_loop(model: Arc) -> Result> { - let plan_actor = LlmAgentBuilder::new("plan_actor") - .instruction(PLAN_ACTOR_INSTRUCTION) - .model(model.clone()) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(GetDesignTool)) - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(LoadPrdDocTool)) // Load PRD document - .tool(Arc::new(LoadDesignDocTool)) // Load design document - .tool(Arc::new(ReviewWithFeedbackContentTool)) // HITL tool (content-based) - .tool(Arc::new(CreateTaskTool)) - .tool(Arc::new(SavePlanDocTool)) // Save final plan document - .include_contents(IncludeContents::None) - .build()?; - - let plan_critic = LlmAgentBuilder::new("plan_critic") - .instruction(PLAN_CRITIC_INSTRUCTION) - .model(model) - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(LoadPlanDocTool)) // Verify plan markdown - .tool(Arc::new(CheckTaskDependenciesTool)) - .tool(Arc::new(ProvideFeedbackTool)) - .include_contents(IncludeContents::None) - .build()?; - - let mut loop_agent = LoopAgent::new("plan_loop", vec![Arc::new(plan_actor), Arc::new(plan_critic)]); - loop_agent = loop_agent.with_max_iterations(1); - - Ok(Arc::new(loop_agent)) -} - -pub fn create_plan_loop_with_id(model: Arc, iteration_id: String) -> Result> { - // Replace {ITERATION_ID} placeholder in instructions - let actor_instruction = PLAN_ACTOR_INSTRUCTION.replace("{ITERATION_ID}", &iteration_id); - let critic_instruction = PLAN_CRITIC_INSTRUCTION.replace("{ITERATION_ID}", &iteration_id); - - let plan_actor = LlmAgentBuilder::new("plan_actor") - .instruction(&actor_instruction) - .model(model.clone()) - .tool(Arc::new(LoadFeedbackHistoryTool)) // For incremental update support - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(GetDesignTool)) - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(LoadPrdDocTool)) // Load PRD document - .tool(Arc::new(LoadDesignDocTool)) // Load design document - .tool(Arc::new(CreateTaskTool)) - .tool(Arc::new(SavePlanDocTool)) // Save final plan document - .tool(Arc::new(QueryMemoryTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveInsightTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveIssueTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveLearningTool::new(iteration_id.clone()))) - .include_contents(IncludeContents::None) - .build()?; - - let plan_critic = LlmAgentBuilder::new("plan_critic") - .instruction(&critic_instruction) - .model(model) - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(LoadPlanDocTool)) // Verify plan markdown - .tool(Arc::new(CheckTaskDependenciesTool)) - .tool(Arc::new(ProvideFeedbackTool)) - .tool(Arc::new(QueryMemoryTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveIssueTool::new(iteration_id.clone()))) - .include_contents(IncludeContents::None) - .build()?; - - let mut loop_agent = LoopAgent::new("plan_loop", vec![Arc::new(plan_actor), Arc::new(plan_critic)]); - loop_agent = loop_agent.with_max_iterations(1); - - Ok(Arc::new(loop_agent)) -} - -// ============================================================================ -// Coding Loop - Actor + Critic -// ============================================================================ - -pub fn create_coding_loop(model: Arc) -> Result> { - let coding_actor = LlmAgentBuilder::new("coding_actor") - .instruction(CODING_ACTOR_INSTRUCTION) - .model(model.clone()) - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(UpdateTaskStatusTool)) - .tool(Arc::new(UpdateFeatureStatusTool)) - .tool(Arc::new(ReadFileTool)) - .tool(Arc::new(WriteFileTool)) - .tool(Arc::new(ListFilesTool)) - .tool(Arc::new(RunCommandTool)) - .tool(Arc::new(CheckTestsTool)) - .include_contents(IncludeContents::None) - .build()?; - - let coding_critic = LlmAgentBuilder::new("coding_critic") - .instruction(CODING_CRITIC_INSTRUCTION) - .model(model) - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(ReadFileTool)) - .tool(Arc::new(ListFilesTool)) - .tool(Arc::new(RunCommandTool)) - // Removed check_tests and check_lint - not applicable for pure frontend projects - .tool(Arc::new(ProvideFeedbackTool)) - .include_contents(IncludeContents::None) - .build()?; - - // Coding needs more iterations to implement and review tasks - let mut loop_agent = LoopAgent::new("coding_loop", vec![Arc::new(coding_actor), Arc::new(coding_critic)]); - loop_agent = loop_agent.with_max_iterations(5); - - Ok(Arc::new(loop_agent)) -} - -pub fn create_coding_loop_with_id(model: Arc, iteration_id: String) -> Result> { - // Replace {ITERATION_ID} placeholder in instructions - let actor_instruction = CODING_ACTOR_INSTRUCTION.replace("{ITERATION_ID}", &iteration_id); - let critic_instruction = CODING_CRITIC_INSTRUCTION.replace("{ITERATION_ID}", &iteration_id); - - let coding_actor = LlmAgentBuilder::new("coding_actor") - .instruction(&actor_instruction) - .model(model.clone()) - .tool(Arc::new(LoadFeedbackHistoryTool)) // For incremental update support - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(UpdateTaskStatusTool)) - .tool(Arc::new(UpdateFeatureStatusTool)) - .tool(Arc::new(ReadFileTool)) - .tool(Arc::new(WriteFileTool)) - .tool(Arc::new(ListFilesTool)) - .tool(Arc::new(RunCommandTool)) - .tool(Arc::new(CheckTestsTool)) - .tool(Arc::new(QueryMemoryTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveInsightTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveIssueTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveLearningTool::new(iteration_id.clone()))) - .include_contents(IncludeContents::None) - .build()?; - - let coding_critic = LlmAgentBuilder::new("coding_critic") - .instruction(&critic_instruction) - .model(model) - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(ReadFileTool)) - .tool(Arc::new(ListFilesTool)) - .tool(Arc::new(RunCommandTool)) - // Removed check_tests and check_lint - not applicable for pure frontend projects - .tool(Arc::new(ProvideFeedbackTool)) - .tool(Arc::new(QueryMemoryTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveIssueTool::new(iteration_id.clone()))) - .include_contents(IncludeContents::None) - .build()?; - - // Coding needs more iterations to implement and review tasks - let mut loop_agent = LoopAgent::new("coding_loop", vec![Arc::new(coding_actor), Arc::new(coding_critic)]); - loop_agent = loop_agent.with_max_iterations(5); - - Ok(Arc::new(loop_agent)) -} - -// ============================================================================ -// Check Agent - Quality assurance -// ============================================================================ - -pub fn create_check_agent(model: Arc) -> Result> { - let agent = LlmAgentBuilder::new("check_agent") - .instruction(CHECK_AGENT_INSTRUCTION) - .model(model) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(GetDesignTool)) - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(CheckDataFormatTool)) - .tool(Arc::new(CheckFeatureCoverageTool)) - .tool(Arc::new(CheckTaskDependenciesTool)) - .tool(Arc::new(RunCommandTool)) - .tool(Arc::new(ReadFileTool)) - .tool(Arc::new(ListFilesTool)) - .tool(Arc::new(CheckTestsTool)) - .tool(Arc::new(CheckLintTool)) - .tool(Arc::new(ProvideFeedbackTool)) - .tool(Arc::new(GotoStageTool)) - .tool(Arc::new(SaveCheckReportTool)) - .include_contents(IncludeContents::None) - .build()?; - - Ok(Arc::new(agent)) -} - -pub fn create_check_agent_with_id(model: Arc, iteration_id: String) -> Result> { - // Replace {ITERATION_ID} placeholder in instruction - let instruction = CHECK_AGENT_INSTRUCTION.replace("{ITERATION_ID}", &iteration_id); - - let agent = LlmAgentBuilder::new("check_agent") - .instruction(&instruction) - .model(model) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(GetDesignTool)) - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(CheckDataFormatTool)) - .tool(Arc::new(CheckFeatureCoverageTool)) - .tool(Arc::new(CheckTaskDependenciesTool)) - .tool(Arc::new(RunCommandTool)) - .tool(Arc::new(ReadFileTool)) - .tool(Arc::new(ListFilesTool)) - .tool(Arc::new(CheckTestsTool)) - .tool(Arc::new(CheckLintTool)) - .tool(Arc::new(ProvideFeedbackTool)) - .tool(Arc::new(GotoStageTool)) - .tool(Arc::new(SaveCheckReportTool)) - .tool(Arc::new(QueryMemoryTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveInsightTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveIssueTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveLearningTool::new(iteration_id.clone()))) - .tool(Arc::new(PromoteToDecisionTool::new(iteration_id.clone()))) - .include_contents(IncludeContents::None) - .build()?; - - Ok(Arc::new(agent)) -} - -// ============================================================================ -// Delivery Agent - Final report generation -// ============================================================================ - -pub fn create_delivery_agent(model: Arc) -> Result> { - let agent = LlmAgentBuilder::new("delivery_agent") - .instruction(DELIVERY_AGENT_INSTRUCTION) - .model(model) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(GetDesignTool)) - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(LoadFeedbackHistoryTool)) - .tool(Arc::new(ListFilesTool)) // To verify project files exist - .tool(Arc::new(LoadIdeaTool)) // Load idea document - .tool(Arc::new(LoadPrdDocTool)) // Load PRD document - .tool(Arc::new(LoadDesignDocTool)) // Load design document - .tool(Arc::new(SaveDeliveryReportTool)) - .tool(Arc::new(CopyWorkspaceToProjectTool)) // Copy files to project root - .include_contents(IncludeContents::None) - .build()?; - - Ok(Arc::new(agent)) -} - -pub fn create_delivery_agent_with_id(model: Arc, iteration_id: String) -> Result> { - // Replace {ITERATION_ID} placeholder in instruction - let instruction = DELIVERY_AGENT_INSTRUCTION.replace("{ITERATION_ID}", &iteration_id); - - let agent = LlmAgentBuilder::new("delivery_agent") - .instruction(&instruction) - .model(model) - .tool(Arc::new(GetRequirementsTool)) - .tool(Arc::new(GetDesignTool)) - .tool(Arc::new(GetPlanTool)) - .tool(Arc::new(LoadFeedbackHistoryTool)) - .tool(Arc::new(ListFilesTool)) // To verify project files exist - .tool(Arc::new(LoadIdeaTool)) // Load idea document - .tool(Arc::new(LoadPrdDocTool)) // Load PRD document - .tool(Arc::new(LoadDesignDocTool)) // Load design document - .tool(Arc::new(SaveDeliveryReportTool)) - .tool(Arc::new(CopyWorkspaceToProjectTool)) // Copy files to project root - .tool(Arc::new(QueryMemoryTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveInsightTool::new(iteration_id.clone()))) - .tool(Arc::new(SaveLearningTool::new(iteration_id.clone()))) - .tool(Arc::new(PromoteToPatternTool::new(iteration_id.clone()))) - .include_contents(IncludeContents::None) - .build()?; - - Ok(Arc::new(agent)) -} - // ============================================================================ // Summary Agent - Generates summaries of iteration documents // ============================================================================ @@ -559,10 +109,10 @@ pub fn create_project_manager_agent(model: Arc, iteration_id: String) - .tool(Arc::new(ListFilesTool)) // Allow PM to see project files .tool(Arc::new(ReadFileTool)) // Allow PM to read files .include_contents(IncludeContents::None); - + // Add MCP toolsets if available builder = crate::config_definition::agent_factory::add_mcp_toolsets_to_builder(builder); - + let agent = builder.build()?; Ok(Arc::new(agent)) @@ -571,12 +121,12 @@ pub fn create_project_manager_agent(model: Arc, iteration_id: String) - /// Load artifacts summary for a given iteration fn load_artifacts_summary_for_pm(iteration_store: &IterationStore, iteration_id: &str) -> Result { use std::fs; - + let iteration_dir = iteration_store.iteration_path(iteration_id) .map_err(|e| format!("Failed to get iteration path: {}", e))?; - + let mut summary = String::new(); - + // Load key artifacts let artifacts_to_load = [ ("idea", "idea.md"), @@ -584,7 +134,7 @@ fn load_artifacts_summary_for_pm(iteration_store: &IterationStore, iteration_id: ("design", "design.md"), ("plan", "plan.md"), ]; - + for (name, filename) in artifacts_to_load.iter() { let path = iteration_dir.join("artifacts").join(filename); if path.exists() { @@ -599,7 +149,7 @@ fn load_artifacts_summary_for_pm(iteration_store: &IterationStore, iteration_id: } } } - + // Add code structure info let code_dir = iteration_dir.join("workspace"); if code_dir.exists() { @@ -612,7 +162,7 @@ fn load_artifacts_summary_for_pm(iteration_store: &IterationStore, iteration_id: } } } - + Ok(summary) } @@ -688,7 +238,7 @@ pub async fn execute_pm_agent_message_streaming( // Load artifacts summary for context let artifacts_summary = load_artifacts_summary_for_pm(&iteration_store, &iteration_id) .unwrap_or_else(|e| { - eprintln!("[PM Agent] Warning: Failed to load artifacts: {}", e); + tracing::warn!("[PM Agent] Failed to load artifacts: {}", e); String::new() }); @@ -697,7 +247,7 @@ pub async fn execute_pm_agent_message_streaming( let project_memory = memory_store.load_project_memory() .map_err(|e| format!("Failed to load memory: {}", e)) .unwrap_or_default(); - + let decisions_summary = if !project_memory.decisions.is_empty() { let mut summary = String::from("\n\n## Previous Decisions:\n"); for decision in project_memory.decisions.iter().take(10) { @@ -786,7 +336,7 @@ pub async fn execute_pm_agent_message_streaming( if let Some(text) = extract_text_from_event(&event) { if !text.trim().is_empty() { agent_message.push_str(&text); - + // Call streaming callback if provided if let Some(ref callback) = stream_callback { callback.on_text_chunk(&text, is_first_chunk, false).await; @@ -794,7 +344,7 @@ pub async fn execute_pm_agent_message_streaming( is_first_chunk = false; } } - + // Collect all parts (includes function calls) if let Some(content) = event.content() { for part in &content.parts { @@ -822,19 +372,19 @@ pub async fn execute_pm_agent_message_streaming( } _ => {} } - + // Notify callback about tool call if let Some(ref callback) = stream_callback { callback.on_tool_call(name, args).await; } } - + all_parts.push(part.clone()); } } } Err(e) => { - eprintln!("[PM Agent] Event error: {}", e); + tracing::warn!("[PM Agent] Event error: {}", e); } } } @@ -856,9 +406,9 @@ pub async fn execute_pm_agent_message_streaming( if new_iteration.id != iteration_id { detected_actions.push(PMAgentAction::CreateIteration { iteration_id: new_iteration.id.clone(), - title: title, - description: description, - inheritance: inheritance, + title, + description, + inheritance, }); } } @@ -868,7 +418,7 @@ pub async fn execute_pm_agent_message_streaming( // Fallback: if no actions detected but message contains tool references if detected_actions.is_empty() { let msg_lower = agent_message.to_lowercase(); - + if msg_lower.contains("goto_stage") || msg_lower.contains("跳转") || msg_lower.contains("返回") { // Try to extract stage from message for stage in &["coding", "design", "plan", "prd", "idea"] { @@ -891,7 +441,7 @@ pub async fn execute_pm_agent_message_streaming( let mut seen_stages: std::collections::HashSet = std::collections::HashSet::new(); let mut seen_iterations: std::collections::HashSet = std::collections::HashSet::new(); let mut unique_actions: Vec = Vec::new(); - + for action in detected_actions { match &action { PMAgentAction::GotoStage { target_stage, .. } => { diff --git a/crates/cowork-core/src/config_definition/agent_definition.rs b/crates/cowork-core/src/config_definition/agent_definition.rs index 8bc3235..2f5210a 100644 --- a/crates/cowork-core/src/config_definition/agent_definition.rs +++ b/crates/cowork-core/src/config_definition/agent_definition.rs @@ -23,7 +23,7 @@ pub enum AgentType { /// Model configuration for an agent #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct ModelConfig { - /// Model identifier (e.g., "gpt-4", "claude-3-opus") + /// Model identifier (e.g., "gpt-5") pub model_id: Option, /// Temperature for sampling (0.0 - 2.0) pub temperature: Option, @@ -64,34 +64,34 @@ pub struct AgentDefinition { pub description: Option, /// Version of this definition (semver) pub version: Option, - + /// Agent type (Simple or Loop) #[serde(default)] pub agent_type: AgentType, - + /// Prompt template path or inline content - /// Can reference: + /// Can reference: /// - Built-in: "builtin://idea_actor" /// - File: "file://./prompts/idea_actor.md" /// - Inline: "inline://..." pub instruction: String, - + /// List of tools this agent can use #[serde(default)] pub tools: Vec, - + /// Model configuration (overrides global default) #[serde(default)] pub model: ModelConfig, - + /// Content inclusion mode for context #[serde(default)] pub include_contents: IncludeContentsMode, - + /// Tags for categorization and skill matching #[serde(default)] pub tags: Vec, - + /// Metadata for extensions #[serde(default)] pub metadata: HashMap, @@ -101,10 +101,12 @@ pub struct AgentDefinition { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] #[serde(rename_all = "snake_case")] pub enum IncludeContentsMode { - /// No content included + /// No content included - stateless, only current turn #[default] None, - /// Include all content + /// Default behavior - include conversation history within the current session + Default, + /// Include all available content All, /// Include only specified content types Selected(Vec), @@ -138,7 +140,7 @@ impl AgentDefinition { metadata: HashMap::new(), } } - + /// Add a tool reference pub fn with_tool(mut self, tool_id: impl Into) -> Self { self.tools.push(ToolReference { @@ -147,7 +149,7 @@ impl AgentDefinition { }); self } - + /// Add a tool with configuration pub fn with_tool_config(mut self, tool_id: impl Into, config: HashMap) -> Self { self.tools.push(ToolReference { @@ -156,19 +158,19 @@ impl AgentDefinition { }); self } - + /// Add a tag for skill matching pub fn with_tag(mut self, tag: impl Into) -> Self { self.tags.push(tag.into()); self } - + /// Set the agent type to Loop pub fn as_loop(mut self, max_iterations: Option) -> Self { self.agent_type = AgentType::Loop { max_iterations }; self } - + /// Set model configuration pub fn with_model(mut self, model: ModelConfig) -> Self { self.model = model; @@ -179,16 +181,16 @@ impl AgentDefinition { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_agent_definition_serialization() { let agent = AgentDefinition::new("idea_agent", "Idea Agent", "builtin://idea_actor") .with_tool("save_idea") .with_tool("query_memory"); - + let json = serde_json::to_string_pretty(&agent).unwrap(); println!("{}", json); - + let parsed: AgentDefinition = serde_json::from_str(&json).unwrap(); assert_eq!(parsed.id, "idea_agent"); assert_eq!(parsed.tools.len(), 2); diff --git a/crates/cowork-core/src/config_definition/agent_factory.rs b/crates/cowork-core/src/config_definition/agent_factory.rs index f06ec34..16b5813 100644 --- a/crates/cowork-core/src/config_definition/agent_factory.rs +++ b/crates/cowork-core/src/config_definition/agent_factory.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use anyhow::{Result, Context}; use crate::config_definition::{ - AgentDefinition, StageDefinition, StageType, + AgentDefinition, StageDefinition, StageType, IncludeContentsMode, global_registry, }; use crate::instructions::*; @@ -16,6 +16,7 @@ use crate::skills::{SkillManager, SelectionPolicy}; use adk_agent::{LlmAgentBuilder, LoopAgent}; use adk_core::{Llm, Agent, IncludeContents}; use adk_skill::select_skill_prompt_block; +use adk_tool::ExitLoopTool; use crate::llm::config::McpConfig; use crate::tools::{create_mcp_toolsets_from_config, ConnectedMcpToolset}; @@ -229,9 +230,8 @@ pub fn create_agent_from_config_with_stage( // Add MCP toolsets if available builder = add_mcp_toolsets_to_builder(builder); - // Set content inclusion mode - // Note: Current adk_core only supports None, LastN, All variants may not be available - let include_contents = IncludeContents::None; // All agents use None for now + // Set content inclusion mode from config + let include_contents = include_contents_mode_to_adk(&definition.include_contents); builder = builder.include_contents(include_contents); // Build the agent @@ -319,9 +319,19 @@ fn create_simple_agent_from_config_with_stage( // Add MCP toolsets if available builder = add_mcp_toolsets_to_builder(builder); - // Set content inclusion mode - // Note: Current adk_core only supports None, LastN, All variants may not be available - let include_contents = IncludeContents::None; // All agents use None for now + // Set content inclusion mode: + // - Actor agents use Default to see Critic feedback across LoopAgent iterations + // (they explicitly set "include_contents": "default" in JSON configs). + // - Critic agents use None to avoid paying for Actor's full conversation history; + // Critics load artifacts via tools (load_prd_doc, get_plan, etc.) rather than + // reading conversation history, which significantly reduces token usage. + // - Simple agents default to None to keep context focused. + let include_contents = match &definition.include_contents { + IncludeContentsMode::None => IncludeContents::None, + IncludeContentsMode::Default => IncludeContents::Default, + IncludeContentsMode::All => IncludeContents::Default, + IncludeContentsMode::Selected(_) => IncludeContents::Default, + }; builder = builder.include_contents(include_contents); let agent = builder.build() @@ -401,6 +411,7 @@ fn create_tool_from_reference(tool_id: &str, iteration_id: &str) -> Result Arc::new(GetRequirementsTool), "add_feature" => Arc::new(AddFeatureTool), "update_feature" => Arc::new(UpdateFeatureTool), + "update_feature_status" => Arc::new(UpdateFeatureStatusTool), "create_task" => Arc::new(CreateTaskTool), "update_task_status" => Arc::new(UpdateTaskStatusTool), "get_design" => Arc::new(GetDesignTool), @@ -441,6 +452,9 @@ fn create_tool_from_reference(tool_id: &str, iteration_id: &str) -> Result Arc::new(ProvideFeedbackTool), "load_feedback_history" => Arc::new(LoadFeedbackHistoryTool), + "review_with_feedback_content" => Arc::new(ReviewWithFeedbackContentTool), + "request_human_review" => Arc::new(RequestHumanReviewTool), + "ask_user" => Arc::new(AskUserTool), // Memory tools "query_memory" => Arc::new(QueryMemoryTool::new(iteration_id.to_string())), @@ -454,6 +468,7 @@ fn create_tool_from_reference(tool_id: &str, iteration_id: &str) -> Result Arc::new(CopyWorkspaceToProjectTool), // Flow control tools + "exit_loop" => Arc::new(ExitLoopTool::new()), "goto_stage" => Arc::new(GotoStageTool), // PM tools (require iteration_id) @@ -540,3 +555,13 @@ pub fn initialize_config_registry() -> Result<()> { Ok(()) } + +/// Convert config IncludeContentsMode to adk_core::IncludeContents +fn include_contents_mode_to_adk(mode: &IncludeContentsMode) -> IncludeContents { + match mode { + IncludeContentsMode::None => IncludeContents::None, + IncludeContentsMode::Default => IncludeContents::Default, + IncludeContentsMode::All => IncludeContents::Default, + IncludeContentsMode::Selected(_) => IncludeContents::Default, + } +} diff --git a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/check_agent.json b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/check_agent.json index a82ef70..a080798 100644 --- a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/check_agent.json +++ b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/check_agent.json @@ -38,6 +38,9 @@ }, { "tool_id": "save_check_report" + }, + { + "tool_id": "goto_stage" } ], "model": { diff --git a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/coding_actor.json b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/coding_actor.json index 56f63c1..6e381ab 100644 --- a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/coding_actor.json +++ b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/coding_actor.json @@ -12,9 +12,24 @@ { "tool_id": "get_implementation_plan" }, + { + "tool_id": "get_requirements" + }, + { + "tool_id": "load_design_doc" + }, + { + "tool_id": "load_plan_doc" + }, + { + "tool_id": "create_task" + }, { "tool_id": "update_task_status" }, + { + "tool_id": "update_feature_status" + }, { "tool_id": "read_file" }, @@ -49,6 +64,6 @@ "model": { "temperature": 0.7 }, - "include_contents": "none", + "include_contents": "default", "tags": ["built-in", "coding", "actor"] -} +} \ No newline at end of file diff --git a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/coding_critic.json b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/coding_critic.json index d5b4d65..0590008 100644 --- a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/coding_critic.json +++ b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/coding_critic.json @@ -9,6 +9,9 @@ { "tool_id": "get_implementation_plan" }, + { + "tool_id": "load_plan_doc" + }, { "tool_id": "read_file" }, @@ -27,6 +30,12 @@ { "tool_id": "provide_feedback" }, + { + "tool_id": "exit_loop" + }, + { + "tool_id": "request_human_review" + }, { "tool_id": "query_memory" }, @@ -39,4 +48,4 @@ }, "include_contents": "none", "tags": ["built-in", "coding", "critic"] -} +} \ No newline at end of file diff --git a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_actor.json b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_actor.json index 8fab954..880d2ad 100644 --- a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_actor.json +++ b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_actor.json @@ -18,6 +18,9 @@ { "tool_id": "load_prd_doc" }, + { + "tool_id": "review_with_feedback_content" + }, { "tool_id": "create_design_component" }, @@ -49,6 +52,6 @@ "model": { "temperature": 0.7 }, - "include_contents": "none", + "include_contents": "default", "tags": ["built-in", "design", "actor"] -} +} \ No newline at end of file diff --git a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_critic.json b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_critic.json index 0a56b7e..7085a2e 100644 --- a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_critic.json +++ b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_critic.json @@ -21,6 +21,12 @@ { "tool_id": "provide_feedback" }, + { + "tool_id": "exit_loop" + }, + { + "tool_id": "request_human_review" + }, { "tool_id": "read_file" }, @@ -42,4 +48,4 @@ }, "include_contents": "none", "tags": ["built-in", "design", "critic"] -} +} \ No newline at end of file diff --git a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_actor.json b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_actor.json index 199ee0a..9973f79 100644 --- a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_actor.json +++ b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_actor.json @@ -18,6 +18,9 @@ { "tool_id": "load_design_doc" }, + { + "tool_id": "review_with_feedback_content" + }, { "tool_id": "create_task" }, @@ -55,6 +58,6 @@ "model": { "temperature": 0.7 }, - "include_contents": "none", + "include_contents": "default", "tags": ["built-in", "planning", "actor"] -} +} \ No newline at end of file diff --git a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_critic.json b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_critic.json index 6a2d3a2..4282f1e 100644 --- a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_critic.json +++ b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_critic.json @@ -18,6 +18,12 @@ { "tool_id": "provide_feedback" }, + { + "tool_id": "exit_loop" + }, + { + "tool_id": "request_human_review" + }, { "tool_id": "read_file" }, @@ -39,4 +45,4 @@ }, "include_contents": "none", "tags": ["built-in", "planning", "critic"] -} +} \ No newline at end of file diff --git a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_actor.json b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_actor.json index fff0002..3fd7e46 100644 --- a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_actor.json +++ b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_actor.json @@ -30,6 +30,9 @@ { "tool_id": "get_requirements" }, + { + "tool_id": "review_with_feedback_content" + }, { "tool_id": "save_prd_doc" }, @@ -52,6 +55,6 @@ "model": { "temperature": 0.7 }, - "include_contents": "none", + "include_contents": "default", "tags": ["built-in", "requirements", "actor"] -} +} \ No newline at end of file diff --git a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_critic.json b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_critic.json index b55ba8c..e30ae1c 100644 --- a/crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_critic.json +++ b/crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_critic.json @@ -12,9 +12,18 @@ { "tool_id": "load_idea" }, + { + "tool_id": "load_prd_doc" + }, { "tool_id": "provide_feedback" }, + { + "tool_id": "exit_loop" + }, + { + "tool_id": "request_human_review" + }, { "tool_id": "read_file" }, @@ -36,4 +45,4 @@ }, "include_contents": "none", "tags": ["built-in", "requirements", "critic"] -} +} \ No newline at end of file diff --git a/crates/cowork-core/src/config_definition/default_configs/stages/coding.json b/crates/cowork-core/src/config_definition/default_configs/stages/coding.json index 4fd9e48..a676e11 100644 --- a/crates/cowork-core/src/config_definition/default_configs/stages/coding.json +++ b/crates/cowork-core/src/config_definition/default_configs/stages/coding.json @@ -6,7 +6,7 @@ "actor_critic": { "actor": "coding_actor", "critic": "coding_critic", - "max_iterations": 1 + "max_iterations": 3 }, "needs_confirmation": true, "artifacts": [ diff --git a/crates/cowork-core/src/config_definition/default_configs/stages/design.json b/crates/cowork-core/src/config_definition/default_configs/stages/design.json index 9f4eeb4..0085de2 100644 --- a/crates/cowork-core/src/config_definition/default_configs/stages/design.json +++ b/crates/cowork-core/src/config_definition/default_configs/stages/design.json @@ -6,7 +6,7 @@ "actor_critic": { "actor": "design_actor", "critic": "design_critic", - "max_iterations": 1 + "max_iterations": 2 }, "needs_confirmation": true, "artifacts": [ diff --git a/crates/cowork-core/src/config_definition/default_configs/stages/plan.json b/crates/cowork-core/src/config_definition/default_configs/stages/plan.json index 4b0cadd..ea9d810 100644 --- a/crates/cowork-core/src/config_definition/default_configs/stages/plan.json +++ b/crates/cowork-core/src/config_definition/default_configs/stages/plan.json @@ -6,7 +6,7 @@ "actor_critic": { "actor": "plan_actor", "critic": "plan_critic", - "max_iterations": 1 + "max_iterations": 2 }, "needs_confirmation": true, "artifacts": [ diff --git a/crates/cowork-core/src/config_definition/default_configs/stages/prd.json b/crates/cowork-core/src/config_definition/default_configs/stages/prd.json index 43d24d5..26fac1d 100644 --- a/crates/cowork-core/src/config_definition/default_configs/stages/prd.json +++ b/crates/cowork-core/src/config_definition/default_configs/stages/prd.json @@ -6,7 +6,7 @@ "actor_critic": { "actor": "prd_actor", "critic": "prd_critic", - "max_iterations": 1 + "max_iterations": 2 }, "needs_confirmation": true, "artifacts": [ diff --git a/crates/cowork-core/src/data/models.rs b/crates/cowork-core/src/data/models.rs index 1e8526f..855d02c 100644 --- a/crates/cowork-core/src/data/models.rs +++ b/crates/cowork-core/src/data/models.rs @@ -309,6 +309,9 @@ pub enum FeedbackType { BuildError, QualityIssue, MissingRequirement, + MissingArtifact, + ArchitectureIssue, + TaskScopeIssue, Suggestion, } diff --git a/crates/cowork-core/src/instructions/check.rs b/crates/cowork-core/src/instructions/check.rs index 1091bba..d4abe85 100644 --- a/crates/cowork-core/src/instructions/check.rs +++ b/crates/cowork-core/src/instructions/check.rs @@ -1,415 +1,93 @@ -// Check Agent instruction (AI-DRIVEN VERSION with README-based validation) - pub const CHECK_AGENT_INSTRUCTION: &str = r##" # Your Role -You are Check Agent. Read README.md and autonomously execute the commands it specifies to verify the project. - -# 🚨🚨🚨 CRITICAL RULE - BUILD FAILURES MUST RETURN TO CODING 🚨🚨🚨 -**This is the MOST IMPORTANT rule - violating this will cause broken code to be deployed!** - -## If TypeScript/Build Command FAILS with compilation errors: -1. ❌ DO NOT just save check_report and let pipeline continue -2. ❌ DO NOT proceed to Delivery stage -3. ✅ You MUST call `goto_stage("coding", )` to fix the errors -4. ✅ The build MUST pass before pipeline can proceed +You are Check Agent. Validate the project by reading README.md, executing its specified commands, and verifying the build works. -## Example - TypeScript Compilation Error (MANDATORY goto_stage): -``` -1. execute_shell_command("npm run build", "Build project") - → status: "failed", stderr: - "src/services/performance-service.ts:12:2 - error TS2305: Module has no exported member 'ToolDefinition'" - -2. ❌ WRONG: - save_check_report("Build failed with 3 errors") - // Then do nothing - pipeline continues with broken code! +# Non-Negotiable Rules +1. **Build failures MUST return to Coding**: If compilation/build fails, call `goto_stage("coding", )`. Do NOT save_check_report and do NOT let broken code proceed. +2. **All checks passing MUST save report**: When everything passes, call `save_check_report()` to record results. +3. **Always end with a tool call**: You MUST call either `save_check_report()` (success) or `goto_stage()` (failure). Never end with plain text. -3. ✅ CORRECT: - goto_stage("coding", "构建失败:TypeScript编译错误,共3个错误: +# Workflow - 错误1: src/services/performance-service.ts:12:2 - - 错误类型: TS2305 - - 错误描述: Module '../types/metrics.js' has no exported member 'ToolDefinition' - - 修复建议: 在 metrics.ts 中添加 ToolDefinition 类型导出,或从 plugin-impl.ts 导入该类型 +## Step 0: Validate Project Structure (FIRST) +Use `list_files(".")` and verify required files exist based on project type: - 错误2: src/decorators/performance-monitor.ts:73:4 - - 错误类型: TS2722 - - 错误描述: Cannot invoke an object which is possibly 'undefined' - - 修复建议: 添加类型守卫或非空断言 +| Project Type | Required Files | +|---|---| +| Web (React/Vue/Vite) | package.json, index.html, src/main.js(x)/ts(x), build config (vite.config.*) | +| Node.js CLI/Backend | package.json (with "bin" if CLI), src/index.js or index.js | +| Rust | Cargo.toml, src/main.rs or src/lib.rs | +| Python | requirements.txt or pyproject.toml, main.py or src/__init__.py | - 请修复这些 TypeScript 编译错误后重新执行。") - // Pipeline returns to Coding stage to fix the errors -``` +If ANY required file is missing → immediately `goto_stage("coding", "Missing required files: ")`. STOP. -# ⚠️ CRITICAL REQUIREMENT - YOU MUST CALL save_check_report() -**This is the MOST IMPORTANT requirement - without this tool call, your work will be LOST!** +## Step 1: Read README.md +Call `read_file("README.md")`. If README.md is missing → `goto_stage("coding", "Missing README.md. Coding stage must generate it.")`. STOP. -## ⚠️ MANDATORY WORKFLOW - YOU MUST FOLLOW THIS EXACTLY: +## Step 2: Extract & Execute Commands +Extract from README the commands for: +- Dependency installation (npm install / pip install -r requirements.txt / cargo build / etc.) +- Build (npm run build / cargo build --release / etc.) +- For static HTML projects without build commands: just verify key files exist. -### When ALL checks PASS (dependencies installed, build succeeded): -``` -1. Install dependencies: execute_shell_command("bun install" or "npm install") -2. Build project: execute_shell_command("bun run build" or "npm run build") -3. Verify dist/ output exists -4. ✅ MUST call: save_check_report("# Check Report\n\n## Results\n- Dependencies: ✅\n- Build: ✅\n\n## Conclusion\n项目构建成功,可以正常运行。") -5. STOP - Do NOT continue without calling save_check_report() -``` +Execute them sequentially using `run_command(command, description)`. -### When BUILD FAILS (TypeScript/compilation errors): -``` -1. Try to build: execute_shell_command("bun run build" or "npm run build") -2. If build fails with errors: -3. ✅ MUST call: goto_stage("coding", "构建失败:TypeScript编译错误...") -4. STOP - Do NOT call save_check_report() when build fails -``` +## Step 3: Analyze Results & Decide -### When PROJECT STRUCTURE is incomplete: +### ALL CHECKS PASS → save_check_report: ``` -1. Check files with list_files(".") -2. If essential files missing (package.json, src/, etc.): -3. ✅ MUST call: goto_stage("coding", "项目结构不完整...") -4. STOP - Do NOT call save_check_report() when structure is broken +save_check_report("# Check Report\n\n## Results\n- Structure: ✅\n- Dependencies: ✅\n- Build: ✅\n\n## Conclusion\n项目构建成功,可以正常运行。") ``` -## ⚠️ DO NOT END WITHOUT A TOOL CALL! -- ❌ WRONG: Output text and end without calling any tool -- ❌ WRONG: Say "Check completed" without calling save_check_report() or goto_stage() -- ✅ CORRECT: Always end with either `save_check_report()` OR `goto_stage()` - -# 🚨🚨🚨 CRITICAL RULE - BUILD FAILURES MUST RETURN TO CODING 🚨🚨🚨 -**This is a critical rule - violating this will cause broken code to be deployed!** -- **Read README.md**: Check stage starts by reading the project README.md -- **Extract commands**: Analyze README to find environment setup, dependency installation, and build/run commands -- **Execute autonomously**: Run these commands using execute_shell_command tool -- **Make decisions**: Based on command execution results, either approve the project or return to Coding stage with specific feedback - -# ⚠️ CRITICAL: PROJECT STRUCTURE VALIDATION (NEW - FIRST PRIORITY) -**BEFORE checking README or running commands, you MUST verify project file structure:** - -## Step 0: Validate Essential Files (NEW - MANDATORY FIRST STEP) -**This MUST be done BEFORE reading README.md:** - -1. Use `list_files(".")` to see all project files -2. **CRITICAL CHECKS** - Verify these files exist based on project type: - -### For Web/Frontend Projects (React/Vue/Vanilla): -**REQUIRED FILES:** -- [ ] `package.json` - MUST exist and contain dependencies -- [ ] Entry HTML (`index.html`) - MUST exist -- [ ] Build config (`vite.config.js` or similar) - should exist -- [ ] Main entry script (`src/main.js` or `src/main.jsx`) - MUST exist -- [ ] `.gitignore` - should exist - -**IF ANY REQUIRED FILE IS MISSING:** +### BUILD/COMPILATION FAILS → goto_stage: +Analyze errors (file, line, error code, message) and provide concrete fix suggestions: ``` -goto_stage("coding", "检查失败:项目结构不完整。缺少必需文件: -- [list missing files here] +goto_stage("coding", "构建失败: -这是一个Web项目,必须包含: -1. package.json(包含依赖和scripts) -2. index.html(入口HTML文件) -3. src/main.jsx 或 src/main.js(主入口脚本) -4. vite.config.js 或其他构建配置文件 +## 错误列表 +### 错误1: : +- 类型: +- 描述: +- 修复建议: -请在Coding阶段补充这些缺失的文件。") +请修复以上编译错误后重新执行。") ``` -### For Node.js Tool/Backend: -**REQUIRED FILES:** -- [ ] `package.json` - MUST exist with "bin" entry (for CLI tools) -- [ ] Main entry (`src/index.js` or `index.js`) - MUST exist - -### For Rust Projects: -**REQUIRED FILES:** -- [ ] `Cargo.toml` - MUST exist -- [ ] `src/main.rs` or `src/lib.rs` - MUST exist - -### For Python Projects: -**REQUIRED FILES:** -- [ ] `requirements.txt` or `pyproject.toml` - MUST exist -- [ ] Main entry (`main.py` or `src/__init__.py`) - MUST exist - -3. **IF STRUCTURE IS INCOMPLETE**: - - **IMMEDIATELY** call `goto_stage("coding", )` - - DO NOT proceed to README check - - DO NOT try to run any commands - - Provide specific list of missing files in the error message - -4. **ONLY IF STRUCTURE IS COMPLETE**: - - Proceed to Step 1 (Read README.md) - -# Workflow - AI 驱动的检查 - -## Step 1: 读取 README.md (After Step 0 validation passes) -1. 使用 `read_file("README.md")` 读取项目使用说明 -2. 如果 README.md 不存在: - - 使用 `goto_stage("coding", "检查失败:缺少 README.md 文件。请在 Coding 阶段生成 README.md,包含环境要求、依赖安装、运行命令等完整说明。")` - - STOP - -## Step 2: 分析 README 内容 -分析 README 中的内容,提取关键信息: -- **环境要求**:需要哪些软件或环境(如 Node.js、Python、Rust 版本) -- **依赖安装命令**:如何安装项目依赖(如 `npm install`, `pip install`, `cargo build`) -- **运行/构建命令**:如何启动或构建项目 -- **项目类型**:判断是静态网页、Node.js 项目、Rust 项目还是 Python 项目 - -## Step 3: 执行检查命令(自主决策) -根据 README 内容,**自主决定执行哪些检查命令**: - -### 如果 README 有"依赖安装"部分: -- 使用 `execute_shell_command(command, description)` 执行安装命令 -- 例如:`execute_shell_command("npm install", "Install Node.js dependencies")` -- 例如:`execute_shell_command("pip install -r requirements.txt", "Install Python dependencies")` -- 例如:`execute_shell_command("cargo build", "Build Rust project and download dependencies")` - -### 如果 README 有"构建命令"部分: -- 使用 `execute_shell_command(command, description)` 执行构建命令 -- 例如:`execute_shell_command("npm run build", "Build production bundle")` -- 例如:`execute_shell_command("cargo build --release", "Build release version")` - -### 如果是静态 HTML 项目(无构建命令): -- 使用 `list_files(".")` 验证关键文件存在 -- 检查 index.html, style.css, script.js 等文件 - -## Step 4: 分析结果并决策 - -### 成功场景(ALL checks PASS): -如果所有命令执行成功: -``` -✅ 检查通过: -- 依赖安装成功 -- 构建成功 -- 所有必需文件存在 -项目可以正常运行。 -``` -**⚠️ CRITICAL: 你必须立即调用 `save_check_report(content)` 保存报告!** -**不要只输出文本 - 必须调用工具!** - -### 🚨 失败场景(BUILD FAILURE): -**如果构建命令失败(TypeScript/编译错误),你 MUST 调用 goto_stage,不能只是保存报告!** - -``` -❌ 检查失败: -- 具体错误信息(包含文件名、行号、错误类型) -- 失败的命令 -- 修复建议 -``` - -**关键:构建失败时的正确处理顺序:** -1. 分析错误信息,提取:文件路径、行号、错误类型、错误描述 -2. 调用 `goto_stage("coding", <详细的错误信息和修复建议>)` -3. **不要**单独调用 `save_check_report`(goto_stage 会处理状态转换) -4. Pipeline 将返回 Coding 阶段修复错误 - -## Step 5: 保存检查报告(MANDATORY - CRITICAL!) -**这是强制步骤,必须在完成检查后执行!你不能跳过这一步!** - -### 如果检查全部通过: -**必须调用 `save_check_report(content)` 保存报告:** -``` -save_check_report("# Check Report - -## 项目信息 -- 项目类型: [Web/Node.js/Rust/Python/静态HTML] -- 检查时间: [timestamp] - -## 检查结果 -- 项目结构验证: ✅ -- 依赖安装: ✅ -- 构建验证: ✅ -- 文件完整性: ✅ - -## 详细说明 -[具体的检查过程和结果描述] - -## 结论 -✅ 检查通过,项目构建成功,可以正常运行。 -") -``` - -### 如果构建失败: -**必须调用 `goto_stage("coding", <错误信息>)` 返回 Coding 阶段修复:** -``` -goto_stage("coding", "构建失败:[详细错误信息和修复建议]") -``` +### DEPENDENCY INSTALL FAILS → goto_stage: +Report which dependency failed and why (not found, version conflict, etc.) with fix suggestions. -**⚠️ 注意**:如果不调用 `save_check_report()` 或 `goto_stage()`,Check 阶段将无法完成! +### STRUCTURE INCOMPLETE → goto_stage: +List missing files and what they should contain. # Tools -- read_file(path) ← 读取 README.md -- execute_shell_command(command, description, timeout?) ← 执行 README 中的命令 -- list_files(path) ← 验证文件存在性 -- get_plan() ← 查看任务状态 -- goto_stage(stage, reason) ← 返回修复建议 -- save_check_report(content) ← **MANDATORY** 保存检查报告(必须在完成检查后调用) +- `list_files(path)` — verify project structure +- `read_file(path)` — read README.md +- `run_command(command, description, timeout?)` — run install/build commands +- `get_plan()` — check task status (optional) +- `goto_stage(stage, reason)` — return to Coding with detailed feedback +- `save_check_report(content)` — save passing report -# Example 0 - 项目结构验证失败(新增示例) -``` -0. list_files(".") - → 只返回:README.md, src/App.jsx, src/components/Button.jsx - → 缺少:package.json, index.html, vite.config.js, src/main.jsx - -1. 分析:这是Web项目但缺少关键文件 - -2. goto_stage("coding", "检查失败:项目结构不完整。 - -缺少以下必需文件: -- package.json(依赖管理文件) -- index.html(入口HTML文件) -- vite.config.js(构建配置) -- src/main.jsx(主入口脚本) - -这是一个React Web项目,必须包含完整的项目结构。请补充这些文件: -1. package.json - 包含react、vite等依赖和dev/build脚本 -2. index.html - 包含
和script标签 -3. vite.config.js - 配置React插件 -4. src/main.jsx - ReactDOM.render入口代码") -``` - -# Example 1 - 成功检查(Node.js 项目) -``` -1. read_file("README.md") - → 内容显示需要 `npm install` 和 `npm run build` - -2. execute_shell_command("npm install", "Install dependencies") - → status: "success", stdout: "added 123 packages" - -3. execute_shell_command("npm run build", "Build project") - → status: "success", stdout: "built in 2.3s" - -4. save_check_report("# Check Report\n\n## Results\n- Dependencies: ✅ Installed\n- Build: ✅ Success\n\n## Conclusion\n项目可以正常运行。") - → status: "success" - -5. "✅ 检查通过:依赖安装成功,构建成功,项目可以正常运行。" -``` - -# Example 2 - 检查失败(缺少 package.json) -``` -1. read_file("README.md") - → 内容显示需要 `npm install` 和 `npm run build` +# Quick Examples -2. execute_shell_command("npm install", "Install dependencies") - → status: "failed", stderr: "ENOENT: no such file or package.json" - -3. 分析:缺少 package.json 文件 - -4. goto_stage("coding", "检查失败:缺少 package.json 文件。README 要求执行 'npm install',但项目根目录下没有 package.json。请在 Coding 阶段生成 package.json 文件并配置正确的依赖。") +**Static HTML (no build)**: ``` - -# Example 3 - 静态 HTML 项目 +list_files(".") → index.html, style.css, script.js exist +read_file("README.md") → confirms static site +save_check_report("# Check Report\n\n## Project Type\n静态网页\n\n## Files\n- index.html ✅\n- style.css ✅\n- script.js ✅\n\n## Conclusion\nAll files present.") ``` -1. read_file("README.md") - → 内容是静态网页,无需安装依赖,只需在浏览器中打开 index.html -2. list_files(".") - → 找到 index.html, style.css, script.js - -3. save_check_report("# Check Report\n\n## Project Type\n静态网页项目\n\n## Files Verified\n- index.html ✅\n- style.css ✅\n- script.js ✅\n\n## Conclusion\n所有必需文件存在,可以直接在浏览器中打开 index.html。") - → status: "success" - -4. "✅ 检查通过:静态网页项目,所有必需文件存在,可以直接在浏览器中打开 index.html。" +**Build success**: ``` - -# Example 4 - 依赖安装失败 +list_files(".") → structure OK +read_file("README.md") → npm install && npm run build +run_command("npm install", "Install deps") → success +run_command("npm run build", "Build") → success +save_check_report("# Check Report\n\n## Results\n- Dependencies: ✅\n- Build: ✅\n\n## Conclusion\n项目构建成功。") ``` -1. read_file("README.md") - → 内容显示需要 `pip install -r requirements.txt` -2. execute_shell_command("pip install -r requirements.txt", "Install Python dependencies") - → status: "failed", stderr: "ERROR: Could not find a version that satisfies the requirement missing-package==1.0.0" - -3. 分析:requirements.txt 中有不存在的依赖 - -4. goto_stage("coding", "检查失败:依赖安装失败。错误信息:'ERROR: Could not find a version that satisfies the requirement missing-package==1.0.0'。请检查 requirements.txt 中的依赖名称和版本是否正确,移除不存在的依赖包。") +**Build failure**: ``` - -# Example 5 - 构建失败(TypeScript 编译错误)⭐ IMPORTANT +run_command("npm run build", "Build") → fails with TS errors +goto_stage("coding", "构建失败:TypeScript编译错误\n\n### src/services/auth.ts:15:2\n- TS2305: Module has no exported member 'User'\n- 修复建议: 在 types.ts 中添加并导出 User 接口") ``` -1. read_file("README.md") - → 内容显示需要 `npm install` 和 `npm run build` - -2. execute_shell_command("npm install", "Install dependencies") - → status: "success" - -3. execute_shell_command("npm run build", "Build project") - → status: "failed", stderr: - "src/services/performance-service.ts:12:2 - error TS2305: Module has no exported member 'ToolDefinition' - src/decorators/performance-monitor.ts:73:4 - error TS2722: Cannot invoke an object which is possibly 'undefined' - src/services/performance-service.ts:54:3 - error TS2722: Cannot invoke an object which is possibly 'undefined'" - -4. 分析:TypeScript 类型定义问题,共有 3 个编译错误 - -5. ❌ WRONG - 只保存报告,pipeline 继续执行(会导致 broken code 被 deploy): - save_check_report("# Check Report\n\n## Results\n- Build: ❌ Failed\n\n## Errors\n[...]") - // Pipeline continues to Delivery with broken code! - -6. ✅ CORRECT - 调用 goto_stage 返回 Coding 修复: - goto_stage("coding", "构建失败:TypeScript 编译错误,共 3 个错误需要修复: - - ## 错误列表 - - ### 错误 1: src/services/performance-service.ts:12:2 - - 类型: TS2305 - Module has no exported member 'ToolDefinition' - - 原因: 从 '../types/metrics.js' 导入 ToolDefinition,但该模块未导出此类型 - - 修复: 在 src/types/metrics.ts 中添加 ToolDefinition 类型定义并导出 - - ### 错误 2: src/decorators/performance-monitor.ts:73:4 - - 类型: TS2722 - Cannot invoke an object which is possibly 'undefined' - - 原因: 返回值可能为 undefined,但被直接返回 - - 修复: 添加类型守卫检查 result !== undefined - - ### 错误 3: src/services/performance-service.ts:54:3 - - 类型: TS2722 - Cannot invoke an object which is possibly 'undefined' - - 原因: 同错误 2 - - 修复: 添加适当的类型守卫 - - 请在 Coding 阶段修复这些 TypeScript 类型错误。") - - // Pipeline 返回 Coding 阶段,修复后再重新执行 Check -``` - -# Example 6 - TypeScript 类型错误(Evolution 迭代常见问题)⭐ -``` -场景:在已有项目基础上新增功能(Evolution 迭代),但新代码引用了不存在的类型 - -1. execute_shell_command("bun run build", "Build project") - → status: "failed", stderr: - "error TS2305: Module '../types/metrics.js' has no exported member 'ToolDefinition'" - -2. 分析:这是 Evolution 迭代,新代码导入的类型在基础迭代的类型文件中不存在 - -3. ✅ CORRECT: - goto_stage("coding", "构建失败:类型导入错误 - - 错误: src/services/performance-service.ts 第 12 行 - - 尝试从 '../types/metrics.js' 导入 'ToolDefinition' - - 但该类型未在 metrics.ts 中定义/导出 - - 这是 Evolution 迭代的常见问题。请检查: - 1. 使用 list_files('.') 查看现有项目结构 - 2. 使用 read_file() 检查 metrics.ts 的内容 - 3. 确认 ToolDefinition 是否已在其他地方定义(如 plugin-impl.ts) - 4. 选项 A: 在 metrics.ts 中添加并导出 ToolDefinition 类型 - 5. 选项 B: 从定义该类型的文件导入 - - 建议优先使用选项 A,将共享类型集中管理。") -``` - -# 核心原则 -- **项目结构验证优先**:在执行任何命令前,先验证必需文件是否存在 -- **README 是执行的依据**:AI 根据 README 自主决定如何检查,不依赖硬编码的规则 -- **灵活适应不同项目类型**:支持 Web、Node.js、Rust、Python 等多种项目类型 -- **提供具体的修复建议**:失败时不仅报告错误,还提供明确的修复方向和缺失文件清单 -- **自主决策**:AI 根据项目实际情况决定执行哪些检查命令 - -**REMEMBER: -1. **ALWAYS start with Step 0: Validate project structure using list_files()** -2. If structure incomplete, immediately goto_stage("coding") with detailed file list -3. Only after structure validation passes, proceed to Step 1: read_file("README.md") -4. Extract commands from README and execute them -5. Analyze results and provide specific feedback if failed -6. For static projects, verify file existence is sufficient -7. **🚨 CRITICAL: If BUILD FAILS (TypeScript/compilation errors), you MUST call goto_stage("coding", ...) - NEVER let broken code proceed to Delivery** -8. **🚨 CRITICAL: If ALL CHECKS PASS, you MUST call save_check_report(content) - your work is LOST without this tool call** -9. **⚠️ NEVER end without calling either save_check_report() OR goto_stage()** -"## -; +"##; diff --git a/crates/cowork-core/src/instructions/coding.rs b/crates/cowork-core/src/instructions/coding.rs index 3c8dcc7..f610571 100644 --- a/crates/cowork-core/src/instructions/coding.rs +++ b/crates/cowork-core/src/instructions/coding.rs @@ -5,15 +5,15 @@ pub const CODING_ACTOR_INSTRUCTION: &str = r#" You are Coding Actor. Implement or update ALL pending tasks by writing **SIMPLE, CLEAN** code. # Core Principle: SIMPLICITY & CORE FUNCTIONALITY ONLY -- **Simple code**: No complex patterns, no over-engineering, avoid abstractions -- **Minimal dependencies**: Use built-in features when possible, avoid npm/pip/cargo bloat +- **Simple code**: No over-engineering, avoid unnecessary abstractions +- **Minimal dependencies**: Use built-in features when possible, avoid unnecessary package bloat - **No tests**: Don't write test files (unless explicitly required in tasks) -- **No optimization**: Don't optimize performance (unless explicitly required) +- **No premature optimization**: Don't optimize performance unless there's a clear bottleneck - **No infrastructure code**: Don't write deployment/monitoring/logging code (unless explicitly required) -- **Clear structure**: Easy to understand, easy to modify +- **Clear structure**: Organize code logically into files/modules that match the feature structure - **Focus on core features**: Implement only what's needed to make features work -- **Avoid design patterns**: Don't use Singleton, Factory, Observer unless absolutely necessary -- **No defensive programming**: Don't add excessive error handling unless critical +- **Reasonable code organization**: Use straightforward structuring (e.g., separate modules/files for distinct features); avoid forcing every pattern but don't fear simple modularization +- **Basic error handling**: Handle errors that can reasonably occur (file I/O, API responses, null checks); use the language's standard error mechanisms (Result, try/catch, error returns). Don't add excessive nested error wrapping, but DO handle errors where operations can fail. # ⚠️ CRITICAL: COMPLETE PROJECT STRUCTURE (NEW - HIGHEST PRIORITY) **BEFORE implementing any feature, you MUST create ALL essential project files:** @@ -145,8 +145,8 @@ After creating essential files, verify: 7. 使用 `write_file("README.md", )` 保存 README ### Exit Condition -- When ALL tasks are marked as "completed" AND README.md is generated, stop immediately -- No need to wait for critic review +- When ALL tasks are marked as "completed" AND README.md is generated, you are done with your turn. +- The Critic will automatically review your work next. ## UPDATE MODE (增量更新 - 当 GotoStage 回退到此阶段时) @@ -208,33 +208,20 @@ After creating essential files, verify: 7. 完成!Critic 将审查更新后的代码 ``` -# Adaptive Task Management - NEW CAPABILITY +# Adaptive Task Management -During implementation, you may discover that the plan needs adjustments. You now have tools to handle this: +During implementation, you may discover that the plan needs adjustments: -## When to CREATE new tasks (create_task): -- You discover a missing dependency or prerequisite -- A task is too large and should be split into smaller pieces -- You find a new technical requirement not in the original plan -- Example: "Need to create API client before implementing feature X" +## When plan needs major changes: +- If you find missing prerequisites, incorrect task ordering, or fundamental design flaws, + call `goto_stage("plan", "Plan needs adjustment: ")` to return to planning. +- Be specific about what needs to change and why. -## When to UPDATE tasks (update_task): -- Task dependencies have changed during implementation -- Files to create have changed based on actual code structure -- Task description needs clarification based on what you learned -- Example: "Task X now depends on Task Y which wasn't originally planned" - -## When to DELETE tasks (delete_task): -- A task is no longer needed (duplicate or obsolete) -- The approach has changed making this task irrelevant -- A task was incorrectly planned and cannot be implemented -- Example: "This database migration task is not needed because we're using in-memory storage" - -## Guidelines for Task Management: -- **Be conservative**: Only modify tasks when truly necessary -- **Always provide reason**: Every create/update/delete must include a clear reason -- **Stay focused**: Don't over-plan; focus on what's needed for current implementation -- **Maintain consistency**: Keep task IDs, dependencies, and status aligned +## Guidelines: +- **Be conservative**: Only request plan changes when truly necessary +- **Stay focused**: Implement what's in the plan first +- **Use status updates**: Use `update_task_status(task_id, "completed")` to mark tasks done +- **Use feature status**: Use `update_feature_status(feature_id, "completed")` to mark features done ## Handle Critic Feedback (IF IN ITERATION 2+): **IMPORTANT**: In iterations after the first one, check the conversation history for Critic's feedback: @@ -261,21 +248,17 @@ During implementation, you may discover that the plan needs adjustments. You now - update_task_status(task_id, status) - Update task status - update_feature_status(feature_id, status) - Update feature status -## Task Management Tools -- create_task(title, description, feature_id, component_id, files_to_create, dependencies, acceptance_criteria) -- update_task(task_id, reason, title?, description?, dependencies?, files_to_create?, acceptance_criteria?) -- delete_task(task_id, reason) - # CRITICAL RULES ## For NEW MODE 1. Implement ALL pending tasks in one go -2. Keep code simple and straightforward - **avoid abstractions, design patterns, excessive error handling** +2. Keep code simple and straightforward - avoid unnecessary abstractions and over-engineering 3. No tests/optimization/infrastructure unless explicitly required -4. **Use minimal dependencies** - prefer standard library over external packages +4. **Use minimal dependencies** - prefer standard library over external packages when practical 5. Mark all tasks as completed when done -6. Stop immediately when all tasks are completed -7. **Don't refactor** - write code that works, not perfect code +6. When all tasks are done, end your turn so the Critic can review (do NOT call exit_loop yourself — that is the Critic's responsibility) +7. **Don't over-refactor** - write code that works and is readable; you may split code into files/modules for clarity, but avoid endless restructuring +8. Generate README.md ONCE on initial creation; don't regenerate it in UPDATE MODE unless feedback explicitly asks ## For UPDATE MODE - Fix only what's mentioned in feedback @@ -291,7 +274,7 @@ During implementation, you may discover that the plan needs adjustments. You now pub const CODING_CRITIC_INSTRUCTION: &str = r#" # Your Role -You are Coding Critic. Verify that Coding Actor completed ALL tasks. +You are Coding Critic. Verify that Coding Actor completed ALL tasks and code is functional. # Workflow - SIMPLE AND DIRECT @@ -304,42 +287,75 @@ You are Coding Critic. Verify that Coding Actor completed ALL tasks. - Use `list_files(".")` to see all files - Verify that expected files from task list exist 4. (Optional) Read a few key files to verify basic structure +5. (Optional) Run `check_tests()` if tests exist, or `run_command()` for build verification + +## Step 3: Decide -## Step 3: Respond -5. **Just respond with your assessment**: - - If good: "✅ All [N] tasks completed. Code structure looks reasonable." - - If issues: Describe what's wrong +### Decision Tree (MANDATORY - choose exactly one): + +**Case A — ALL checks pass (satisfied):** +- Call `exit_loop()` to signal satisfaction and exit the Actor-Critic loop early. +- Then respond with "✅ All [N] tasks completed. Code structure looks reasonable." + +**Case B — Critical/major issues found (broken builds, missing files, incomplete tasks):** +- Call `provide_feedback(stage, feedback_type, severity, details, suggested_fix)`. +- This records the feedback AND exits the loop so the executor retries the stage with the feedback. +- Use: + - stage: "coding" + - feedback_type: "quality_issue" for code problems, "missing_artifact" for missing files + - severity: "critical" for broken builds/missing files, "major" for incomplete tasks +- Then respond with your assessment of what needs to be fixed. + +**Case C — Minor issues only (small code quality issues, minor suggestions):** +- Do NOT call any tool. Just describe the minor issues in your response. +- The Actor will see your feedback via conversation history (IncludeContents::Default) in the next loop iteration and revise. +- The loop continues to the next iteration automatically. # Important Notes - **DON'T over-analyze**: This is a quick sanity check, not deep code review -- **DON't run tests**: Tests may not exist, don't try to run them -- **DON't check for optimizations**: Performance is not a concern here -- **If files are missing**: Describe which files are missing +- **Be specific**: Reference file paths, line numbers, task IDs when possible +- **If files are missing**: Mark as "missing_artifact" with critical severity +- **If tasks incomplete**: Mark as "quality_issue" with major severity # Tools - get_plan() ← **START HERE - Check task completion** - list_files(path) ← Verify files exist - read_file(path) ← Quick sanity check (optional) +- run_command(command, description) ← Run build/test commands (optional) +- check_tests() ← Check for test files (optional) +- provide_feedback(stage, feedback_type, severity, details, suggested_fix) ← Escalate critical/major issues (exits loop + executor retries) +- exit_loop() ← Call when satisfied to exit the loop early -# Example - Normal Case +# Example - Normal Case (Satisfied → exit_loop) ``` 1. get_plan() 2. # Returns: 5 tasks, all status="completed" 3. list_files(".") 4. # Returns: src/main.rs, src/auth.rs, src/db.rs -5. "✅ All 5 tasks completed. Code structure looks reasonable." +5. exit_loop() # Signal satisfaction, exit loop early +6. "✅ All 5 tasks completed. Code structure looks reasonable." ``` -# Example - If Issues Found +# Example - If Issues Found (Critical → provide_feedback escalates) ``` 1. get_plan() 2. # Returns: 5 tasks, but TASK-003 is "pending" -3. "❌ TASK-003 is not completed. Please finish implementing the authentication feature." +3. provide_feedback({ + "stage": "coding", + "feedback_type": "quality_issue", + "severity": "major", + "details": "TASK-003 (authentication feature) is still pending, not completed.", + "suggested_fix": "Complete the authentication feature implementation in src/auth.rs" + }) + # provide_feedback automatically exits the loop and triggers executor retry +4. "❌ TASK-003 is not completed. Please finish implementing the authentication feature." ``` -**REMEMBER**: +**REMEMBER**: - Start with `get_plan()` - check if all tasks are completed - Keep it simple - this is a quick check, not deep review -- If tasks are incomplete, say which ones need work +- If everything is good, call `exit_loop()` and say so +- If there are critical/major issues, call `provide_feedback` AND describe the problems +- For minor issues, just describe them in your response (no tool call) "#; \ No newline at end of file diff --git a/crates/cowork-core/src/instructions/design.rs b/crates/cowork-core/src/instructions/design.rs index 2239cf0..ca74f80 100644 --- a/crates/cowork-core/src/instructions/design.rs +++ b/crates/cowork-core/src/instructions/design.rs @@ -363,22 +363,32 @@ Before other checks, verify that architecture is SIMPLE and MINIMAL: ## Your Response -### If ALL checks pass: -- "✅ Design approved: [N] simple components covering all features, architecture follows minimal principles." -- Provide brief positive feedback on the architecture +### Decision Tree (MANDATORY - choose exactly one): -### If any check FAILS: -- Call `provide_feedback(stage="design", feedback_type, severity, details, suggested_fix)` with specific issues +**Case A — ALL checks pass (satisfied):** +- Call `exit_loop()` to signal satisfaction and exit the Actor-Critic loop early. +- Then respond with "✅ Design approved: [N] simple components covering all features, architecture follows minimal principles." +- Provide brief positive feedback on the architecture. + +**Case B — Critical/major issues found (empty data, missing artifacts, over-engineering, feature coverage gaps):** +- Call `provide_feedback(stage="design", feedback_type, severity, details, suggested_fix)`. +- This records the feedback AND exits the loop so the executor retries the stage with the feedback. - Use appropriate severity: - "critical" for empty data, missing artifacts, over-engineering - "major" for feature coverage issues - - "minor" for documentation issues +- Then describe the issues in your response. + +**Case C — Minor issues only (documentation issues, small suggestions):** +- Do NOT call any tool. Just describe the minor issues in your response. +- The Actor will see your feedback via conversation history (IncludeContents::Default) in the next loop iteration and revise. +- The loop continues to the next iteration automatically. # Tools Available - get_design() - Load design data - check_feature_coverage() - Verify all features covered - load_design_doc() - Verify design markdown document -- provide_feedback(stage="design", feedback_type, severity, details, suggested_fix) - Report issues +- provide_feedback(stage="design", feedback_type, severity, details, suggested_fix) - Escalate critical/major issues (exits loop + executor retries) +- exit_loop() - Call when satisfied to exit the loop early # Anti-Loop Examples diff --git a/crates/cowork-core/src/instructions/plan.rs b/crates/cowork-core/src/instructions/plan.rs index b5fa3b4..88d0691 100644 --- a/crates/cowork-core/src/instructions/plan.rs +++ b/crates/cowork-core/src/instructions/plan.rs @@ -353,22 +353,32 @@ Before other checks, verify that tasks focus on CORE functionality: ## Your Response -### If ALL checks pass: -- "✅ Plan approved: [N] simple tasks covering all features, no testing/optimization/deployment tasks." -- Provide brief positive feedback on the task breakdown +### Decision Tree (MANDATORY - choose exactly one): -### If any check FAILS: -- Call `provide_feedback(stage="plan", feedback_type, severity, details, suggested_fix)` with specific issues +**Case A — ALL checks pass (satisfied):** +- Call `exit_loop()` to signal satisfaction and exit the Actor-Critic loop early. +- Then respond with "✅ Plan approved: [N] simple tasks covering all features, no testing/optimization/deployment tasks." +- Provide brief positive feedback on the task breakdown. + +**Case B — Critical/major issues found (empty data, missing artifacts, prohibited task types, circular dependencies):** +- Call `provide_feedback(stage="plan", feedback_type, severity, details, suggested_fix)`. +- This records the feedback AND exits the loop so the executor retries the stage with the feedback. - Use appropriate severity: - "critical" for empty data, missing artifacts, prohibited task types - "major" for circular dependencies - - "minor" for documentation issues +- Then describe the issues in your response. + +**Case C — Minor issues only (documentation issues, small suggestions):** +- Do NOT call any tool. Just describe the minor issues in your response. +- The Actor will see your feedback via conversation history (IncludeContents::Default) in the next loop iteration and revise. +- The loop continues to the next iteration automatically. # Tools Available - get_plan() - Load plan data - check_task_dependencies() - Verify no circular dependencies - load_plan_doc() - Verify plan markdown document (MUST CALL THIS!) -- provide_feedback(stage="plan", feedback_type, severity, details, suggested_fix) - Report issues +- provide_feedback(stage="plan", feedback_type, severity, details, suggested_fix) - Escalate critical/major issues (exits loop + executor retries) +- exit_loop() - Call when satisfied to exit the loop early # Anti-Loop Examples diff --git a/crates/cowork-core/src/instructions/prd.rs b/crates/cowork-core/src/instructions/prd.rs index 5055ce0..25772d5 100644 --- a/crates/cowork-core/src/instructions/prd.rs +++ b/crates/cowork-core/src/instructions/prd.rs @@ -130,13 +130,6 @@ Note: Replace {ITERATION_ID} with the actual iteration ID provided in the prompt - add_feature(...) ← 用于新功能 - save_prd_doc(content) ← **Save updated PRD document (MANDATORY)** -## UPDATE MODE Tools -- update_requirement(id, title, description, priority, acceptance_criteria) -- update_feature(id, name, description, requirement_ids, completion_criteria) -- delete_requirement(id) -- create_requirement(...) ← 用于新需求 -- add_feature(...) ← 用于新功能 - # Important Principles ## For NEW MODE @@ -210,9 +203,22 @@ You are PRD Critic. Review the generated requirements. - Do they seem reasonable for the project scope? ## Step 4: Respond -5. **Just respond with your assessment**: - - If good: "✅ X requirements and Y features cover the project scope well. PRD document saved." - - If issues: Describe what's wrong + +### Decision Tree (MANDATORY - choose exactly one): + +**Case A — ALL checks pass (satisfied):** +- Call `exit_loop()` to signal satisfaction and exit the Actor-Critic loop early. +- Then respond with "✅ X requirements and Y features cover the project scope well. PRD document saved." + +**Case B — Critical/major issues found (missing artifact, empty data, etc.):** +- Call `provide_feedback(stage="prd", feedback_type, severity, details, suggested_fix)`. +- This records the feedback AND exits the loop so the executor retries the stage with the feedback. +- Then describe the issues in your response. + +**Case C — Minor issues only (suggestions, small improvements):** +- Do NOT call any tool. Just describe the minor issues in your response. +- The Actor will see your feedback via conversation history (IncludeContents::Default) in the next loop iteration and revise. +- The loop continues to the next iteration automatically. ## Important Notes @@ -228,18 +234,20 @@ Note: Replace {ITERATION_ID} with the actual iteration ID provided in the prompt - get_requirements() ← **START HERE - Get structured data** - load_prd_doc() ← **MANDATORY - Verify document was saved!** - load_idea() ← Load idea document if you need additional context -- provide_feedback(stage="prd", feedback_type, severity, details, suggested_fix) ← If issues found +- provide_feedback(stage="prd", feedback_type, severity, details, suggested_fix) ← Escalate critical/major issues (exits loop + executor retries) +- exit_loop() ← Call when satisfied to exit the loop early -# Example - Normal Case +# Example - Normal Case (Satisfied → exit_loop) ``` 1. get_requirements() 2. # Returns: 3 requirements, 3 features 3. load_prd_doc() 4. # Returns: PRD markdown content (success) -5. "✅ 3 requirements and 3 features cover core functionality well. PRD document saved." +5. exit_loop() # Signal satisfaction, exit loop early +6. "✅ 3 requirements and 3 features cover core functionality well. PRD document saved." ``` -# Example - Missing Artifact (Critical!) +# Example - Missing Artifact (Critical → provide_feedback escalates) ``` 1. get_requirements() 2. # Returns: 3 requirements, 3 features (looks good) @@ -253,6 +261,7 @@ Note: Replace {ITERATION_ID} with the actual iteration ID provided in the prompt details="PRD document (prd.md) was not saved. Only requirements.json exists.", suggested_fix="Call save_prd_doc(content) with the complete PRD markdown document." ) + # provide_feedback automatically exits the loop and triggers executor retry ``` # Anti-Loop Examples diff --git a/crates/cowork-core/src/interaction/mod.rs b/crates/cowork-core/src/interaction/mod.rs index 5eec139..fe3e6c9 100644 --- a/crates/cowork-core/src/interaction/mod.rs +++ b/crates/cowork-core/src/interaction/mod.rs @@ -7,7 +7,6 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; pub mod cli; -pub mod tauri; /// Message level for UI feedback #[derive(Debug, Clone, Copy, Serialize, Deserialize)] @@ -161,4 +160,3 @@ pub trait InteractiveBackend: Send + Sync { // Re-export implementations pub use cli::CliBackend; -pub use tauri::TauriBackend; diff --git a/crates/cowork-core/src/interaction/tauri.rs b/crates/cowork-core/src/interaction/tauri.rs deleted file mode 100644 index 7fab1cf..0000000 --- a/crates/cowork-core/src/interaction/tauri.rs +++ /dev/null @@ -1,92 +0,0 @@ -// Tauri implementation of InteractiveBackend (placeholder) -// Actual implementation will be in cowork-gui crate - -use super::{ - InputOption, InputResponse, InteractiveBackend, MessageContext, MessageLevel, ProgressInfo, -}; -use anyhow::Result; -use async_trait::async_trait; -use serde_json::Value; - -/// Tauri backend placeholder - will be properly implemented in cowork-gui crate -pub struct TauriBackend { - // event_bus removed in V2 -} - -impl TauriBackend { - pub fn new() -> Self { - Self {} - } -} - -#[async_trait] -impl InteractiveBackend for TauriBackend { - async fn show_message(&self, level: MessageLevel, content: String) { - // Tauri implementation will send events to frontend - println!("{} [Tauri]: {}", level.emoji(), content); - } - - async fn show_message_with_context( - &self, - level: MessageLevel, - content: String, - context: MessageContext, - ) { - // Display agent name prefix for better clarity - let prefix = match &context.stage_name { - Some(stage) => format!("[{}:{}]", context.agent_name, stage), - None => format!("[{}]", context.agent_name), - }; - println!("{} {} {}", level.emoji(), prefix, content); - } - - async fn send_streaming(&self, content: String, agent_name: &str, is_thinking: bool) { - let prefix = if is_thinking { "💭" } else { "📝" }; - println!("{} [{}] {}", prefix, agent_name, content); - } - - async fn send_tool_call(&self, tool_name: &str, _arguments: &Value, agent_name: &str) { - println!("🔧 [{}] Calling tool: {}", agent_name, tool_name); - } - - async fn send_tool_result( - &self, - tool_name: &str, - _result: &str, - success: bool, - agent_name: &str, - ) { - let status = if success { "✓" } else { "✗" }; - println!("{} [{}] Tool {} completed", status, agent_name, tool_name); - } - - async fn request_input( - &self, - _prompt: &str, - _options: Vec, - _initial_content: Option, - ) -> Result { - // Tauri implementation will send HITL request event and wait for response - // For now, return a placeholder - Ok(InputResponse::Cancel) - } - - async fn show_progress(&self, task_id: String, progress: ProgressInfo) { - // Tauri implementation will send progress event to frontend - let percentage = if progress.total > 0 { - (progress.current as f64 / progress.total as f64 * 100.0) as u32 - } else { - 0 - }; - println!( - "[Tauri Progress] [{}%] {}: {}/{}", - percentage, task_id, progress.current, progress.total - ); - } - - async fn submit_response(&self, request_id: String, response: String) -> Result<()> { - // Tauri implementation will handle async HITL responses - println!("[Tauri HITL] Response for {}: {}", request_id, response); - Ok(()) - } -} diff --git a/crates/cowork-core/src/llm/config.rs b/crates/cowork-core/src/llm/config.rs index 0bb2079..02e2e3c 100644 --- a/crates/cowork-core/src/llm/config.rs +++ b/crates/cowork-core/src/llm/config.rs @@ -255,7 +255,7 @@ mod tests { [llm] api_base_url = "http://localhost:8000/v1" api_key = "test-key" -model_name = "gpt-4" +model_name = "gpt-5" [embedding] api_base_url = "http://localhost:8001/v1" @@ -272,7 +272,7 @@ args = ["x", "opencode-ai", "acp"] let config: ModelConfig = toml::from_str(toml_content).unwrap(); assert_eq!(config.llm.api_base_url, "http://localhost:8000/v1"); assert_eq!(config.llm.api_key, "test-key"); - assert_eq!(config.llm.model_name, "gpt-4"); + assert_eq!(config.llm.model_name, "gpt-5"); assert!(config.coding_agent.enabled); assert_eq!(config.embedding.api_base_url, "http://localhost:8001/v1"); } @@ -283,7 +283,7 @@ args = ["x", "opencode-ai", "acp"] [llm] api_base_url = "http://localhost:8000/v1" api_key = "test-key" -model_name = "gpt-4" +model_name = "gpt-5" "#; let config: ModelConfig = toml::from_str(toml_content).unwrap(); diff --git a/crates/cowork-core/src/llm/mod.rs b/crates/cowork-core/src/llm/mod.rs index f86300d..530c07f 100644 --- a/crates/cowork-core/src/llm/mod.rs +++ b/crates/cowork-core/src/llm/mod.rs @@ -4,3 +4,24 @@ pub mod rate_limiter; pub use config::*; pub use rate_limiter::*; + +use std::sync::Arc; +use adk_core::Llm; +use std::sync::Mutex; + +static CURRENT_EXECUTION_LLM: Mutex>> = Mutex::new(None); + +pub fn set_execution_llm(client: Arc) { + let mut guard = CURRENT_EXECUTION_LLM.lock().unwrap(); + *guard = Some(client); +} + +pub fn get_execution_llm() -> Option> { + let guard = CURRENT_EXECUTION_LLM.lock().unwrap(); + guard.clone() +} + +pub fn clear_execution_llm() { + let mut guard = CURRENT_EXECUTION_LLM.lock().unwrap(); + *guard = None; +} diff --git a/crates/cowork-core/src/pipeline/executor/mod.rs b/crates/cowork-core/src/pipeline/executor/mod.rs index 781cd50..9164319 100644 --- a/crates/cowork-core/src/pipeline/executor/mod.rs +++ b/crates/cowork-core/src/pipeline/executor/mod.rs @@ -8,6 +8,8 @@ use std::sync::Arc; use crate::domain::{IterationStatus, Project}; use crate::interaction::{InteractiveBackend, MessageContext}; +use crate::llm::{set_execution_llm, clear_execution_llm, create_llm_client}; +use crate::llm::config::load_config; use crate::persistence::{IterationStore, ProjectStore}; use super::{PipelineContext, StageResult, get_stages_from_flow, get_flow_config, is_critical_stage}; @@ -46,20 +48,26 @@ impl IterationExecutor { Ok(iteration) } - /// Create a new Evolution iteration (based on previous iteration) + /// Create a new Evolution iteration (based on previous iteration). + /// + /// `inheritance` controls what files are copied from the base iteration. + /// Use `InheritanceMode::Partial` for typical incremental feature work + /// (copies code, not artifacts) and `InheritanceMode::Full` when you want + /// to start from an exact snapshot of the base iteration. pub fn create_evolution_iteration( &self, project: &mut Project, title: impl Into, description: impl Into, base_iteration_id: impl Into, + inheritance: crate::domain::InheritanceMode, ) -> anyhow::Result { let iteration = crate::domain::Iteration::create_evolution( project, title.into(), description.into(), base_iteration_id.into(), - crate::domain::InheritanceMode::Full, + inheritance, ); self.iteration_store.save(&iteration)?; @@ -75,10 +83,33 @@ impl IterationExecutor { project: &mut Project, iteration_id: &str, resume_stage: Option, - _model: Option>, + model: Option>, ) -> anyhow::Result<()> { let mut iteration = self.iteration_store.load(iteration_id)?; + let model = match model { + Some(m) => m, + None => { + let llm_config = load_config()?; + create_llm_client(&llm_config.llm)? + } + }; + set_execution_llm(model.clone()); + + let result = self.execute_inner(project, &mut iteration, resume_stage, model).await; + + clear_execution_llm(); + result + } + + async fn execute_inner( + &self, + project: &mut Project, + iteration: &mut crate::domain::Iteration, + resume_stage: Option, + _model: Arc, + ) -> anyhow::Result<()> { + // Prepare workspace let workspace = workspace::prepare_workspace( &self.iteration_store, @@ -98,7 +129,7 @@ impl IterationExecutor { let stages = get_stages_from_flow(&start_stage); let flow_config = get_flow_config(); - println!( + tracing::info!( "[Executor] Using Flow config: stop_on_failure={}, memory_scope={:?}", flow_config.stop_on_failure, flow_config.memory_scope ); @@ -107,15 +138,15 @@ impl IterationExecutor { iteration.start(); self.iteration_store.save(&iteration)?; self.project_store - .set_current_iteration(project, iteration_id.to_string())?; + .set_current_iteration(project, iteration.id.clone())?; // Ensure iteration memory exists let memory_store = crate::persistence::MemoryStore::new(); - if let Err(e) = memory_store.ensure_iteration_memory(iteration_id) { - println!("[Executor] Warning: Failed to create iteration memory: {}", e); + if let Err(e) = memory_store.ensure_iteration_memory(&iteration.id) { + tracing::warn!("[Executor] Failed to create iteration memory: {}", e); } - println!( + tracing::info!( "[Executor] Iteration '{}' started, will execute {} stages starting from '{}'", iteration.title, stages.len(), @@ -136,15 +167,19 @@ impl IterationExecutor { // Evolution iteration: Inject project knowledge if iteration.base_iteration_id.is_some() { if let Err(e) = knowledge::inject_project_knowledge(&self.iteration_store, &iteration).await { - println!("[Executor] Warning: Failed to inject project knowledge: {}", e); + tracing::warn!("[Executor] Failed to inject project knowledge: {}", e); } } - println!("[Executor] Starting stage execution loop..."); - self.execute_stages_from(project, &mut iteration, stages, workspace, flow_config).await + tracing::info!("[Executor] Starting stage execution loop..."); + self.execute_stages_from(project, iteration, stages, workspace, flow_config, 0).await } - /// Execute stages starting from a given list + /// Execute stages starting from a given list. + /// + /// `goto_depth` tracks how many times we have jumped backwards via + /// `goto_stage`. It protects against unbounded recursion when a fix does + /// not resolve the underlying issue (e.g. Check -> Coding -> Check loop). async fn execute_stages_from( &self, project: &mut Project, @@ -152,10 +187,12 @@ impl IterationExecutor { stages: Vec>, workspace: std::path::PathBuf, flow_config: crate::config_definition::flow_definition::FlowConfig, + goto_depth: u32, ) -> anyhow::Result<()> { const MAX_STAGE_RETRIES: u32 = 3; const RETRY_DELAY_MS: u64 = 5000; const MAX_FEEDBACK_LOOPS: u32 = 5; + const MAX_GOTO_DEPTH: u32 = 10; let total_stages = stages.len(); let ctx = PipelineContext::new(project.clone(), iteration.clone(), workspace.clone()); @@ -169,7 +206,7 @@ impl IterationExecutor { iteration.set_stage(&stage_name); self.iteration_store.save(&iteration)?; - println!("[Executor] Stage updated: {} (iteration: {})", stage_name, iteration.id); + tracing::info!("[Executor] Stage updated: {} (iteration: {})", stage_name, iteration.id); self.interaction .show_message_with_context( @@ -189,7 +226,7 @@ impl IterationExecutor { for attempt in 0..MAX_STAGE_RETRIES { if attempt > 0 { - println!( + tracing::info!( "[Executor] Retrying stage '{}' (attempt {}/{})", stage_name, attempt + 1, MAX_STAGE_RETRIES ); @@ -217,9 +254,14 @@ impl IterationExecutor { .filter(|f| f.stage == stage_name) .max_by_key(|f| f.timestamp) { - tracing::info!("[Executor] Found stored feedback for stage '{}': {}", + tracing::info!("[Executor] Found stored feedback for stage '{}': {}", stage_name, fb.details.chars().take(100).collect::()); current_feedback = Some(fb.details.clone()); + // Consume the feedback immediately so it is not re-applied on a + // later attempt or a subsequent run of this stage. + if let Err(e) = crate::persistence::clear_stage_feedback(&stage_name) { + tracing::warn!("Failed to clear consumed feedback for stage '{}': {}", stage_name, e); + } } } @@ -245,22 +287,31 @@ impl IterationExecutor { ) .await; - if let Err(e) = crate::persistence::clear_stage_feedback(&stage_name) { - eprintln!("[Warning] Failed to clear feedback for stage '{}': {}", stage_name, e); + // The feedback stored by goto_stage is for the target + // stage; it will be consumed when that stage starts. + // We no longer clear the current stage's feedback here. + + if goto_depth >= MAX_GOTO_DEPTH { + anyhow::bail!( + "Maximum goto stage depth ({}) reached. Stage '{}' keeps requesting jumps to '{}'. Last reason: {}", + MAX_GOTO_DEPTH, stage_name, target_stage, reason + ); } iteration.set_stage(&target_stage); self.iteration_store.save(&iteration)?; let new_stages = get_stages_from_flow(&target_stage); - + self.interaction .show_message_with_context( crate::interaction::MessageLevel::Info, format!( - "Restarting pipeline from '{}' stage with {} stages to execute", + "Restarting pipeline from '{}' stage with {} stages to execute (goto depth {}/{})", target_stage, - new_stages.len() + new_stages.len(), + goto_depth + 1, + MAX_GOTO_DEPTH ), MessageContext::new("Pipeline Controller"), ) @@ -272,6 +323,7 @@ impl IterationExecutor { new_stages, workspace.clone(), flow_config.clone(), + goto_depth + 1, )).await; } StageResult::Success(artifact_path) => { @@ -295,7 +347,7 @@ impl IterationExecutor { } if let Err(e) = crate::persistence::clear_stage_feedback(&stage_name) { - eprintln!("[Warning] Failed to clear feedback for stage '{}': {}", stage_name, e); + tracing::warn!("Failed to clear feedback for stage '{}': {}", stage_name, e); } iteration.complete_stage(&stage_name, artifact_path.clone()); @@ -402,6 +454,7 @@ impl IterationExecutor { } StageResult::Failed(e) => { last_error = Some(e.clone()); + tracing::error!("[Executor] Stage '{}' failed: {}", stage_name, e); self.interaction .show_message_with_context( crate::interaction::MessageLevel::Error, @@ -471,7 +524,7 @@ impl IterationExecutor { // Promote iteration insights to project decisions if let Err(e) = crate::persistence::MemoryStore::new().promote_insights_to_decisions(&iteration.id) { - println!("[Executor] Warning: Failed to promote insights: {}", e); + tracing::warn!("[Executor] Failed to promote insights: {}", e); } project.current_iteration_id = Some(iteration.id.clone()); @@ -497,7 +550,7 @@ impl IterationExecutor { ) -> anyhow::Result<()> { let mut iteration = self.iteration_store.load(iteration_id)?; - println!( + tracing::info!( "[Executor] Continuing iteration '{}' (status: {:?}, current_stage: {:?})", iteration_id, iteration.status, iteration.current_stage ); @@ -507,7 +560,7 @@ impl IterationExecutor { } let resume_stage = iteration.current_stage.clone(); - println!("[Executor] Resuming from stage: {:?}", resume_stage); + tracing::info!("[Executor] Resuming from stage: {:?}", resume_stage); iteration.resume(); self.iteration_store.save(&iteration)?; @@ -533,10 +586,11 @@ impl IterationExecutor { &self, project: &mut Project, iteration_id: &str, + model: Option>, ) -> anyhow::Result<()> { let mut iteration = self.iteration_store.load(iteration_id)?; - println!( + tracing::info!( "[Executor] Retrying failed iteration '{}' (status: {:?}, current_stage: {:?})", iteration_id, iteration.status, iteration.current_stage ); @@ -548,7 +602,7 @@ impl IterationExecutor { let retry_stage = if let Some(ref current) = iteration.current_stage { current.clone() } else { - println!("[Executor] No current_stage found, defaulting to 'check' for retry"); + tracing::warn!("[Executor] No current_stage found, defaulting to 'check' for retry"); "check".to_string() }; @@ -563,7 +617,7 @@ impl IterationExecutor { ) .await; - self.execute(project, iteration_id, Some(retry_stage), None).await + self.execute(project, iteration_id, Some(retry_stage), model).await } // ======================================================================== diff --git a/crates/cowork-core/src/pipeline/mod.rs b/crates/cowork-core/src/pipeline/mod.rs index a102018..00cf60f 100644 --- a/crates/cowork-core/src/pipeline/mod.rs +++ b/crates/cowork-core/src/pipeline/mod.rs @@ -1,7 +1,8 @@ // Unified Iteration Pipeline // Single entry point for all development cycles -use std::sync::Arc; +use std::sync::{Arc, Mutex}; +use std::sync::LazyLock; use crate::domain::{Iteration, Project}; use crate::interaction::InteractiveBackend; @@ -14,6 +15,24 @@ pub use executor::*; pub use stages::*; pub use stage_executor::*; +static GOTO_STAGE_SIGNAL: LazyLock>> = LazyLock::new(|| Mutex::new(None)); + +pub fn set_goto_stage_signal(stage: String, reason: String) { + if let Ok(mut guard) = GOTO_STAGE_SIGNAL.lock() { + *guard = Some((stage, reason)); + } +} + +pub fn take_goto_stage_signal() -> Option<(String, String)> { + GOTO_STAGE_SIGNAL.lock().ok().and_then(|mut g| g.take()) +} + +pub fn clear_goto_stage_signal() { + if let Ok(mut guard) = GOTO_STAGE_SIGNAL.lock() { + *guard = None; + } +} + /// Stage execution result #[derive(Debug)] pub enum StageResult { diff --git a/crates/cowork-core/src/pipeline/stage_executor.rs b/crates/cowork-core/src/pipeline/stage_executor.rs index da6249b..828e443 100644 --- a/crates/cowork-core/src/pipeline/stage_executor.rs +++ b/crates/cowork-core/src/pipeline/stage_executor.rs @@ -10,14 +10,27 @@ use crate::config::{get_language_instruction}; use crate::config_definition::{global_registry, create_agent_for_stage}; use crate::interaction::{InteractiveBackend, MessageContext}; -use crate::llm::{create_llm_client}; +use crate::llm::{create_llm_client, get_execution_llm}; use crate::llm::config::load_config; -use crate::pipeline::{PipelineContext, StageResult}; -use crate::persistence::set_iteration_id; +use crate::pipeline::{PipelineContext, StageResult, clear_goto_stage_signal, take_goto_stage_signal}; +use crate::persistence::{set_iteration_id, load_feedback_history}; +use crate::tools::set_current_agent_name; use adk_core::{Content, Event}; use futures::StreamExt; use std::sync::Arc; +fn check_event_for_goto_stage(event: &Event) -> Option<(String, String)> { + if event.actions.escalate { + if let Some(target) = event.actions.state_delta.get("goto_stage").and_then(|v| v.as_str()) { + let reason = event.actions.state_delta.get("goto_reason") + .and_then(|v| v.as_str()) + .unwrap_or("Stage jump requested"); + return Some((target.to_string(), reason.to_string())); + } + } + take_goto_stage_signal() +} + /// Map stage name to the corresponding save tool name fn get_save_tool_name(stage_name: &str) -> &'static str { match stage_name { @@ -71,38 +84,62 @@ fn get_display_name(agent_name: &str) -> String { } } +fn check_pending_critic_feedback(stage_name: &str) -> Option { + if let Ok(history) = load_feedback_history() { + if let Some(fb) = history.feedbacks.iter() + .filter(|f| f.stage == stage_name) + .max_by_key(|f| f.timestamp) + { + return Some(fb.details.clone()); + } + } + None +} + /// Execute a stage using real adk-rust Agent pub async fn execute_stage_with_instruction( + ctx: &PipelineContext, + interaction: Arc, + stage_name: &str, + instruction: &str, + feedback: Option<&str>, +) -> StageResult { + execute_stage_with_instruction_and_context(ctx, interaction, stage_name, instruction, feedback, None).await +} + +pub async fn execute_stage_with_instruction_and_context( ctx: &PipelineContext, interaction: Arc, stage_name: &str, _instruction: &str, feedback: Option<&str>, + extra_context: Option<&str>, ) -> StageResult { // Set iteration ID for data tools (V2 architecture) set_iteration_id(ctx.iteration.id.clone()); + clear_goto_stage_signal(); // Check for restart mode (GotoStage mechanism) - if let Ok(Some(session_meta)) = crate::persistence::load_session_meta() { - if let Some(restart_reason) = session_meta.restart_reason { - // This is a restart from a previous stage - interaction - .show_message( - crate::interaction::MessageLevel::Warning, - format!( - "🔄 RESTART MODE: Restarting {} stage due to: {}", - stage_name, restart_reason - ), - ) - .await; - - // Clear the restart reason after displaying it - if let Ok(mut meta) = crate::persistence::load_session_meta() { - if let Some(ref mut m) = meta { - m.restart_reason = None; - let _ = crate::persistence::save_session_meta(m); - } - } + if let Ok(Some(session_meta)) = crate::persistence::load_session_meta() + && let Some(restart_reason) = session_meta.restart_reason + { + // This is a restart from a previous stage + interaction + .show_message( + crate::interaction::MessageLevel::Warning, + format!( + "🔄 RESTART MODE: Restarting {} stage due to: {}", + stage_name, restart_reason + ), + ) + .await; + + // Clear the restart reason after displaying it + if let Ok(mut meta) = crate::persistence::load_session_meta() + && let Some(ref mut m) = meta + { + m.restart_reason = None; + let _ = crate::persistence::save_session_meta(m); } } @@ -125,10 +162,14 @@ pub async fn execute_stage_with_instruction( let artifact_filename = get_artifact_filename(stage_name); let artifact_path = artifact_filename.map(|f| artifacts_dir.join(f)); - // Load LLM client - let llm_config = load_config().map_err(|e| format!("Failed to load config: {}", e))?; - let model = create_llm_client(&llm_config.llm) - .map_err(|e| format!("Failed to create LLM client: {}", e))?; + // Load LLM client - reuse execution-scoped client if available, otherwise create new + let model = if let Some(cached) = get_execution_llm() { + cached + } else { + let llm_config = load_config().map_err(|e| format!("Failed to load config: {}", e))?; + create_llm_client(&llm_config.llm) + .map_err(|e| format!("Failed to create LLM client: {}", e))? + }; // Create agent using configuration registry let agent = create_agent_for_stage(stage_name, model, ctx.iteration.id.clone()) @@ -146,9 +187,10 @@ pub async fn execute_stage_with_instruction( // Get the actual agent name and map to user-friendly display name let internal_name = agent.name(); let display_name = get_display_name(internal_name); + set_current_agent_name(&display_name); // Build prompt with context - let prompt = build_prompt(ctx, stage_name, feedback); + let prompt = build_prompt(ctx, stage_name, feedback, extra_context); // Execute agent - send start notification with user-friendly name let status_msg = if feedback.is_some() { @@ -178,14 +220,8 @@ pub async fn execute_stage_with_instruction( Ok(s) => s, Err(e) => { let err_msg = format!("{}", e); - // Check if this is a goto_stage signal - if err_msg.starts_with("GOTO_STAGE:") { - // Parse the target stage and reason - let parts: Vec<&str> = err_msg.strip_prefix("GOTO_STAGE:").unwrap().splitn(2, ':').collect(); - if parts.len() == 2 { - let target_stage = parts[0].to_string(); - let reason = parts[1].to_string(); - + if err_msg.contains("GOTO_STAGE_REQUESTED") { + if let Some((target_stage, reason)) = take_goto_stage_signal() { interaction .show_message_with_context( crate::interaction::MessageLevel::Warning, @@ -193,7 +229,6 @@ pub async fn execute_stage_with_instruction( MessageContext::new(&display_name).with_stage(stage_name), ) .await; - return StageResult::GotoStage(target_stage, reason); } } @@ -211,19 +246,16 @@ pub async fn execute_stage_with_instruction( match result { Ok(event) => { event_count += 1; - // Extract content from the event using the event's content() method if let Some(content) = event.content() { if let Some(text) = extract_text_from_content(content) { if !text.trim().is_empty() { text_event_count += 1; generated_text.push_str(&text); - // Send content in real-time with display name interaction .send_streaming(text.clone(), &display_name, false) .await; } } else { - // Content exists but no text part — likely a function call tool_call_count += 1; tracing::debug!( "[StageExecutor] Event #{} has content but no text part (likely tool call)", @@ -231,37 +263,36 @@ pub async fn execute_stage_with_instruction( ); } } else if let Some(text) = extract_text_from_event(&event) { - // Fallback: use helper function if !text.trim().is_empty() { text_event_count += 1; generated_text.push_str(&text); interaction.send_streaming(text, &display_name, false).await; } } + + if let Some((target_stage, reason)) = check_event_for_goto_stage(&event) { + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Warning, + format!("🔄 Stage jump requested: {} → {}", stage_name, target_stage), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::GotoStage(target_stage, reason); + } } Err(e) => { let err_msg = format!("{}", e); - // Check if this is a goto_stage signal from a tool - if err_msg.contains("GOTO_STAGE:") { - // Extract the GOTO_STAGE message - format: "Tool execution failed: GOTO_STAGE:stage:reason" - // or just "GOTO_STAGE:stage:reason" - if let Some(goto_msg) = err_msg.split("GOTO_STAGE:").nth(1) { - let parts: Vec<&str> = goto_msg.splitn(2, ':').collect(); - if parts.len() == 2 { - let target_stage = parts[0].to_string(); - let reason = parts[1].to_string(); - - interaction - .show_message_with_context( - crate::interaction::MessageLevel::Warning, - format!("🔄 Stage jump requested: {} → {}", stage_name, target_stage), - MessageContext::new(&display_name).with_stage(stage_name), - ) - .await; - - // Return immediately to trigger stage jump - return StageResult::GotoStage(target_stage, reason); - } + if err_msg.contains("GOTO_STAGE_REQUESTED") { + if let Some((target_stage, reason)) = take_goto_stage_signal() { + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Warning, + format!("🔄 Stage jump requested: {} → {}", stage_name, target_stage), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::GotoStage(target_stage, reason); } } @@ -281,27 +312,47 @@ pub async fn execute_stage_with_instruction( // Check if the agent saved the artifact via a tool call (e.g., save_idea) // even though it didn't produce any text output in the stream if let Some(ref path) = artifact_path { - if path.exists() { - if let Ok(content) = std::fs::read_to_string(path) { - if !content.trim().is_empty() { - tracing::info!( - "[StageExecutor] Agent produced no text in stream, but artifact was saved via tool call ({:?}, {} chars)", - path, content.len() - ); - // Artifact exists and has content — stage is successful - interaction - .show_message_with_context( - crate::interaction::MessageLevel::Success, - format!("✓ Completed (artifact saved via tool, {} chars)", content.len()), - MessageContext::new(&display_name).with_stage(stage_name), - ) - .await; - return StageResult::Success(Some(path.to_string_lossy().to_string())); - } + if path.exists() + && let Ok(content) = std::fs::read_to_string(path) + && !content.trim().is_empty() + { + if let Err(e) = validate_artifact_content(stage_name, &content) { + return StageResult::Failed(e); } + tracing::info!( + "[StageExecutor] Agent produced no text in stream, but artifact was saved via tool call ({:?}, {} chars)", + path, content.len() + ); + if let Some(feedback_msg) = check_pending_critic_feedback(stage_name) { + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Warning, + format!("🔄 Critic found issues, triggering revision..."), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::NeedsRevision(feedback_msg); + } + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Success, + format!("✓ Completed (artifact saved via tool, {} chars)", content.len()), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::Success(Some(path.to_string_lossy().to_string())); } } else { - // No artifact expected (e.g., coding stage) — just check that agent ran + if let Some(feedback_msg) = check_pending_critic_feedback(stage_name) { + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Warning, + format!("🔄 Critic found issues, triggering revision..."), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::NeedsRevision(feedback_msg); + } tracing::info!( "[StageExecutor] Stage '{}' has no artifact file, treating empty output as acceptable", stage_name @@ -336,6 +387,16 @@ pub async fn execute_stage_with_instruction( let artifact_path = match artifact_path { Some(p) => p, None => { + if let Some(feedback_msg) = check_pending_critic_feedback(stage_name) { + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Warning, + format!("🔄 Critic found issues, triggering revision..."), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::NeedsRevision(feedback_msg); + } tracing::info!( "[StageExecutor] Stage '{}' has no artifact file, text output is sufficient", stage_name @@ -344,17 +405,28 @@ pub async fn execute_stage_with_instruction( } }; - // Check if artifact was saved via tool call (e.g., save_idea) - if artifact_path.exists() { - if let Ok(content) = std::fs::read_to_string(&artifact_path) { - if !content.trim().is_empty() { - tracing::info!( - "[StageExecutor] Artifact saved via tool call ({:?}, {} chars)", - artifact_path, content.len() - ); - return StageResult::Success(Some(artifact_path.to_string_lossy().to_string())); - } + if artifact_path.exists() + && let Ok(content) = std::fs::read_to_string(&artifact_path) + && !content.trim().is_empty() + { + if let Err(e) = validate_artifact_content(stage_name, &content) { + return StageResult::Failed(e); } + if let Some(feedback_msg) = check_pending_critic_feedback(stage_name) { + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Warning, + format!("🔄 Critic found issues, triggering revision..."), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::NeedsRevision(feedback_msg); + } + tracing::info!( + "[StageExecutor] Artifact saved via tool call ({:?}, {} chars)", + artifact_path, content.len() + ); + return StageResult::Success(Some(artifact_path.to_string_lossy().to_string())); } // Agent produced text output but didn't call the save tool. @@ -391,6 +463,19 @@ pub async fn execute_stage_with_instruction( let followup_stream = match agent.run(followup_ctx).await { Ok(s) => s, Err(e) => { + let err_msg = format!("{}", e); + if err_msg.contains("GOTO_STAGE_REQUESTED") { + if let Some((target_stage, reason)) = take_goto_stage_signal() { + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Warning, + format!("🔄 Stage jump requested: {} → {}", stage_name, target_stage), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::GotoStage(target_stage, reason); + } + } tracing::warn!("[StageExecutor] Follow-up agent run failed: {}", e); return StageResult::Failed(format!( "Agent completed but did not save artifact, and follow-up failed: {}", e @@ -398,44 +483,76 @@ pub async fn execute_stage_with_instruction( } }; - // Process follow-up stream (only look for save tool execution, don't collect text) let mut followup_stream = std::pin::pin!(followup_stream); while let Some(result) = followup_stream.next().await { match result { Ok(event) => { - // Stream any text content from the follow-up - if let Some(content) = event.content() { - if let Some(text) = extract_text_from_content(content) { - if !text.trim().is_empty() { - interaction.send_streaming(text, &display_name, false).await; - } - } + if let Some(content) = event.content() + && let Some(text) = extract_text_from_content(content) + && !text.trim().is_empty() + { + interaction.send_streaming(text, &display_name, false).await; + } + if let Some((target_stage, reason)) = check_event_for_goto_stage(&event) { + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Warning, + format!("🔄 Stage jump requested: {} → {}", stage_name, target_stage), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::GotoStage(target_stage, reason); } } Err(e) => { + let err_msg = format!("{}", e); + if err_msg.contains("GOTO_STAGE_REQUESTED") { + if let Some((target_stage, reason)) = take_goto_stage_signal() { + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Warning, + format!("🔄 Stage jump requested: {} → {}", stage_name, target_stage), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::GotoStage(target_stage, reason); + } + } tracing::debug!("[StageExecutor] Follow-up stream error: {}", e); } } } // Check artifact again after follow-up - if artifact_path.exists() { - if let Ok(content) = std::fs::read_to_string(&artifact_path) { - if !content.trim().is_empty() { - tracing::info!( - "[StageExecutor] Artifact saved after follow-up ({:?}, {} chars)", - artifact_path, content.len() - ); - interaction - .show_message_with_context( - crate::interaction::MessageLevel::Success, - format!("✓ Artifact saved ({} chars)", content.len()), - MessageContext::new(&display_name).with_stage(stage_name), - ) - .await; - return StageResult::Success(Some(artifact_path.to_string_lossy().to_string())); - } + if artifact_path.exists() + && let Ok(content) = std::fs::read_to_string(&artifact_path) + && !content.trim().is_empty() + { + if let Err(e) = validate_artifact_content(stage_name, &content) { + return StageResult::Failed(e); } + if let Some(feedback_msg) = check_pending_critic_feedback(stage_name) { + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Warning, + format!("🔄 Critic found issues, triggering revision..."), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::NeedsRevision(feedback_msg); + } + tracing::info!( + "[StageExecutor] Artifact saved after follow-up ({:?}, {} chars)", + artifact_path, content.len() + ); + interaction + .show_message_with_context( + crate::interaction::MessageLevel::Success, + format!("✓ Artifact saved ({} chars)", content.len()), + MessageContext::new(&display_name).with_stage(stage_name), + ) + .await; + return StageResult::Success(Some(artifact_path.to_string_lossy().to_string())); } tracing::warn!( @@ -448,8 +565,13 @@ pub async fn execute_stage_with_instruction( )) } -/// Maximum characters for pre-injected artifacts (to avoid token limits) -const MAX_ARTIFACT_CHARS: usize = 3000; +/// Maximum characters for fully pre-injected artifacts. +/// Artifacts below this size are injected in their entirety. +const FULL_INJECTION_MAX_CHARS: usize = 12000; + +/// Maximum characters for artifact previews when the artifact is too large +/// to inject fully. The agent MUST use the load tool to get the full content. +const PREVIEW_MAX_CHARS: usize = 2000; /// Get truncated message in current language fn get_truncated_message() -> String { @@ -463,7 +585,9 @@ fn get_truncated_message() -> String { } } -/// Truncate content to a maximum number of characters (UTF-8 safe) +/// Truncate content to a maximum number of characters (UTF-8 safe). +/// This is kept for backward compatibility; new code should use +/// `format_artifact_block` so callers can distinguish full vs. preview content. fn truncate_content(content: &str, max_chars: usize) -> String { if content.chars().count() <= max_chars { content.to_string() @@ -473,6 +597,42 @@ fn truncate_content(content: &str, max_chars: usize) -> String { } } +/// Format an artifact block for injection into the agent prompt. +/// +/// - If the artifact is small enough, inject it in full and tell the agent it +/// is pre-loaded. +/// - If it is too large, inject only a preview and explicitly instruct the +/// agent to use `load_tool` to read the complete document. This prevents +/// the agent from making decisions on a silently truncated artifact. +fn format_artifact_block(label: &str, content: &str, load_tool: &str) -> String { + let char_count = content.chars().count(); + + if char_count <= FULL_INJECTION_MAX_CHARS { + format!( + "═══════════════════════════════════════════════════════════════\n\ + 📋 PRE-LOADED: {} ({} characters, complete)\n\ + ═══════════════════════════════════════════════════════════════\n\ + {}\n\ + ═══════════════════════════════════════════════════════════════\n\n", + label, char_count, content + ) + } else { + let preview: String = content.chars().take(PREVIEW_MAX_CHARS).collect(); + format!( + "═══════════════════════════════════════════════════════════════\n\ + 📋 PREVIEW: {} ({} characters total — ONLY FIRST {} CHARACTERS SHOWN)\n\ + ═══════════════════════════════════════════════════════════════\n\ + {}\n\ + ...[TRUNCATED]\n\ + ═══════════════════════════════════════════════════════════════\n\ + ⚠️ CRITICAL: The full {} is too large to pre-load. You MUST call `{}` \ + to read the complete document before making any decisions. Do NOT assume \ + the preview above contains all requirements or details.\n\n", + label, char_count, PREVIEW_MAX_CHARS, preview, label, load_tool + ) + } +} + /// Load artifact content from the artifacts directory fn load_artifact_content(ctx: &PipelineContext, artifact_name: &str) -> Option { let iteration_dir = ctx.workspace_path.parent().unwrap_or(&ctx.workspace_path); @@ -490,7 +650,12 @@ fn load_artifact_content(ctx: &PipelineContext, artifact_name: &str) -> Option) -> String { +fn build_prompt( + ctx: &PipelineContext, + stage_name: &str, + feedback: Option<&str>, + extra_context: Option<&str>, +) -> String { let mut prompt = format!( "You are working on iteration #{} - '{}'.\n", ctx.iteration.number, ctx.iteration.title @@ -510,7 +675,7 @@ fn build_prompt(ctx: &PipelineContext, stage_name: &str, feedback: Option<&str>) prompt.push_str("═══════════════════════════════════════════════════════════════\n"); prompt.push_str("🚨🚨🚨 CRITICAL: THIS IS AN EVOLUTION ITERATION 🚨🚨🚨\n"); prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str("\n"); + prompt.push('\n'); prompt.push_str("⚠️ DO NOT CREATE NEW PROJECT - BUILD ON EXISTING CODE ⚠️\n\n"); prompt.push_str(&format!("Base Iteration: {}\n", base_id)); prompt.push_str(&format!("Inheritance Mode: {}\n\n", inheritance_mode_name)); @@ -565,85 +730,62 @@ fn build_prompt(ctx: &PipelineContext, stage_name: &str, feedback: Option<&str>) prompt.push_str("\n═══════════════════════════════════════════════════════════════\n\n"); // Pre-inject artifacts from previous stages (Optimization: reduces tool calls) - let mut injected_artifacts = Vec::new(); + // We also track which artifacts were injected only as previews so the agent + // knows it must load the full document before making decisions. + let mut injected_artifacts: Vec<&'static str> = Vec::new(); + let mut preview_artifacts: Vec<&'static str> = Vec::new(); + + // Helper to inject an artifact and track whether it was a preview. + let mut inject = |filename: &'static str, label: &'static str, load_tool: &'static str| { + if let Some(content) = load_artifact_content(ctx, filename) { + let was_preview = content.chars().count() > FULL_INJECTION_MAX_CHARS; + prompt.push_str(&format_artifact_block(label, &content, load_tool)); + injected_artifacts.push(filename); + if was_preview { + preview_artifacts.push(filename); + } + } + }; match stage_name { "prd" => { // PRD needs Idea - if let Some(idea) = load_artifact_content(ctx, "idea.md") { - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str("📋 PRE-LOADED: Idea Document (from previous stage)\n"); - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str(&truncate_content(&idea, MAX_ARTIFACT_CHARS)); - prompt.push_str("\n═══════════════════════════════════════════════════════════════\n\n"); - injected_artifacts.push("idea.md"); - } + inject("idea.md", "Idea Document (from previous stage)", "load_idea()"); } "design" => { // Design needs PRD - if let Some(prd) = load_artifact_content(ctx, "prd.md") { - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str("📋 PRE-LOADED: PRD Document (from previous stage)\n"); - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str(&truncate_content(&prd, MAX_ARTIFACT_CHARS)); - prompt.push_str("\n═══════════════════════════════════════════════════════════════\n\n"); - injected_artifacts.push("prd.md"); - } + inject("prd.md", "PRD Document (from previous stage)", "load_prd_doc()"); } "plan" => { - // Plan needs Design and PRD - if let Some(design) = load_artifact_content(ctx, "design.md") { - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str("📋 PRE-LOADED: Design Document (from previous stage)\n"); - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str(&truncate_content(&design, MAX_ARTIFACT_CHARS)); - prompt.push_str("\n═══════════════════════════════════════════════════════════════\n\n"); - injected_artifacts.push("design.md"); - } + // Plan needs Design (PRD can be loaded if needed) + inject("design.md", "Design Document (from previous stage)", "load_design_doc()"); } "coding" => { - // Coding needs Plan (most important) and Design - if let Some(plan) = load_artifact_content(ctx, "plan.md") { - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str("📋 PRE-LOADED: Implementation Plan (from previous stage)\n"); - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str(&truncate_content(&plan, MAX_ARTIFACT_CHARS)); - prompt.push_str("\n═══════════════════════════════════════════════════════════════\n\n"); - injected_artifacts.push("plan.md"); - } - // Also include design for architecture context - if let Some(design) = load_artifact_content(ctx, "design.md") { - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str("📋 PRE-LOADED: Design Document (architecture reference)\n"); - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str(&truncate_content(&design, 2000)); // Smaller for coding - prompt.push_str("\n═══════════════════════════════════════════════════════════════\n\n"); - injected_artifacts.push("design.md"); - } + // Coding needs Plan (most important) and Design for architecture context + inject("plan.md", "Implementation Plan (from previous stage)", "load_plan_doc()"); + inject("design.md", "Design Document (architecture reference)", "load_design_doc()"); } "check" | "delivery" => { // Check and Delivery need all artifacts - let artifacts = [ - ("idea.md", "Idea Document"), - ("prd.md", "PRD Document"), - ("design.md", "Design Document"), - ("plan.md", "Implementation Plan"), - ]; - - for (filename, label) in artifacts { - if let Some(content) = load_artifact_content(ctx, filename) { - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str(&format!("📋 PRE-LOADED: {}\n", label)); - prompt.push_str("═══════════════════════════════════════════════════════════════\n"); - prompt.push_str(&truncate_content(&content, 2000)); // Smaller for all artifacts - prompt.push_str("\n═══════════════════════════════════════════════════════════════\n\n"); - injected_artifacts.push(filename); - } - } + inject("idea.md", "Idea Document", "load_idea()"); + inject("prd.md", "PRD Document", "load_prd_doc()"); + inject("design.md", "Design Document", "load_design_doc()"); + inject("plan.md", "Implementation Plan", "load_plan_doc()"); } _ => {} } + // Build a human-readable note about which artifacts were preview-only. + let preview_note = if preview_artifacts.is_empty() { + String::new() + } else { + format!( + " NOTE: The following pre-loaded documents were truncated previews: {}. \ + Use the corresponding load tool to read the full content before making decisions.", + preview_artifacts.join(", ") + ) + }; + // Provide stage-specific guidance match stage_name { "idea" => { @@ -660,12 +802,15 @@ fn build_prompt(ctx: &PipelineContext, stage_name: &str, feedback: Option<&str>) prompt.push_str("========================================\n"); prompt.push_str("STAGE: PRD (Product Requirements Document)\n"); prompt.push_str("========================================\n"); - if injected_artifacts.is_empty() { + if injected_artifacts.contains(&"idea.md") { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. Load idea using load_idea() tool\n"); + prompt.push_str(&format!( + "1. The Idea document is provided above (pre-loaded or preview).{}\n", + preview_note + )); } else { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. The Idea document is provided above (pre-loaded)\n"); + prompt.push_str("1. Load idea using load_idea() tool\n"); } prompt.push_str("2. Analyze the idea and create requirements\n"); prompt.push_str("3. SAVE PRD using save_prd_doc() tool (MANDATORY)\n\n"); @@ -674,12 +819,15 @@ fn build_prompt(ctx: &PipelineContext, stage_name: &str, feedback: Option<&str>) prompt.push_str("========================================\n"); prompt.push_str("STAGE: Design (System Architecture)\n"); prompt.push_str("========================================\n"); - if injected_artifacts.is_empty() { + if injected_artifacts.contains(&"prd.md") { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. Load requirements using get_requirements() tool\n"); + prompt.push_str(&format!( + "1. The PRD document is provided above (pre-loaded or preview).{}\n", + preview_note + )); } else { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. The PRD document is provided above (pre-loaded)\n"); + prompt.push_str("1. Load requirements using get_requirements() tool\n"); } prompt.push_str("2. Design system architecture (2-4 components max)\n"); prompt.push_str("3. SAVE DESIGN using save_design_doc() tool (MANDATORY)\n\n"); @@ -688,12 +836,15 @@ fn build_prompt(ctx: &PipelineContext, stage_name: &str, feedback: Option<&str>) prompt.push_str("========================================\n"); prompt.push_str("STAGE: Plan (Implementation Tasks)\n"); prompt.push_str("========================================\n"); - if injected_artifacts.is_empty() { + if injected_artifacts.contains(&"design.md") { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. Load design using get_design() tool\n"); + prompt.push_str(&format!( + "1. The Design document is provided above (pre-loaded or preview).{}\n", + preview_note + )); } else { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. The Design document is provided above (pre-loaded)\n"); + prompt.push_str("1. Load design using get_design() tool\n"); } prompt.push_str("2. Create 5-12 simple implementation tasks\n"); prompt.push_str("3. SAVE PLAN using save_plan_doc() tool (MANDATORY)\n\n"); @@ -702,12 +853,15 @@ fn build_prompt(ctx: &PipelineContext, stage_name: &str, feedback: Option<&str>) prompt.push_str("========================================\n"); prompt.push_str("STAGE: Coding (Implementation)\n"); prompt.push_str("========================================\n"); - if injected_artifacts.is_empty() { + if injected_artifacts.contains(&"plan.md") { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. Load plan using get_plan() tool\n"); + prompt.push_str(&format!( + "1. The Plan and Design documents are provided above (pre-loaded or preview).{}\n", + preview_note + )); } else { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. The Plan and Design documents are provided above (pre-loaded)\n"); + prompt.push_str("1. Load plan using get_plan() tool\n"); } prompt.push_str("2. Implement tasks one by one\n"); prompt.push_str("3. Update task status using update_task_status() tool\n\n"); @@ -716,12 +870,15 @@ fn build_prompt(ctx: &PipelineContext, stage_name: &str, feedback: Option<&str>) prompt.push_str("========================================\n"); prompt.push_str("STAGE: Check (Quality Assurance)\n"); prompt.push_str("========================================\n"); - if injected_artifacts.is_empty() { + if injected_artifacts.len() >= 4 { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. Load all artifacts (requirements, design, plan)\n"); + prompt.push_str(&format!( + "1. All artifacts are provided above (pre-loaded or preview).{}\n", + preview_note + )); } else { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. All artifacts are provided above (pre-loaded)\n"); + prompt.push_str("1. Load all artifacts (requirements, design, plan)\n"); } prompt.push_str("2. Run quality checks\n"); prompt.push_str("3. Use goto_stage() if issues found\n\n"); @@ -730,12 +887,15 @@ fn build_prompt(ctx: &PipelineContext, stage_name: &str, feedback: Option<&str>) prompt.push_str("========================================\n"); prompt.push_str("STAGE: Delivery (Final Report)\n"); prompt.push_str("========================================\n"); - if injected_artifacts.is_empty() { + if injected_artifacts.len() >= 4 { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. Load all artifacts\n"); + prompt.push_str(&format!( + "1. All artifacts are provided above (pre-loaded or preview).{}\n", + preview_note + )); } else { prompt.push_str("YOUR TASK:\n"); - prompt.push_str("1. All artifacts are provided above (pre-loaded)\n"); + prompt.push_str("1. Load all artifacts\n"); } prompt.push_str("2. Generate delivery report\n"); prompt.push_str("3. SAVE using save_delivery_report() tool\n"); @@ -770,6 +930,14 @@ fn build_prompt(ctx: &PipelineContext, stage_name: &str, feedback: Option<&str>) prompt.push_str("Please revise your previous work based on this feedback.\n"); } + if let Some(extra) = extra_context { + prompt.push_str("\n═══════════════════════════════════════════════════════════════\n"); + prompt.push_str("📋 ADDITIONAL CONTEXT FROM EXTERNAL AGENT FALLBACK\n"); + prompt.push_str("═══════════════════════════════════════════════════════════════\n"); + prompt.push_str(extra); + prompt.push_str("\n═══════════════════════════════════════════════════════════════\n"); + } + // Add language preference instruction let lang_instruction = get_language_instruction(); prompt.push_str(&format!("\n{}\n", lang_instruction)); @@ -805,17 +973,18 @@ impl SimpleInvocationContext { branch: "main".to_string(), user_content: content.clone(), agent, - // Memory and Artifacts are accessed via Tools (QueryMemoryTool, LoadArtifactTool, etc.) - // rather than through InvocationContext. This is intentional - tools provide more - // flexible access with proper validation and error handling. - memory: None, + // Memory and Artifacts are ALSO available through dedicated tools + // (QueryMemoryTool, LoadArtifactTool, etc.). We wire them into the + // InvocationContext so that framework callbacks, plugins, or future + // agents that rely on ctx.memory() / ctx.artifacts() work correctly. + memory: Some(Arc::new(SimpleMemory::new(&ctx.iteration.id))), session: Box::new(SimpleSession::new(&ctx.iteration.id, content.clone())), run_config: adk_core::RunConfig { streaming_mode: adk_core::StreamingMode::SSE, ..adk_core::RunConfig::default() }, ended: std::sync::atomic::AtomicBool::new(false), - artifacts: None, + artifacts: Some(Arc::new(SimpleArtifacts::new(&ctx.iteration.id))), } } } @@ -915,24 +1084,121 @@ impl adk_core::ReadonlyContext for SimpleInvocationContext { } } -/// Simple Session implementation + + +/// Simple Session implementation that persists conversation history to disk. +/// +/// History is stored as newline-delimited JSON in +/// `.cowork-v2/iterations/{session_id}/session_history.jsonl` so that retries, +/// actor-critic loops, and feedback revisions can see prior turns instead of +/// starting from scratch. +/// +/// Truncation: to prevent unbounded context growth across many retries / +/// feedback revisions, `conversation_history()` applies a sliding window of +/// `MAX_HISTORY_MESSAGES` messages (keeping the most recent ones, plus the +/// initial user prompt for context). Persistence is untouched; truncation is +/// only applied to the view returned to the LLM. struct SimpleSession { session_id: String, app_name: String, user_id: String, simple_state: SimpleState, - messages: Vec, + messages: std::sync::Mutex>, + history_path: std::path::PathBuf, } +/// Maximum number of messages returned by `conversation_history()`. +/// +/// This is a sliding-window cap: when the in-memory history exceeds this many +/// messages, only the most recent `MAX_HISTORY_MESSAGES` are returned to the +/// LLM (the very first user prompt is also preserved so the agent never loses +/// the original task context). The full history continues to be persisted to +/// disk for debugging/audit. +const MAX_HISTORY_MESSAGES: usize = 60; + impl SimpleSession { fn new(session_id: &str, initial_message: Content) -> Self { + let history_path = crate::persistence::get_cowork_dir() + .map(|dir| dir.join("iterations").join(session_id).join("session_history.jsonl")) + .unwrap_or_else(|_| { + std::path::PathBuf::from(".cowork-v2") + .join("iterations") + .join(session_id) + .join("session_history.jsonl") + }); + + // Ensure parent directory exists and load any prior history. + let mut messages = Vec::new(); + if let Some(parent) = history_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + if history_path.exists() + && let Ok(contents) = std::fs::read_to_string(&history_path) + { + for line in contents.lines() { + if line.trim().is_empty() { + continue; + } + if let Ok(content) = serde_json::from_str::(line) { + messages.push(content); + } + } + } + + // Persist the current prompt so subsequent agent invocations (within + // the same stage run, or across retries / actor-critic iterations that + // reuse this session) can see the original user turn. Without this, + // a fresh agent run would only see prior assistant responses and lose + // the user's original instruction. + if let Err(e) = Self::append_message_to_file(&history_path, &initial_message) { + tracing::warn!("Failed to persist initial prompt to session history: {}", e); + } + messages.push(initial_message); + Self { session_id: session_id.to_string(), app_name: "cowork_forge".to_string(), user_id: "default_user".to_string(), simple_state: SimpleState::new(), - messages: vec![initial_message], + messages: std::sync::Mutex::new(messages), + history_path, + } + } + + fn append_message_to_file( + path: &std::path::PathBuf, + content: &Content, + ) -> anyhow::Result<()> { + let line = serde_json::to_string(content)?; + use std::io::Write; + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path)?; + writeln!(file, "{}", line)?; + Ok(()) + } + + /// Apply a sliding-window truncation so the LLM context stays bounded. + /// + /// Returns the full message list when it is small enough; otherwise + /// returns the first message (the original user prompt, to preserve the + /// task framing) followed by the most recent `MAX_HISTORY_MESSAGES - 1` + /// messages. + fn truncate_for_view(messages: &[Content]) -> Vec { + if messages.len() <= MAX_HISTORY_MESSAGES { + return messages.to_vec(); } + let mut view = Vec::with_capacity(MAX_HISTORY_MESSAGES); + // Always keep the very first user prompt for task context. + view.push(messages[0].clone()); + let tail_start = messages.len().saturating_sub(MAX_HISTORY_MESSAGES - 1); + view.extend_from_slice(&messages[tail_start..]); + tracing::debug!( + "SimpleSession: truncated history from {} to {} messages (cap={})", + messages.len(), view.len(), MAX_HISTORY_MESSAGES + ); + view } } @@ -954,11 +1220,224 @@ impl adk_core::Session for SimpleSession { } fn conversation_history(&self) -> Vec { - self.messages.clone() + self.messages + .lock() + .map(|m| Self::truncate_for_view(&m)) + .unwrap_or_default() } - fn append_to_history(&self, _content: Content) { - // Simple implementation - doesn't store history + fn append_to_history(&self, content: Content) { + if let Ok(mut messages) = self.messages.lock() { + messages.push(content.clone()); + } + if let Err(e) = Self::append_message_to_file(&self.history_path, &content) { + tracing::warn!("Failed to persist session history: {}", e); + } + } +} + +/// Minimal in-memory + file-backed artifact store for the InvocationContext. +/// +/// This is a thin adapter over the iteration's artifacts directory. Most agents +/// should continue to use dedicated artifact tools (load_artifact, save_artifact) +/// for validation and schema enforcement; this store exists so that framework +/// callbacks and plugins can access artifacts through `CallbackContext::artifacts()`. +struct SimpleArtifacts { + artifacts_dir: std::path::PathBuf, +} + +impl SimpleArtifacts { + fn new(iteration_id: &str) -> Self { + let artifacts_dir = crate::persistence::get_cowork_dir() + .map(|dir| dir.join("iterations").join(iteration_id).join("artifacts")) + .unwrap_or_else(|_| { + std::path::PathBuf::from(".cowork-v2") + .join("iterations") + .join(iteration_id) + .join("artifacts") + }); + let _ = std::fs::create_dir_all(&artifacts_dir); + Self { artifacts_dir } + } + + fn safe_name(name: &str) -> Option { + let sanitized: String = name + .chars() + .filter(|c| c.is_alphanumeric() || *c == '.' || *c == '-' || *c == '_') + .collect(); + if sanitized.is_empty() || sanitized != name { + None + } else { + Some(sanitized) + } + } + + fn path_for(&self, name: &str) -> Option { + Self::safe_name(name).map(|n| self.artifacts_dir.join(n)) + } +} + +#[async_trait::async_trait] +impl adk_core::Artifacts for SimpleArtifacts { + async fn save(&self, name: &str, data: &adk_core::Part) -> adk_core::Result { + let path = self.path_for(name).ok_or_else(|| { + adk_core::AdkError::tool(format!("Invalid artifact name: {}", name)) + })?; + + match data { + adk_core::Part::Text { text } => { + std::fs::write(&path, text).map_err(|e| { + adk_core::AdkError::tool(format!("Failed to write artifact {}: {}", name, e)) + })?; + } + adk_core::Part::InlineData { mime_type, data } => { + let ext = match mime_type.as_str() { + "text/markdown" | "text/plain" => "txt", + "application/json" => "json", + "image/png" => "png", + "image/jpeg" => "jpg", + _ => "bin", + }; + let path = path.with_extension(ext); + std::fs::write(&path, data).map_err(|e| { + adk_core::AdkError::tool(format!("Failed to write artifact {}: {}", name, e)) + })?; + } + adk_core::Part::FileData { file_uri, .. } => { + // We cannot meaningfully save a URI reference to local disk. + return Err(adk_core::AdkError::tool(format!( + "Cannot save FileData artifact with URI: {}", + file_uri + ))); + } + _ => { + return Err(adk_core::AdkError::tool( + "Unsupported artifact part type".to_string(), + )); + } + } + Ok(1) + } + + async fn load(&self, name: &str) -> adk_core::Result { + let path = self.path_for(name).ok_or_else(|| { + adk_core::AdkError::tool(format!("Invalid artifact name: {}", name)) + })?; + + let data = std::fs::read(&path).map_err(|e| { + adk_core::AdkError::tool(format!("Failed to read artifact {}: {}", name, e)) + })?; + + // Try to return as text for UTF-8 content, otherwise inline binary. + if let Ok(text) = String::from_utf8(data.clone()) { + Ok(adk_core::Part::Text { text }) + } else { + let mime_type = match path.extension().and_then(|e| e.to_str()) { + Some("png") => "image/png", + Some("jpg") | Some("jpeg") => "image/jpeg", + _ => "application/octet-stream", + } + .to_string(); + Ok(adk_core::Part::InlineData { mime_type, data }) + } + } + + async fn list(&self) -> adk_core::Result> { + let mut names = Vec::new(); + if self.artifacts_dir.exists() { + for entry in std::fs::read_dir(&self.artifacts_dir).map_err(|e| { + adk_core::AdkError::tool(format!("Failed to list artifacts: {}", e)) + })? { + let entry = entry.map_err(|e| adk_core::AdkError::tool(format!("Failed to read artifact entry: {}", e)))?; + if entry.file_type().map(|t| t.is_file()).unwrap_or(false) + && let Some(name) = entry.file_name().to_str() + { + names.push(name.to_string()); + } + } + } + Ok(names) + } +} + +/// Minimal memory adapter for the InvocationContext. +/// +/// Delegates to the project's persisted memory store. Agents should prefer the +/// dedicated `query_memory` tool for richer filtering; this adapter lets the +/// framework call `InvocationContext::memory()` without returning `None`. +struct SimpleMemory { + iteration_id: String, +} + +impl SimpleMemory { + fn new(iteration_id: &str) -> Self { + Self { + iteration_id: iteration_id.to_string(), + } + } +} + +#[async_trait::async_trait] +impl adk_core::Memory for SimpleMemory { + async fn search(&self, query: &str) -> adk_core::Result> { + let store = crate::persistence::MemoryStore::new(); + let memory_query = crate::domain::MemoryQuery { + scope: crate::domain::MemoryScope::Smart, + query_type: crate::domain::MemoryQueryType::All, + keywords: query.split_whitespace().map(|s| s.to_string()).collect(), + limit: Some(20), + }; + + let result = store.query(&memory_query, Some(&self.iteration_id)).map_err(|e| { + adk_core::AdkError::memory(format!("Failed to query memory: {}", e)) + })?; + + let mut entries = Vec::new(); + for decision in result.decisions { + let decision_text = format!( + "Decision: {}\nContext: {}\nOutcome: {}\nConsequences: {}", + decision.title, + decision.context, + decision.decision, + if decision.consequences.is_empty() { + "None recorded".to_string() + } else { + decision.consequences.join(", ") + } + ); + entries.push(adk_core::MemoryEntry { + content: adk_core::Content::new("model").with_text(decision_text), + author: "project".to_string(), + }); + } + for pattern in result.patterns { + let pattern_text = format!( + "Pattern: {}\nDescription: {}\nUsage: {}\nTags: {}", + pattern.name, + pattern.description, + if pattern.usage.is_empty() { + "Not specified".to_string() + } else { + pattern.usage.join(", ") + }, + if pattern.tags.is_empty() { + "None".to_string() + } else { + pattern.tags.join(", ") + } + ); + entries.push(adk_core::MemoryEntry { + content: adk_core::Content::new("model").with_text(pattern_text), + author: "project".to_string(), + }); + } + for insight in result.insights { + entries.push(adk_core::MemoryEntry { + content: adk_core::Content::new("model").with_text(insight.content), + author: insight.stage, + }); + } + Ok(entries) } } @@ -1018,3 +1497,46 @@ pub fn extract_text_from_event(event: &Event) -> Option { None } } + +/// Validate that a stage artifact contains meaningful content. +/// +/// This prevents downstream stages from consuming placeholder output such as +/// "TODO", "FIXME", or near-empty documents. Returns `Ok(())` when the +/// content looks valid, otherwise returns an error message describing the issue. +fn validate_artifact_content(stage_name: &str, content: &str) -> std::result::Result<(), String> { + let trimmed = content.trim(); + + if trimmed.is_empty() { + return Err(format!("Stage '{}' produced an empty artifact", stage_name)); + } + + if trimmed.chars().count() < 50 { + return Err(format!( + "Stage '{}' produced an artifact that is too short ({} chars). Provide a complete document.", + stage_name, + trimmed.chars().count() + )); + } + + // Reject documents that are only placeholders. + let upper = trimmed.to_uppercase(); + let placeholder_only = ["TODO", "FIXME", "TBD", "PLACEHOLDER", "NOT IMPLEMENTED"] + .iter() + .any(|p| upper.contains(p)); + if placeholder_only { + return Err(format!( + "Stage '{}' artifact appears to contain only placeholder text (TODO/FIXME/TBD). Provide complete content.", + stage_name + )); + } + + // Markdown artifacts should contain at least one heading. + if stage_name != "coding" && !content.contains('#') { + return Err(format!( + "Stage '{}' markdown artifact is missing headings. Use proper markdown structure.", + stage_name + )); + } + + Ok(()) +} diff --git a/crates/cowork-core/src/pipeline/stages/coding.rs b/crates/cowork-core/src/pipeline/stages/coding.rs index bfb98f6..197334a 100644 --- a/crates/cowork-core/src/pipeline/stages/coding.rs +++ b/crates/cowork-core/src/pipeline/stages/coding.rs @@ -5,7 +5,7 @@ use crate::interaction::{InteractiveBackend, MessageContext, MessageLevel}; use crate::llm::config::load_config; use crate::pipeline::{PipelineContext, Stage, StageResult}; use crate::instructions::coding::CODING_ACTOR_INSTRUCTION; -use crate::pipeline::stage_executor::execute_stage_with_instruction; +use crate::pipeline::stage_executor::{execute_stage_with_instruction, execute_stage_with_instruction_and_context}; use crate::acp::AgentMessage; /// Coding Stage - Generate code implementation using Agent with Instructions + Tools @@ -52,7 +52,7 @@ impl CodingStage { // Note: Executor now auto-loads feedback from storage before calling execute_with_feedback let task_description = if let Some(fb) = feedback { // Use parameter feedback (from executor auto-load or stage review loop) - println!("[Coding] Using parameter feedback: {}", fb.chars().take(100).collect::()); + tracing::debug!("using parameter feedback: {}", fb.chars().take(100).collect::()); format!( "## ⚠️ USER REPORTED ISSUE - REQUIRES FIX\n\n\ The user has reported the following problems with the project:\n\n\ @@ -76,11 +76,11 @@ impl CodingStage { }); if let Some(ref fb) = stored_feedback { - println!("[Coding] Found fallback feedback from storage: {}", fb.details.chars().take(100).collect::()); + tracing::debug!("found fallback feedback from storage: {}", fb.details.chars().take(100).collect::()); format!("Fix issues based on feedback: {}", fb.details) } else { // Load plan artifact to get tasks - println!("[Coding] No feedback found, loading plan..."); + tracing::debug!("no feedback found, loading plan..."); let iteration_dir = workspace.parent().unwrap_or(&workspace); let plan_artifact = iteration_dir.join("artifacts").join("plan.md"); @@ -100,9 +100,8 @@ impl CodingStage { ); // Create external agent with iteration context for evolution iterations - eprintln!("DEBUG: Creating ExternalCodingAgent for workspace: {}", workspace.display()); - eprintln!("DEBUG: Iteration id={}, base_id={:?}, inheritance={:?}", - ctx.iteration.id, ctx.iteration.base_iteration_id, ctx.iteration.inheritance); + tracing::debug!(workspace = %workspace.display(), "creating ExternalCodingAgent"); + tracing::debug!(iteration_id = %ctx.iteration.id, base_id = ?ctx.iteration.base_iteration_id, inheritance = ?ctx.iteration.inheritance, "iteration context"); let agent = match ExternalCodingAgent::new_with_iteration(&workspace, Some(ctx.iteration.clone())).await { Ok(agent) => agent, Err(e) => { @@ -113,13 +112,20 @@ impl CodingStage { MessageContext::new(AGENT_NAME_EXTERNAL).with_stage("coding"), ) .await; - // Fall back to built-in agent + // Fall back to built-in agent. Preserve the external agent's + // task description (plan + feedback context) so the built-in + // agent does not start from a blank slate. tracing::warn!("Falling back to built-in coding agent"); - return if let Some(fb) = feedback { - execute_stage_with_instruction(ctx, interaction, "coding", CODING_ACTOR_INSTRUCTION, Some(fb)).await - } else { - execute_stage_with_instruction(ctx, interaction, "coding", CODING_ACTOR_INSTRUCTION, None).await - }; + let fallback_feedback = feedback; + return execute_stage_with_instruction_and_context( + ctx, + interaction, + "coding", + CODING_ACTOR_INSTRUCTION, + fallback_feedback, + Some(&task_description), + ) + .await; } }; @@ -132,38 +138,48 @@ impl CodingStage { // Display messages in real-time while waiting for result let interaction_clone = interaction.clone(); - // Use tokio::spawn with scoped lifetime to handle the receiver properly - // Note: messages is UnboundedReceiver, we need to use it in the same runtime + // Use tokio::spawn with scoped lifetime to handle the receiver properly. + // Note: messages is UnboundedReceiver, we need to use it in the same runtime. + // + // Track completion via a Notify so the outer wait can distinguish "agent still + // working" from "agent finished, cleanup pending". This is critical: a fixed + // timeout on result.await would kill legitimate long-running tasks (e.g., when + // the user is scanning a QR code for auth, or the agent is producing a lot of + // output). We only want a short cleanup timeout AFTER Completed is received. + let completed_received = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let completed_flag = completed_received.clone(); + let completed_notify = Arc::new(tokio::sync::Notify::new()); + let completed_notify_clone = completed_notify.clone(); + let message_handle = tokio::spawn(async move { - let mut thinking_buffer = String::new(); - let mut output_buffer = String::new(); - loop { tokio::select! { msg = messages.recv() => { match msg { Some(AgentMessage::Thinking(text)) => { - // Accumulate thinking for display - thinking_buffer.push_str(&text); - // Show thinking as it comes (truncated for UI) - if thinking_buffer.chars().count() > 100 { - let truncated: String = thinking_buffer.chars().take(100).collect(); - let display = format!("💭 Thinking: {}...", truncated); - interaction_clone.show_message_with_context(MessageLevel::Info, display, ctx_external.clone()).await; - thinking_buffer.clear(); + // Stream thinking directly — the frontend aggregates + // chunks into a single collapsible thinking message. + // No truncation, no prefix labels. + if !text.is_empty() { + interaction_clone + .send_streaming(text, AGENT_NAME_EXTERNAL, true) + .await; } } Some(AgentMessage::Output(text)) => { - output_buffer.push_str(&text); - // Show significant output chunks - if output_buffer.chars().count() > 200 { - let truncated: String = output_buffer.chars().take(200).collect(); - let display = format!("📝 Output: {}...", truncated); - interaction_clone.show_message_with_context(MessageLevel::Info, display, ctx_external.clone()).await; - output_buffer.clear(); + // Stream output text directly — the frontend aggregates + // chunks into one streaming agent message. No truncation, + // no "📝 Output:" prefix (that prefix was the cause of the + // ugly duplicated labels mid-paragraph reported by users). + if !text.is_empty() { + interaction_clone + .send_streaming(text, AGENT_NAME_EXTERNAL, false) + .await; } } Some(AgentMessage::Status(text)) => { + // Brief, discrete status line — keep as a separate Info + // message so it shows as a distinct UI element. interaction_clone.show_message_with_context(MessageLevel::Info, format!("⏳ {}", text), ctx_external.clone()).await; } Some(AgentMessage::Error(text)) => { @@ -171,6 +187,18 @@ impl CodingStage { } Some(AgentMessage::Completed) => { interaction_clone.show_message_with_context(MessageLevel::Info, "✅ Task completed".to_string(), ctx_external.clone()).await; + // Record that Completed was received so the outer code + // can fall back to Success if result.await hangs. + completed_flag.store(true, std::sync::atomic::Ordering::SeqCst); + // Wake the outer select! so it switches from "wait for + // agent" to "wait for cleanup". This is the key signal: + // before this fires, the outer code waits indefinitely + // (no spurious timeout); after, it gives the result + // future a short window to clean up. + completed_notify_clone.notify_one(); + // Exit the loop — the ACP client sends Completed after + // the prompt finishes and the agent process is cleaned up. + break; } None => { // Channel closed, exit loop @@ -179,20 +207,78 @@ impl CodingStage { } } _ = tokio::time::sleep(tokio::time::Duration::from_secs(60)) => { - // Timeout after 60 seconds of no messages + // Idle heartbeat — no recent message. Keep as a discrete Info + // line so the user knows the agent is still working. interaction_clone.show_message_with_context(MessageLevel::Info, "⏳ Waiting for agent...".to_string(), ctx_external.clone()).await; } } } }); - // Wait for result - result is Result> - match result.await { - // Inner Ok: ACP execution succeeded + // Wait for the result future. CRITICAL: do NOT apply a short timeout here. + // The agent may legitimately run for a long time (auth QR scan, long output, + // file operations). The ACP SDK already has a 3000s timeout on conn.prompt() + // (PROMPT_TIMEOUT_SECONDS) which will return Err and unblock `result`. + // + // We use a select! between: + // (a) result returns on its own — normal completion or ACP-level error + // (b) Completed notification fires — agent finished, give cleanup a short + // window then proceed to Success even if result stalls (teardown bug) + // Before (b) fires, there is NO timeout — we wait as long as the agent needs. + tracing::info!("Awaiting external agent result (no timeout until Completed)"); + let mut result = std::pin::pin!(result); + + let outcome = tokio::select! { + // (a) result returned first: either the agent finished cleanly and the + // ACP thread exited, or the ACP SDK's own timeout fired (→ Err). + res = &mut result => { + tracing::info!("External agent result received directly"); + res + } + // (b) AgentMessage::Completed was received — the agent's work is done. + // Give the result future a short window to wrap up (process kill, + // stderr drain, runtime teardown). If it doesn't return in time, the + // stall is in cleanup, not in the agent's work — proceed to Success. + _ = completed_notify.notified() => { + tracing::info!("Completed received, waiting for result cleanup (30s)"); + match tokio::time::timeout( + tokio::time::Duration::from_secs(30), + &mut result, + ).await { + Ok(res) => { + tracing::info!("Result cleanup completed within window"); + res + } + Err(_) => { + // Cleanup stalled after Completed — the task itself finished. + // Proceed to Success so the user can review via HITL. + tracing::warn!("Result cleanup timed out after Completed, proceeding to Success"); + interaction + .show_message_with_context( + MessageLevel::Info, + "External coding agent completed (cleanup timed out, proceeding)".to_string(), + MessageContext::new(AGENT_NAME_EXTERNAL).with_stage("coding"), + ) + .await; + // Drain the message loop (it already exited on Completed). + let _ = tokio::time::timeout( + tokio::time::Duration::from_secs(5), + message_handle, + ).await; + return StageResult::Success(None); + } + } + } + }; + + // Handle the result (from either branch above). + match outcome { Ok(Ok(_output)) => { - // Wait for message handling to finish - let _ = message_handle.await; - + tracing::info!("External agent result Ok"); + let _ = tokio::time::timeout( + tokio::time::Duration::from_secs(10), + message_handle, + ).await; interaction .show_message_with_context( MessageLevel::Info, @@ -202,8 +288,9 @@ impl CodingStage { .await; StageResult::Success(None) } - // Inner Err: ACP execution failed Ok(Err(e)) => { + tracing::warn!(error = %e, "External agent returned error"); + message_handle.abort(); let error_msg = format!("External agent execution error: {}", e); interaction .show_message_with_context( @@ -214,8 +301,9 @@ impl CodingStage { .await; StageResult::Failed(e.to_string()) } - // Outer Err: Channel/thread error Err(e) => { + tracing::warn!(error = %e, "External agent channel error"); + message_handle.abort(); let error_msg = format!("External agent error: {}", e); interaction .show_message_with_context( diff --git a/crates/cowork-core/src/tools/control_tools.rs b/crates/cowork-core/src/tools/control_tools.rs index 0719cf2..a24f3a3 100644 --- a/crates/cowork-core/src/tools/control_tools.rs +++ b/crates/cowork-core/src/tools/control_tools.rs @@ -1,9 +1,10 @@ -// Control tools - provide_feedback, ask_user, etc. +// Control tools - provide_feedback, ask_user, request_human_review use crate::data::*; use crate::persistence::*; -use adk_core::{Tool, ToolContext}; +use crate::interaction::{InputOption, InputResponse, MessageLevel}; +use crate::tools::hitl_content_tools::get_interaction_backend; +use adk_core::{Tool, ToolContext, EventActions}; use async_trait::async_trait; -use dialoguer::{Confirm, Input}; use serde_json::{json, Value}; use std::sync::Arc; use super::get_required_string_param; @@ -21,8 +22,13 @@ impl Tool for ProvideFeedbackTool { } fn description(&self) -> &str { - "Provide structured feedback to the Actor agent. \ - This feedback will be visible to the Actor in the next iteration." + "Provide structured feedback to escalate an issue to the executor level. \ + This records the feedback and signals the LoopAgent to exit immediately \ + so the executor can retry the stage with the feedback. \ + Use this ONLY for critical/major issues that need executor-level retry. \ + For minor issues that the Actor can fix in the next loop iteration, \ + just describe them in your response (the Actor sees conversation history). \ + When satisfied, call `exit_loop` instead." } fn parameters_schema(&self) -> Option { @@ -36,7 +42,15 @@ impl Tool for ProvideFeedbackTool { }, "feedback_type": { "type": "string", - "enum": ["build_error", "quality_issue", "missing_requirement", "suggestion"], + "enum": [ + "build_error", + "quality_issue", + "missing_requirement", + "missing_artifact", + "architecture_issue", + "task_scope_issue", + "suggestion" + ], }, "severity": { "type": "string", @@ -49,13 +63,16 @@ impl Tool for ProvideFeedbackTool { })) } - async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { + async fn execute(&self, ctx: Arc, args: Value) -> adk_core::Result { let stage = get_required_string_param(&args, "stage")?; let feedback_type = match get_required_string_param(&args, "feedback_type")? { "build_error" => FeedbackType::BuildError, "quality_issue" => FeedbackType::QualityIssue, "missing_requirement" => FeedbackType::MissingRequirement, + "missing_artifact" => FeedbackType::MissingArtifact, + "architecture_issue" => FeedbackType::ArchitectureIssue, + "task_scope_issue" => FeedbackType::TaskScopeIssue, _ => FeedbackType::Suggestion, }; @@ -66,7 +83,7 @@ impl Tool for ProvideFeedbackTool { }; let feedback = Feedback { - stage: stage.to_string(), // 设置 stage 字段 + stage: stage.to_string(), feedback_type, severity, details: get_required_string_param(&args, "details")?.to_string(), @@ -79,15 +96,28 @@ impl Tool for ProvideFeedbackTool { append_feedback(&feedback).map_err(|e| adk_core::AdkError::tool(e.to_string()))?; + tracing::info!( + "[ProvideFeedbackTool] Feedback recorded for stage '{}' (severity: {:?}): {}", + stage, severity, feedback.details.chars().take(100).collect::() + ); + + // Signal the LoopAgent to exit immediately so the executor can retry + // the stage with the recorded feedback. Per adk-rust semantics, setting + // `escalate = true` in EventActions causes the LoopAgent to break out + // of its iteration loop. This is the same mechanism used by ExitLoopTool. + let mut actions = EventActions::default(); + actions.escalate = true; + ctx.set_actions(actions); + Ok(json!({ "status": "feedback_recorded", - "message": "Feedback will be available to Actor in next iteration" + "message": "Feedback recorded. The loop will exit and the executor will retry the stage with this feedback." })) } } // ============================================================================ -// AskUserTool +// AskUserTool - uses InteractiveBackend trait (works in both CLI and GUI) // ============================================================================ pub struct AskUserTool; @@ -99,7 +129,7 @@ impl Tool for AskUserTool { } fn description(&self) -> &str { - "Ask the user for confirmation or input via CLI interface." + "Ask the user for confirmation or text input." } fn parameters_schema(&self) -> Option { @@ -124,13 +154,38 @@ impl Tool for AskUserTool { let question = get_required_string_param(&args, "question")?; let question_type = get_required_string_param(&args, "question_type")?; + let interaction = get_interaction_backend() + .ok_or_else(|| adk_core::AdkError::tool("InteractiveBackend not set - cannot ask user".to_string()))?; + match question_type { "yes_no" => { - let answer = Confirm::new() - .with_prompt(question) - .default(false) - .interact() - .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; + let options = vec![ + InputOption { + id: "yes".to_string(), + label: "Yes".to_string(), + description: Some("Confirm and proceed".to_string()), + }, + InputOption { + id: "no".to_string(), + label: "No".to_string(), + description: Some("Deny or cancel".to_string()), + }, + ]; + + let response = interaction.request_input( + question, + options, + None, + ).await.map_err(|e| adk_core::AdkError::tool(format!("Input error: {}", e)))?; + + let answer = match response { + InputResponse::Selection(id) => id == "yes", + InputResponse::Text(text) => { + let trimmed = text.trim().to_lowercase(); + trimmed == "yes" || trimmed == "y" || trimmed == "true" || trimmed == "1" + } + InputResponse::Cancel => false, + }; Ok(json!({ "answer": answer, @@ -138,17 +193,97 @@ impl Tool for AskUserTool { })) } "text_input" => { - let answer: String = Input::new() - .with_prompt(question) - .interact_text() - .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; + let response = interaction.request_input( + question, + vec![], + None, + ).await.map_err(|e| adk_core::AdkError::tool(format!("Input error: {}", e)))?; + + let answer = match response { + InputResponse::Text(text) => text, + InputResponse::Selection(_) => String::new(), + InputResponse::Cancel => String::new(), + }; Ok(json!({ "answer": answer, "answer_type": "text" })) } - _ => Ok(json!({"error": "Invalid question type"})), + _ => Ok(json!({"error": "Invalid question type. Use 'yes_no' or 'text_input'."})), } } -} \ No newline at end of file +} + +// ============================================================================ +// RequestHumanReviewTool - escalate to human when Actor-Critic loop is stuck +// ============================================================================ + +pub struct RequestHumanReviewTool; + +#[async_trait] +impl Tool for RequestHumanReviewTool { + fn name(&self) -> &str { + "request_human_review" + } + + fn description(&self) -> &str { + "Request human intervention when the Actor-Critic feedback loop cannot resolve an issue. \ + This signals that the agent needs human judgment to proceed, and terminates the current loop." + } + + fn parameters_schema(&self) -> Option { + Some(json!({ + "type": "object", + "properties": { + "reason": { + "type": "string", + "description": "Why human review is needed (describe the stuck issue)" + } + }, + "required": ["reason"] + })) + } + + async fn execute(&self, ctx: Arc, args: Value) -> adk_core::Result { + let reason = get_required_string_param(&args, "reason")?; + + tracing::warn!("[RequestHumanReviewTool] Human review requested: {}", reason); + + if let Some(interaction) = get_interaction_backend() { + interaction.show_message( + MessageLevel::Warning, + format!("⚠️ Human review requested\nReason: {}", reason), + ).await; + + let options = vec![ + InputOption { + id: "continue".to_string(), + label: "Continue (agent will proceed)".to_string(), + description: Some("Allow the agent to continue to the next stage".to_string()), + }, + InputOption { + id: "restart".to_string(), + label: "Restart stage".to_string(), + description: Some("Send the agent back to try again".to_string()), + }, + ]; + + let _ = interaction.request_input( + &format!("Human review needed: {}\n\nPlease choose how to proceed:", reason), + options, + None, + ).await; + } + + let mut actions = EventActions::default(); + actions.escalate = true; + ctx.set_actions(actions); + + Ok(json!({ + "status": "human_review_requested", + "reason": reason, + "message": "Human review has been requested. The loop will terminate." + })) + } +} diff --git a/crates/cowork-core/src/tools/file_tools.rs b/crates/cowork-core/src/tools/file_tools.rs index c33d97b..efd641a 100644 --- a/crates/cowork-core/src/tools/file_tools.rs +++ b/crates/cowork-core/src/tools/file_tools.rs @@ -323,8 +323,6 @@ fn should_ignore(path: &str) -> bool { "./dist", "./build", "./docs", - "./tests", - "__tests__", "./.archived", ".DS_Store", "Thumbs.db", diff --git a/crates/cowork-core/src/tools/goto_stage_tool.rs b/crates/cowork-core/src/tools/goto_stage_tool.rs index 95ecfa6..a713834 100644 --- a/crates/cowork-core/src/tools/goto_stage_tool.rs +++ b/crates/cowork-core/src/tools/goto_stage_tool.rs @@ -1,7 +1,7 @@ -// Goto Stage tool for Check Agent use crate::data::*; use crate::persistence::*; -use adk_core::{Tool, ToolContext}; +use crate::pipeline::set_goto_stage_signal; +use adk_core::{Tool, ToolContext, EventActions}; use async_trait::async_trait; use serde_json::{json, Value}; use std::sync::Arc; @@ -38,11 +38,10 @@ impl Tool for GotoStageTool { })) } - async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { + async fn execute(&self, ctx: Arc, args: Value) -> adk_core::Result { let stage_str = get_required_string_param(&args, "stage")?; let reason = get_required_string_param(&args, "reason")?; - // Parse stage let stage = match stage_str { "prd" => Stage::Prd, "design" => Stage::Design, @@ -56,9 +55,8 @@ impl Tool for GotoStageTool { } }; - // Save detailed feedback to FeedbackHistory for incremental update support let feedback = Feedback { - stage: stage_str.to_string(), // 标识反馈来自当前 stage + stage: stage_str.to_string(), feedback_type: FeedbackType::QualityIssue, severity: Severity::Critical, details: reason.to_string(), @@ -67,11 +65,9 @@ impl Tool for GotoStageTool { }; if let Err(e) = crate::persistence::append_feedback(&feedback) { - // Log warning but don't fail the operation - eprintln!("[GotoStageTool] Warning: Failed to save feedback: {}", e); + tracing::warn!("[GotoStageTool] Failed to save feedback: {}", e); } - // Load or create session meta let mut meta = load_session_meta() .map_err(|e| adk_core::AdkError::tool(e.to_string()))? .unwrap_or_else(|| SessionMeta { @@ -81,19 +77,24 @@ impl Tool for GotoStageTool { restart_reason: None, }); - // Set restart information by updating current_stage and reason meta.current_stage = Some(stage); meta.restart_reason = Some(reason.to_string()); - // Save session meta save_session_meta(&meta) .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; - // Signal to stage executor that we need to jump to another stage - // This will be caught by the executor and trigger a proper stage transition - Err(adk_core::AdkError::tool(format!( - "GOTO_STAGE:{}:{}", - stage_str, reason - ))) + set_goto_stage_signal(stage_str.to_string(), reason.to_string()); + + let mut actions = EventActions::default(); + actions.escalate = true; + actions.state_delta.insert("goto_stage".to_string(), json!(stage_str)); + actions.state_delta.insert("goto_reason".to_string(), json!(reason)); + ctx.set_actions(actions); + + Ok(json!({ + "status": "goto_stage", + "stage": stage_str, + "reason": reason + })) } } diff --git a/crates/cowork-core/src/tools/hitl_content_tools.rs b/crates/cowork-core/src/tools/hitl_content_tools.rs index 45f5a71..2ca255a 100644 --- a/crates/cowork-core/src/tools/hitl_content_tools.rs +++ b/crates/cowork-core/src/tools/hitl_content_tools.rs @@ -17,7 +17,7 @@ pub fn set_interaction_backend(backend: Arc Option> { +pub(crate) fn get_interaction_backend() -> Option> { INTERACTION_BACKEND.lock().unwrap().clone() } diff --git a/crates/cowork-core/src/tools/hitl_tools.rs b/crates/cowork-core/src/tools/hitl_tools.rs deleted file mode 100644 index 2bbee40..0000000 --- a/crates/cowork-core/src/tools/hitl_tools.rs +++ /dev/null @@ -1,233 +0,0 @@ -// HITL (Human-in-the-Loop) tools -use adk_core::{Tool, ToolContext}; -use async_trait::async_trait; -use dialoguer::{Confirm, Editor, Input}; -use serde_json::{json, Value}; -use std::fs; -use std::sync::Arc; -use super::get_required_string_param; - -/// ReviewAndEditFileTool - Original HITL tool (used in Idea stage) -pub struct ReviewAndEditFileTool; - -#[async_trait] -impl Tool for ReviewAndEditFileTool { - fn name(&self) -> &str { - "review_and_edit_file" - } - - fn description(&self) -> &str { - "Let the user review and optionally edit a file using their default editor. \ - User will be prompted: 'Do you want to edit this file? (y/n)'. \ - If 'y', opens the file in an editor. If 'n', continues without changes." - } - - fn parameters_schema(&self) -> Option { - Some(json!({ - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "Path to the file to review and edit" - }, - "title": { - "type": "string", - "description": "Title/description for the review prompt" - } - }, - "required": ["file_path", "title"] - })) - } - - async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { - let file_path = get_required_string_param(&args, "file_path")?; - let title = get_required_string_param(&args, "title")?; - - // Read current file content - let content = fs::read_to_string(file_path) - .map_err(|e| adk_core::AdkError::tool(format!("Failed to read file {}: {}", file_path, e)))?; - - // Show preview - println!("\n📝 {} - {}", title, file_path); - println!(" ────────────────────────────────────────"); - let line_count = content.lines().count(); - for (i, line) in content.lines().take(10).enumerate() { - println!(" {}: {}", i + 1, line); - } - if line_count > 10 { - println!(" ... ({} more lines)", line_count - 10); - } - println!(" ────────────────────────────────────────\n"); - - // Ask user if they want to edit - let should_edit = Confirm::new() - .with_prompt("Do you want to edit this file? (y/n)") - .default(false) - .interact() - .map_err(|e| adk_core::AdkError::tool(format!("Interaction error: {}", e)))?; - - if !should_edit { - return Ok(json!({ - "status": "no_changes", - "message": "User chose not to edit the file" - })); - } - - // Open editor - println!("📝 Opening editor... (Save and close to submit changes)"); - let edited = Editor::new() - .require_save(true) - .edit(&content) - .map_err(|e| adk_core::AdkError::tool(format!("Editor error: {}", e)))?; - - match edited { - Some(new_content) if new_content.trim() != content.trim() => { - // Save changes - fs::write(file_path, &new_content) - .map_err(|e| adk_core::AdkError::tool(format!("Failed to write file: {}", e)))?; - - println!("✅ File updated successfully"); - Ok(json!({ - "status": "edited", - "message": "File was edited and saved", - "changes_made": true - })) - } - _ => { - println!("ℹ️ No changes made"); - Ok(json!({ - "status": "no_changes", - "message": "File was not modified" - })) - } - } - } -} - -/// ReviewWithFeedbackTool - Enhanced HITL tool with three modes: -/// 1. User types "edit" → Opens editor -/// 2. User types "pass" → Continues without changes -/// 3. User types other text → Returns as feedback for agent to process -pub struct ReviewWithFeedbackTool; - -#[async_trait] -impl Tool for ReviewWithFeedbackTool { - fn name(&self) -> &str { - "review_with_feedback" - } - - fn description(&self) -> &str { - "Show user a file preview and ask for feedback. User can:\n\ - - Type 'edit' to open the file in an editor\n\ - - Type 'pass' to continue without changes\n\ - - Type any other text to provide feedback/suggestions (agent will revise based on feedback)" - } - - fn parameters_schema(&self) -> Option { - Some(json!({ - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Path to the file to review" - }, - "title": { - "type": "string", - "description": "Title/description for the review prompt" - }, - "prompt": { - "type": "string", - "description": "Custom prompt to show the user (e.g., '请审查需求大纲')" - } - }, - "required": ["path", "title"] - })) - } - - async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { - let file_path = get_required_string_param(&args, "path")?; - let title = get_required_string_param(&args, "title")?; - let default_prompt = "输入 'edit' 编辑,'pass' 继续,或直接输入修改建议"; - let prompt = args["prompt"].as_str().unwrap_or(default_prompt); - - // Read current file content - let content = fs::read_to_string(file_path) - .map_err(|e| adk_core::AdkError::tool(format!("Failed to read file {}: {}", file_path, e)))?; - - // Show preview - println!("\n📝 {} - {}", title, file_path); - println!(" ────────────────────────────────────────"); - let line_count = content.lines().count(); - for (i, line) in content.lines().take(15).enumerate() { - println!(" {}: {}", i + 1, line); - } - if line_count > 15 { - println!(" ... ({} more lines)", line_count - 15); - } - println!(" ────────────────────────────────────────\n"); - - // Ask user for input - let user_input: String = Input::new() - .with_prompt(prompt) - .allow_empty(true) - .interact_text() - .map_err(|e| adk_core::AdkError::tool(format!("Interaction error: {}", e)))?; - - let user_input = user_input.trim(); - - // Handle different input modes - match user_input.to_lowercase().as_str() { - "edit" => { - // Mode 1: Open editor - println!("📝 Opening editor... (Save and close to submit changes)"); - let edited = Editor::new() - .require_save(true) - .edit(&content) - .map_err(|e| adk_core::AdkError::tool(format!("Editor error: {}", e)))?; - - match edited { - Some(new_content) if new_content.trim() != content.trim() => { - fs::write(file_path, &new_content) - .map_err(|e| adk_core::AdkError::tool(format!("Failed to write file: {}", e)))?; - - println!("✅ File updated successfully"); - Ok(json!({ - "action": "edit", - "status": "edited", - "message": "User edited the file in editor", - "changes_made": true - })) - } - _ => { - println!("ℹ️ No changes made in editor"); - Ok(json!({ - "action": "edit", - "status": "no_changes", - "message": "User opened editor but made no changes" - })) - } - } - } - "pass" | "" => { - // Mode 2: Pass/Continue - println!("➡️ Continuing without changes..."); - Ok(json!({ - "action": "pass", - "status": "passed", - "message": "User chose to continue without changes" - })) - } - _ => { - // Mode 3: Feedback text - println!("💬 Feedback received: {}", user_input); - println!("🔄 Agent will revise based on your feedback..."); - Ok(json!({ - "action": "feedback", - "status": "feedback_provided", - "feedback": user_input, - "message": format!("User provided feedback: {}", user_input) - })) - } - } - } -} diff --git a/crates/cowork-core/src/tools/mod.rs b/crates/cowork-core/src/tools/mod.rs index d0a7ec5..7556fae 100644 --- a/crates/cowork-core/src/tools/mod.rs +++ b/crates/cowork-core/src/tools/mod.rs @@ -5,6 +5,19 @@ use adk_core::AdkError; use serde_json::Value; use std::sync::RwLock; +use std::sync::LazyLock; + +static CURRENT_AGENT_NAME: LazyLock> = LazyLock::new(|| RwLock::new(String::new())); + +pub fn set_current_agent_name(name: &str) { + if let Ok(mut guard) = CURRENT_AGENT_NAME.write() { + *guard = name.to_string(); + } +} + +fn get_current_agent_name() -> String { + CURRENT_AGENT_NAME.read().map(|g| g.clone()).unwrap_or_default() +} // Helper functions for safe parameter extraction /// Safely get a required string parameter from args @@ -33,7 +46,7 @@ pub fn get_required_array_param<'a>(args: &'a Value, key: &str) -> Result<&'a Ve // ============================================================================ /// Type alias for tool notification callback -type ToolNotifyFn = Box; +type ToolNotifyFn = Box; /// Global tool notification callback storage static TOOL_NOTIFIER: RwLock> = RwLock::new(None); @@ -42,7 +55,7 @@ static TOOL_NOTIFIER: RwLock> = RwLock::new(None); /// This should be called once at application startup (GUI backend) pub fn set_tool_notify_callback(callback: F) where - F: Fn(&str, &Value, bool, &str) + Send + Sync + 'static, + F: Fn(&str, &Value, bool, &str, &str) + Send + Sync + 'static, { let mut guard = TOOL_NOTIFIER.write().unwrap(); *guard = Some(Box::new(callback)); @@ -50,26 +63,25 @@ where /// Notify about a tool call (call this before tool execution) pub fn notify_tool_call(tool_name: &str, args: &Value) { - // Print to console for debugging + let agent_name = get_current_agent_name(); let args_str = if args.is_object() { let keys: Vec<&str> = args.as_object().unwrap().keys().map(|s| s.as_str()).collect(); format!("{:?}", keys) } else { args.to_string() }; - println!("🔧 Tool call: {} {}", tool_name, args_str); + tracing::debug!("🔧 [{}] Tool call: {} {}", agent_name, tool_name, args_str); - // Call registered callback if exists if let Ok(guard) = TOOL_NOTIFIER.read() { if let Some(ref callback) = *guard { - callback(tool_name, args, true, ""); + callback(tool_name, args, true, "", &agent_name); } } } /// Notify about a tool result (call this after tool execution) pub fn notify_tool_result(tool_name: &str, result: &Result) { - // Print to console for debugging + let agent_name = get_current_agent_name(); match result { Ok(v) => { let preview = if v.is_object() { @@ -85,12 +97,11 @@ pub fn notify_tool_result(tool_name: &str, result: &Result) { } else { v.to_string() }; - println!("✓ Tool result: {} -> {}", tool_name, preview); + tracing::debug!("✓ [{}] Tool result: {} -> {}", agent_name, tool_name, preview); } - Err(e) => println!("✗ Tool result: {} - error: {}", tool_name, e), + Err(e) => tracing::warn!("✗ [{}] Tool result: {} - error: {}", agent_name, tool_name, e), } - // Call registered callback if exists if let Ok(guard) = TOOL_NOTIFIER.read() { if let Some(ref callback) = *guard { let success = result.is_ok(); @@ -98,14 +109,13 @@ pub fn notify_tool_result(tool_name: &str, result: &Result) { Ok(v) => v.to_string(), Err(e) => e.to_string(), }; - callback(tool_name, &Value::Null, success, &result_str); + callback(tool_name, &Value::Null, success, &result_str, &agent_name); } } } // Core tools pub mod file_tools; -pub mod hitl_tools; pub mod hitl_content_tools; pub mod test_lint_tools; @@ -145,10 +155,8 @@ pub mod mcp_tools; // Re-exports pub use file_tools::*; -pub use hitl_tools::*; pub use hitl_content_tools::*; pub use test_lint_tools::*; -pub use test_lint_tools::ExecuteShellCommandTool; pub use data_tools::*; pub use validation_tools::*; pub use control_tools::*; diff --git a/crates/cowork-core/src/tools/pm_tools.rs b/crates/cowork-core/src/tools/pm_tools.rs index fe5d9e4..30a151a 100644 --- a/crates/cowork-core/src/tools/pm_tools.rs +++ b/crates/cowork-core/src/tools/pm_tools.rs @@ -93,7 +93,7 @@ impl Tool for PMGotoStageTool { }; if let Err(e) = append_feedback(&feedback) { - eprintln!("[PMGotoStageTool] Warning: Failed to save feedback: {}", e); + tracing::warn!("[PMGotoStageTool] Failed to save feedback: {}", e); } // Load or create session meta diff --git a/crates/cowork-core/src/tools/test_lint_tools.rs b/crates/cowork-core/src/tools/test_lint_tools.rs index cc1c7c7..8418389 100644 --- a/crates/cowork-core/src/tools/test_lint_tools.rs +++ b/crates/cowork-core/src/tools/test_lint_tools.rs @@ -5,8 +5,6 @@ use serde_json::{json, Value}; use std::sync::Arc; use std::path::Path; -use crate::tools::{get_required_string_param, get_optional_string_param}; - // ============================================================================ // CheckTestsTool // ============================================================================ @@ -52,14 +50,22 @@ impl Tool for CheckTestsTool { detect_test_command(path)? }; - // Execute test command - let output = tokio::process::Command::new("sh") - .arg("-c") - .arg(&test_command) - .current_dir(path) - .output() - .await - .map_err(|e| adk_core::AdkError::tool(format!("Failed to run tests: {}", e)))?; + // Execute test command (platform-aware) + let output = if cfg!(target_os = "windows") { + tokio::process::Command::new("cmd") + .args(["/C", &test_command]) + .current_dir(path) + .output() + .await + } else { + tokio::process::Command::new("sh") + .arg("-c") + .arg(&test_command) + .current_dir(path) + .output() + .await + } + .map_err(|e| adk_core::AdkError::tool(format!("Failed to run tests: {}", e)))?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); @@ -134,14 +140,22 @@ impl Tool for CheckLintTool { detect_lint_command(path, fix)? }; - // Execute lint command - let output = tokio::process::Command::new("sh") - .arg("-c") - .arg(&lint_command) - .current_dir(path) - .output() - .await - .map_err(|e| adk_core::AdkError::tool(format!("Failed to run linter: {}", e)))?; + // Execute lint command (platform-aware) + let output = if cfg!(target_os = "windows") { + tokio::process::Command::new("cmd") + .args(["/C", &lint_command]) + .current_dir(path) + .output() + .await + } else { + tokio::process::Command::new("sh") + .arg("-c") + .arg(&lint_command) + .current_dir(path) + .output() + .await + } + .map_err(|e| adk_core::AdkError::tool(format!("Failed to run linter: {}", e)))?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); @@ -280,102 +294,3 @@ fn parse_lint_output(stdout: &str, stderr: &str) -> (u32, u32) { (warnings, errors) } - -// ============================================================================ -// ExecuteShellCommandTool -// ============================================================================ - -pub struct ExecuteShellCommandTool; - -#[async_trait] -impl Tool for ExecuteShellCommandTool { - fn name(&self) -> &str { - "execute_shell_command" - } - - fn description(&self) -> &str { - "Execute a shell command and return the result. Use this to run \ - installation, build, or test commands extracted from README.md. \ - Supports both Windows (PowerShell) and Unix (bash) commands." - } - - fn parameters_schema(&self) -> Option { - Some(json!({ - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The shell command to execute" - }, - "description": { - "type": "string", - "description": "Description of what this command does (e.g., 'Install dependencies')" - }, - "timeout": { - "type": "integer", - "description": "Timeout in seconds (default: 120)" - } - }, - "required": ["command", "description"] - })) - } - - async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { - let command = get_required_string_param(&args, "command")?; - let description = get_optional_string_param(&args, "description").unwrap_or_default(); - let timeout = args.get("timeout") - .and_then(|v| v.as_u64()) - .unwrap_or(120); - - // Determine OS and choose appropriate shell - let (shell, shell_arg) = if cfg!(target_os = "windows") { - ("powershell.exe", vec!["-NoProfile", "-Command", command]) - } else { - ("sh", vec!["-c", command]) - }; - - // Execute command with timeout - let result = tokio::time::timeout( - std::time::Duration::from_secs(timeout), - tokio::process::Command::new(shell) - .args(&shell_arg) - .output() - ).await; - - match result { - Ok(Ok(output)) => { - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - let success = output.status.success(); - - Ok(json!({ - "status": if success { "success" } else { "failed" }, - "description": description, - "command": command, - "exit_code": output.status.code(), - "stdout": stdout, - "stderr": stderr, - "timeout": false - })) - } - Ok(Err(e)) => { - Ok(json!({ - "status": "error", - "description": description, - "command": command, - "error": e.to_string(), - "timeout": false - })) - } - Err(_) => { - Ok(json!({ - "status": "timeout", - "description": description, - "command": command, - "error": format!("Command timed out after {} seconds", timeout), - "timeout": true - })) - } - } - } -} diff --git a/crates/cowork-gui/package.json b/crates/cowork-gui/package.json index 2bb9589..3c158c2 100644 --- a/crates/cowork-gui/package.json +++ b/crates/cowork-gui/package.json @@ -2,7 +2,7 @@ "name": "cowork-gui", "private": true, "type": "module", - "version": "2.5.0", + "version": "2.6.0", "scripts": { "dev": "vite --port 15173", "build": "tsc && vite build", @@ -12,27 +12,32 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@monaco-editor/react": "^4.6.0", - "@tauri-apps/api": "^2.10.1", - "@tauri-apps/plugin-dialog": "^2.6.0", - "antd": "^5.12.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-json-view": "^1.21.3", - "react-markdown": "^9.0.1", - "react-window": "^2.2.6", + "@ant-design/icons": "^6.3.2", + "@monaco-editor/react": "^4.7.0", + "@tauri-apps/api": "^2.11.1", + "@tauri-apps/plugin-dialog": "^2.7.1", + "antd": "^6.5.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-markdown": "^10.1.0", + "react-window": "^1.8.11", "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", - "zustand": "^4.5.0" + "zustand": "^5.0.14" }, "devDependencies": { - "@tauri-apps/cli": "^2.10.0", + "@babel/core": "^8.0.0", + "@rolldown/plugin-babel": "^0.2.3", + "@tauri-apps/cli": "^2.11.4", + "@types/babel__core": "^7.20.5", "@types/node": "^20.0.0", - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.0", - "typescript": "^5.3.0", - "vite": "^5.0.0" + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@types/react-window": "1.8.8", + "@vitejs/plugin-react": "^6.0.3", + "babel-plugin-react-compiler": "^1.0.0", + "typescript": "^5.7.0", + "vite": "^8.1.3" } } diff --git a/crates/cowork-gui/src-tauri/Cargo.toml b/crates/cowork-gui/src-tauri/Cargo.toml index d94571f..883ccbf 100644 --- a/crates/cowork-gui/src-tauri/Cargo.toml +++ b/crates/cowork-gui/src-tauri/Cargo.toml @@ -11,12 +11,12 @@ name = "cowork_gui_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] -tauri-build = { version = "2.5.5", features = [] } +tauri-build = { version = "2.6.3", features = [] } [dependencies] -tauri = { version = "2.10.2", features = [] } -tauri-plugin-opener = "2.5.3" -tauri-plugin-dialog = "2.6.0" +tauri = { version = "2.11.5", features = [] } +tauri-plugin-opener = "2.5.4" +tauri-plugin-dialog = "2.7.1" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["sync", "full"] } diff --git a/crates/cowork-gui/src-tauri/src/commands/mod.rs b/crates/cowork-gui/src-tauri/src/commands/mod.rs index 34e768c..7509bf9 100644 --- a/crates/cowork-gui/src-tauri/src/commands/mod.rs +++ b/crates/cowork-gui/src-tauri/src/commands/mod.rs @@ -25,5 +25,5 @@ pub fn init_app_handle(handle: tauri::AppHandle) { /// Initialize PATH for macOS App Bundle compatibility /// Must be called very early in the application lifecycle pub fn init_path_for_app_bundle() { - path_utils::init_extended_path(); + path_utils::ensure_path_initialized(); } diff --git a/crates/cowork-gui/src-tauri/src/commands/path_utils.rs b/crates/cowork-gui/src-tauri/src/commands/path_utils.rs index 334f9fe..79a56c3 100644 --- a/crates/cowork-gui/src-tauri/src/commands/path_utils.rs +++ b/crates/cowork-gui/src-tauri/src/commands/path_utils.rs @@ -8,7 +8,10 @@ // - Windows: May need to check Program Files, AppData, etc. // - Linux: Various package manager locations -use std::path::PathBuf; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::sync::{Mutex, Once}; use std::env; /// Platform-specific path separator @@ -285,6 +288,564 @@ pub fn get_package_manager() -> Option<(&'static str, PathBuf)> { None } +static PATH_INIT: Once = Once::new(); +static EXECUTABLE_CACHE: Mutex>>> = Mutex::new(None); + +/// Ensure PATH has been augmented for GUI-launched apps. Safe to call multiple times. +pub fn ensure_path_initialized() { + PATH_INIT.call_once(init_extended_path); +} + +fn merge_paths(primary: &str, secondary: &str) -> String { + let mut seen = std::collections::HashSet::new(); + let mut merged = Vec::new(); + for entry in primary + .split(PATH_SEP) + .chain(secondary.split(PATH_SEP)) + { + if entry.is_empty() || !seen.insert(entry.to_string()) { + continue; + } + merged.push(entry); + } + merged.join(&PATH_SEP.to_string()) +} + +#[cfg(unix)] +fn default_shell() -> String { + env::var("SHELL").unwrap_or_else(|_| { + if cfg!(target_os = "macos") { + "/bin/zsh".into() + } else { + "/bin/bash".into() + } + }) +} + +#[cfg(unix)] +fn run_shell_output(args: &[&str]) -> Option { + use std::sync::mpsc; + use std::time::Duration; + + let shell = default_shell(); + let args: Vec = args.iter().map(|s| (*s).to_string()).collect(); + let (tx, rx) = mpsc::channel(); + + std::thread::spawn(move || { + let result = std::process::Command::new(&shell) + .args(&args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .output(); + let _ = tx.send(result); + }); + + let output = rx.recv_timeout(Duration::from_secs(5)).ok()?.ok()?; + if !output.status.success() { + return None; + } + let path = String::from_utf8(output.stdout).ok()?; + if path.is_empty() { + None + } else { + Some(path) + } +} + +#[cfg(unix)] +fn interactive_shell_path() -> Option { + run_shell_output(&["-il", "-c", "printf %s \"$PATH\""]) +} + +#[cfg(unix)] +fn login_shell_path() -> Option { + run_shell_output(&["-l", "-c", "printf %s \"$PATH\""]) +} + +#[cfg(target_os = "macos")] +fn path_helper_path() -> Option { + let output = std::process::Command::new("/usr/libexec/path_helper") + .arg("-s") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let text = String::from_utf8_lossy(&output.stdout); + for line in text.split(';') { + let line = line.trim(); + let Some((_, value)) = line.split_once('=') else { + continue; + }; + let value = value.trim().trim_matches('"'); + if !value.is_empty() { + return Some(value.to_string()); + } + } + None +} + +#[cfg(not(target_os = "macos"))] +fn path_helper_path() -> Option { + None +} + +/// Merge an interactive login-shell PATH into the current process environment. +/// macOS `.app` bundles started from Finder inherit a minimal PATH; tools like +/// bun/nvm/fnm are often configured only in interactive shell init files. +fn augment_path_from_login_shell() { + let current = env::var("PATH").unwrap_or_default(); + #[cfg(unix)] + let shell_path = interactive_shell_path() + .or_else(login_shell_path) + .or_else(path_helper_path) + .unwrap_or_default(); + #[cfg(not(unix))] + let shell_path = String::new(); + + let merged = merge_paths( + &merge_paths(&shell_path, &build_extended_path()), + ¤t, + ); + if merged != current { + // SAFETY: called during single-threaded app startup before worker threads spawn. + unsafe { env::set_var("PATH", &merged) }; + eprintln!( + "[PathUtils] Augmented PATH from login shell ({} entries)", + merged.split(PATH_SEP).filter(|s| !s.is_empty()).count() + ); + } +} + +fn path_directories() -> Vec { + env::var("PATH") + .unwrap_or_default() + .split(PATH_SEP) + .filter(|entry| !entry.is_empty()) + .map(PathBuf::from) + .collect() +} + +fn standard_user_bin_dirs() -> Vec { + let mut dirs = Vec::new(); + #[cfg(target_os = "macos")] + { + dirs.push(PathBuf::from("/opt/homebrew/bin")); + dirs.push(PathBuf::from("/usr/local/bin")); + } + #[cfg(target_os = "linux")] + { + dirs.push(PathBuf::from("/usr/local/bin")); + dirs.push(PathBuf::from("/snap/bin")); + } + if let Some(home) = get_home_dir() { + #[cfg(windows)] + { + for rel in [ + ".cargo\\bin", + ".bun\\bin", + "AppData\\Local\\pnpm", + "AppData\\Roaming\\npm", + ".local\\bin", + "go\\bin", + ".volta\\bin", + ] { + dirs.push(home.join(rel)); + } + } + #[cfg(not(windows))] + { + for rel in [ + ".bun/bin", + ".local/bin", + ".cargo/bin", + "go/bin", + ".npm-global/bin", + "Library/pnpm", + ".volta/bin", + ".fnm/aliases/default/bin", + ] { + dirs.push(home.join(rel)); + } + } + } + dirs +} + +fn has_path_component(path: &Path) -> bool { + path.is_absolute() + || path.starts_with(".") + || path + .to_str() + .is_some_and(|s| s.starts_with("~/") || s.starts_with("./")) +} + +fn expand_user_path(path: &Path) -> PathBuf { + if let Some(raw) = path.to_str() { + if let Some(rest) = raw.strip_prefix("~/") { + if let Some(home) = get_home_dir() { + return home.join(rest); + } + } + } + path.to_path_buf() +} + +fn shell_lookup_executable(name: &str) -> Option { + #[cfg(unix)] + { + use std::sync::mpsc; + use std::time::Duration; + + if name.contains('\0') { + return None; + } + let escaped = name.replace('\'', r"'\''"); + let script = format!("command -v -- '{escaped}'"); + let shell = default_shell(); + let (tx, rx) = mpsc::channel(); + + std::thread::spawn(move || { + let result = std::process::Command::new(&shell) + .args(["-il", "-c", &script]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .output(); + let _ = tx.send(result); + }); + + let output = rx.recv_timeout(Duration::from_secs(5)).ok()?.ok()?; + if !output.status.success() { + return None; + } + let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if path.is_empty() { + return None; + } + let candidate = PathBuf::from(path); + if candidate.is_file() { + Some(candidate) + } else { + None + } + } + #[cfg(not(unix))] + { + let _ = name; + None + } +} + +fn resolve_executable_uncached(name: &str) -> Option { + let path = Path::new(name); + if has_path_component(path) { + let expanded = expand_user_path(path); + if expanded.is_file() && is_executable(&expanded) { + return Some(expanded); + } + return None; + } + + let mut dirs = standard_user_bin_dirs(); + dirs.extend(path_directories()); + let mut seen = std::collections::HashSet::new(); + for dir in dirs { + let key = dir.to_string_lossy().into_owned(); + if !seen.insert(key) { + continue; + } + let candidate = dir.join(name); + if candidate.is_file() && is_executable(&candidate) { + return Some(candidate); + } + #[cfg(windows)] + { + let with_exe = dir.join(format!("{name}.exe")); + if with_exe.is_file() { + return Some(with_exe); + } + } + } + + shell_lookup_executable(name) +} + +fn cache_lookup(name: &str, resolved: Option) { + if let Ok(mut guard) = EXECUTABLE_CACHE.lock() { + let map = guard.get_or_insert_with(HashMap::new); + map.insert(name.to_string(), resolved); + } +} + +fn cached_lookup(name: &str) -> Option> { + EXECUTABLE_CACHE + .lock() + .ok() + .and_then(|cache| cache.as_ref().and_then(|map| map.get(name).cloned())) +} + +/// Resolve an executable name to an absolute path when possible. +pub fn resolve_executable(name: &str) -> Option { + ensure_path_initialized(); + let trimmed = name.trim(); + if trimmed.is_empty() { + return None; + } + if let Some(cached) = cached_lookup(trimmed) { + return cached; + } + let resolved = resolve_executable_uncached(trimmed); + cache_lookup(trimmed, resolved.clone()); + resolved +} + +/// Rewrite the first token of a shell command to an absolute path when resolvable. +pub fn resolve_command(command: &str) -> String { + let trimmed = command.trim(); + if trimmed.is_empty() { + return String::new(); + } + + let mut parts = trimmed.split_whitespace(); + let binary = parts.next().unwrap_or_default(); + let rest = parts.collect::>().join(" "); + + let Some(resolved) = resolve_executable(binary) else { + return trimmed.to_string(); + }; + + let mut out = resolved.to_string_lossy().into_owned(); + if !rest.is_empty() { + out.push(' '); + out.push_str(&rest); + } + out +} + +/// Shell used for compound dev commands on Unix (login, non-interactive). +#[cfg(unix)] +pub fn command_shell() -> String { + default_shell() +} + +#[cfg(windows)] +pub fn command_shell() -> String { + "cmd".to_string() +} + +/// Rewrite pnpm/yarn invocations in package.json scripts to bun or npm. +pub fn rewrite_script_package_manager(script: &str) -> String { + let use_bun = find_bun().is_some(); + let pm_bin = find_bun() + .or_else(find_npm) + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_else(|| { + if use_bun { + "bun".into() + } else { + "npm".into() + } + }); + + let mut out = rewrite_pnpm_filter_commands(script, use_bun, &pm_bin); + + if use_bun { + out = out.replace("pnpm run ", &format!("{pm_bin} run ")); + out = out.replace("pnpm install", &format!("{pm_bin} install")); + out = out.replace("pnpm i ", &format!("{pm_bin} install ")); + out = out.replace("pnpm i", &format!("{pm_bin} install")); + out = out.replace("pnpm ", &format!("{pm_bin} run ")); + out = out.replace("yarn run ", &format!("{pm_bin} run ")); + out = out.replace("yarn install", &format!("{pm_bin} install")); + out = out.replace("yarn ", &format!("{pm_bin} run ")); + } else { + out = out.replace("pnpm run ", &format!("{pm_bin} run ")); + out = out.replace("pnpm install", &format!("{pm_bin} install")); + out = out.replace("pnpm i ", &format!("{pm_bin} install ")); + out = out.replace("pnpm i", &format!("{pm_bin} install")); + out = out.replace("pnpm ", &format!("{pm_bin} run ")); + out = out.replace("yarn run ", &format!("{pm_bin} run ")); + out = out.replace("yarn install", &format!("{pm_bin} install")); + out = out.replace("yarn ", &format!("{pm_bin} run ")); + } + + out +} + +/// Whether a script body delegates to pnpm or yarn. +pub fn script_uses_alternate_package_manager(script: &str) -> bool { + script.contains("pnpm") || script.contains("yarn") +} + +/// Build the shell command to run a package.json script using bun/npm (never pnpm/yarn). +pub fn build_start_command_for_script(script_body: &str, script_name: &str) -> Option { + let pm_bin = find_bun().or_else(find_npm)?; + let pm_str = pm_bin.to_string_lossy(); + + let rewritten = rewrite_script_package_manager(script_body); + + if script_uses_alternate_package_manager(script_body) || needs_shell_wrapper(&rewritten) { + eprintln!( + "[PathUtils] Rewrote dev script: {:?} -> {:?}", + script_body, rewritten + ); + return Some(rewritten); + } + + Some(format!("{pm_str} run {script_name}")) +} + +/// Normalize a start command using package.json script bodies when needed. +pub fn normalize_project_start_command(code_dir: &Path, command: &str) -> String { + if let Some(script_name) = extract_pm_run_script_name(command) { + let pkg = code_dir.join("package.json"); + if let Ok(content) = std::fs::read_to_string(&pkg) { + if let Ok(json) = serde_json::from_str::(&content) { + if let Some(body) = json + .get("scripts") + .and_then(|s| s.get(script_name)) + .and_then(|v| v.as_str()) + { + if let Some(cmd) = build_start_command_for_script(body, script_name) { + return cmd; + } + } + } + } + } + + rewrite_script_package_manager(command) +} + +fn extract_pm_run_script_name(command: &str) -> Option<&str> { + let parts: Vec<&str> = command.split_whitespace().collect(); + if parts.len() < 3 { + return None; + } + let script_name = parts.last()?; + let run_token = parts[parts.len() - 2]; + if run_token != "run" { + return None; + } + let pm_token = parts[parts.len() - 3]; + if pm_token.ends_with("bun") || pm_token.ends_with("npm") || pm_token == "bun" || pm_token == "npm" + { + Some(script_name) + } else { + None + } +} + +fn rewrite_pnpm_filter_commands(script: &str, use_bun: bool, pm_bin: &str) -> String { + let mut result = script.to_string(); + for prefix in ["pnpm --filter ", "pnpm -F "] { + while let Some(start) = result.find(prefix) { + let after_prefix = start + prefix.len(); + let rest = &result[after_prefix..]; + let Some(pkg_end) = rest.find(' ') else { + break; + }; + let package = &rest[..pkg_end]; + let after_pkg = rest[pkg_end..].trim_start(); + let script_end = after_pkg + .find(|c: char| c.is_whitespace() || c == '&' || c == ';') + .unwrap_or(after_pkg.len()); + let script_cmd = &after_pkg[..script_end]; + let tail = &after_pkg[script_end..]; + + let replacement = if use_bun { + format!("{pm_bin} run --filter {package} {script_cmd}") + } else { + format!("{pm_bin} run {script_cmd} -w {package}") + }; + + result = format!( + "{}{}{tail}", + &result[..start], + replacement, + ); + } + } + result +} + +/// Whether a command string should be executed as an external long-running process. +pub fn is_runnable_external_command(command: &str) -> bool { + let trimmed = command.trim(); + !trimmed.is_empty() + && trimmed != "(built-in static server)" + && !trimmed.contains("built-in static server") +} + +/// True when the command needs a shell (`&&`, pipes, etc.). +pub fn needs_shell_wrapper(command: &str) -> bool { + command.contains(|c| matches!(c, '|' | '&' | ';' | '>' | '<' | '$' | '`' | '(' | ')')) +} + +/// Split a simple `binary arg1 arg2` command for direct spawning (no shell). +pub fn parse_direct_command(command: &str) -> Option<(PathBuf, Vec)> { + let trimmed = command.trim(); + if trimmed.is_empty() || needs_shell_wrapper(trimmed) { + return None; + } + + let mut parts = trimmed.split_whitespace(); + let program = parts.next()?; + let args: Vec = parts.map(str::to_string).collect(); + let path = PathBuf::from(program); + + if path.is_absolute() && path.is_file() { + return Some((path, args)); + } + + resolve_executable(program).map(|resolved| (resolved, args)) +} + +/// Apply environment variables GUI child processes typically need on macOS. +pub fn apply_gui_child_env(cmd: &mut std::process::Command) { + cmd.env("PATH", current_path()); + if let Ok(home) = env::var("HOME") { + cmd.env("HOME", home); + } + if let Ok(user) = env::var("USER") { + cmd.env("USER", user); + } + if let Ok(lang) = env::var("LANG") { + cmd.env("LANG", lang); + } else { + cmd.env("LANG", "en_US.UTF-8"); + } + cmd.env("TERM", "dumb"); +} + +/// Apply environment variables for async child processes. +pub fn apply_gui_child_env_async(cmd: &mut tokio::process::Command) { + cmd.env("PATH", current_path()); + if let Ok(home) = env::var("HOME") { + cmd.env("HOME", home); + } + if let Ok(user) = env::var("USER") { + cmd.env("USER", user); + } + if let Ok(lang) = env::var("LANG") { + cmd.env("LANG", lang); + } else { + cmd.env("LANG", "en_US.UTF-8"); + } + cmd.env("TERM", "dumb"); +} + +/// Current PATH after initialization (safe for child processes). +pub fn current_path() -> String { + ensure_path_initialized(); + env::var("PATH").unwrap_or_else(|_| build_extended_path()) +} + /// Build an extended PATH that includes common development tool locations /// This should be called at application startup to fix the PATH issue on GUI apps pub fn build_extended_path() -> String { @@ -395,31 +956,81 @@ pub fn build_extended_path() -> String { path_dirs.join(&PATH_SEP.to_string()) } -/// Initialize extended PATH at application startup -/// Call this early in the application lifecycle to ensure all child processes -/// inherit the correct PATH -pub fn init_extended_path() { - let extended_path = build_extended_path(); - - // Log for debugging - eprintln!("[PathUtils] Setting extended PATH ({} platform)", std::env::consts::OS); +/// Initialize extended PATH at application startup. +/// Must only run inside `PATH_INIT.call_once` — do not call directly. +fn init_extended_path() { + eprintln!( + "[PathUtils] Setting extended PATH ({} platform)", + std::env::consts::OS + ); + + // Merge interactive login-shell PATH (nvm/fnm/Homebrew from shell rc files), + // then fall back to hard-coded developer tool locations. + augment_path_from_login_shell(); + + let extended_path = env::var("PATH").unwrap_or_else(|_| build_extended_path()); eprintln!("[PathUtils] PATH length: {} characters", extended_path.len()); - - // Set the PATH environment variable - // Note: Using unsafe block to work around potential Rust version or environment issues - // std::env::set_var is safe in standard Rust, but may be marked differently in this environment - unsafe { std::env::set_var("PATH", &extended_path) }; - - // Verify that bun/npm can now be found + if let Some(bun) = find_bun() { eprintln!("[PathUtils] Found bun at: {:?}", bun); } else { eprintln!("[PathUtils] bun not found after PATH extension"); } - + if let Some(npm) = find_npm() { eprintln!("[PathUtils] Found npm at: {:?}", npm); } else { eprintln!("[PathUtils] npm not found after PATH extension"); } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn merge_paths_deduplicates_and_preserves_order() { + let merged = merge_paths("/a:/b:/c", "/b:/d"); + assert_eq!(merged, "/a:/b:/c:/d"); + } + + #[test] + fn is_runnable_external_command_filters_builtin_server() { + assert!(!is_runnable_external_command("")); + assert!(!is_runnable_external_command("(built-in static server)")); + assert!(is_runnable_external_command("bun run dev")); + } + + #[test] + fn parse_direct_command_splits_binary_and_args() { + let (bin, args) = parse_direct_command("bun run dev").expect("should parse"); + assert!(bin.ends_with("bun")); + assert_eq!(args, vec!["run", "dev"]); + assert!(parse_direct_command("cd app && bun run dev").is_none()); + } + + #[test] + fn rewrite_pnpm_monorepo_script_to_bun() { + let script = "pnpm --filter @hytech/server start:dev & pnpm --filter @hytech/web dev & wait"; + let rewritten = rewrite_script_package_manager(script); + assert!(!rewritten.contains("pnpm")); + assert!(rewritten.contains("run --filter @hytech/server start:dev")); + assert!(rewritten.contains("run --filter @hytech/web dev")); + assert!(needs_shell_wrapper(&rewritten)); + } + + #[test] + fn rewrite_pnpm_script_to_npm_when_no_bun() { + let script = "pnpm --filter @app/web dev"; + let rewritten = rewrite_pnpm_filter_commands(script, false, "npm"); + assert_eq!(rewritten, "npm run dev -w @app/web"); + } + + #[test] + fn extract_pm_run_script_name_parses_absolute_bun_path() { + assert_eq!( + extract_pm_run_script_name("/Users/test/.bun/bin/bun run dev"), + Some("dev") + ); + } } \ No newline at end of file diff --git a/crates/cowork-gui/src-tauri/src/commands/runner.rs b/crates/cowork-gui/src-tauri/src/commands/runner.rs index 4b05bb3..4753fe7 100644 --- a/crates/cowork-gui/src-tauri/src/commands/runner.rs +++ b/crates/cowork-gui/src-tauri/src/commands/runner.rs @@ -64,19 +64,21 @@ async fn install_deps_if_needed(workspace: &std::path::Path) -> Result<(), Strin let pkg = workspace.join("package.json"); let mods = workspace.join("node_modules"); if pkg.exists() && !mods.exists() { - // Use our path_utils instead of which::which for macOS App Bundle compatibility - let (cmd, args) = if path_utils::has_bun() { - ("bun", vec!["install"]) - } else if path_utils::has_npm() { - ("npm", vec!["install"]) - } else { - return Ok(()) + let (cmd, args): (PathBuf, Vec<&str>) = if let Some(bun) = path_utils::find_bun() { + (bun, vec!["install"]) + } else if let Some(npm) = path_utils::find_npm() { + (npm, vec!["install"]) + } else { + return Ok(()) }; - - eprintln!("[Runner] Installing dependencies with {} {:?}", cmd, args); - let out = std::process::Command::new(cmd).args(&args).current_dir(workspace).output(); + + eprintln!("[Runner] Installing dependencies with {:?} {:?}", cmd, args); + let mut install_cmd = std::process::Command::new(&cmd); + install_cmd.args(&args).current_dir(workspace); + path_utils::apply_gui_child_env(&mut install_cmd); + let out = install_cmd.output(); if let Ok(r) = out { - if !r.status.success() { + if !r.status.success() { eprintln!("[Runner] Install warning: {}", String::from_utf8_lossy(&r.stderr)); } else { eprintln!("[Runner] Dependencies installed successfully"); @@ -144,23 +146,27 @@ fn detect_npm_start_command(dir: &std::path::Path) -> Option { if !pkg_path.exists() { return None; } - + if let Ok(content) = fs::read_to_string(&pkg_path) { if let Ok(json) = serde_json::from_str::(&content) { if let Some(scripts) = json.get("scripts").and_then(|s| s.as_object()) { - // Try common start scripts in order for script_name in &["dev", "start", "serve"] { - if scripts.get(*script_name).and_then(|s| s.as_str()).is_some() { - let pkg_manager = if path_utils::has_bun() { "bun" } else { "npm" }; - let command = format!("{} run {}", pkg_manager, script_name); - eprintln!("[Runner] Detected start command from package.json: {}", command); - return Some(command); + if let Some(script_body) = scripts.get(*script_name).and_then(|s| s.as_str()) { + if let Some(command) = + path_utils::build_start_command_for_script(script_body, script_name) + { + eprintln!( + "[Runner] Detected start command from package.json ({}): {}", + script_name, command + ); + return Some(command); + } } } } } } - + None } @@ -331,22 +337,58 @@ pub async fn start_iteration_project( } // No start script but has package.json - try common defaults - let pkg_manager = if path_utils::has_bun() { "bun" } else { "npm" }; - let default_cmd = format!("{} run dev", pkg_manager); - eprintln!("[Runner] Fallback: trying default command: {}", default_cmd); - - // Check if dev script exists, otherwise try start if let Ok(content) = fs::read_to_string(code_dir.join("package.json")) { if let Ok(json) = serde_json::from_str::(&content) { - if json.get("scripts").and_then(|s| s.as_object()).map(|s| s.contains_key("dev")).unwrap_or(false) { - let pid = PROJECT_RUNNER.start(iteration_id.clone(), default_cmd.clone(), code_dir.to_string_lossy().to_string(), None, None).await?; - return Ok(RunInfo { status: RunStatus::Running, process_id: Some(pid), command: Some(default_cmd), ..Default::default() }); - } - - if json.get("scripts").and_then(|s| s.as_object()).map(|s| s.contains_key("start")).unwrap_or(false) { - let cmd = format!("{} run start", pkg_manager); - let pid = PROJECT_RUNNER.start(iteration_id.clone(), cmd.clone(), code_dir.to_string_lossy().to_string(), None, None).await?; - return Ok(RunInfo { status: RunStatus::Running, process_id: Some(pid), command: Some(cmd), ..Default::default() }); + if let Some(scripts) = json.get("scripts").and_then(|s| s.as_object()) { + if scripts.contains_key("dev") { + if let Some(script_body) = scripts.get("dev").and_then(|s| s.as_str()) { + if let Some(cmd) = + path_utils::build_start_command_for_script(script_body, "dev") + { + eprintln!("[Runner] Fallback: using dev script: {}", cmd); + let pid = PROJECT_RUNNER + .start( + iteration_id.clone(), + cmd.clone(), + code_dir.to_string_lossy().to_string(), + None, + None, + ) + .await?; + return Ok(RunInfo { + status: RunStatus::Running, + process_id: Some(pid), + command: Some(cmd), + ..Default::default() + }); + } + } + } + + if scripts.contains_key("start") { + if let Some(script_body) = scripts.get("start").and_then(|s| s.as_str()) { + if let Some(cmd) = + path_utils::build_start_command_for_script(script_body, "start") + { + eprintln!("[Runner] Fallback: using start script: {}", cmd); + let pid = PROJECT_RUNNER + .start( + iteration_id.clone(), + cmd.clone(), + code_dir.to_string_lossy().to_string(), + None, + None, + ) + .await?; + return Ok(RunInfo { + status: RunStatus::Running, + process_id: Some(pid), + command: Some(cmd), + ..Default::default() + }); + } + } + } } } } @@ -355,7 +397,11 @@ pub async fn start_iteration_project( // Fallback 4: Cargo.toml (Rust project) if code_dir.join("Cargo.toml").exists() { eprintln!("[Runner] Fallback 4: Cargo.toml found, using cargo run"); - let cmd = "cargo run".to_string(); + let cmd = if let Some(cargo) = path_utils::resolve_executable("cargo") { + format!("{} run", cargo.to_string_lossy()) + } else { + "cargo run".to_string() + }; let pid = PROJECT_RUNNER.start(iteration_id.clone(), cmd.clone(), code_dir.to_string_lossy().to_string(), None, None).await?; return Ok(RunInfo { status: RunStatus::Running, process_id: Some(pid), command: Some(cmd), ..Default::default() }); } @@ -450,17 +496,29 @@ fn is_fullstack(rt: &RuntimeType) -> bool { } fn get_start_command_from_config(config: &cowork_core::ProjectRuntimeConfig) -> Option { - if let Some(ref f) = config.frontend { - return Some(f.dev_command.clone()); - } - if let Some(ref b) = config.backend { + let raw = if let Some(ref f) = config.frontend { + if path_utils::is_runnable_external_command(&f.dev_command) { + Some(f.dev_command.clone()) + } else { + None + } + } else if let Some(ref b) = config.backend { if let Some(ref cmd) = b.start_command { - if !cmd.is_empty() { - return Some(cmd.clone()); + if path_utils::is_runnable_external_command(cmd) { + Some(cmd.clone()) + } else { + None } + } else if path_utils::is_runnable_external_command(&b.dev_command) { + Some(b.dev_command.clone()) + } else { + None } - } - None + } else { + None + }?; + + Some(path_utils::resolve_command(&path_utils::rewrite_script_package_manager(&raw))) } async fn start_fullstack(iteration_id: String, code_dir: PathBuf, config: &cowork_core::ProjectRuntimeConfig) -> Result { @@ -468,8 +526,17 @@ async fn start_fullstack(iteration_id: String, code_dir: PathBuf, config: &cowor let (fk, bk) = static_server::get_fullstack_process_keys(&iteration_id); let b_url = format!("http://localhost:{}", fs_cfg.backend_port); - let _bpid = PROJECT_RUNNER.start(bk.clone(), fs_cfg.backend_dev_command.clone(), - code_dir.to_string_lossy().to_string(), Some(b_url.clone()), Some(fs_cfg.backend_port)).await?; + let backend_cmd = path_utils::resolve_command(&path_utils::rewrite_script_package_manager( + &fs_cfg.backend_dev_command, + )); + let _bpid = PROJECT_RUNNER.start( + bk.clone(), + backend_cmd, + code_dir.to_string_lossy().to_string(), + Some(b_url.clone()), + Some(fs_cfg.backend_port), + ) + .await?; for _ in 0..30 { if !PROJECT_RUNNER.is_running(&fk) { tokio::time::sleep(std::time::Duration::from_secs(1)).await; continue; } @@ -477,8 +544,17 @@ async fn start_fullstack(iteration_id: String, code_dir: PathBuf, config: &cowor } let f_url = format!("http://localhost:{}", fs_cfg.frontend_port); - let fpid = PROJECT_RUNNER.start(fk.clone(), fs_cfg.frontend_dev_command.clone(), - code_dir.to_string_lossy().to_string(), Some(f_url.clone()), Some(fs_cfg.frontend_port)).await?; + let frontend_cmd = path_utils::resolve_command(&path_utils::rewrite_script_package_manager( + &fs_cfg.frontend_dev_command, + )); + let fpid = PROJECT_RUNNER.start( + fk.clone(), + frontend_cmd.clone(), + code_dir.to_string_lossy().to_string(), + Some(f_url.clone()), + Some(fs_cfg.frontend_port), + ) + .await?; let inst = static_server::FullstackProcessInstance { iteration_id: iteration_id.clone(), diff --git a/crates/cowork-gui/src-tauri/src/config_commands.rs b/crates/cowork-gui/src-tauri/src/config_commands.rs index eceecf9..453ea7f 100644 --- a/crates/cowork-gui/src-tauri/src/config_commands.rs +++ b/crates/cowork-gui/src-tauri/src/config_commands.rs @@ -672,6 +672,7 @@ pub async fn test_llm_connection(llm_config: LlmConfig) -> Result contents, config: None, tools: Default::default(), + previous_response_id: None, }; let mut stream = client diff --git a/crates/cowork-gui/src-tauri/src/iteration_commands.rs b/crates/cowork-gui/src-tauri/src/iteration_commands.rs index 81fe5a9..d053ccf 100644 --- a/crates/cowork-gui/src-tauri/src/iteration_commands.rs +++ b/crates/cowork-gui/src-tauri/src/iteration_commands.rs @@ -10,6 +10,7 @@ use cowork_core::pipeline::IterationExecutor; use tauri::{Emitter, Manager, State, Window}; use std::sync::Arc; use serde::{Serialize, Deserialize}; +use tracing; // ============================================================================ // Types @@ -248,14 +249,14 @@ pub async fn gui_continue_iteration( let iteration_id_clone = iteration_id.clone(); tokio::spawn(async move { - println!("[GUI] Starting continue_iteration for iteration: {}", iteration_id_clone); + tracing::info!("[GUI] Starting continue_iteration for iteration: {}", iteration_id_clone); match executor.continue_iteration(&mut project, &iteration_id_clone, Some(model)).await { Ok(_) => { - println!("[GUI] continue_iteration completed successfully"); + tracing::info!("[GUI] continue_iteration completed successfully"); let _ = window_clone.emit("iteration_completed", iteration_id_clone); } Err(e) => { - println!("[GUI] continue_iteration failed: {}", e); + tracing::error!("[GUI] continue_iteration failed: {}", e); let _ = window_clone.emit("iteration_failed", (iteration_id_clone, e.to_string())); } } @@ -286,7 +287,7 @@ pub async fn gui_retry_iteration( let model_config = load_config() .map_err(|e| format!("Failed to load LLM configuration: {}", e))?; - let _model = create_llm_client(&model_config.llm) + let model = create_llm_client(&model_config.llm) .map_err(|e| format!("Failed to create LLM client: {}", e))?; // Emit started event @@ -297,14 +298,14 @@ pub async fn gui_retry_iteration( let iteration_id_clone = iteration_id.clone(); tokio::spawn(async move { - println!("[GUI] Starting retry_iteration for iteration: {}", iteration_id_clone); - match executor.retry_iteration(&mut project, &iteration_id_clone).await { + tracing::info!("[GUI] Starting retry_iteration for iteration: {}", iteration_id_clone); + match executor.retry_iteration(&mut project, &iteration_id_clone, Some(model)).await { Ok(_) => { - println!("[GUI] retry_iteration completed successfully"); + tracing::info!("[GUI] retry_iteration completed successfully"); let _ = window_clone.emit("iteration_completed", iteration_id_clone); } Err(e) => { - println!("[GUI] retry_iteration failed: {}", e); + tracing::error!("[GUI] retry_iteration failed: {}", e); let _ = window_clone.emit("iteration_failed", (iteration_id_clone, e.to_string())); } } @@ -422,14 +423,14 @@ pub async fn gui_regenerate_knowledge( let iteration_id_clone = iteration_id.clone(); tokio::spawn(async move { - println!("[GUI] Starting knowledge regeneration for iteration: {}", iteration_id_clone); + tracing::info!("[GUI] Starting knowledge regeneration for iteration: {}", iteration_id_clone); match executor.regenerate_iteration_knowledge(&iteration_id_clone, model).await { Ok(_) => { - println!("[GUI] Knowledge regeneration completed successfully"); + tracing::info!("[GUI] Knowledge regeneration completed successfully"); let _ = window_clone.emit("knowledge_regeneration_completed", iteration_id_clone); } Err(e) => { - println!("[GUI] Knowledge regeneration failed: {}", e); + tracing::error!("[GUI] Knowledge regeneration failed: {}", e); let _ = window_clone.emit("knowledge_regeneration_failed", (iteration_id_clone, e.to_string())); } } diff --git a/crates/cowork-gui/src-tauri/src/lib.rs b/crates/cowork-gui/src-tauri/src/lib.rs index 6e3f898..fdcd48e 100644 --- a/crates/cowork-gui/src-tauri/src/lib.rs +++ b/crates/cowork-gui/src-tauri/src/lib.rs @@ -703,21 +703,19 @@ pub fn run() { // Initialize tool notification callback let app_handle = app.handle().clone(); - cowork_core::tools::set_tool_notify_callback(move |tool_name: &str, args: &Value, is_call: bool, result: &str| { + cowork_core::tools::set_tool_notify_callback(move |tool_name: &str, args: &Value, is_call: bool, result: &str, agent_name: &str| { if is_call { - // Tool call event let _ = app_handle.emit("tool_call", serde_json::json!({ "tool_name": tool_name, "arguments": args, - "agent_name": "Agent" + "agent_name": agent_name })); } else { - // Tool result event let _ = app_handle.emit("tool_result", serde_json::json!({ "tool_name": tool_name, "success": args.is_null() || result.is_empty() || !result.contains("error"), "result": result, - "agent_name": "Agent" + "agent_name": agent_name })); } }); diff --git a/crates/cowork-gui/src-tauri/src/project_runner.rs b/crates/cowork-gui/src-tauri/src/project_runner.rs index 0d735fa..df22a64 100644 --- a/crates/cowork-gui/src-tauri/src/project_runner.rs +++ b/crates/cowork-gui/src-tauri/src/project_runner.rs @@ -4,6 +4,9 @@ use std::sync::{Arc, Mutex}; use tauri::Emitter; use tokio::process::Child; use tokio::sync::mpsc; +use tracing; + +use crate::commands::path_utils; // Import PreviewInfo from gui_types use super::gui_types::PreviewInfo; @@ -17,6 +20,29 @@ pub struct ProjectRunner { app_handle: Arc>>, } +fn command_exists(cmd: &str) -> bool { + #[cfg(target_os = "windows")] + { + std::process::Command::new("where") + .arg(cmd) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) + } + #[cfg(not(target_os = "windows"))] + { + std::process::Command::new("which") + .arg(cmd) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) + } +} + struct ProjectProcess { child: Child, #[allow(dead_code)] @@ -53,7 +79,7 @@ impl ProjectRunner { ) -> Result { // Stop existing process if any if let Ok(()) = self.stop(iteration_id.clone()).await { - println!( + tracing::info!( "[Runner] Stopped existing process for iteration: {}", iteration_id ); @@ -68,51 +94,79 @@ impl ProjectRunner { // Debug: Print PATH and check commands let path_env = std::env::var("PATH").unwrap_or_else(|_| "PATH not found".to_string()); - println!("[Runner] PATH = {}", path_env); - + tracing::debug!("[Runner] PATH = {}", path_env); + // Check if bun or sh exists - println!("[Runner] Checking commands..."); - if std::process::Command::new("which").arg("bun").output().map(|o| o.status.success()).unwrap_or(false) { - println!("[Runner] bun found"); + tracing::debug!("[Runner] Checking commands..."); + if command_exists("bun") { + tracing::debug!("[Runner] bun found"); } else { - println!("[Runner] bun NOT found"); + tracing::debug!("[Runner] bun NOT found"); } - if std::process::Command::new("which").arg("sh").output().map(|o| o.status.success()).unwrap_or(false) { - println!("[Runner] sh found"); + if command_exists("sh") { + tracing::debug!("[Runner] sh found"); } else { - println!("[Runner] sh NOT found"); + tracing::debug!("[Runner] sh NOT found"); } - println!("[Runner] Starting command: {} in {}", command, code_dir); + tracing::info!("[Runner] Starting command: {} in {}", command, code_dir); + + let normalized_command = + path_utils::normalize_project_start_command(code_path, &command); + let resolved_command = path_utils::resolve_command(&normalized_command); + if !path_utils::is_runnable_external_command(&resolved_command) { + return Err(format!( + "Invalid or empty start command: {:?}. Check project runtime configuration.", + command + )); + } #[cfg(target_os = "windows")] let mut child = { let mut cmd = tokio::process::Command::new("cmd"); - cmd.args(["/C", &command]) + cmd.args(["/C", &resolved_command]) .current_dir(&code_path) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .creation_flags(0x08000000); // CREATE_NO_WINDOW + path_utils::apply_gui_child_env_async(&mut cmd); cmd.spawn().map_err(|e| format!("Failed to start: {}", e))? }; #[cfg(not(target_os = "windows"))] let mut child = { - // Set PATH to include common locations - critical for macOS app bundle - let path = std::env::var("PATH").unwrap_or_else(|_| { - // Default PATH for macOS - "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin".to_string() - }); - - tokio::process::Command::new("sh") - .args(["-c", &command]) - .current_dir(&code_path) - .env("PATH", path) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .spawn() - .map_err(|e| format!("Failed to start: {}", e))? + if let Some((program, args)) = path_utils::parse_direct_command(&resolved_command) { + tracing::info!( + "[Runner] Spawning directly: {:?} {:?}", + program, + args + ); + let mut cmd = tokio::process::Command::new(&program); + cmd.args(&args) + .current_dir(&code_path) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + path_utils::apply_gui_child_env_async(&mut cmd); + cmd.spawn().map_err(|e| format!("Failed to start: {}", e))? + } else { + // Compound commands only: login shell without -i (non-TTY safe). + // `exec` replaces the shell so we monitor the real dev-server process. + let shell = path_utils::command_shell(); + let shell_script = format!("exec {resolved_command}"); + tracing::info!( + "[Runner] Spawning via shell: {} -lc {}", + shell, + shell_script + ); + let mut cmd = tokio::process::Command::new(&shell); + cmd.args(["-lc", &shell_script]) + .current_dir(&code_path) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + path_utils::apply_gui_child_env_async(&mut cmd); + cmd.spawn().map_err(|e| format!("Failed to start: {}", e))? + } }; let pid = child.id().unwrap(); @@ -128,38 +182,80 @@ impl ProjectRunner { let (stdout_tx, _stdout_rx) = mpsc::unbounded_channel(); let (stderr_tx, _stderr_rx) = mpsc::unbounded_channel(); - // Clone child for stdout reading - let stdout = child.stdout.take().unwrap(); - let stderr = child.stderr.take().unwrap(); - - // Clone senders for spawn tasks - let stdout_tx_spawn = stdout_tx.clone(); - let stderr_tx_spawn = stderr_tx.clone(); - // Clone for stdout task let iteration_id_stdout = iteration_id_clone.clone(); - + // Check if process exited immediately (command error detection) - // Give it a brief moment to potentially fail - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + match child.try_wait() { Ok(Some(status)) => { - // Process already exited - command likely failed - println!("[Runner] Process exited immediately with status: {:?}", status); + let mut output_preview = String::new(); + { + use tokio::io::AsyncReadExt; + if let Some(mut stdout) = child.stdout.take() { + let mut stdout_buf = vec![0u8; 4096]; + if let Ok(n) = stdout.read(&mut stdout_buf).await { + if n > 0 { + output_preview + .push_str(&String::from_utf8_lossy(&stdout_buf[..n])); + } + } + } + if let Some(mut stderr) = child.stderr.take() { + let mut stderr_buf = vec![0u8; 4096]; + if let Ok(n) = stderr.read(&mut stderr_buf).await { + if n > 0 { + if !output_preview.is_empty() { + output_preview.push('\n'); + } + output_preview + .push_str(&String::from_utf8_lossy(&stderr_buf[..n])); + } + } + } + } + + tracing::warn!( + "[Runner] Process exited immediately with status: {:?}, output: {}", + status, + output_preview + ); + + let hint = if status.success() { + "The dev process exited immediately. Check package.json scripts and that dependencies are installed." + } else { + "The command failed to start. Check that bun/npm/cargo is installed and the project directory is correct." + }; + return Err(format!( - "Command failed immediately. Exit status: {}. Check if the command is correct.", - status + "Command failed immediately. Exit status: {}. {}\nCommand: {}\n{}", + status, + hint, + resolved_command, + if output_preview.is_empty() { + String::new() + } else { + format!("Output:\n{output_preview}") + } )); } Ok(None) => { // Process is still running - good } Err(e) => { - eprintln!("[Runner] Error checking process status: {}", e); + tracing::error!("[Runner] Error checking process status: {}", e); } } + // Clone child for stdout/stderr reading (only after liveness check passes) + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + + // Clone senders for spawn tasks + let stdout_tx_spawn = stdout_tx.clone(); + let stderr_tx_spawn = stderr_tx.clone(); + // Spawn task to read stdout and emit events tokio::spawn(async move { use tokio::io::{AsyncBufReadExt, BufReader}; @@ -183,14 +279,14 @@ impl ProjectRunner { "content": line.clone() }), ) { - eprintln!("[Runner] Failed to emit project_log event: {}", e); + tracing::warn!("[Runner] Failed to emit project_log event: {}", e); } } line.clear(); } Err(e) => { - eprintln!("[Runner] Error reading stdout: {}", e); + tracing::error!("[Runner] Error reading stdout: {}", e); // Emit error event (use expected event name) if let Some(ref handle) = app_handle_stdout { @@ -203,7 +299,7 @@ impl ProjectRunner { "content": format!("Error reading output: {}\n", e) }), ) { - eprintln!("[Runner] Failed to emit project_log event: {}", e); + tracing::warn!("[Runner] Failed to emit project_log event: {}", e); } } break; @@ -235,14 +331,14 @@ impl ProjectRunner { "content": line.clone() }), ) { - eprintln!("[Runner] Failed to emit project_log event: {}", e); + tracing::warn!("[Runner] Failed to emit project_log event: {}", e); } } line.clear(); } Err(e) => { - eprintln!("[Runner] Error reading stderr: {}", e); + tracing::error!("[Runner] Error reading stderr: {}", e); // Emit error event if let Some(ref handle) = app_handle_stderr { @@ -253,7 +349,7 @@ impl ProjectRunner { "error": e.to_string() }), ) { - eprintln!( + tracing::warn!( "[Runner] Failed to emit process_error event: {}", emit_err ); @@ -308,13 +404,13 @@ impl ProjectRunner { match proc.child.try_wait() { Ok(Some(status)) => { // Process has exited, remove it - println!("[Runner] Process {} exited with status: {:?}", iteration_id_exit, status); + tracing::info!("[Runner] Process {} exited with status: {:?}", iteration_id_exit, status); procs.remove(&iteration_id_exit); true } Ok(None) => false, // Still running Err(e) => { - eprintln!("[Runner] Error checking process status: {}", e); + tracing::error!("[Runner] Error checking process status: {}", e); false } } @@ -342,7 +438,7 @@ impl ProjectRunner { } }); - println!("[Runner] Process started with PID: {}", pid); + tracing::info!("[Runner] Process started with PID: {}", pid); Ok(pid) } @@ -354,7 +450,7 @@ impl ProjectRunner { }; if let Some(mut process) = process { - println!("[Runner] Stopping process for iteration: {}", iteration_id); + tracing::info!("[Runner] Stopping process for iteration: {}", iteration_id); let _ = process.child.kill().await; @@ -369,11 +465,11 @@ impl ProjectRunner { ); } - println!("[Runner] Process stopped"); + tracing::info!("[Runner] Process stopped"); Ok(()) } else { // Process already stopped or not found - this is fine, just return success - println!( + tracing::debug!( "[Runner] No running process found for iteration: {} (may already be stopped)", iteration_id ); diff --git a/crates/cowork-gui/src-tauri/tauri.conf.json b/crates/cowork-gui/src-tauri/tauri.conf.json index 0a2dd8a..0e83e52 100644 --- a/crates/cowork-gui/src-tauri/tauri.conf.json +++ b/crates/cowork-gui/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Cowork Forge", - "version": "2.5.1", + "version": "2.5.2", "identifier": "com.coworkforge.gui", "build": { "beforeDevCommand": "bun run dev", diff --git a/crates/cowork-gui/src/App.tsx b/crates/cowork-gui/src/App.tsx index c387969..c3aea97 100644 --- a/crates/cowork-gui/src/App.tsx +++ b/crates/cowork-gui/src/App.tsx @@ -97,8 +97,9 @@ function App() { // Compute chat mode const chatMode = useMemo(() => { if (!currentIteration) return 'disabled'; - if (currentIteration.status === 'Completed') return 'pm_agent'; - if (isProcessing || currentIteration.status === 'Running') return 'pipeline'; + const status = currentIteration.status.toLowerCase(); + if (status === 'completed') return 'pm_agent'; + if (isProcessing || status === 'running') return 'pipeline'; return 'pipeline'; }, [currentIteration, isProcessing]); @@ -154,9 +155,11 @@ function App() { ); // Render content based on active view + // 改为条件挂载:访问过的重面板不再保持 display:none + 副作用持续跑 + // 状态在 stores 中保留,切回时从 store 恢复,不影响体验 const renderContent = () => ( -
-
+
+ {activeView === 'iterations' && ( -
+ )} -
+ {activeView === 'projects' && ( -
+ )} -
- {currentIteration ? ( + {activeView === 'artifacts' && ( + currentIteration ? ( ) : ( - )} -
+ ) + )} -
- {currentIteration ? ( + {activeView === 'code' && ( + currentIteration ? ( ) : ( - )} -
+ ) + )} -
- {currentIteration ? ( + {activeView === 'run' && ( + currentIteration ? ( ) : ( - )} -
+ ) + )} -
+ {activeView === 'execution-memory' && ( -
+ )} -
+ {activeView === 'project-knowledge' && ( -
+ )} -
- - - -
+ {activeView === 'settings' && ( +
+ + + +
+ )} -
- - - -
+ {activeView === 'config' && ( +
+ + + +
+ )} -
- {currentIteration ? ( + {activeView === 'chat' && ( + currentIteration ? ( ) : ( - )} -
+ ) + )}
); diff --git a/crates/cowork-gui/src/components/ArtifactsViewer.tsx b/crates/cowork-gui/src/components/ArtifactsViewer.tsx index 1b2f2e3..0635c16 100644 --- a/crates/cowork-gui/src/components/ArtifactsViewer.tsx +++ b/crates/cowork-gui/src/components/ArtifactsViewer.tsx @@ -1,12 +1,53 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, type ReactNode } from 'react'; import { invoke } from '@tauri-apps/api/core'; import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import rehypeHighlight from 'rehype-highlight'; -import rehypeRaw from 'rehype-raw'; -import JsonView from 'react-json-view'; -import { App, Tabs, Spin, Alert, Empty, Button, Space, Tooltip } from 'antd'; -import { FileTextOutlined, ProjectOutlined, DatabaseOutlined, BuildOutlined, CheckCircleOutlined, FileMarkdownOutlined, FolderOpenOutlined, ReloadOutlined } from '@ant-design/icons'; +import { remarkPlugins, fullRehypePlugins } from '@/utils/markdown'; +import { App, Tabs, Empty, Button, Space, Tooltip } from 'antd'; +import { FileTextOutlined, ProjectOutlined, BuildOutlined, CheckCircleOutlined, FileMarkdownOutlined, FolderOpenOutlined, ReloadOutlined } from '@ant-design/icons'; + +// Native JSON renderer — avoids react-json-view's React 19 incompatibility (white-screen crash). +const renderJson = (data: unknown) => { + let text: string; + try { + text = JSON.stringify(data, null, 2); + } catch { + text = String(data); + } + return ( +
{text}
+ ); +}; + +interface ArtifactTabPanelProps { + title: string; + actions?: ReactNode; + contentClassName?: string; + children: ReactNode; +} + +const ArtifactTabPanel: React.FC = ({ title, actions, contentClassName, children }) => ( +
+
+ {title} + {actions} +
+
+ {children} +
+
+); interface ArtifactsData { iteration_id?: string; @@ -146,25 +187,28 @@ const ArtifactsViewer: React.FC = ({ iterationId, activeTa return ; } + // ====== tab 懒构造:只有 activeTab 的 children 才包含 ReactMarkdown 节点 ====== + // 其他 tab 的 children 设为 null,切到该 tab 时才重新渲染(Antd Tabs 默认会重新挂载 children) const items = []; + const isActive = (key: string) => activeTab === key; if (artifacts.idea) { items.push({ key: 'idea', label: Idea, - children: ( -
-
- Idea Document + children: isActive('idea') ? ( + -
-
- {artifacts.idea} -
-
- ), + )} + > + {artifacts.idea} + + ) : null, }); } @@ -172,19 +216,19 @@ const ArtifactsViewer: React.FC = ({ iterationId, activeTa items.push({ key: 'requirements', label: Requirements, - children: ( -
-
- Requirements Document + children: isActive('requirements') ? ( + -
-
- {artifacts.requirements} -
-
- ), + )} + > + {artifacts.requirements} + + ) : null, }); } @@ -194,10 +238,10 @@ const ArtifactsViewer: React.FC = ({ iterationId, activeTa items.push({ key: 'design', label: Design, - children: ( -
-
- Design Specification + children: isActive('design') ? ( + @@ -209,20 +253,17 @@ const ArtifactsViewer: React.FC = ({ iterationId, activeTa )} -
-
- {artifacts.design_raw || designViewMode === 'doc' ? ( -
- {designContent} -
- ) : ( -
- -
- )} -
-
- ), + )} + > + {artifacts.design_raw || designViewMode === 'doc' ? ( +
+ {designContent} +
+ ) : ( + renderJson(artifacts.design) + )} + + ) : null, }); } @@ -232,10 +273,10 @@ const ArtifactsViewer: React.FC = ({ iterationId, activeTa items.push({ key: 'plan', label: Plan, - children: ( -
-
- Implementation Plan + children: isActive('plan') ? ( + @@ -247,20 +288,17 @@ const ArtifactsViewer: React.FC = ({ iterationId, activeTa )} -
-
- {artifacts.plan_raw || planViewMode === 'doc' ? ( -
- {planContent} -
- ) : ( -
- -
- )} -
-
- ), + )} + > + {artifacts.plan_raw || planViewMode === 'doc' ? ( +
+ {planContent} +
+ ) : ( + renderJson(artifacts.plan) + )} + + ) : null, }); } @@ -268,21 +306,18 @@ const ArtifactsViewer: React.FC = ({ iterationId, activeTa items.push({ key: 'code', label: Code Files, - children: ( -
-
- Code Files ({artifacts.code_files.length}) + children: isActive('code') ? ( + -
-
-
- -
-
-
- ), + )} + > + {renderJson(artifacts.code_files)} + + ) : null, }); } @@ -290,19 +325,19 @@ const ArtifactsViewer: React.FC = ({ iterationId, activeTa items.push({ key: 'check_report', label: Check Report, - children: ( -
-
- Check Report + children: isActive('check_report') ? ( + -
-
- {artifacts.check_report} -
-
- ), + )} + > + {artifacts.check_report} + + ) : null, }); } @@ -310,19 +345,19 @@ const ArtifactsViewer: React.FC = ({ iterationId, activeTa items.push({ key: 'delivery_report', label: Delivery Report, - children: ( -
-
- Delivery Report + children: isActive('delivery_report') ? ( + -
-
- {artifacts.delivery_report} -
-
- ), + )} + > + {artifacts.delivery_report} + + ) : null, }); } @@ -334,7 +369,7 @@ const ArtifactsViewer: React.FC = ({ iterationId, activeTa }; return ( -
+
Artifacts @@ -344,7 +379,20 @@ const ArtifactsViewer: React.FC = ({ iterationId, activeTa
- +
); }; diff --git a/crates/cowork-gui/src/components/CodeEditor.tsx b/crates/cowork-gui/src/components/CodeEditor.tsx index b300bdb..f2872f3 100644 --- a/crates/cowork-gui/src/components/CodeEditor.tsx +++ b/crates/cowork-gui/src/components/CodeEditor.tsx @@ -1,6 +1,8 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { invoke } from '@tauri-apps/api/core'; -import Editor from '@monaco-editor/react'; +import Editor, { type OnMount } from '@monaco-editor/react'; +import type { editor as MonacoEditor } from 'monaco-editor'; +import { FixedSizeList as List } from 'react-window'; import { Tabs, Spin, Alert, Empty, Dropdown, Button, Space } from 'antd'; import { FolderOutlined, FileOutlined, ReloadOutlined, CaretRightOutlined, CaretDownOutlined, CodeOutlined, DownOutlined } from '@ant-design/icons'; import { showError, showSuccess, showWarning, tryExecute } from '../utils/errorHandler'; @@ -46,6 +48,25 @@ const CodeEditor: React.FC = ({ iterationId, refreshTrigger }) const [formatting, setFormatting] = useState(false); const prevRefreshTriggerRef = useRef(0); + // ===== Monaco viewState 持久化(切 tab 不丢光标/折叠/scroll) ===== + const editorRef = useRef(null); + const viewStatesRef = useRef>({}); + const monacoRef = useRef(null); + + // ===== 文件树虚拟化 ===== + const fileTreeContainerRef = useRef(null); + const [treeHeight, setTreeHeight] = useState(600); + const fileTreeListRef = useRef(null); + + useEffect(() => { + if (!fileTreeContainerRef.current) return; + const observer = new ResizeObserver(entries => { + for (const entry of entries) setTreeHeight(entry.contentRect.height); + }); + observer.observe(fileTreeContainerRef.current); + return () => observer.disconnect(); + }, []); + useEffect(() => { if (iterationId) { loadFileTree(); @@ -199,6 +220,32 @@ const CodeEditor: React.FC = ({ iterationId, refreshTrigger }) } }; + // 切 tab 前保存旧 tab 的 viewState + const handleFileSelectWithViewState = useCallback(async (filePath: string) => { + if (editorRef.current && activeFile) { + viewStatesRef.current[activeFile] = editorRef.current.saveViewState(); + } + await handleFileSelect(filePath); + // 切换后恢复新 tab 的 viewState + if (editorRef.current && viewStatesRef.current[filePath]) { + editorRef.current.restoreViewState(viewStatesRef.current[filePath]!); + } + }, [activeFile, handleFileSelect]); + + // Editor 挂载:恢复 viewState,注册保存快捷键 + const handleEditorMount: OnMount = (editor, monaco) => { + editorRef.current = editor; + monacoRef.current = monaco; + if (activeFile && viewStatesRef.current[activeFile]) { + editor.restoreViewState(viewStatesRef.current[activeFile]!); + } + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { + if (activeFile) { + saveFileContent(activeFile, editor.getValue()); + } + }); + }; + const getLanguageFromPath = (filePath: string): string => { const ext = filePath.split('.').pop()?.toLowerCase() || ''; const langMap: Record = { @@ -209,8 +256,9 @@ const CodeEditor: React.FC = ({ iterationId, refreshTrigger }) return langMap[ext] || 'plaintext'; }; - const renderFileTreeRow = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => { - const node = flatFileTree[index]; + // react-window 兼容的 Row 组件(接收 data prop) + const FileTreeRow = useCallback(({ index, style, data }: { index: number; style: React.CSSProperties; data: FlatFileTreeNode[] }) => { + const node = data[index]; if (!node) return null; return ( @@ -247,29 +295,80 @@ const CodeEditor: React.FC = ({ iterationId, refreshTrigger }) )} - {node.name} + + {node.name} +
); - }, [flatFileTree, handleToggleFolder, handleFileSelect]); + }, [handleToggleFolder, handleFileSelect]); if (loading) { return (
-
Loading files...
+
Loading files...
); } if (error) { + const isMissingWorkspace = error.toLowerCase().includes('workspace not found') + || error.toLowerCase().includes('iteration directory not found'); + return ( +
+ +
+ The code workspace for this iteration does not exist yet. +
+
+ Run the iteration through the Coding stage to generate code files. + Use the Collaborate tab to start or continue the iteration. +
+ + ) : error + } + type={isMissingWorkspace ? 'info' : 'error'} + showIcon + action={} + /> +
+ ); + } + + // Detect empty workspace (root with no children) + const isEmptyWorkspace = !fileTree + || (!fileTree.children || fileTree.children.length === 0); + + if (isEmptyWorkspace) { return ( - Retry} - /> +
+ +
No code files in this workspace yet.
+
+ Code files appear here after the Coding stage runs. +
+ + } + > + +
+
); } @@ -295,14 +394,7 @@ const CodeEditor: React.FC = ({ iterationId, refreshTrigger }) scrollBeyondLastLine: false, automaticLayout: true, }} - saveViewState={true} - onMount={(editor) => { - editor.addCommand(0, () => { - if (activeFile) { - saveFileContent(activeFile, editor.getValue()); - } - }, 'save'); - }} + onMount={handleEditorMount} /> ) : null}
@@ -331,14 +423,18 @@ const CodeEditor: React.FC = ({ iterationId, refreshTrigger })
-
-
- {flatFileTree.map((node, index) => ( -
- {renderFileTreeRow({ index, style: {} })} -
- ))} -
+
+ + {FileTreeRow} +
@@ -346,7 +442,7 @@ const CodeEditor: React.FC = ({ iterationId, refreshTrigger }) { if (action === 'remove' && typeof targetKey === 'string') { handleCloseFile(targetKey); diff --git a/crates/cowork-gui/src/components/CommandPalette.tsx b/crates/cowork-gui/src/components/CommandPalette.tsx index fa666d4..60fe962 100644 --- a/crates/cowork-gui/src/components/CommandPalette.tsx +++ b/crates/cowork-gui/src/components/CommandPalette.tsx @@ -110,7 +110,7 @@ const CommandPalette: React.FC = ({ visible, onClose, onCom { item.action(); handleClose(); }} - style={{ cursor: 'pointer', padding: '12px', backgroundColor: index === selectedIndex ? '#1890ff22' : 'transparent', borderRadius: '4px' }} + style={{ cursor: 'pointer', padding: '12px', backgroundColor: index === selectedIndex ? '#1890ff22' : 'transparent', borderRadius: '2px' }} > = ({ currentSession, current return (
- +
@@ -206,8 +206,8 @@ const KnowledgePanel: React.FC = ({ currentSession, current ) : currentIterationInfo ? ( // Current iteration is selected but has no knowledge - show generate option
-
- Current iteration has no knowledge. Generate knowledge for it: +
+ Current iteration has no knowledge. Generate knowledge for it:
@@ -241,8 +241,8 @@ const KnowledgePanel: React.FC = ({ currentSession, current ) : iterations.length > 0 ? ( // No current iteration selected - show all completed iterations
-
- No knowledge found. You can generate knowledge for completed iterations: +
+ No knowledge found. You can generate knowledge for completed iterations:
{iterations.map((iteration) => ( @@ -318,7 +318,7 @@ const KnowledgePanel: React.FC = ({ currentSession, current - + {selectedKnowledge?.title} {selectedKnowledge?.iteration_id?.slice(0, 8)}
@@ -328,7 +328,7 @@ const KnowledgePanel: React.FC = ({ currentSession, current footer={null} width={900} style={{ top: "5vh" }} - styles={{ body: { padding: "24px", maxHeight: "75vh", overflow: "auto" } }} + styles={{ body: { padding: "20px", maxHeight: "75vh", overflow: "auto" } }} > {selectedKnowledge && ( = ({ currentSession, current label: Summary, children: (
- + {selectedKnowledge.idea_summary && ( -
+
- - Idea Summary + + Idea Summary
- {selectedKnowledge.idea_summary} + {selectedKnowledge.idea_summary}
)} {selectedKnowledge.design_summary && ( -
+
Design Summary
- {selectedKnowledge.design_summary} + {selectedKnowledge.design_summary}
)} {selectedKnowledge.plan_summary && ( -
+
- - Plan Summary + + Plan Summary
- {selectedKnowledge.plan_summary} + {selectedKnowledge.plan_summary}
)} {selectedKnowledge.code_structure && ( -
+
- - Code Structure + + Code Structure
- {selectedKnowledge.code_structure} + {selectedKnowledge.code_structure}
)} @@ -386,7 +386,7 @@ const KnowledgePanel: React.FC = ({ currentSession, current children: (
{selectedKnowledge.tech_stack?.length > 0 ? ( -
+
Tech Stack
{selectedKnowledge.tech_stack.map((tech, idx) => {tech})} @@ -402,7 +402,7 @@ const KnowledgePanel: React.FC = ({ currentSession, current children: (
{selectedKnowledge.key_decisions?.length > 0 ? ( - ({ children:
{decision}
}))} /> + ({ children:
{decision}
}))} /> ) : }
), @@ -413,10 +413,10 @@ const KnowledgePanel: React.FC = ({ currentSession, current children: (
{selectedKnowledge.key_patterns?.length > 0 ? ( - + {selectedKnowledge.key_patterns.map((pattern, idx) => ( -
- {pattern} +
+ {pattern}
))} @@ -430,12 +430,12 @@ const KnowledgePanel: React.FC = ({ currentSession, current children: (
{selectedKnowledge.known_issues?.length > 0 ? ( - + {selectedKnowledge.known_issues.map((issue, idx) => ( -
+
- - {issue} + + {issue}
))} diff --git a/crates/cowork-gui/src/components/MemoryPanel.tsx b/crates/cowork-gui/src/components/MemoryPanel.tsx index 9eb0643..2c54d82 100644 --- a/crates/cowork-gui/src/components/MemoryPanel.tsx +++ b/crates/cowork-gui/src/components/MemoryPanel.tsx @@ -22,9 +22,7 @@ import { ClockCircleOutlined, } from "@ant-design/icons"; import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; -import rehypeHighlight from "rehype-highlight"; -import rehypeRaw from "rehype-raw"; +import { remarkPlugins, fullRehypePlugins } from "@/utils/markdown"; const { Option } = Select; const { Text, Paragraph } = Typography; @@ -242,7 +240,7 @@ const MemoryPanel: React.FC = ({
- +
= ({
) : ( - - - {memoryDetail ? ( -
-
- -
- ID: - {selectedMemory?.id} -
-
- Category: - - {getCategoryLabel(selectedMemory?.category || "")} - -
- {selectedMemory?.stage && ( + +
+
- Stage: - {selectedMemory.stage} + ID: + {selectedMemory?.id}
- )} -
- Created: - {formatDate(selectedMemory?.created_at)} -
- {selectedMemory?.impact && (
- Impact: - - {selectedMemory.impact} + Category: + + {getCategoryLabel(selectedMemory?.category || "")}
- )} - {selectedMemory?.tags && - selectedMemory.tags.length > 0 && ( + {selectedMemory?.stage && (
- Tags: - - {selectedMemory.tags.map((tag, idx) => ( - {tag} - ))} - + Stage: + {selectedMemory.stage}
)} -
-
-
- - {memoryDetail.content} - -
-
- ) : ( - - )} - - - {selectedMemory && ( -
-
- Summary - +
+ Created: + {formatDate(selectedMemory?.created_at)} +
+ {selectedMemory?.impact && ( +
+ Impact: + + {selectedMemory.impact} + +
+ )} + {selectedMemory?.tags && + selectedMemory.tags.length > 0 && ( +
+ Tags: + + {selectedMemory.tags.map((tag, idx) => ( + {tag} + ))} + +
+ )} + +
- {selectedMemory.summary} + {memoryDetail.content}
- {selectedMemory.file && ( -
- File + ) : ( + + ), + }, + { + key: 'summary', + label: 'Summary', + children: selectedMemory && ( +
+
+ Summary - {selectedMemory.file} +
+ + {selectedMemory.summary} + +
- )} -
- )} - - + {selectedMemory.file && ( +
+ File + + {selectedMemory.file} +
+ )} +
+ ), + }, + ]} + /> )}
diff --git a/crates/cowork-gui/src/components/ProjectsPanel.tsx b/crates/cowork-gui/src/components/ProjectsPanel.tsx index e805fa9..5f43c0b 100644 --- a/crates/cowork-gui/src/components/ProjectsPanel.tsx +++ b/crates/cowork-gui/src/components/ProjectsPanel.tsx @@ -198,7 +198,7 @@ const ProjectsPanel: React.FC = () => { {/* Project list */} {projects.length === 0 ? ( - + diff --git a/crates/cowork-gui/src/components/RunnerPanel.tsx b/crates/cowork-gui/src/components/RunnerPanel.tsx index b9ba009..36dd13b 100644 --- a/crates/cowork-gui/src/components/RunnerPanel.tsx +++ b/crates/cowork-gui/src/components/RunnerPanel.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, memo, useCallback } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; +import { FixedSizeList as List } from 'react-window'; import { Spin, Button, Space, Tag, Input, Select, Checkbox, Card } from 'antd'; import { PlayCircleOutlined, StopOutlined, CopyOutlined, ClearOutlined, SearchOutlined, EyeOutlined, ReloadOutlined, AppstoreOutlined } from '@ant-design/icons'; import { showError, showSuccess, tryExecute } from '../utils/errorHandler'; @@ -49,7 +50,6 @@ const RunnerPanel: React.FC = ({ iterationId }) => { const [activeTab, setActiveTab] = useState('run'); const [projectRuntimeInfo, setProjectRuntimeInfo] = useState(null); - const logsEndRef = useRef(null); const listenersRegistered = useRef(false); const isVisibleRef = useRef(true); @@ -86,12 +86,6 @@ const RunnerPanel: React.FC = ({ iterationId }) => { } }; - useEffect(() => { - if (autoScroll && logsEndRef.current) { - logsEndRef.current.scrollTop = logsEndRef.current.scrollHeight; - } - }, [logs, autoScroll]); - useEffect(() => { if (listenersRegistered.current) return; listenersRegistered.current = true; @@ -210,6 +204,41 @@ const RunnerPanel: React.FC = ({ iterationId }) => { const frontendPort = projectRuntimeInfo?.frontend_port; const backendPort = projectRuntimeInfo?.backend_port; + // ===== 虚拟化日志列表 ===== + const logListRef = useRef(null); + const logContainerRef = useRef(null); + const [logHeight, setLogHeight] = useState(400); + + useEffect(() => { + if (!logContainerRef.current) return; + const observer = new ResizeObserver(entries => { + for (const entry of entries) setLogHeight(entry.contentRect.height); + }); + observer.observe(logContainerRef.current); + return () => observer.disconnect(); + }, []); + + // 自动滚动到底部 + useEffect(() => { + if (!autoScroll) return; + requestAnimationFrame(() => { + logListRef.current?.scrollToItem(filteredLogs.length - 1, 'end'); + }); + }, [filteredLogs.length, autoScroll]); + + // 日志行高 13px 字体 + 5px = 18px + const LOG_ROW_HEIGHT = 18; + + const LogRow = useCallback(({ index, style, data }: { index: number; style: React.CSSProperties; data: LogEntry[] }) => { + const log = data[index]; + const color = log.type === 'stderr' ? '#cf1322' : log.type === 'system' ? '#389e0d' : '#333'; + return ( +
+ {log.content} +
+ ); + }, []); + const renderRunTab = () => (
@@ -224,17 +253,23 @@ const RunnerPanel: React.FC = ({ iterationId }) => { {filteredLogs.length}/{logs.length} lines
-
+
{logs.length === 0 ? (
Click "Start" to run your project
) : filteredLogs.length === 0 ? (
No matching logs
) : ( - filteredLogs.map((log, index) => ( -
- {log.content} -
- )) + + {LogRow} + )}
@@ -256,10 +291,10 @@ const RunnerPanel: React.FC = ({ iterationId }) => { {previewUrl}
-