Merge pull request #215 from multica-ai/codex/docs-prune-and-regenerate-core-docs
docs: prune stale docs and regenerate prioritized core docs
This commit is contained in:
commit
6e71598c2c
33 changed files with 517 additions and 8712 deletions
411
CLAUDE.md
411
CLAUDE.md
|
|
@ -1,379 +1,84 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
This file gives coding agents high-signal guidance for this repository.
|
||||
|
||||
## Project Overview
|
||||
## 1. Project Context
|
||||
|
||||
Super Multica is a distributed AI agent framework with a monorepo architecture. It includes an agent engine with multi-provider LLM support, an Electron desktop app with embedded Hub, a WebSocket gateway for remote access, and a Next.js web app.
|
||||
Super Multica is a distributed AI agent framework/product monorepo.
|
||||
It is used to run local-first agent workflows and support CLI/Desktop/Web/Gateway-based usage.
|
||||
|
||||
## Monorepo Structure
|
||||
Core purpose:
|
||||
|
||||
```
|
||||
super-multica/
|
||||
├── apps/
|
||||
│ ├── cli/ ← Command-line interface (`@multica/cli`)
|
||||
│ ├── desktop/ ← Electron + Vite + React (`@multica/desktop`) — primary target
|
||||
│ ├── gateway/ ← NestJS WebSocket gateway (`@multica/gateway`)
|
||||
│ ├── server/ ← NestJS REST API server (`@multica/server`)
|
||||
│ ├── web/ ← Next.js 16 web app (`@multica/web`, port 3000)
|
||||
│ └── mobile/ ← React Native mobile app (`@multica/mobile`)
|
||||
│
|
||||
├── packages/
|
||||
│ ├── core/ ← Core agent engine, hub, channels (`@multica/core`)
|
||||
│ ├── sdk/ ← Gateway client SDK (`@multica/sdk`, Socket.io)
|
||||
│ ├── ui/ ← Shared UI components (`@multica/ui`, Shadcn/Tailwind v4)
|
||||
│ ├── store/ ← Zustand state management (`@multica/store`)
|
||||
│ ├── hooks/ ← React hooks (`@multica/hooks`)
|
||||
│ ├── types/ ← Shared TypeScript types (`@multica/types`)
|
||||
│ └── utils/ ← Utility functions (`@multica/utils`)
|
||||
│
|
||||
└── skills/ ← Bundled agent skills
|
||||
```
|
||||
- execute agent tasks with tools and skills
|
||||
- persist sessions/profiles/credentials across runs
|
||||
- support development, testing, and operational automation workflows
|
||||
|
||||
## Common Commands
|
||||
## 2. Documentation Scope
|
||||
|
||||
Documentation in this repo should prioritize:
|
||||
|
||||
1. Development workflow
|
||||
2. Testing methods
|
||||
3. Operational process
|
||||
|
||||
Architecture explanations should stay minimal in docs.
|
||||
Treat source code as the architecture source of truth.
|
||||
|
||||
## 3. Core Workflow Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Multica CLI (unified entry point)
|
||||
pnpm multica # Interactive mode (default)
|
||||
pnpm multica run "<prompt>" # Run a single prompt
|
||||
pnpm multica chat # Interactive REPL mode
|
||||
pnpm multica session list # List sessions
|
||||
pnpm multica profile list # List profiles
|
||||
pnpm multica skills list # List skills
|
||||
pnpm multica tools list # List tools
|
||||
pnpm multica credentials init # Initialize credentials
|
||||
pnpm multica help # Show help
|
||||
|
||||
# Development servers
|
||||
pnpm dev # Desktop app (connects to dev gateway by default)
|
||||
pnpm dev:desktop # Same as above
|
||||
pnpm dev:gateway # WebSocket gateway only
|
||||
pnpm dev:web # Next.js web app
|
||||
pnpm dev:all # Gateway + web app
|
||||
|
||||
# Override gateway URL (e.g. local gateway)
|
||||
GATEWAY_URL=http://localhost:3000 pnpm dev
|
||||
|
||||
# Build
|
||||
pnpm build # Build all (turbo-orchestrated)
|
||||
pnpm --filter @multica/desktop build
|
||||
pnpm --filter @multica/core build
|
||||
|
||||
# Type checking
|
||||
pnpm multica
|
||||
pnpm multica run "<prompt>"
|
||||
pnpm dev
|
||||
pnpm dev:gateway
|
||||
pnpm dev:web
|
||||
pnpm dev:local
|
||||
pnpm build
|
||||
pnpm typecheck
|
||||
|
||||
# Testing (vitest)
|
||||
pnpm test # Single run
|
||||
pnpm test:watch # Watch mode
|
||||
pnpm test:coverage # With v8 coverage
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## Architecture
|
||||
## 4. Data and Credentials Workflow
|
||||
|
||||
```
|
||||
Desktop App (standalone, recommended)
|
||||
└─ Hub (embedded)
|
||||
└─ Agent Engine (LLM runner, sessions, skills, tools)
|
||||
└─ (Optional) Gateway connection for remote access
|
||||
- Default data dir: `~/.super-multica` (override with `SMC_DATA_DIR`)
|
||||
- Credentials: `~/.super-multica/credentials.json5` (override with `SMC_CREDENTIALS_PATH`)
|
||||
- Initialize credentials via `pnpm multica credentials init`
|
||||
|
||||
Web App (requires Gateway)
|
||||
→ @multica/sdk (GatewayClient, Socket.io)
|
||||
→ Gateway (NestJS, WebSocket, port 3000)
|
||||
→ Hub + Agent Engine
|
||||
```
|
||||
## 5. Coding Rules
|
||||
|
||||
**Agent Engine** (`packages/core/src/agent/`): Orchestrates LLM interactions with multi-provider support (OpenAI, Anthropic, DeepSeek, Kimi, Groq, Mistral, Google, Together). Features session management (JSONL-based, UUIDv7 IDs), profile system (`~/.super-multica/agent-profiles/`), modular skills with hot-reload, and token-aware context window guards.
|
||||
- TypeScript strict mode is enabled; keep types explicit.
|
||||
- Keep comments in code **English only**.
|
||||
- Prefer existing patterns/components over introducing parallel abstractions.
|
||||
- Avoid broad refactors unless required by the task.
|
||||
- Keep docs concise and aligned with current code behavior.
|
||||
|
||||
**Hub** (`packages/core/src/hub/`): Manages agents and communication channels. Embedded in desktop app, or runs standalone for web clients.
|
||||
## 6. Testing Rules
|
||||
|
||||
**Gateway** (`apps/gateway/`): NestJS WebSocket server with Socket.io for remote client access, message routing, and device verification.
|
||||
- Test runner: Vitest.
|
||||
- Mock policy: mock external/third-party dependencies only.
|
||||
- Do not mock internal modules when real integration can be tested.
|
||||
- Prefer temp directories and real file I/O for storage-related tests.
|
||||
|
||||
**CLI** (`apps/cli/`): Command-line interface. Entry point: `apps/cli/src/index.ts`.
|
||||
## 7. Commit Rules
|
||||
|
||||
## Tech Stack & Config
|
||||
- Use atomic commits grouped by logical intent.
|
||||
- Conventional format:
|
||||
- `feat(scope): ...`
|
||||
- `fix(scope): ...`
|
||||
- `refactor(scope): ...`
|
||||
- `docs: ...`
|
||||
- `test(scope): ...`
|
||||
- `chore(scope): ...`
|
||||
|
||||
- **Package manager**: pnpm 10 with workspaces (`pnpm-workspace.yaml`)
|
||||
- **Build orchestration**: Turborepo (`turbo.json`)
|
||||
- **TypeScript**: ESNext target, NodeNext modules, strict mode
|
||||
- **Testing**: Vitest with globals enabled
|
||||
- **Frontend**: React 19, Next.js 16, Tailwind CSS v4, Shadcn/UI
|
||||
- **Backend**: NestJS 11, Socket.io, Pino logging
|
||||
- **Desktop**: Electron 33+, electron-vite, electron-builder
|
||||
|
||||
## pnpm Configuration
|
||||
|
||||
**Required `.npmrc` for Electron packaging:**
|
||||
|
||||
```ini
|
||||
shamefully-hoist=true
|
||||
```
|
||||
|
||||
After adding/changing `.npmrc`:
|
||||
## 8. Minimum Pre-Push Checks
|
||||
|
||||
```bash
|
||||
rm -rf node_modules apps/*/node_modules packages/*/node_modules
|
||||
rm pnpm-lock.yaml
|
||||
pnpm install
|
||||
pnpm typecheck
|
||||
pnpm test
|
||||
```
|
||||
|
||||
See `docs/package-management.md` for detailed package management guide.
|
||||
## 9. E2E Process Docs
|
||||
|
||||
## Code Style
|
||||
|
||||
- **Comments**: Always write code comments in English, regardless of the conversation language.
|
||||
|
||||
## Design System
|
||||
|
||||
The UI follows a **restrained, professional** design language. This is a work tool, not a consumer app.
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Restraint over decoration** — No flashy colors, minimal animations
|
||||
2. **Clarity over cleverness** — Obvious > subtle, explicit > implicit
|
||||
3. **Consistency over novelty** — Use Shadcn/UI patterns, don't reinvent
|
||||
4. **Density over sprawl** — Respect screen real estate
|
||||
|
||||
### Typography
|
||||
|
||||
| Font | CSS Variable | Usage |
|
||||
|------|--------------|-------|
|
||||
| Geist Sans | `font-sans` | Primary UI text |
|
||||
| Geist Mono | `font-mono` | Code, technical values |
|
||||
| Playfair Display | `font-brand` | Brand name "Multica" ONLY |
|
||||
|
||||
Fonts are loaded via `@fontsource` packages (not Google Fonts) for cross-platform consistency.
|
||||
|
||||
### Colors
|
||||
|
||||
- **No brand color** — Purple/blue "AI colors" feel generic. We use neutral grays.
|
||||
- **Color is for state** — Running (blue), success (green), error (red)
|
||||
- **Dark mode is true dark** — Not gray, actual near-black
|
||||
|
||||
### Component Library
|
||||
|
||||
- **Base**: Shadcn/UI (Radix primitives + Tailwind)
|
||||
- **Styling**: Tailwind CSS v4 with OKLCH colors
|
||||
- **Config**: `packages/ui/src/styles/globals.css`
|
||||
|
||||
### When Building UI
|
||||
|
||||
- Prefer existing Shadcn components over custom implementations
|
||||
- Use semantic color variables (`--muted`, `--destructive`), not raw colors
|
||||
- Keep animations subtle and purposeful (no gratuitous motion)
|
||||
- Test in both light and dark modes
|
||||
|
||||
## Debugging: Run Log
|
||||
|
||||
The agent engine supports structured run logging for debugging. When enabled, it writes all key execution events to `~/.super-multica/sessions/{sessionId}/run-log.jsonl` alongside the session data.
|
||||
|
||||
```bash
|
||||
# Enable via CLI flag
|
||||
pnpm multica run --run-log "your prompt"
|
||||
|
||||
# Or via environment variable
|
||||
MULTICA_RUN_LOG=1 pnpm multica run "your prompt"
|
||||
|
||||
# Or programmatically
|
||||
const agent = new Agent({ enableRunLog: true });
|
||||
```
|
||||
|
||||
When `--run-log` is enabled, the CLI prints the session directory path to stderr:
|
||||
```
|
||||
[session: 019c584a-...]
|
||||
[session-dir: ~/.super-multica/sessions/019c584a-...]
|
||||
```
|
||||
|
||||
Logged events: `run_start`, `run_end`, `llm_call`, `llm_result`, `tool_start`, `tool_end`, `context_overflow`, `auth_rotate`, `error_classify`, `preflight_compact_start/end`, `tool_result_pruning`, `compaction`, `compaction_detail`.
|
||||
|
||||
Each line is a JSON object with `ts` (timestamp) and `event` (type), suitable for AI-assisted log analysis. Full event reference: `packages/core/src/agent/run-log.ts`.
|
||||
|
||||
## SWE-bench (Agent Benchmark)
|
||||
|
||||
Run the Multica agent against [SWE-bench](https://www.swebench.com/), the standard benchmark for evaluating AI coding agents on real GitHub issues.
|
||||
|
||||
```bash
|
||||
# Download dataset
|
||||
python scripts/swe-bench/download-dataset.py --dataset lite --limit 5
|
||||
|
||||
# Run agent against tasks
|
||||
npx tsx scripts/swe-bench/run.ts --limit 5 --provider kimi-coding
|
||||
|
||||
# Analyze results
|
||||
npx tsx scripts/swe-bench/analyze.ts
|
||||
|
||||
# Official evaluation (requires Docker)
|
||||
bash scripts/swe-bench/evaluate.sh
|
||||
```
|
||||
|
||||
Scripts are in `scripts/swe-bench/`. Full guide: `docs/swe-bench.md`.
|
||||
|
||||
## E2E Testing (Agent-Driven)
|
||||
|
||||
E2E tests are executed and analyzed by the Coding Agent (Claude Code), not by vitest. The Coding Agent runs the Multica agent via CLI, reads the structured run-log, and intelligently analyzes intermediate behavior and results.
|
||||
|
||||
### How to Run
|
||||
|
||||
E2E tests use an isolated data directory (`~/.super-multica-e2e`) to avoid polluting dev or production session data.
|
||||
|
||||
```bash
|
||||
# Basic E2E test (web_search/data tools require MULTICA_API_URL)
|
||||
SMC_DATA_DIR=~/.super-multica-e2e MULTICA_API_URL=https://api-dev.copilothub.ai pnpm multica run --run-log "your test prompt"
|
||||
|
||||
# With specific provider
|
||||
SMC_DATA_DIR=~/.super-multica-e2e MULTICA_API_URL=https://api-dev.copilothub.ai pnpm multica run --run-log --provider kimi-coding "your test prompt"
|
||||
|
||||
# Multi-turn test (reuse session)
|
||||
SMC_DATA_DIR=~/.super-multica-e2e MULTICA_API_URL=https://api-dev.copilothub.ai pnpm multica run --run-log --session <session-id> "follow-up prompt"
|
||||
|
||||
# Clean up all E2E test data
|
||||
rm -rf ~/.super-multica-e2e
|
||||
```
|
||||
|
||||
### Analysis Workflow
|
||||
|
||||
After running, the Coding Agent should:
|
||||
1. Read `{session-dir}/run-log.jsonl` — structured execution events
|
||||
2. Read `{session-dir}/session.jsonl` — full conversation transcript (if needed)
|
||||
3. Analyze event sequence, tool calls, errors, and timing
|
||||
4. Report findings with verdict (pass/fail + details)
|
||||
|
||||
### What to Check
|
||||
|
||||
- **Event completeness**: `run_start` → ... → `run_end` (no orphaned starts)
|
||||
- **Tool pairing**: every `tool_start` has a matching `tool_end`
|
||||
- **Error handling**: `is_error`, `error_classify`, `auth_rotate` events
|
||||
- **Compaction health**: `tokens_removed > 0` when compaction fires
|
||||
- **Performance**: `llm_result.duration_ms`, tool execution times
|
||||
|
||||
### Important
|
||||
|
||||
- **`SMC_DATA_DIR=~/.super-multica-e2e`** isolates E2E test sessions from dev (`~/.super-multica-dev`) and production (`~/.super-multica`) data. Always set this.
|
||||
- **`MULTICA_API_URL=https://api-dev.copilothub.ai`** is required for `web_search` and `data` tools. Without it, these tools fail with `MULTICA_API_URL is required`.
|
||||
- **Auth for `web_search`/`data`**: These tools need dev backend auth. The auth store auto-falls back to `~/.super-multica-dev/auth.json`. If missing, run `pnpm dev:local` first and log in through the Desktop app.
|
||||
- Default provider is `kimi-coding`. Override with `--provider`.
|
||||
- Run-log and session data are at `~/.super-multica-e2e/sessions/{sessionId}/`
|
||||
- Detailed guide with feature-specific test playbooks: `docs/e2e-testing-guide.md`
|
||||
|
||||
## Credentials Setup
|
||||
|
||||
```bash
|
||||
pnpm multica credentials init
|
||||
```
|
||||
|
||||
Creates:
|
||||
- `~/.super-multica/credentials.json5` (LLM providers + built-in tools)
|
||||
|
||||
Skill-specific API keys go in `.env` files within each skill's directory:
|
||||
- `~/.super-multica/skills/<skill-id>/.env`
|
||||
|
||||
## Atomic Commits
|
||||
|
||||
After completing any task that modifies code, create atomic commits:
|
||||
|
||||
1. Run `git status` and `git diff` to see all modifications
|
||||
2. Skip if no changes exist
|
||||
3. Group changes by logical purpose (feature, fix, refactor, docs, test, chore)
|
||||
4. Stage and commit each group separately
|
||||
|
||||
**Format**: `<type>(<scope>): <description>`
|
||||
|
||||
Types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
git add packages/core/src/agent/runner.ts packages/core/src/agent/runner.test.ts
|
||||
git commit -m "feat(agent): add streaming support"
|
||||
|
||||
git add packages/utils/src/format.ts
|
||||
git commit -m "refactor(utils): simplify date formatting"
|
||||
|
||||
git add README.md
|
||||
git commit -m "docs: update API documentation"
|
||||
```
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Mock Policy: External Only
|
||||
|
||||
**CRITICAL RULE**: Only mock third-party/external dependencies. NEVER mock internal modules.
|
||||
|
||||
| Type | Examples | Can Mock? |
|
||||
|------|----------|-----------|
|
||||
| Internal modules | `./runner.js`, `../utils/format.js` | NO |
|
||||
| Monorepo packages | `@multica/core`, `@multica/utils` | NO |
|
||||
| Third-party packages | `openai`, `@anthropic-ai/sdk`, `@mariozechner/*` | YES |
|
||||
| System/time APIs | `vi.useFakeTimers()`, `vi.setSystemTime()` | YES |
|
||||
| Network calls | External HTTP requests, WebSocket connections | YES |
|
||||
|
||||
When AI writes code, tests become more valuable than the code itself. Mocking internal modules creates brittle tests that don't verify real integration between modules, hides bugs, and requires maintaining parallel mock implementations.
|
||||
|
||||
### Preferred Patterns
|
||||
|
||||
**Temp directories for I/O tests** (no filesystem mocking):
|
||||
```typescript
|
||||
const testDir = join(tmpdir(), `multica-test-${Date.now()}`);
|
||||
beforeEach(() => mkdirSync(testDir, { recursive: true }));
|
||||
afterEach(() => rmSync(testDir, { recursive: true, force: true }));
|
||||
```
|
||||
|
||||
**Test reset functions for stateful modules**:
|
||||
```typescript
|
||||
// In the module itself:
|
||||
export function resetForTests() { /* clear in-memory state */ }
|
||||
|
||||
// In tests:
|
||||
beforeEach(() => resetForTests());
|
||||
```
|
||||
|
||||
**Pure function tests** — no mocking needed:
|
||||
```typescript
|
||||
const result = resolveContextWindowInfo({ modelContextWindow: 100_000 });
|
||||
expect(result.tokens).toBe(100_000);
|
||||
```
|
||||
|
||||
**Constructor/parameter injection** over module mocking:
|
||||
```typescript
|
||||
// Good: pass baseDir as parameter
|
||||
const session = new SessionManager({ sessionId: "test", baseDir: testDir });
|
||||
|
||||
// Bad: mock the paths module
|
||||
vi.mock("../../shared/paths.js", () => ({ DATA_DIR: "/tmp/test" }));
|
||||
```
|
||||
|
||||
### Anti-Patterns
|
||||
|
||||
- `vi.mock("./internal-module.js")` — NEVER mock internal modules
|
||||
- Mock objects with 10+ method stubs — sign you should use the real implementation
|
||||
- `vi.mock("../context-window/index.js")` with simplified logic — hides real behavior
|
||||
- Tests that pass but don't exercise any real code paths ("fake green")
|
||||
|
||||
### Reference Tests
|
||||
|
||||
Good patterns to follow:
|
||||
- `packages/core/src/agent/session/session-manager.display.test.ts` — real SessionManager + temp dirs
|
||||
- `packages/core/src/agent/skills/loader.test.ts` — real skill loading + temp filesystem
|
||||
- `packages/core/src/agent/context-window/guard.test.ts` — pure function tests
|
||||
- `packages/core/src/agent/subagent/registry.test.ts` — real registry + `resetSubagentRegistryForTests()`
|
||||
|
||||
Known violations (to be migrated):
|
||||
- `packages/core/src/agent/async-agent.test.ts` — mocks internal `./runner.js`
|
||||
- `packages/core/src/agent/session/compaction.test.ts` — mocks internal `../context-window/index.js`
|
||||
|
||||
## Pre-push Checks
|
||||
|
||||
Before pushing, always run:
|
||||
|
||||
```bash
|
||||
pnpm typecheck # Type check all packages
|
||||
pnpm test # Run tests
|
||||
```
|
||||
|
||||
This ensures CI will pass. For a clean check (no cache):
|
||||
|
||||
```bash
|
||||
pnpm turbo typecheck --force
|
||||
```
|
||||
- `docs/e2e-testing-guide.md`
|
||||
- `docs/e2e-finance-benchmark.md`
|
||||
|
|
|
|||
163
README.md
163
README.md
|
|
@ -1,60 +1,51 @@
|
|||
# Super Multica
|
||||
|
||||
**Multiplexed Information & Computing Agent**
|
||||
Super Multica is a distributed AI agent framework and product monorepo.
|
||||
It provides a local-first agent runtime plus CLI, gateway, web, and mobile integration surfaces.
|
||||
|
||||
An always-on AI agent that pulls real data, runs real computation, and takes real action — monitoring, analyzing, and acting within user-defined authorization boundaries.
|
||||
What this project does:
|
||||
|
||||
See [Memo](./docs/memo.md) for product vision, architecture, and roadmap.
|
||||
- runs AI agent sessions with tools, skills, and persistent session state
|
||||
- supports scheduled/automated execution workflows
|
||||
- supports both standalone local usage and remote-access client workflows
|
||||
|
||||
## Project Structure
|
||||
This repository keeps docs focused on:
|
||||
|
||||
```
|
||||
apps/
|
||||
├── cli/ # Command-line interface
|
||||
├── desktop/ # Electron desktop app (recommended)
|
||||
├── gateway/ # NestJS WebSocket gateway
|
||||
├── server/ # NestJS REST API server
|
||||
├── web/ # Next.js web app
|
||||
└── mobile/ # React Native mobile app
|
||||
1. Development workflow
|
||||
2. Testing workflow
|
||||
3. Operational process
|
||||
|
||||
packages/
|
||||
├── core/ # Agent engine, hub, channels
|
||||
├── sdk/ # Gateway client SDK
|
||||
├── ui/ # Shared UI components (Shadcn/Tailwind v4)
|
||||
├── store/ # Zustand state management
|
||||
├── hooks/ # React hooks
|
||||
├── types/ # Shared TypeScript types
|
||||
└── utils/ # Utility functions
|
||||
Architecture details are still source-of-truth in code, but docs keep minimal project context for onboarding.
|
||||
|
||||
skills/ # Bundled agent skills
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
## Quick Start (Workflow)
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm multica credentials init
|
||||
pnpm multica
|
||||
```
|
||||
|
||||
### Development
|
||||
Run local desktop workflow:
|
||||
|
||||
```bash
|
||||
pnpm dev # Desktop app (standalone, no Gateway needed)
|
||||
pnpm dev:gateway # Gateway only
|
||||
pnpm dev:web # Web app only
|
||||
pnpm dev:all # Gateway + Web
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Local Full-Stack Development
|
||||
## Local Full-Stack Development (`pnpm dev:local`)
|
||||
|
||||
`pnpm dev:local` starts the entire stack locally (Gateway + Desktop + Web) with isolated data directories, useful for end-to-end development and testing.
|
||||
Use this when you need **Gateway + Web + Desktop** together for end-to-end dev.
|
||||
|
||||
**Setup:**
|
||||
Setup:
|
||||
|
||||
1. Copy `.env.example` to `.env` at the repo root
|
||||
2. Fill in `TELEGRAM_BOT_TOKEN` (get from [@BotFather](https://t.me/BotFather))
|
||||
3. Run `pnpm dev:local`
|
||||
1. Copy `.env.example` to `.env` in repo root
|
||||
2. Set `TELEGRAM_BOT_TOKEN` in `.env` (from `@BotFather`)
|
||||
3. Run:
|
||||
|
||||
**What it starts:**
|
||||
```bash
|
||||
pnpm dev:local
|
||||
```
|
||||
|
||||
What starts:
|
||||
|
||||
| Service | Address | Notes |
|
||||
|---------|---------|-------|
|
||||
|
|
@ -62,61 +53,77 @@ pnpm dev:all # Gateway + Web
|
|||
| Web | `http://localhost:3000` | OAuth login flow |
|
||||
| Desktop | — | Connects to local Gateway + Web |
|
||||
|
||||
**Data isolation:** All data goes to `~/.super-multica-dev` and `~/Documents/Multica-dev`, separate from production `~/.super-multica`.
|
||||
Data isolation:
|
||||
|
||||
**Related commands:**
|
||||
- runtime data: `~/.super-multica-dev`
|
||||
- workspace data: `~/Documents/Multica-dev`
|
||||
|
||||
Related:
|
||||
|
||||
```bash
|
||||
pnpm dev:local:archive # Archive dev data and start fresh
|
||||
pnpm dev:local:archive
|
||||
```
|
||||
|
||||
## Architecture
|
||||
## Workflow Commands
|
||||
|
||||
```
|
||||
Desktop App (standalone, recommended)
|
||||
└─ Hub (embedded)
|
||||
└─ Agent Engine
|
||||
```bash
|
||||
# CLI
|
||||
pnpm multica
|
||||
pnpm multica run "Hello"
|
||||
pnpm multica chat
|
||||
pnpm multica help
|
||||
|
||||
Web/Mobile Clients
|
||||
→ Gateway (WebSocket, :3000)
|
||||
→ Hub
|
||||
→ Agent Engine
|
||||
# Development
|
||||
pnpm dev
|
||||
pnpm dev:desktop
|
||||
pnpm dev:gateway
|
||||
pnpm dev:web
|
||||
pnpm dev:local
|
||||
pnpm dev:local:archive
|
||||
|
||||
# Build / quality
|
||||
pnpm build
|
||||
pnpm typecheck
|
||||
pnpm test
|
||||
```
|
||||
|
||||
- **Desktop App**: Electron app with embedded Hub, no Gateway needed
|
||||
- **Gateway**: WebSocket server for remote clients
|
||||
- **Hub**: Agent lifecycle and event distribution
|
||||
## Testing Workflow
|
||||
|
||||
## Documentation
|
||||
```bash
|
||||
# Unit/integration
|
||||
pnpm test
|
||||
pnpm test:watch
|
||||
pnpm test:coverage
|
||||
|
||||
**Getting Started**
|
||||
# Type safety gate
|
||||
pnpm typecheck
|
||||
|
||||
| Topic | Link |
|
||||
|-------|------|
|
||||
| Development guide | [docs/development.md](./docs/development.md) |
|
||||
| Credentials & LLM providers | [docs/credentials.md](./docs/credentials.md) |
|
||||
| CLI usage | [docs/cli.md](./docs/cli.md) |
|
||||
| Skills & tools | [docs/skills-and-tools.md](./docs/skills-and-tools.md) |
|
||||
| Package management | [docs/package-management.md](./docs/package-management.md) |
|
||||
| Mobile development | [docs/mobile/guide.md](./docs/mobile/guide.md) |
|
||||
# Agent E2E
|
||||
pnpm multica run --run-log "your test prompt"
|
||||
```
|
||||
|
||||
**Testing & Benchmarks**
|
||||
E2E process docs:
|
||||
|
||||
| Topic | Link |
|
||||
|-------|------|
|
||||
| SWE-bench runner | [docs/swe-bench.md](./docs/swe-bench.md) |
|
||||
| E2E testing guide | [docs/e2e-testing-guide.md](./docs/e2e-testing-guide.md) |
|
||||
- `docs/e2e-testing-guide.md`
|
||||
- `docs/e2e-finance-benchmark.md`
|
||||
|
||||
**Architecture & Protocols**
|
||||
## Runtime Paths
|
||||
|
||||
| Topic | Link |
|
||||
|-------|------|
|
||||
| Product capabilities | [docs/product-capabilities.md](./docs/product-capabilities.md) |
|
||||
| Message paths (Desktop/Web/Channel) | [docs/message-paths.md](./docs/message-paths.md) |
|
||||
| Client streaming protocol | [docs/client-streaming-protocol.md](./docs/client-streaming-protocol.md) |
|
||||
| Hub RPC protocol | [docs/rpc.md](./docs/rpc.md) |
|
||||
| Exec approval protocol | [docs/exec-approval.md](./docs/exec-approval.md) |
|
||||
| Time injection design | [docs/time-injection.md](./docs/time-injection.md) |
|
||||
| Channel system | [docs/channels/README.md](./docs/channels/README.md) |
|
||||
| Channel media handling | [docs/channels/media-handling.md](./docs/channels/media-handling.md) |
|
||||
| Desktop login integration | [docs/auth/desktop-integration.md](./docs/auth/desktop-integration.md) |
|
||||
By default, runtime data is stored under:
|
||||
|
||||
- `~/.super-multica`
|
||||
|
||||
You can isolate environments with:
|
||||
|
||||
- `SMC_DATA_DIR=~/.super-multica-dev` (or other path)
|
||||
|
||||
## Process Docs
|
||||
|
||||
- `CLAUDE.md`
|
||||
- `docs/development.md`
|
||||
- `docs/cli.md`
|
||||
- `docs/credentials.md`
|
||||
- `docs/skills-and-tools.md`
|
||||
- `docs/package-management.md`
|
||||
- `docs/e2e-testing-guide.md`
|
||||
- `docs/e2e-finance-benchmark.md`
|
||||
|
|
|
|||
|
|
@ -1,490 +0,0 @@
|
|||
# Multica Desktop App 设计文档
|
||||
|
||||
## 产品定位
|
||||
|
||||
Multica Desktop 是一个统一的桌面应用,具有双重身份:
|
||||
|
||||
1. **Host 模式**: 本机运行 Hub + Agent,可供其他设备连接
|
||||
2. **Client 模式**: 连接到其他 Hub 的 Agent 进行对话
|
||||
|
||||
用户安装同一个 App,既可以作为 Agent 的宿主(让其他设备扫码连接),也可以扫码连接到别人的 Agent。
|
||||
|
||||
### 架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Multica Desktop App │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ React UI (Renderer) │ │
|
||||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
||||
│ │ │ Home │ │ Chat │ │ Tools │ │ Skills │ │Settings │ │ │
|
||||
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────┴───────────────┐ │
|
||||
│ │ │ │
|
||||
│ 直接调用 (本地) WebSocket (远程) │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
|
||||
│ │ Local Hub + Agent │ │ Remote Hub (via Gateway) │ │
|
||||
│ │ (进程内) │ │ (另一台设备) │ │
|
||||
│ └─────────────────────────────┘ └─────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ WebSocket
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Gateway │
|
||||
│ (公网 WebSocket) │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
|
||||
- **统一应用**: 不区分 Admin App 和 Client App,一个 App 两种用法
|
||||
- **Chat 双模式**: Chat 页面可以选择与本地 Agent 对话,或连接远程 Agent 对话
|
||||
- **本地 Agent**: Hub + Agent 跑在 Electron 主进程内,UI 通过 IPC 调用访问
|
||||
- **远程连接**: 通过 Gateway WebSocket 连接到其他设备的 Hub
|
||||
|
||||
**约束**: 第一阶段 1 Client - 1 Hub - 1 Agent Session
|
||||
|
||||
---
|
||||
|
||||
## 技术实现设计
|
||||
|
||||
### 技术栈
|
||||
|
||||
| 层级 | 技术 | 说明 |
|
||||
| ------ | ------------------------ | -------------- |
|
||||
| 框架 | Electron 30 | 桌面应用 |
|
||||
| 前端 | React 19 + Vite | 渲染进程 |
|
||||
| 路由 | react-router-dom v7 | HashRouter |
|
||||
| 状态 | @multica/store (Zustand) | 复用现有 store |
|
||||
| UI | @multica/ui (Shadcn) | 复用现有组件 |
|
||||
| 二维码 | qrcode.react | 生成二维码 |
|
||||
| 通信 | @multica/sdk | Gateway 连接 |
|
||||
|
||||
### 文件结构规划
|
||||
|
||||
```
|
||||
apps/desktop/
|
||||
├── electron/
|
||||
│ ├── main.ts # 主进程 (Hub + Agent)
|
||||
│ └── preload.ts # 预加载脚本 (如需 IPC)
|
||||
├── src/
|
||||
│ ├── main.tsx # React 入口
|
||||
│ ├── App.tsx # 路由配置
|
||||
│ ├── pages/
|
||||
│ │ ├── home.tsx # Home 入口页 (三个选项)
|
||||
│ │ ├── chat.tsx # Chat 页面 (Local/Remote 双模式)
|
||||
│ │ ├── tools.tsx # Tools 管理页
|
||||
│ │ ├── skills.tsx # Skills 管理页
|
||||
│ │ └── layout.tsx # 全局布局 (Header + Tabs)
|
||||
│ ├── components/
|
||||
│ │ ├── qr-code.tsx # 二维码组件
|
||||
│ │ ├── qr-scanner.tsx # 扫码组件
|
||||
│ │ ├── connection-status.tsx # 连接状态
|
||||
│ │ ├── tool-list.tsx # Tools 列表
|
||||
│ │ └── skill-list.tsx # Skills 列表
|
||||
│ └── hooks/
|
||||
│ ├── use-local-agent.ts # 本地 Agent 管理
|
||||
│ ├── use-remote-agent.ts # 远程 Agent 连接
|
||||
│ └── use-connection.ts # 连接状态管理
|
||||
└── package.json
|
||||
```
|
||||
|
||||
### 核心实现点
|
||||
|
||||
#### 1. 二维码生成与连接
|
||||
|
||||
二维码内容格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "multica-connect",
|
||||
"gateway": "wss://gateway.multica.ai",
|
||||
"hubId": "019c1d32-xxxx",
|
||||
"agentId": "019c1d32-yyyy",
|
||||
"token": "random-uuid-token",
|
||||
"expires": 1234567890
|
||||
}
|
||||
```
|
||||
|
||||
连接流程:
|
||||
|
||||
```
|
||||
1. Admin 启动 → Hub 连接公网 Gateway → 注册为 deviceType: "hub"
|
||||
2. Admin 创建 Agent → 生成 token → 编码到二维码 (含 hubId + agentId + token)
|
||||
3. Client 扫码 → 解析二维码 → 连接同一 Gateway
|
||||
4. Client 发送 "connect-request" 到 hubId (带 token)
|
||||
5. Admin 验证 token 有效且未过期 → 建立配对关系
|
||||
6. Client 后续消息发到 hubId,payload 带 agentId
|
||||
7. Hub 路由消息到对应 Agent
|
||||
```
|
||||
|
||||
#### 2. Tools 管理
|
||||
|
||||
**现有 CLI 命令** (已实现):
|
||||
|
||||
```bash
|
||||
multica tools list # 列出所有 tools
|
||||
multica tools list --profile coding # 按 profile 过滤
|
||||
multica tools groups # 显示 tool groups
|
||||
multica tools profiles # 显示预设 profiles
|
||||
```
|
||||
|
||||
**Admin App 实现方式** - 通过 IPC 调用 Main Process:
|
||||
|
||||
```typescript
|
||||
// Renderer 进程 (React Hook)
|
||||
const tools = await window.electronAPI.tools.list();
|
||||
const groups = await window.electronAPI.tools.getGroups();
|
||||
const profiles = await window.electronAPI.tools.getProfiles();
|
||||
await window.electronAPI.tools.setStatus('exec', false);
|
||||
|
||||
// Main 进程 (IPC Handler)
|
||||
ipcMain.handle('tools:list', async () => {
|
||||
const allTools = createAllTools(process.cwd());
|
||||
return allTools.map((t) => ({
|
||||
name: t.name,
|
||||
group: TOOL_GROUPS[t.name],
|
||||
enabled: true,
|
||||
}));
|
||||
});
|
||||
```
|
||||
|
||||
**注意**: Renderer 进程运行在沙盒中,不能直接访问 Node.js API,必须通过 IPC 调用 Main Process。
|
||||
|
||||
#### 3. Skills 管理
|
||||
|
||||
**现有 CLI 命令** (已实现):
|
||||
|
||||
```bash
|
||||
multica skills list # 列出所有 skills
|
||||
multica skills status # 显示状态摘要
|
||||
multica skills status <id> # 单个 skill 详情
|
||||
multica skills add owner/repo # 从 GitHub 添加
|
||||
multica skills remove <name> # 删除 skill
|
||||
multica skills install <id> # 安装依赖
|
||||
```
|
||||
|
||||
**Admin App 实现方式** - 通过 IPC 调用 Main Process:
|
||||
|
||||
```typescript
|
||||
// Renderer 进程 (React Hook)
|
||||
const skills = await window.electronAPI.skills.list();
|
||||
await window.electronAPI.skills.add('anthropics/skills');
|
||||
await window.electronAPI.skills.remove('pdf');
|
||||
await window.electronAPI.skills.setEnabled('commit', false);
|
||||
|
||||
// Main 进程 (IPC Handler)
|
||||
ipcMain.handle('skills:list', async () => {
|
||||
return await listAllSkillsWithStatus();
|
||||
});
|
||||
ipcMain.handle('skills:add', async (_, source: string) => {
|
||||
await addSkill({ source, force: false });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、Hub 集成技术方案
|
||||
|
||||
### 架构概述
|
||||
|
||||
Desktop App 采用 **Electron IPC + Hub 实例** 架构:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Electron Desktop App │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Renderer Process (React UI) │ │
|
||||
│ │ │ │
|
||||
│ │ home.tsx → useHub() → window.electronAPI.hub.getStatus() │ │
|
||||
│ │ tools.tsx → useTools() → window.electronAPI.tools.list() │ │
|
||||
│ │ skills.tsx→ useSkills()→ window.electronAPI.skills.list() │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────┬─────────────────────────────────────────┘ │
|
||||
│ │ IPC (contextBridge) │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Main Process (Node.js) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Hub Instance │ │ │
|
||||
│ │ │ - hubId: UUIDv7 │ │ │
|
||||
│ │ │ - agents: Map<agentId, AsyncAgent> │ │ │
|
||||
│ │ │ - status: 'starting' | 'ready' | 'error' │ │ │
|
||||
│ │ │ - GatewayClient: 连接公网 Gateway (可选) │ │ │
|
||||
│ │ └──────────────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌────────────────────────────▼────────────────────────────────┐ │ │
|
||||
│ │ │ AsyncAgent Instance │ │ │
|
||||
│ │ │ - agentId: UUIDv7 │ │ │
|
||||
│ │ │ - runner: AgentRunner (LLM interaction) │ │ │
|
||||
│ │ │ - tools: Tool[] (可动态更新) │ │ │
|
||||
│ │ │ - skills: SkillInfo[] │ │ │
|
||||
│ │ └─────────────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ └────────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ WebSocket (可选,用于 Client 远程连接)
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Public Gateway │
|
||||
│ (wss://xxx) │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### IPC 通信机制
|
||||
|
||||
**工作原理**:
|
||||
|
||||
1. **Main Process**: 在 Electron 主进程中创建 Hub 和 Agent 实例
|
||||
2. **Preload Script**: 通过 `contextBridge.exposeInMainWorld` 暴露安全 API
|
||||
3. **Renderer Process**: React UI 通过 `window.electronAPI` 调用主进程功能
|
||||
|
||||
**与 CLI 命令的关系**:
|
||||
|
||||
| CLI 命令 | IPC Handler | 底层调用 |
|
||||
| -------------------------- | ----------------- | -------------------------------------------- |
|
||||
| `multica tools list` | `tools:list` | `createAllTools()` + `getToolStatus()` |
|
||||
| `multica tools enable xxx` | `tools:setStatus` | `setToolStatus()` |
|
||||
| `multica skills list` | `skills:list` | `loadSkills()` + `listAllSkillsWithStatus()` |
|
||||
| `multica skills add xxx` | `skills:add` | `addSkill()` |
|
||||
|
||||
**本质上 CLI 和 Admin App 调用的是同一套底层模块**,区别仅在于:
|
||||
|
||||
- CLI: 通过命令行参数解析后直接调用
|
||||
- Admin App: 通过 IPC 转发调用
|
||||
|
||||
### 核心文件
|
||||
|
||||
```
|
||||
apps/desktop/
|
||||
├── electron/
|
||||
│ ├── main.ts # 主进程入口,创建窗口 + 注册 IPC
|
||||
│ ├── preload.ts # 暴露 electronAPI
|
||||
│ └── ipc/
|
||||
│ ├── index.ts # 统一注册所有 IPC handlers
|
||||
│ ├── hub.ts # Hub 管理 (创建/状态/连接 Gateway)
|
||||
│ ├── agent.ts # Agent 管理 (Tools 读写)
|
||||
│ └── skills.ts # Skills 管理
|
||||
├── src/
|
||||
│ └── hooks/
|
||||
│ ├── use-hub.ts # 获取 Hub 状态
|
||||
│ ├── use-tools.ts # Tools CRUD
|
||||
│ └── use-skills.ts # Skills CRUD
|
||||
```
|
||||
|
||||
### IPC 接口定义
|
||||
|
||||
```typescript
|
||||
// electron/preload.ts 暴露的 API
|
||||
interface ElectronAPI {
|
||||
hub: {
|
||||
getStatus: () => Promise<HubStatus>;
|
||||
getAgentInfo: () => Promise<AgentInfo | null>;
|
||||
};
|
||||
tools: {
|
||||
list: () => Promise<ToolStatus[]>;
|
||||
setStatus: (toolName: string, enabled: boolean) => Promise<void>;
|
||||
getGroups: () => Promise<Record<string, string[]>>;
|
||||
getProfiles: () => Promise<string[]>;
|
||||
};
|
||||
skills: {
|
||||
list: () => Promise<SkillInfo[]>;
|
||||
add: (source: string) => Promise<void>;
|
||||
remove: (name: string) => Promise<void>;
|
||||
setEnabled: (name: string, enabled: boolean) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
// 类型定义
|
||||
interface HubStatus {
|
||||
hubId: string;
|
||||
status: 'starting' | 'ready' | 'error';
|
||||
agentCount: number;
|
||||
gatewayConnected: boolean;
|
||||
gatewayUrl?: string;
|
||||
}
|
||||
|
||||
interface AgentInfo {
|
||||
agentId: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
status: 'idle' | 'running';
|
||||
}
|
||||
|
||||
interface ToolStatus {
|
||||
name: string;
|
||||
group: string;
|
||||
enabled: boolean;
|
||||
needsConfig?: boolean;
|
||||
}
|
||||
|
||||
interface SkillInfo {
|
||||
name: string;
|
||||
command: string;
|
||||
source: 'bundled' | 'global' | 'profile';
|
||||
status: 'ready' | 'missing-deps' | 'disabled';
|
||||
description?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Hub 生命周期
|
||||
|
||||
```typescript
|
||||
// electron/ipc/hub.ts 简化逻辑
|
||||
|
||||
let hub: Hub | null = null;
|
||||
|
||||
export function registerHubHandlers(ipcMain: IpcMain) {
|
||||
// App 启动时自动创建 Hub
|
||||
ipcMain.handle('hub:getStatus', async () => {
|
||||
if (!hub) {
|
||||
hub = new Hub();
|
||||
await hub.start();
|
||||
// 创建默认 Agent
|
||||
const agent = await hub.createAgent({
|
||||
provider: credentialManager.getLlmProvider(),
|
||||
model: credentialManager.getLlmProviderConfig()?.model,
|
||||
});
|
||||
}
|
||||
return {
|
||||
hubId: hub.id,
|
||||
status: hub.status,
|
||||
agentCount: hub.agents.size,
|
||||
gatewayConnected: hub.gateway?.connected ?? false,
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Tools 实时更新机制
|
||||
|
||||
当用户在 UI 中切换 Tool 开关时:
|
||||
|
||||
```
|
||||
1. UI: Switch onChange → useTools.setToolStatus('exec', false)
|
||||
2. Hook: await window.electronAPI.tools.setStatus('exec', false)
|
||||
3. IPC: ipcMain.handle('tools:setStatus') → agent.updateTools(...)
|
||||
4. Agent: 重新过滤 tools 列表,下次 LLM 调用使用新配置
|
||||
```
|
||||
|
||||
**注意**: Tools 状态目前保存在内存中,重启后重置。后续可持久化到 `~/.super-multica/tool-config.json`。
|
||||
|
||||
---
|
||||
|
||||
## 六、关于 RPC 与 IPC 的区别
|
||||
|
||||
**问**: Admin UI 和 Hub/Agent 之间是通过什么方式通信?
|
||||
|
||||
**答**: 通过 **Electron IPC (进程间通信)**,不是网络 RPC。
|
||||
|
||||
| 通信类型 | 场景 | 协议 |
|
||||
| -------- | ------------------------------- | ------------------- |
|
||||
| IPC | Admin UI ↔ Hub (同一设备) | Electron IPC (内存) |
|
||||
| RPC | Client ↔ Gateway ↔ Hub (跨设备) | WebSocket |
|
||||
|
||||
**为什么选择 IPC 而不是直接 import?**
|
||||
|
||||
1. **安全隔离**: Renderer 进程不应直接访问 Node.js API 和文件系统
|
||||
2. **进程隔离**: Electron 推荐 Renderer 运行在沙盒中
|
||||
3. **一致性**: 与 CLI 调用相同的底层模块,便于维护
|
||||
4. **扩展性**: 后续可以轻松添加 RPC 支持,供远程管理
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Electron App │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ┌─────────────────────────────┐ │
|
||||
│ │ Renderer Process │ │ Main Process │ │
|
||||
│ │ (React UI, 沙盒) │ │ (Node.js, 完整权限) │ │
|
||||
│ │ │ IPC │ │ │
|
||||
│ │ useTools() ──────────────► │ ipcMain.handle('tools:*') │ │
|
||||
│ │ useSkills() ─────────────► │ ipcMain.handle('skills:*') │ │
|
||||
│ │ useHub() ────────────────► │ Hub + Agent 实例 │ │
|
||||
│ └──────────────────────┘ └─────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**IPC 调用示例**:
|
||||
|
||||
```typescript
|
||||
// Renderer (React 组件)
|
||||
const tools = await window.electronAPI.tools.list();
|
||||
|
||||
// Main Process (IPC Handler)
|
||||
ipcMain.handle('tools:list', async () => {
|
||||
const allTools = createAllTools(process.cwd());
|
||||
return allTools.map((t) => ({
|
||||
name: t.name,
|
||||
group: TOOL_GROUPS[t.name] || 'other',
|
||||
enabled: getToolStatus(t.name),
|
||||
}));
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、依赖安装
|
||||
|
||||
```bash
|
||||
# 二维码生成
|
||||
pnpm --filter @multica/desktop add qrcode.react
|
||||
|
||||
# 类型定义 (如需要)
|
||||
pnpm --filter @multica/desktop add -D @types/qrcode.react
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、数据流架构
|
||||
|
||||
Chat 页面支持两种模式,底层使用相同的 UI 组件和 Store:
|
||||
|
||||
### Local Mode (IPC 直连)
|
||||
|
||||
本地 Agent 对话,不需要 Gateway,直接通过 Electron IPC 通信:
|
||||
|
||||
```
|
||||
ChatInput → useLocalChat.sendMessage()
|
||||
→ IPC: localChat:send → agent.write()
|
||||
→ agent.subscribe() → IPC: localChat:event
|
||||
→ useLocalChat.onEvent() → useMessagesStore.startStream/appendStream/endStream
|
||||
→ MessageList 显示
|
||||
```
|
||||
|
||||
### Remote Mode (Gateway)
|
||||
|
||||
远程 Agent 对话,通过 WebSocket 连接 Gateway:
|
||||
|
||||
```
|
||||
ChatInput → useMessagesStore.sendMessage()
|
||||
→ ConnectionStore.send() → WebSocket → Gateway → Hub → agent.write()
|
||||
→ Hub.consumeAgent() → WebSocket: stream event
|
||||
→ ConnectionStore.onMessage() → useMessagesStore.startStream/appendStream/endStream
|
||||
→ MessageList 显示
|
||||
```
|
||||
|
||||
### 复用层级
|
||||
|
||||
| 层级 | 组件/模块 | 复用情况 |
|
||||
| -------- | ----------------------------------- | ----------- |
|
||||
| UI 层 | `MessageList`, `ChatInput` | ✅ 完全复用 |
|
||||
| Store 层 | `useMessagesStore` | ✅ 完全复用 |
|
||||
| Agent 层 | `AsyncAgent.write()`, `subscribe()` | ✅ 完全复用 |
|
||||
| 传输层 | IPC vs WebSocket | ❌ 各自实现 |
|
||||
|
||||
---
|
||||
|
||||
## 九、TODO
|
||||
|
||||
- [ ] **优化 Memory Tool 逻辑**: 当前 memory tool 和 memory.md 没有统一,需要整合
|
||||
- [ ] **优化 Agent Profile 加载逻辑**: 改进 Profile 的加载机制
|
||||
- [ ] **Agent 自我迭代 Profile**: 添加让 Agent 在对话过程中自己修改 Profile 内文件的能力
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
# Welcome to your Expo app 👋
|
||||
|
||||
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
||||
|
||||
## Get started
|
||||
|
||||
1. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Start the app
|
||||
|
||||
```bash
|
||||
npx expo start
|
||||
```
|
||||
|
||||
In the output, you'll find options to open the app in a
|
||||
|
||||
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
||||
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
||||
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
||||
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
||||
|
||||
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
||||
|
||||
## Get a fresh project
|
||||
|
||||
When you're ready, run:
|
||||
|
||||
```bash
|
||||
npm run reset-project
|
||||
```
|
||||
|
||||
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
|
||||
|
||||
## Learn more
|
||||
|
||||
To learn more about developing your project with Expo, look at the following resources:
|
||||
|
||||
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
|
||||
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
|
||||
|
||||
## Join the community
|
||||
|
||||
Join our community of developers creating universal apps.
|
||||
|
||||
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
||||
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
# @multica/web
|
||||
|
||||
Next.js web client for Super Multica. This app is a **thin shell** — it contains only layout and page entry points. All business logic, state management, UI components, and network requests live in shared packages.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
apps/web/app/
|
||||
├── layout.tsx — Root layout, setConfig(), providers
|
||||
├── page.tsx — Page entry, renders <Chat />
|
||||
└── icon.png — Favicon
|
||||
```
|
||||
|
||||
Everything else comes from packages:
|
||||
|
||||
| Package | Responsibility | Examples |
|
||||
|---------|---------------|----------|
|
||||
| `@multica/store` | Global state (Zustand) | Hub, Messages, Gateway, DeviceId |
|
||||
| `@multica/ui` | Components & UI hooks | Chat, HubSidebar, Skeleton, useScrollFade |
|
||||
| `@multica/fetch` | HTTP client & URL config | `consoleApi`, `setConfig()` |
|
||||
| `@multica/sdk` | WebSocket client | `GatewayClient` |
|
||||
|
||||
### Where does new code go?
|
||||
|
||||
- **Page-scoped UI hook** (e.g. form toggle, scroll position) → `@multica/ui/hooks/`
|
||||
- **Cross-component state** (e.g. user preferences, notifications) → `@multica/store`
|
||||
- **Reusable component** → `@multica/ui/components/`
|
||||
- **HTTP request helper** → `@multica/fetch`
|
||||
- **This app** → Only if it's Next.js-specific (middleware, route handlers, `next.config`)
|
||||
|
||||
> Principle: desktop also consumes these packages, so anything reusable must NOT live in `apps/web`.
|
||||
|
||||
## Network Requests
|
||||
|
||||
Two communication channels, two packages:
|
||||
|
||||
```
|
||||
HTTP → @multica/fetch (consoleApi) → Console :4000 (Hub, Agent CRUD)
|
||||
WS → @multica/store (gateway) → Gateway :3000 (Chat messages)
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
1. **Never hardcode URLs.** Use `consoleApi` for HTTP, `useGatewayStore` for WS. Both read from `setConfig()` in `layout.tsx`.
|
||||
2. **HTTP for management, WS for real-time.** Creating/deleting agents is HTTP. Sending/receiving chat messages is WS.
|
||||
3. **Future: gateway may proxy HTTP.** The current two-endpoint setup may merge into one. Because all requests go through `@multica/fetch` and `@multica/store`, business code won't need changes.
|
||||
|
||||
## State Management
|
||||
|
||||
We use Zustand. Follow these rules:
|
||||
|
||||
### Subscribe only to what you render
|
||||
|
||||
```tsx
|
||||
// Good — component re-renders only when status changes
|
||||
const status = useHubStore((s) => s.status)
|
||||
|
||||
// Bad — re-renders on ANY store change
|
||||
const { status } = useHubStore()
|
||||
```
|
||||
|
||||
### Use getState() in callbacks
|
||||
|
||||
Don't subscribe to state that's only used inside event handlers. Read it at call time instead.
|
||||
|
||||
```tsx
|
||||
// Good — no subscription, no re-render
|
||||
const handleSend = useCallback((text: string) => {
|
||||
const hub = useHubStore.getState().hub
|
||||
const agentId = useHubStore.getState().activeAgentId
|
||||
if (!hub?.hubId || !agentId) return
|
||||
useMessagesStore.getState().addUserMessage(text, agentId)
|
||||
useGatewayStore.getState().send(hub.hubId, "message", { agentId, content: text })
|
||||
}, [])
|
||||
|
||||
// Bad — subscribes to hub and activeAgentId just to use them in onClick
|
||||
const hub = useHubStore((s) => s.hub)
|
||||
const activeAgentId = useHubStore((s) => s.activeAgentId)
|
||||
```
|
||||
|
||||
### Subscribe to derived values, not raw objects
|
||||
|
||||
```tsx
|
||||
// Good — re-renders only when the boolean flips
|
||||
const isConnected = useHubStore((s) => s.status === "connected")
|
||||
|
||||
// Bad — re-renders when any field of hub changes
|
||||
const hub = useHubStore((s) => s.hub)
|
||||
const isConnected = hub !== null
|
||||
```
|
||||
|
||||
### Filter/derive with useMemo, not inside selectors
|
||||
|
||||
Selectors that return new references (`.filter()`, `.map()`) cause infinite re-renders. Derive outside the selector.
|
||||
|
||||
```tsx
|
||||
// Good
|
||||
const messages = useMessagesStore((s) => s.messages)
|
||||
const filtered = useMemo(
|
||||
() => messages.filter((m) => m.agentId === activeAgentId),
|
||||
[messages, activeAgentId]
|
||||
)
|
||||
|
||||
// Bad — .filter() returns a new array every time, triggers infinite loop
|
||||
const filtered = useMessagesStore((s) => s.messages.filter(...))
|
||||
```
|
||||
|
||||
### Initialize once
|
||||
|
||||
Side-effectful operations (WS connection, SDK init) must have guards to prevent double execution.
|
||||
|
||||
```tsx
|
||||
// Inside store
|
||||
connect: (deviceId) => {
|
||||
if (client) return // Already connected, skip
|
||||
client = new GatewayClient(...)
|
||||
client.connect()
|
||||
}
|
||||
```
|
||||
|
||||
## Imports
|
||||
|
||||
### Use direct paths for @multica/ui
|
||||
|
||||
```tsx
|
||||
// Good
|
||||
import { Chat } from "@multica/ui/components/chat"
|
||||
import { Button } from "@multica/ui/components/ui/button"
|
||||
import { useScrollFade } from "@multica/ui/hooks/use-scroll-fade"
|
||||
|
||||
// Bad — barrel import pulls in everything
|
||||
import { Chat, Button, useScrollFade } from "@multica/ui"
|
||||
```
|
||||
|
||||
`@multica/store` barrel import is fine — it has few exports and all are lightweight Zustand stores.
|
||||
|
||||
### Heavy components: use dynamic import
|
||||
|
||||
For large dependencies (code editors, chart libraries, PDF viewers), lazy-load to keep the initial bundle small.
|
||||
|
||||
```tsx
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
const CodeEditor = dynamic(
|
||||
() => import("@multica/ui/components/code-editor"),
|
||||
{ ssr: false }
|
||||
)
|
||||
```
|
||||
|
||||
## Conditional Rendering
|
||||
|
||||
Use ternary expressions, not `&&`, to avoid rendering `0` or `""` as visible content.
|
||||
|
||||
```tsx
|
||||
// Good
|
||||
{status === "connected" ? <AgentList /> : null}
|
||||
|
||||
// Bad — if agents is 0, renders "0" on screen
|
||||
{agents.length && <AgentList />}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Start web dev server (port 3001)
|
||||
multica dev web
|
||||
|
||||
# Or start all services
|
||||
multica dev
|
||||
|
||||
# Typecheck
|
||||
cd apps/web && npx tsc --noEmit
|
||||
```
|
||||
|
||||
## Adding a New Feature — Checklist
|
||||
|
||||
1. Does it need global state? → Create a store in `@multica/store`
|
||||
2. Does it need HTTP calls? → Use `consoleApi` from `@multica/fetch`
|
||||
3. Does it need a UI component? → Add to `@multica/ui/components/`
|
||||
4. Does it need a UI hook? → Add to `@multica/ui/hooks/`
|
||||
5. Is it Next.js-specific? → Only then add to `apps/web`
|
||||
6. Is the component heavy (>50KB)? → Use `next/dynamic` with `{ ssr: false }`
|
||||
28
docs/README.md
Normal file
28
docs/README.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Documentation Index (Priority-Based)
|
||||
|
||||
This repo keeps documentation intentionally small to reduce stale AI context.
|
||||
Only workflow/testing/process documentation should be maintained.
|
||||
Project-intro and architecture explanation docs are intentionally omitted.
|
||||
|
||||
## P0 (Keep Fresh)
|
||||
|
||||
1. `README.md`
|
||||
2. `CLAUDE.md`
|
||||
3. `docs/development.md`
|
||||
4. `docs/cli.md`
|
||||
5. `docs/credentials.md`
|
||||
|
||||
## P1 (Operational)
|
||||
|
||||
1. `docs/skills-and-tools.md`
|
||||
2. `docs/package-management.md`
|
||||
3. `docs/e2e-testing-guide.md`
|
||||
|
||||
## P2 (Benchmarks / Specialized)
|
||||
|
||||
1. `docs/e2e-finance-benchmark.md`
|
||||
|
||||
## Regeneration Rule
|
||||
|
||||
When code behavior changes, update only impacted P0/P1 docs first.
|
||||
If unsure, prefer deleting stale sections over keeping speculative content.
|
||||
|
|
@ -1,433 +0,0 @@
|
|||
<mxfile host="app.diagrams.net" modified="2026-02-15T00:00:00.000Z" agent="Claude" version="24.0.0" type="device">
|
||||
<diagram id="multica-architecture" name="Architecture Overview">
|
||||
<mxGraphModel dx="2400" dy="1600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="3300" pageHeight="2400" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<!-- ==================== TITLE ==================== -->
|
||||
<mxCell id="title" value="Super Multica — Architecture & Module Map" style="text;html=1;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;whiteSpace=wrap;fontFamily=Helvetica;fontColor=#1a1a2e;" vertex="1" parent="1">
|
||||
<mxGeometry x="400" y="20" width="800" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- ==================== APPS LAYER ==================== -->
|
||||
<mxCell id="apps-group" value="Apps Layer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f0f4ff;strokeColor=#4361ee;strokeWidth=2;fontSize=14;fontStyle=1;verticalAlign=top;align=left;spacingLeft=10;spacingTop=5;arcSize=8;fontColor=#4361ee;dashed=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="80" width="1530" height="260" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Desktop App -->
|
||||
<mxCell id="desktop" value="Desktop App" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#4361ee;strokeColor=#3a56d4;fontSize=13;fontStyle=1;fontColor=#ffffff;arcSize=12;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="120" width="340" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="desktop-main" value="Main Process (Electron)
├ IPC Handlers (14+ modules)
├ System Tray
└ Auto Updater" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8edff;strokeColor=#4361ee;fontSize=11;align=left;spacingLeft=10;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="170" width="165" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="desktop-renderer" value="Renderer (React 19)
├ Zustand Stores (9)
├ Chat Interface
└ Settings / Dashboard" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8edff;strokeColor=#4361ee;fontSize=11;align=left;spacingLeft=10;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="245" y="170" width="165" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="desktop-ipc-arrow" value="" style="endArrow=classic;startArrow=classic;html=1;strokeColor=#4361ee;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="desktop-main" target="desktop-renderer">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="desktop-ipc-label" value="IPC" style="edgeLabel;html=1;align=center;verticalAlign=middle;fontSize=9;fontColor=#4361ee;fontStyle=1;" vertex="1" connectable="0" parent="desktop-ipc-arrow">
|
||||
<mxGeometry x="-0.1" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="desktop-tag" value="@multica/desktop · Electron + Vite" style="text;html=1;fontSize=9;fontColor=#888;align=center;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="255" width="340" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Gateway -->
|
||||
<mxCell id="gateway" value="Gateway" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#7209b7;strokeColor=#5a078f;fontSize=13;fontStyle=1;fontColor=#ffffff;arcSize=12;" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="120" width="240" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="gateway-detail" value="NestJS WebSocket Server
├ Socket.io (port 3000)
├ Device Registration
├ RPC Message Routing
├ Heartbeat (25s ping)
└ Telegram Channel" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e8ff;strokeColor=#7209b7;fontSize=11;align=left;spacingLeft=10;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="170" width="240" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="gateway-tag" value="@multica/gateway · NestJS + Socket.io" style="text;html=1;fontSize=9;fontColor=#888;align=center;" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="275" width="240" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Web App -->
|
||||
<mxCell id="web" value="Web App" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f72585;strokeColor=#d01f6e;fontSize=13;fontStyle=1;fontColor=#ffffff;arcSize=12;" vertex="1" parent="1">
|
||||
<mxGeometry x="710" y="120" width="200" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="web-detail" value="Next.js 16 + React 19
├ App Router
├ Gateway Client (SDK)
└ Zustand Store" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe0ef;strokeColor=#f72585;fontSize=11;align=left;spacingLeft=10;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="710" y="170" width="200" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="web-tag" value="@multica/web · Next.js 16" style="text;html=1;fontSize=9;fontColor=#888;align=center;" vertex="1" parent="1">
|
||||
<mxGeometry x="710" y="250" width="200" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- CLI -->
|
||||
<mxCell id="cli" value="CLI" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ff6d00;strokeColor=#e06200;fontSize=13;fontStyle=1;fontColor=#ffffff;arcSize=12;" vertex="1" parent="1">
|
||||
<mxGeometry x="940" y="120" width="200" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="cli-detail" value="Interactive & Non-interactive
├ run / chat / session
├ profile / skills / tools
├ credentials / cron / dev
└ Autocomplete + Help" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff6d00;fontSize=11;align=left;spacingLeft=10;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="940" y="170" width="200" height="85" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="cli-tag" value="@multica/cli · Node.js" style="text;html=1;fontSize=9;fontColor=#888;align=center;" vertex="1" parent="1">
|
||||
<mxGeometry x="940" y="260" width="200" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Mobile -->
|
||||
<mxCell id="mobile" value="Mobile" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#06d6a0;strokeColor=#05b88a;fontSize=13;fontStyle=1;fontColor=#ffffff;arcSize=12;" vertex="1" parent="1">
|
||||
<mxGeometry x="1170" y="120" width="170" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mobile-detail" value="React Native
└ (In Development)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0fff5;strokeColor=#06d6a0;fontSize=11;align=left;spacingLeft=10;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1170" y="170" width="170" height="45" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="mobile-tag" value="@multica/mobile · React Native" style="text;html=1;fontSize=9;fontColor=#888;align=center;" vertex="1" parent="1">
|
||||
<mxGeometry x="1170" y="220" width="170" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Server -->
|
||||
<mxCell id="server" value="Server" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#adb5bd;strokeColor=#868e96;fontSize=13;fontStyle=1;fontColor=#ffffff;arcSize=12;" vertex="1" parent="1">
|
||||
<mxGeometry x="1370" y="120" width="170" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="server-detail" value="NestJS REST API
└ Port 4000 (Legacy)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f1f3f5;strokeColor=#adb5bd;fontSize=11;align=left;spacingLeft=10;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1370" y="170" width="170" height="45" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- ==================== CONNECTION ARROWS (Apps → Core) ==================== -->
|
||||
<!-- Desktop → Hub (embedded) -->
|
||||
<mxCell id="arrow-desktop-hub" value="" style="endArrow=classic;html=1;strokeColor=#4361ee;strokeWidth=2;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.15;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="desktop-main" target="core-group">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="arrow-desktop-hub-label" value="Embedded Hub" style="edgeLabel;html=1;align=center;verticalAlign=middle;fontSize=10;fontColor=#4361ee;fontStyle=1;labelBackgroundColor=#ffffff;" vertex="1" connectable="0" parent="arrow-desktop-hub">
|
||||
<mxGeometry x="0.2" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<!-- Web → Gateway (Socket.io) -->
|
||||
<mxCell id="arrow-web-gateway" value="" style="endArrow=classic;startArrow=classic;html=1;strokeColor=#f72585;strokeWidth=2;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.3;entryDx=0;entryDy=0;" edge="1" parent="1" source="web-detail" target="gateway-detail">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="arrow-web-gateway-label" value="Socket.io" style="edgeLabel;html=1;align=center;verticalAlign=middle;fontSize=10;fontColor=#f72585;fontStyle=1;labelBackgroundColor=#ffffff;" vertex="1" connectable="0" parent="arrow-web-gateway">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<!-- Gateway → Hub -->
|
||||
<mxCell id="arrow-gateway-hub" value="" style="endArrow=classic;html=1;strokeColor=#7209b7;strokeWidth=2;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.35;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="gateway-detail" target="core-group">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="arrow-gateway-hub-label" value="RPC" style="edgeLabel;html=1;align=center;verticalAlign=middle;fontSize=10;fontColor=#7209b7;fontStyle=1;labelBackgroundColor=#ffffff;" vertex="1" connectable="0" parent="arrow-gateway-hub">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<!-- CLI → Core -->
|
||||
<mxCell id="arrow-cli-core" value="" style="endArrow=classic;html=1;strokeColor=#ff6d00;strokeWidth=2;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.85;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="cli-detail" target="core-group">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="arrow-cli-core-label" value="Direct Import" style="edgeLabel;html=1;align=center;verticalAlign=middle;fontSize=10;fontColor=#ff6d00;fontStyle=1;labelBackgroundColor=#ffffff;" vertex="1" connectable="0" parent="arrow-cli-core">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<!-- ==================== CORE PACKAGE ==================== -->
|
||||
<mxCell id="core-group" value="@multica/core — Core Engine" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff8f0;strokeColor=#e76f51;strokeWidth=2;fontSize=14;fontStyle=1;verticalAlign=top;align=left;spacingLeft=10;spacingTop=5;arcSize=6;fontColor=#e76f51;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="370" width="1530" height="590" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Hub -->
|
||||
<mxCell id="hub" value="Hub" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e76f51;strokeColor=#d05a3e;fontSize=13;fontStyle=1;fontColor=#ffffff;arcSize=10;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="410" width="350" height="35" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="hub-detail" value="Multi-Agent Coordination

├ Agent Manager — create / delete / list agents
├ RPC Dispatcher — 12+ handlers for remote ops
├ Gateway Client — Socket.io ↔ remote devices
├ Agent Store — persistent agent list (JSON)
├ Device Store — device metadata
├ Auth Store — Hub authentication tokens
├ Exec Approval Manager — tool approval flow
├ Message Aggregator — message coalescing
└ Block Chunker — large payload chunking" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fef0eb;strokeColor=#e76f51;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="455" width="350" height="180" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Agent Engine -->
|
||||
<mxCell id="agent-engine" value="Agent Engine" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#264653;strokeColor=#1d3740;fontSize=13;fontStyle=1;fontColor=#ffffff;arcSize=10;" vertex="1" parent="1">
|
||||
<mxGeometry x="450" y="410" width="530" height="35" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="agent-runner" value="Agent Runner
├ Agent (sync orchestrator)
├ AsyncAgent (event-driven)
├ SyncAgent (blocking)
├ pi-agent-core integration
└ Run Log (JSONL debug)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f4f0;strokeColor=#264653;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="450" y="455" width="165" height="105" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="agent-session" value="Session Manager
├ JSONL Persistence
├ Compaction (3 modes)
│ ├ Count-based
│ ├ Token-aware
│ └ Summary (LLM)
├ File Repair
├ Write Lock
└ UUIDv7 IDs" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f4f0;strokeColor=#264653;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="625" y="455" width="165" height="150" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="agent-ctx" value="Context Window Guard
├ Token Estimation
├ Budget Enforcement
├ LLM Summarization
├ Tool Result Pruning
└ Compaction Metadata" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f4f0;strokeColor=#264653;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="800" y="455" width="175" height="105" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- System Prompt -->
|
||||
<mxCell id="agent-prompt" value="System Prompt Builder
├ Dynamic Construction
├ Sections (tools, skills,
│ memory, channels)
├ Constitution (safety)
└ Runtime Info" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f4f0;strokeColor=#264653;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="450" y="570" width="165" height="105" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Providers -->
|
||||
<mxCell id="providers" value="LLM Providers" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#264653;strokeColor=#1d3740;fontSize=13;fontStyle=1;fontColor=#ffffff;arcSize=10;" vertex="1" parent="1">
|
||||
<mxGeometry x="1010" y="410" width="230" height="35" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="providers-detail" value="Multi-Provider Registry

├ Anthropic (Claude)
├ OpenAI (GPT / o-series)
├ Google (Gemini)
├ DeepSeek
├ Kimi (Moonshot)
├ Groq
├ Mistral
├ Together
├ OpenRouter
└ xAI (Grok)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f4f0;strokeColor=#264653;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="1010" y="455" width="145" height="185" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Auth Profiles -->
|
||||
<mxCell id="auth-profiles" value="Auth Profiles
├ Multi-credential store
├ Rotation on error
├ Cooldown mechanism
├ OAuth support
└ API key resolution" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f4f0;strokeColor=#264653;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="1165" y="455" width="155" height="105" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Tools -->
|
||||
<mxCell id="tools" value="Tools" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#2a9d8f;strokeColor=#218778;fontSize=13;fontStyle=1;fontColor=#ffffff;arcSize=10;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="650" width="350" height="35" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="tools-detail" value="Tool Registry & Execution

├ Exec (shell commands)
│ ├ Safety Classification
│ ├ Approval Callbacks
│ └ Command Allowlist
├ Web Fetch & Web Search
│ ├ SSRF Protection
│ └ Response Cache
├ File Glob
├ Memory Search
├ Sessions Spawn / List (subagents)
├ Cron Tool
├ Data / Finance APIs
├ Send File & Image Resize
├ Process Management
└ Policy Engine
 ├ Allow / Deny Lists
 ├ Provider Overrides
 └ Group Patterns (web:*, fs:*)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f8f5;strokeColor=#2a9d8f;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="695" width="350" height="245" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Skills System -->
|
||||
<mxCell id="skills-sys" value="Skills System" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e9c46a;strokeColor=#d4a843;fontSize=13;fontStyle=1;fontColor=#333;arcSize=10;" vertex="1" parent="1">
|
||||
<mxGeometry x="450" y="690" width="170" height="35" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skills-detail" value="Modular Skill Loader
├ YAML Frontmatter
├ Eligibility Filtering
│ (OS, binaries, configs)
├ Hot-Reload Watcher
├ GitHub Install
└ OpenClaw Compatible" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fdf6e3;strokeColor=#e9c46a;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="450" y="735" width="170" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Profile System -->
|
||||
<mxCell id="profile-sys" value="Profile System" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e9c46a;strokeColor=#d4a843;fontSize=13;fontStyle=1;fontColor=#333;arcSize=10;" vertex="1" parent="1">
|
||||
<mxGeometry x="640" y="690" width="170" height="35" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="profile-detail" value="Agent Identity & Config
├ Soul (personality)
├ User info
├ Workspace context
├ Memory (long-term)
├ Heartbeat prompt
└ Config (YAML)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fdf6e3;strokeColor=#e9c46a;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="640" y="735" width="170" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Subagent System -->
|
||||
<mxCell id="subagent-sys" value="Subagent System" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e9c46a;strokeColor=#d4a843;fontSize=13;fontStyle=1;fontColor=#333;arcSize=10;" vertex="1" parent="1">
|
||||
<mxGeometry x="840" y="690" width="180" height="35" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="subagent-detail" value="Child Agent Orchestration
├ Registry (lifecycle)
├ JSONL Persistence
├ Result Announcement
├ Coalesced Batching
├ Command Queue
└ Lane-based Concurrency" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fdf6e3;strokeColor=#e9c46a;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="840" y="735" width="180" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Cron System -->
|
||||
<mxCell id="cron-sys" value="Cron" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e9c46a;strokeColor=#d4a843;fontSize=13;fontStyle=1;fontColor=#333;arcSize=10;" vertex="1" parent="1">
|
||||
<mxGeometry x="1050" y="690" width="130" height="35" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="cron-detail" value="Scheduled Tasks
├ Cron Expressions
├ Job Execution
└ JSONL Storage" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fdf6e3;strokeColor=#e9c46a;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="1050" y="735" width="130" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Heartbeat -->
|
||||
<mxCell id="heartbeat-sys" value="Heartbeat" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e9c46a;strokeColor=#d4a843;fontSize=13;fontStyle=1;fontColor=#333;arcSize=10;" vertex="1" parent="1">
|
||||
<mxGeometry x="1200" y="690" width="130" height="35" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="heartbeat-detail" value="Always-On Agent
├ Periodic Runner
├ Wake-from-sleep
└ System Events" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fdf6e3;strokeColor=#e9c46a;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="1200" y="735" width="130" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Channels -->
|
||||
<mxCell id="channels-sys" value="Channels" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e9c46a;strokeColor=#d4a843;fontSize=13;fontStyle=1;fontColor=#333;arcSize=10;" vertex="1" parent="1">
|
||||
<mxGeometry x="1350" y="690" width="190" height="35" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="channels-detail" value="Messaging Integrations
├ Channel Manager
├ Plugin Registry
├ Inbound Debouncer
└ Telegram (via Gateway)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fdf6e3;strokeColor=#e9c46a;fontSize=11;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="1350" y="735" width="190" height="90" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Media -->
|
||||
<mxCell id="media-sys" value="Media" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e9c46a;strokeColor=#d4a843;fontSize=11;fontStyle=1;fontColor=#333;arcSize=10;" vertex="1" parent="1">
|
||||
<mxGeometry x="1350" y="835" width="190" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="media-detail" value="Image / Video / Audio
├ describe-image (LLM)
├ describe-video (frames)
└ transcribe (Whisper)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fdf6e3;strokeColor=#e9c46a;fontSize=10;align=left;spacingLeft=10;arcSize=6;fontColor=#333;verticalAlign=top;spacingTop=4;" vertex="1" parent="1">
|
||||
<mxGeometry x="1350" y="870" width="190" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Hub → Agent Engine arrow -->
|
||||
<mxCell id="hub-agent-arrow" value="" style="endArrow=classic;startArrow=classic;html=1;strokeColor=#264653;strokeWidth=1.5;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="hub" target="agent-engine">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="hub-agent-label" value="manages" style="edgeLabel;html=1;align=center;verticalAlign=middle;fontSize=9;fontColor=#264653;fontStyle=2;" vertex="1" connectable="0" parent="hub-agent-arrow">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<!-- ==================== SHARED PACKAGES ==================== -->
|
||||
<mxCell id="shared-group" value="Shared Packages" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f0fff4;strokeColor=#38b000;strokeWidth=2;fontSize=14;fontStyle=1;verticalAlign=top;align=left;spacingLeft=10;spacingTop=5;arcSize=6;fontColor=#38b000;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="990" width="1080" height="130" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="pkg-sdk" value="SDK
@multica/sdk
Gateway Client
Socket.io" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d4edda;strokeColor=#38b000;fontSize=11;align=center;arcSize=8;fontColor=#333;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="60" y="1025" width="130" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pkg-ui" value="UI
@multica/ui
Shadcn + Radix
Tailwind v4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d4edda;strokeColor=#38b000;fontSize=11;align=center;arcSize=8;fontColor=#333;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="210" y="1025" width="130" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pkg-store" value="Store
@multica/store
Zustand" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d4edda;strokeColor=#38b000;fontSize=11;align=center;arcSize=8;fontColor=#333;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="360" y="1025" width="130" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pkg-hooks" value="Hooks
@multica/hooks
React Hooks" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d4edda;strokeColor=#38b000;fontSize=11;align=center;arcSize=8;fontColor=#333;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="510" y="1025" width="130" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pkg-types" value="Types
@multica/types
Shared TypeScript
Zero-dep" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d4edda;strokeColor=#38b000;fontSize=11;align=center;arcSize=8;fontColor=#333;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="660" y="1025" width="130" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pkg-utils" value="Utils
@multica/utils
Paths, Retry
Errors, Device ID" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d4edda;strokeColor=#38b000;fontSize=11;align=center;arcSize=8;fontColor=#333;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="810" y="1025" width="140" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pkg-utils2" value="Credentials
@multica/core
credentials.json5
skills.env.json5" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d4edda;strokeColor=#38b000;fontSize=11;align=center;arcSize=8;fontColor=#333;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="970" y="1025" width="130" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- ==================== SKILLS LAYER ==================== -->
|
||||
<mxCell id="skills-group" value="Bundled Skills" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fffde7;strokeColor=#f9a825;strokeWidth=2;fontSize=14;fontStyle=1;verticalAlign=top;align=left;spacingLeft=10;spacingTop=5;arcSize=6;fontColor=#f9a825;" vertex="1" parent="1">
|
||||
<mxGeometry x="1150" y="990" width="420" height="130" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skill-pdf" value="pdf" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff9c4;strokeColor=#f9a825;fontSize=10;align=center;arcSize=10;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1170" y="1025" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skill-pptx" value="pptx" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff9c4;strokeColor=#f9a825;fontSize=10;align=center;arcSize=10;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1235" y="1025" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skill-docx" value="docx" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff9c4;strokeColor=#f9a825;fontSize=10;align=center;arcSize=10;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1300" y="1025" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skill-xlsx" value="xlsx" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff9c4;strokeColor=#f9a825;fontSize=10;align=center;arcSize=10;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1365" y="1025" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skill-whisper" value="whisper" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff9c4;strokeColor=#f9a825;fontSize=10;align=center;arcSize=10;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1430" y="1025" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skill-finance" value="finance-
research" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff9c4;strokeColor=#f9a825;fontSize=10;align=center;arcSize=10;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1495" y="1025" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skill-profile" value="profile-
setup" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff9c4;strokeColor=#f9a825;fontSize=10;align=center;arcSize=10;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1170" y="1065" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skill-creator" value="skill-
creator" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff9c4;strokeColor=#f9a825;fontSize=10;align=center;arcSize=10;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1240" y="1065" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skill-earnings" value="earnings-
analysis" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff9c4;strokeColor=#f9a825;fontSize=10;align=center;arcSize=10;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1310" y="1065" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skill-dcf" value="dcf-
valuation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff9c4;strokeColor=#f9a825;fontSize=10;align=center;arcSize=10;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="1380" y="1065" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- ==================== DATA LAYER ==================== -->
|
||||
<mxCell id="data-group" value="Persistent Storage (~/.super-multica/)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666;strokeWidth=2;fontSize=14;fontStyle=1;verticalAlign=top;align=left;spacingLeft=10;spacingTop=5;arcSize=6;fontColor=#666;dashed=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="1150" width="1530" height="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data-sessions" value="sessions/
{id}.jsonl" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#eee;strokeColor=#999;fontSize=10;align=center;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="1185" width="100" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data-profiles" value="agent-profiles/
{id}/*.md" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#eee;strokeColor=#999;fontSize=10;align=center;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="190" y="1185" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data-creds" value="credentials
.json5" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#eee;strokeColor=#999;fontSize=10;align=center;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="320" y="1185" width="90" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data-agents" value="agents
.json" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#eee;strokeColor=#999;fontSize=10;align=center;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="430" y="1185" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data-subagents" value="subagents
.jsonl" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#eee;strokeColor=#999;fontSize=10;align=center;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="530" y="1185" width="90" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data-cron" value="cron-jobs
.jsonl" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#eee;strokeColor=#999;fontSize=10;align=center;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="640" y="1185" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data-skills-env" value="skills.env
.json5" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#eee;strokeColor=#999;fontSize=10;align=center;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="740" y="1185" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="data-runlog" value="run-log
.jsonl" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#eee;strokeColor=#999;fontSize=10;align=center;arcSize=8;fontColor=#333;" vertex="1" parent="1">
|
||||
<mxGeometry x="840" y="1185" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- ==================== EXTERNAL DEPS ==================== -->
|
||||
<mxCell id="ext-group" value="External Dependencies" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fafafa;strokeColor=#aaa;strokeWidth=1;fontSize=12;fontStyle=1;verticalAlign=top;align=left;spacingLeft=10;spacingTop=5;arcSize=6;fontColor=#888;dashed=1;dashPattern=5 5;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="1270" width="1530" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-piagent" value="pi-agent-core" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="1295" width="100" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-electron" value="Electron 33+" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="185" y="1295" width="100" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-nestjs" value="NestJS 11" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="300" y="1295" width="80" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-nextjs" value="Next.js 16" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="395" y="1295" width="80" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-react" value="React 19" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="490" y="1295" width="70" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-socketio" value="Socket.io" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="575" y="1295" width="70" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-zustand" value="Zustand" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="660" y="1295" width="70" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-turborepo" value="Turborepo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="745" y="1295" width="70" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-pnpm" value="pnpm 10" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="830" y="1295" width="70" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-vitest" value="Vitest" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="915" y="1295" width="60" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-tailwind" value="Tailwind v4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="990" y="1295" width="80" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ext-shadcn" value="Shadcn/UI" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f8f8;strokeColor=#ccc;fontSize=10;align=center;arcSize=8;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="1085" y="1295" width="75" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- ==================== LEGEND ==================== -->
|
||||
<mxCell id="legend-box" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#ddd;strokeWidth=1;arcSize=6;" vertex="1" parent="1">
|
||||
<mxGeometry x="1200" y="1270" width="370" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-title" value="Legend" style="text;html=1;fontSize=11;fontStyle=1;align=left;fontColor=#666;" vertex="1" parent="1">
|
||||
<mxGeometry x="1210" y="1273" width="50" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-1" value="" style="rounded=1;fillColor=#4361ee;strokeColor=none;fontSize=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1210" y="1295" width="12" height="12" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-1t" value="Apps" style="text;fontSize=9;fontColor=#666;align=left;" vertex="1" parent="1">
|
||||
<mxGeometry x="1226" y="1293" width="35" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-2" value="" style="rounded=1;fillColor=#264653;strokeColor=none;fontSize=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1270" y="1295" width="12" height="12" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-2t" value="Engine" style="text;fontSize=9;fontColor=#666;align=left;" vertex="1" parent="1">
|
||||
<mxGeometry x="1286" y="1293" width="40" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-3" value="" style="rounded=1;fillColor=#e76f51;strokeColor=none;fontSize=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1335" y="1295" width="12" height="12" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-3t" value="Hub" style="text;fontSize=9;fontColor=#666;align=left;" vertex="1" parent="1">
|
||||
<mxGeometry x="1351" y="1293" width="30" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-4" value="" style="rounded=1;fillColor=#2a9d8f;strokeColor=none;fontSize=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1390" y="1295" width="12" height="12" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-4t" value="Tools" style="text;fontSize=9;fontColor=#666;align=left;" vertex="1" parent="1">
|
||||
<mxGeometry x="1406" y="1293" width="35" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-5" value="" style="rounded=1;fillColor=#e9c46a;strokeColor=none;fontSize=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1450" y="1295" width="12" height="12" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-5t" value="Systems" style="text;fontSize=9;fontColor=#666;align=left;" vertex="1" parent="1">
|
||||
<mxGeometry x="1466" y="1293" width="50" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-6" value="" style="rounded=1;fillColor=#38b000;strokeColor=none;fontSize=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1520" y="1295" width="12" height="12" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="legend-6t" value="Shared" style="text;fontSize=9;fontColor=#666;align=left;" vertex="1" parent="1">
|
||||
<mxGeometry x="1536" y="1293" width="40" height="16" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# Desktop 登录集成
|
||||
|
||||
## 登录流程
|
||||
|
||||
```
|
||||
Desktop 点击登录
|
||||
↓
|
||||
启动本地 HTTP 服务器 (随机端口,如 54321)
|
||||
↓
|
||||
打开浏览器 → http://localhost:3000/api/desktop/session?port=54321&platform=web
|
||||
↓
|
||||
Web 重定向 → /login?next=...
|
||||
↓
|
||||
用户登录,调用 /api/v1/auth/login (代理到 api-dev.copilothub.ai)
|
||||
↓
|
||||
登录成功,回调 → http://127.0.0.1:54321/callback?sid=xxx&user=xxx
|
||||
↓
|
||||
Desktop 保存到 ~/.super-multica/auth.json
|
||||
```
|
||||
|
||||
## 前端逻辑
|
||||
|
||||
### Web 端
|
||||
|
||||
- 端口:**3000**
|
||||
- 登录 API:`/api/v1/auth/login`(通过 Next.js rewrites 代理到后端)
|
||||
- 登录成功后回调:`http://127.0.0.1:{port}/callback?sid=xxx&user=xxx`
|
||||
|
||||
### Desktop 端
|
||||
|
||||
- 点击登录 → 启动本地服务器 → 打开浏览器
|
||||
- 收到回调 → 保存到本地文件
|
||||
|
||||
## 存储
|
||||
|
||||
**路径:** `~/.super-multica/auth.json`
|
||||
|
||||
Desktop 登录成功后,SID 和用户信息存储在本地文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"sid": "session-id-from-backend",
|
||||
"user": {
|
||||
"uid": "user-id",
|
||||
"name": "User Name",
|
||||
"email": "user@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
后续请求可从此文件读取 `sid` 进行认证。
|
||||
|
||||
## 退出登录
|
||||
|
||||
**后端只需要返回错误,前端会自动处理退出。**
|
||||
|
||||
前端收到认证错误后:
|
||||
1. 调用 `auth:clear` 清除本地数据
|
||||
2. 跳转到登录页
|
||||
|
||||
## 本地调试
|
||||
|
||||
```bash
|
||||
# 1. 启动 Web(Next.js rewrites 自动代理 /api/* 到 api-dev.copilothub.ai)
|
||||
pnpm dev:web
|
||||
|
||||
# 2. 启动 Desktop
|
||||
pnpm dev:desktop
|
||||
```
|
||||
|
||||
本地调试时,Next.js rewrites(配置在 `apps/web/next.config.ts`)自动将 `/api/*` 请求代理到 `MULTICA_API_URL` 指定的后端。
|
||||
|
||||
## 参考
|
||||
|
||||
- **Cap** - https://github.com/CapSoftware/Cap
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
# Channel System
|
||||
|
||||
The Channel system connects external messaging platforms (Telegram, Discord, etc.) to the Hub's agent. Each platform is a **plugin** that translates platform-specific APIs into a unified interface.
|
||||
|
||||
> For media handling details (audio transcription, image/video description), see [media-handling.md](./media-handling.md).
|
||||
> For message flow across all three I/O paths (Desktop / Web / Channel), see [message-paths.md](../message-paths.md).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ credentials.json5 │
|
||||
│ { channels: { telegram: { default: { botToken } } } } │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│ loadChannelsConfig()
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Channel Manager (manager.ts) │
|
||||
│ │
|
||||
│ startAll() → iterate plugins → startAccount() per account │
|
||||
│ ensureSubscribed() → listen for agent lifecycle events │
|
||||
│ │
|
||||
│ Incoming: │
|
||||
│ routeIncoming() → 👀 ack + debouncer → agent.write() │
|
||||
│ Outgoing: │
|
||||
│ activeRoute → aggregator → plugin.outbound.*() │
|
||||
│ │
|
||||
│ State: │
|
||||
│ pendingRoutes[] ─(FIFO)→ activeRoute + activeAcks │
|
||||
│ ackBuffer[] ─(snapshot on flush)→ pendingRoutes[].acks │
|
||||
└──────────┬──────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ InboundDebouncer (inbound-debouncer.ts) │
|
||||
│ 500ms idle window / 2000ms hard cap per conversationId │
|
||||
│ Each flush → snapshot route + acks → agent.write() │
|
||||
└──────────┬──────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Plugin Registry (registry.ts) │
|
||||
│ registerChannel(plugin) / listChannels() / getChannel(id) │
|
||||
└──────────┬──────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Channel Plugins (e.g. telegram.ts) │
|
||||
│ │
|
||||
│ config — resolve account credentials │
|
||||
│ gateway — receive messages (polling / webhook) │
|
||||
│ outbound — send replies, typing, reactions (👀 ack) │
|
||||
│ downloadMedia() — download media files to local disk │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Plugin Interface
|
||||
|
||||
Each channel plugin implements `ChannelPlugin` (defined in `types.ts`):
|
||||
|
||||
```typescript
|
||||
interface ChannelPlugin {
|
||||
readonly id: string; // "telegram", "discord", etc.
|
||||
readonly meta: { name: string; description: string };
|
||||
readonly chunkerConfig?: BlockChunkerConfig; // override text chunking per platform
|
||||
readonly config: ChannelConfigAdapter; // credential resolution
|
||||
readonly gateway: ChannelGatewayAdapter; // receive messages
|
||||
readonly outbound: ChannelOutboundAdapter; // send replies
|
||||
downloadMedia?(fileId: string, accountId: string): Promise<string>; // optional
|
||||
}
|
||||
```
|
||||
|
||||
### Three Adapters
|
||||
|
||||
| Adapter | Role | Key Methods |
|
||||
|---------|------|-------------|
|
||||
| **config** | Resolve credentials from `credentials.json5` | `listAccountIds()`, `resolveAccount()`, `isConfigured()` |
|
||||
| **gateway** | Receive inbound messages from the platform | `start(accountId, config, onMessage, signal)` |
|
||||
| **outbound** | Send replies back to the platform | `sendText()`, `replyText()`, `sendTyping?()`, `addReaction?()`, `removeReaction?()` |
|
||||
|
||||
### downloadMedia (optional)
|
||||
|
||||
Platforms that support media (voice, image, video, document) implement `downloadMedia()` to download files to `~/.super-multica/cache/media/` with UUID filenames. The Manager calls this before processing media.
|
||||
|
||||
## Message Flow
|
||||
|
||||
### Inbound (Platform → Agent)
|
||||
|
||||
```
|
||||
User sends message in Telegram
|
||||
→ grammy long-polling → onMessage callback
|
||||
→ ChannelManager.routeIncoming()
|
||||
1. Update lastRoute (reply target)
|
||||
2. Start typing indicator (repeats every 5s)
|
||||
3. Add 👀 reaction to this message (ack)
|
||||
4. Push ack route to ackBuffer
|
||||
5. If media: routeMedia() → download → transcribe/describe → text
|
||||
6. Push text into InboundDebouncer
|
||||
|
||||
InboundDebouncer (per conversationId):
|
||||
┌─ 500ms idle window: wait for more messages
|
||||
│ If another message arrives within 500ms, reset timer and append
|
||||
│ If 2000ms since first message, force-flush immediately
|
||||
└─ On flush:
|
||||
1. Snapshot lastRoute → route
|
||||
2. Snapshot ackBuffer → acks, clear buffer
|
||||
3. Push { route, acks } to pendingRoutes queue
|
||||
4. Call agent.write(combinedText)
|
||||
```
|
||||
|
||||
All media is converted to text before the agent sees it. See [media-handling.md](./media-handling.md) for details.
|
||||
|
||||
### Outbound (Agent → Platform)
|
||||
|
||||
```
|
||||
agent.write() queued → agent.run() starts
|
||||
→ agent_start event
|
||||
1. Shift entry from pendingRoutes queue
|
||||
2. Set activeRoute = entry.route (stable for entire run)
|
||||
3. Set activeAcks = entry.acks
|
||||
|
||||
→ message_start (assistant)
|
||||
1. Create MessageAggregator wired to activeRoute
|
||||
→ message_update (assistant)
|
||||
1. Feed text deltas to aggregator
|
||||
→ message_end (assistant)
|
||||
1. Aggregator flushes final block, then null out
|
||||
(May repeat if agent does multi-turn tool calls)
|
||||
|
||||
→ Aggregator emits BlockReply chunks:
|
||||
Block 0: plugin.outbound.replyText() // reply to original message
|
||||
Block N: plugin.outbound.sendText() // follow-up messages
|
||||
|
||||
→ agent_end event
|
||||
1. Remove 👀 from all activeAcks messages
|
||||
2. Clear activeRoute and activeAcks
|
||||
3. If pendingRoutes is empty → stop typing
|
||||
If more pending → keep typing for next run
|
||||
```
|
||||
|
||||
The **MessageAggregator** buffers streaming LLM output and splits it into blocks at natural text boundaries (paragraphs, code blocks). This is necessary because messaging platforms cannot consume raw streaming deltas.
|
||||
|
||||
## Route Queue Pattern
|
||||
|
||||
The channel system uses a FIFO queue to correctly route replies when multiple messages arrive while the agent is busy. This solves the "reply-to mismatch" problem where rapid-fire messages would cause replies to target the wrong original message.
|
||||
|
||||
### State Fields
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| `lastRoute` | `LastRoute \| null` | Where the most recent channel message came from. Updated on every incoming message. |
|
||||
| `pendingRoutes` | `{ route, acks }[]` | FIFO queue of snapshotted routes, one per debouncer flush. Dequeued on `agent_start`. |
|
||||
| `activeRoute` | `LastRoute \| null` | Route for the currently running agent. Set on `agent_start`, cleared on `agent_end`. Stable across all turns within one run. |
|
||||
| `ackBuffer` | `LastRoute[]` | Accumulates 👀 ack targets between debouncer flushes. Snapshotted and cleared on each flush. |
|
||||
| `activeAcks` | `LastRoute[]` | All messages with 👀 in the current run. Cleaned up on `agent_end`. |
|
||||
|
||||
### Lifecycle
|
||||
|
||||
```
|
||||
Message A arrives → lastRoute = A, ackBuffer = [A], 👀 on A
|
||||
Message B arrives (50ms) → lastRoute = B, ackBuffer = [A, B], 👀 on B
|
||||
─── 500ms idle ───
|
||||
Debouncer flushes → pendingRoutes.push({ route: B, acks: [A, B] })
|
||||
ackBuffer = [], agent.write("A\nB")
|
||||
|
||||
Message C arrives → lastRoute = C, ackBuffer = [C], 👀 on C
|
||||
─── 500ms idle ───
|
||||
Debouncer flushes → pendingRoutes.push({ route: C, acks: [C] })
|
||||
ackBuffer = [], agent.write("C")
|
||||
|
||||
agent_start (run 1) → activeRoute = B, activeAcks = [A, B]
|
||||
(agent processes "A\nB", replies to message B)
|
||||
agent_end (run 1) → remove 👀 from A and B, pendingRoutes still has 1 → keep typing
|
||||
|
||||
agent_start (run 2) → activeRoute = C, activeAcks = [C]
|
||||
(agent processes "C", replies to message C)
|
||||
agent_end (run 2) → remove 👀 from C, pendingRoutes empty → stop typing
|
||||
```
|
||||
|
||||
### Why agent_start / agent_end (not message_end)
|
||||
|
||||
In multi-turn agent runs (e.g. when the agent uses tools), `message_end` fires once per assistant message — potentially multiple times per `agent.run()`. Using `message_end` for state management would:
|
||||
- Clear `activeRoute` mid-run, causing the next turn's aggregator to pick up the wrong route
|
||||
- Remove 👀 too early (before the agent is actually done)
|
||||
- Stop typing between tool-call turns
|
||||
|
||||
`agent_start` and `agent_end` fire exactly once per `agent.run()`, making them the correct lifecycle boundaries.
|
||||
|
||||
### lastRoute vs activeRoute
|
||||
|
||||
- **`lastRoute`** — global, updated on every incoming message. Used for: typing indicators, error reporting, creating aggregators when no activeRoute exists.
|
||||
- **`activeRoute`** — per-run, set from queue on `agent_start`. Used for: reply targeting via aggregator. Guarantees that a run's reply goes to the correct message even if new messages arrive during processing.
|
||||
|
||||
Desktop and Web always receive agent events independently via their own mechanisms (IPC / Gateway). `clearLastRoute()` is called when a desktop/web message arrives to prevent channel forwarding.
|
||||
|
||||
## Inbound Debouncer
|
||||
|
||||
The `InboundDebouncer` (`inbound-debouncer.ts`) batches rapid-fire messages from the same conversation into a single `agent.write()` call. This prevents the agent from processing incomplete thoughts when users send multiple short messages quickly.
|
||||
|
||||
**Parameters:**
|
||||
- `delayMs` (default 500ms) — idle window: how long to wait after each message before flushing
|
||||
- `maxWaitMs` (default 2000ms) — hard cap: max time since first message before force-flushing
|
||||
|
||||
**Behavior:**
|
||||
- Messages within 500ms of each other are combined with newlines
|
||||
- Messages >500ms apart get independent flushes and separate agent runs
|
||||
- No busy-awareness: each flush is independent regardless of agent state
|
||||
- Each flush triggers a route snapshot (lastRoute + ackBuffer) pushed to the pendingRoutes queue
|
||||
|
||||
## Typing and Reaction Lifecycle
|
||||
|
||||
### Typing Indicator
|
||||
- **Start:** `routeIncoming()` — starts a 5s repeating interval (Telegram requires re-sending "typing" every 5s)
|
||||
- **Stop:** `agent_end` — only if `pendingRoutes` is empty (all queued runs complete). If runs remain queued, typing persists.
|
||||
- **Also stops on:** `clearLastRoute()` (desktop/web message), `stopAccount()`, `stopAll()`, `agent_error`
|
||||
|
||||
### 👀 Ack Reaction
|
||||
- **Add:** `routeIncoming()` — immediately on each message, before debouncing
|
||||
- **Track:** pushed to `ackBuffer`, then snapshotted into `pendingRoutes[].acks` on debouncer flush, then moved to `activeAcks` on `agent_start`
|
||||
- **Remove:** `agent_end` — iterates `activeAcks` and removes 👀 from each message
|
||||
- **Also removed on:** `agent_error`
|
||||
|
||||
This ensures every queued message shows 👀 while waiting, and all 👀 are cleaned up precisely when the agent finishes processing that batch.
|
||||
|
||||
## Configuration
|
||||
|
||||
Channel credentials are stored in `~/.super-multica/credentials.json5` under the `channels` key:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
telegram: {
|
||||
default: {
|
||||
botToken: "123456:ABC-DEF..."
|
||||
}
|
||||
},
|
||||
// discord: { default: { botToken: "..." } },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each channel ID maps to accounts (keyed by account ID, typically `"default"`). The config adapter for each plugin knows how to extract and validate its credentials.
|
||||
|
||||
## Adding a New Plugin
|
||||
|
||||
1. Create `src/channels/plugins/<name>.ts` implementing `ChannelPlugin`
|
||||
2. Register it in `src/channels/index.ts`:
|
||||
```typescript
|
||||
import { <name>Channel } from "./plugins/<name>.js";
|
||||
registerChannel(<name>Channel);
|
||||
```
|
||||
3. Add the config shape to the `channels` section of `credentials.json5`
|
||||
|
||||
### Implementation Checklist
|
||||
|
||||
- [ ] `config` adapter: parse credentials from `credentials.json5`
|
||||
- [ ] `gateway` adapter: connect to platform, normalize messages to `ChannelMessage`
|
||||
- [ ] `outbound` adapter: `sendText`, `replyText`, optional `sendTyping`, `addReaction`, `removeReaction`
|
||||
- [ ] `downloadMedia` (if platform supports media): download to `MEDIA_CACHE_DIR`
|
||||
- [ ] Group filtering: only respond to messages directed at the bot
|
||||
- [ ] Graceful shutdown: respect the `AbortSignal` passed to `gateway.start()`
|
||||
|
||||
## File Map
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `src/channels/types.ts` | All type definitions (`ChannelPlugin`, `ChannelMessage`, `DeliveryContext`, etc.) |
|
||||
| `src/channels/manager.ts` | `ChannelManager` — bridges plugins to the Hub's agent, route queue, typing/ack lifecycle |
|
||||
| `src/channels/inbound-debouncer.ts` | `InboundDebouncer` — batches rapid-fire messages per conversationId |
|
||||
| `src/channels/registry.ts` | Plugin registry (`registerChannel`, `listChannels`, `getChannel`) |
|
||||
| `src/channels/config.ts` | Load channel config from `credentials.json5` |
|
||||
| `src/channels/index.ts` | Bootstrap: register built-in plugins, re-export public API |
|
||||
| `src/channels/plugins/telegram.ts` | Telegram plugin (grammy, long polling) |
|
||||
| `src/channels/plugins/telegram-format.ts` | Markdown → Telegram HTML converter |
|
||||
| `src/media/transcribe.ts` | Audio transcription (local whisper → OpenAI API) |
|
||||
| `src/media/describe-image.ts` | Image description (OpenAI Vision API) |
|
||||
| `src/media/describe-video.ts` | Video description (ffmpeg frame + Vision API) |
|
||||
| `src/shared/paths.ts` | `MEDIA_CACHE_DIR` path constant |
|
||||
| `src/hub/message-aggregator.ts` | Streaming text → block chunking for channel delivery |
|
||||
| `packages/ui/src/components/message-list.tsx` | UI rendering with `stripUserMetadata()` for clean display |
|
||||
|
||||
## Current Plugins
|
||||
|
||||
| Plugin | Platform | Transport | Library |
|
||||
|--------|----------|-----------|---------|
|
||||
| `telegram` | Telegram | Long polling | grammy |
|
||||
|
||||
Planned: Discord, Feishu, LINE, etc.
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
# Channel Media Handling
|
||||
|
||||
How multimedia messages (voice, image, video, document) from messaging platforms are processed before reaching the Agent.
|
||||
|
||||
## Core Principle
|
||||
|
||||
All media is converted to text before the Agent sees it. The Agent only ever receives plain text via `agent.write()`.
|
||||
|
||||
```
|
||||
Platform message (voice/image/video/doc)
|
||||
→ Plugin: detect type + download file
|
||||
→ Manager: convert to text (API transcription / vision description)
|
||||
→ Agent receives text via agent.write()
|
||||
```
|
||||
|
||||
## Reference Architecture (OpenClaw)
|
||||
|
||||
OpenClaw supports 6 platforms (Telegram, Discord, LINE, Signal, iMessage, Slack). All share the same media processing pipeline.
|
||||
|
||||
### Per-Platform Layer (different for each platform)
|
||||
|
||||
Each platform detects media type using its own API:
|
||||
|
||||
| Platform | Detection Method |
|
||||
|----------|-----------------|
|
||||
| Telegram | `msg.voice`, `msg.audio`, `msg.photo`, `msg.video`, `msg.document` |
|
||||
| Discord | `attachment.content_type` MIME prefix (`audio/`, `image/`, `video/`) |
|
||||
| LINE | `message.type` field (`"audio"`, `"image"`, `"video"`, `"file"`) |
|
||||
| Signal | `attachment.contentType` MIME prefix |
|
||||
| iMessage | `attachment.mime_type` MIME prefix |
|
||||
| Slack | Any file attachment (MIME-based detection happens later) |
|
||||
|
||||
Each platform downloads the file using its own API, saves to local disk, and tags it:
|
||||
- `<media:audio>` for voice/audio
|
||||
- `<media:image>` for images
|
||||
- `<media:video>` for video
|
||||
- `<media:document>` for files
|
||||
|
||||
### Shared Layer (`applyMediaUnderstanding()`)
|
||||
|
||||
One function handles all conversions, called automatically before the Agent sees the message:
|
||||
|
||||
1. Reads local file path + MIME type
|
||||
2. Selects conversion method based on type:
|
||||
- **audio** → transcription (whisper local / OpenAI API / Groq / Deepgram / Google)
|
||||
- **image** → vision model description (Gemini / OpenAI / Anthropic)
|
||||
- **video** → vision model description
|
||||
3. Replaces placeholder with formatted text:
|
||||
- Audio: `[Audio]\nTranscript:\n<transcribed text>`
|
||||
- Image: `[Image]\nDescription:\n<description text>`
|
||||
4. If conversion fails (no provider configured), the raw placeholder stays in the message
|
||||
|
||||
### Transcription Provider Priority
|
||||
|
||||
Auto-detection order:
|
||||
1. sherpa-onnx-offline (local)
|
||||
2. whisper-cli / whisper.cpp (local)
|
||||
3. whisper Python CLI (local)
|
||||
4. gemini CLI (local)
|
||||
5. API providers: OpenAI → Groq → Deepgram → Google
|
||||
|
||||
### Skill Integration
|
||||
|
||||
Whisper skills declare requirements in `SKILL.md` metadata:
|
||||
```yaml
|
||||
requires:
|
||||
bins: ["whisper"] # must exist in PATH
|
||||
```
|
||||
|
||||
If the binary is missing, the skill is filtered out — the Agent never sees it. If present, the Agent can use it for transcription.
|
||||
|
||||
---
|
||||
|
||||
## Our Implementation
|
||||
|
||||
All media is converted to text in the Manager layer (`routeMedia()`) before reaching the Agent, matching OpenClaw's `applyMediaUnderstanding()` pattern.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Platform Plugin (e.g. telegram.ts) │
|
||||
│ │
|
||||
│ bot.on("message:voice") → detect type │
|
||||
│ bot.api.getFile() → download to local disk │
|
||||
│ Emit ChannelMessage with media attachment │
|
||||
└──────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Channel Manager (manager.ts → routeMedia()) │
|
||||
│ │
|
||||
│ Download file via plugin.downloadMedia() │
|
||||
│ audio → transcribeAudio() → text │
|
||||
│ image → describeImage() → text │
|
||||
│ video → describeVideo() (ffmpeg frame + vision) → text │
|
||||
│ document → file path info │
|
||||
│ All results → agent.write(text) │
|
||||
└──────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Agent receives plain text only │
|
||||
│ e.g. "[Voice Message]\nTranscript: ..." │
|
||||
│ e.g. "[Image]\nDescription: ..." │
|
||||
│ e.g. "[Video]\nDescription: ..." │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Media Processing Modules
|
||||
|
||||
| Type | Module | Method | API |
|
||||
|------|--------|--------|-----|
|
||||
| audio | `src/media/transcribe.ts` | `transcribeAudio()` | Local whisper/whisper-cli → OpenAI Whisper API (`whisper-1`) |
|
||||
| image | `src/media/describe-image.ts` | `describeImage()` | OpenAI Vision API (`gpt-4o-mini`) |
|
||||
| video | `src/media/describe-video.ts` | `describeVideo()` | ffmpeg frame extraction + Vision API |
|
||||
| document | (inline in manager) | — | File path info only |
|
||||
|
||||
### Agent Output Format
|
||||
|
||||
| Type | Success | No API Key |
|
||||
|------|---------|------------|
|
||||
| audio | `[Voice Message]\nTranscript: <text>` | `[audio message received]\nFile: <path>` |
|
||||
| image | `[Image]\nDescription: <text>` | `[image message received]\nFile: <path>` |
|
||||
| video | `[Video]\nDescription: <text>` | `[video message received]\nFile: <path>` |
|
||||
| document | `[document message received]\nFile: <path>` | same |
|
||||
|
||||
### Audio Transcription Priority
|
||||
|
||||
`transcribeAudio()` tries providers in order, matching OpenClaw's local-first approach:
|
||||
|
||||
1. **Local whisper/whisper-cli** — Free, no latency, works offline. Detected via `which` and cached.
|
||||
2. **OpenAI Whisper API** (`whisper-1`) — Requires API key in `credentials.json5`.
|
||||
3. **null** — No provider available. Placeholder stays in message, agent naturally responds (e.g. suggests installing whisper).
|
||||
|
||||
### Whisper Skill (Agent Fallback)
|
||||
|
||||
The `skills/whisper/SKILL.md` skill is a secondary safety net. If transcription returned null (no local binary, no API key), the agent receives a placeholder with the file path. If whisper is installed, the skill tells the agent how to transcribe it via the exec tool.
|
||||
|
||||
### File Map
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `src/channels/types.ts` | `ChannelMediaAttachment`, `ChannelMessage.media`, `ChannelPlugin.downloadMedia` |
|
||||
| `src/channels/plugins/telegram.ts` | Detect voice/audio/photo/video/document + download via Grammy API |
|
||||
| `src/channels/manager.ts` | `routeMedia()` — download, convert, `agent.write(text)` |
|
||||
| `src/media/transcribe.ts` | Audio → text (local whisper → OpenAI Whisper API) |
|
||||
| `src/media/describe-image.ts` | Image → text via OpenAI Vision API (gpt-4o-mini) |
|
||||
| `src/media/describe-video.ts` | Video → extract frame (ffmpeg) → text via Vision API |
|
||||
| `src/shared/paths.ts` | `MEDIA_CACHE_DIR` (`~/.super-multica/cache/media/`) |
|
||||
| `skills/whisper/SKILL.md` | Local whisper CLI fallback skill |
|
||||
|
||||
### Future Work
|
||||
|
||||
| Task | Scope |
|
||||
|------|-------|
|
||||
| Groq / Deepgram fallback for audio | `src/media/transcribe.ts` |
|
||||
| Multi-provider vision support (Gemini, Anthropic) | `src/media/describe-image.ts` |
|
||||
| Document text extraction (PDF, DOCX) | `src/media/` |
|
||||
| Media cache cleanup (delete old files) | `src/shared/` |
|
||||
| Outbound media (send images/audio back to channels) | `types.ts`, plugins |
|
||||
133
docs/cli.md
133
docs/cli.md
|
|
@ -1,30 +1,129 @@
|
|||
# CLI
|
||||
# CLI Guide (`multica`)
|
||||
|
||||
## Entry
|
||||
|
||||
```bash
|
||||
multica # Interactive mode
|
||||
multica run "prompt" # Single prompt
|
||||
multica chat --profile my-agent # Use profile
|
||||
multica --session abc123 # Continue session
|
||||
multica session list # List sessions
|
||||
multica profile list # List profiles
|
||||
multica skills list # List skills
|
||||
multica help # Show help
|
||||
pnpm multica
|
||||
```
|
||||
|
||||
Short alias: `mu`
|
||||
Equivalent command names:
|
||||
|
||||
- `multica`
|
||||
- `mu`
|
||||
|
||||
## Core Commands
|
||||
|
||||
```bash
|
||||
multica # interactive chat (default)
|
||||
multica run "<prompt>" # one-shot run
|
||||
multica chat # explicit interactive mode
|
||||
multica session <command> # session management
|
||||
multica profile <command> # profile management
|
||||
multica skills <command> # skill management
|
||||
multica tools <command> # tool policy inspection
|
||||
multica credentials <command> # credentials management
|
||||
multica cron <command> # scheduled tasks
|
||||
multica dev [service] # start dev services
|
||||
multica help
|
||||
```
|
||||
|
||||
## Run Mode
|
||||
|
||||
```bash
|
||||
multica run [options] <prompt>
|
||||
echo "prompt" | multica run
|
||||
```
|
||||
|
||||
Common options:
|
||||
|
||||
- `--profile <id>`
|
||||
- `--provider <name>`
|
||||
- `--model <name>`
|
||||
- `--session <id>`
|
||||
- `--cwd <dir>`
|
||||
- `--run-log`
|
||||
- `--tools-allow a,b,c`
|
||||
- `--tools-deny a,b,c`
|
||||
- `--context-window <tokens>`
|
||||
|
||||
## Chat Mode
|
||||
|
||||
```bash
|
||||
multica chat [options]
|
||||
multica [options]
|
||||
```
|
||||
|
||||
In-chat commands:
|
||||
|
||||
- `/help`
|
||||
- `/exit`
|
||||
- `/clear`
|
||||
- `/session`
|
||||
- `/new`
|
||||
- `/multiline`
|
||||
- `/provider`
|
||||
- `/model`
|
||||
|
||||
## Sessions
|
||||
|
||||
Sessions persist to `~/.super-multica/sessions/<id>/` with JSONL message history and JSON metadata. Context windows are automatically managed with token-aware compaction.
|
||||
```bash
|
||||
multica session list
|
||||
multica session show <id>
|
||||
multica session delete <id>
|
||||
```
|
||||
|
||||
Session data root:
|
||||
|
||||
- `~/.super-multica/sessions/`
|
||||
- or `SMC_DATA_DIR/sessions/`
|
||||
|
||||
## Profiles
|
||||
|
||||
Profiles define agent identity, personality, and memory in `~/.super-multica/agent-profiles/<id>/`.
|
||||
|
||||
```bash
|
||||
multica profile new my-agent # Create profile
|
||||
multica profile list # List all
|
||||
multica profile edit my-agent # Open in file manager
|
||||
multica profile list
|
||||
multica profile new <id>
|
||||
multica profile setup <id>
|
||||
multica profile show <id>
|
||||
multica profile edit <id>
|
||||
multica profile delete <id>
|
||||
```
|
||||
|
||||
Profile files: `soul.md`, `user.md`, `workspace.md`, `memory.md`, `memory/*.md`
|
||||
## Skills
|
||||
|
||||
```bash
|
||||
multica skills list
|
||||
multica skills status [id]
|
||||
multica skills install <id>
|
||||
multica skills add <owner/repo[/skill]>
|
||||
multica skills remove <name>
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
```bash
|
||||
multica tools list
|
||||
multica tools list --allow group:fs,web_fetch
|
||||
multica tools list --deny exec
|
||||
multica tools groups
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
```bash
|
||||
multica credentials init
|
||||
multica credentials show
|
||||
multica credentials edit
|
||||
```
|
||||
|
||||
## Cron
|
||||
|
||||
```bash
|
||||
multica cron status
|
||||
multica cron list
|
||||
multica cron add -n "name" --every "30m" --message "..."
|
||||
multica cron run <id>
|
||||
multica cron enable <id>
|
||||
multica cron disable <id>
|
||||
multica cron remove <id>
|
||||
multica cron logs <id>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,338 +0,0 @@
|
|||
# Client Streaming Protocol
|
||||
|
||||
How clients receive real-time agent events via WebSocket (Gateway mode) or IPC (Desktop mode), and what data structures to use for rendering.
|
||||
|
||||
## Transport Overview
|
||||
|
||||
```
|
||||
Gateway mode (Web App):
|
||||
Client ←──WebSocket──→ Gateway ←──→ Hub ←──→ Agent
|
||||
|
||||
Desktop mode (Electron):
|
||||
Renderer ←──IPC──→ Main Process (Hub + Agent)
|
||||
```
|
||||
|
||||
Both transports deliver the same logical events. The client receives a `StreamPayload` envelope containing an event, and routes it to the store for rendering.
|
||||
|
||||
## StreamPayload Envelope
|
||||
|
||||
Every real-time event arrives wrapped in a `StreamPayload`:
|
||||
|
||||
```ts
|
||||
interface StreamPayload {
|
||||
streamId: string; // groups events belonging to the same assistant turn
|
||||
agentId: string; // which agent produced this event
|
||||
event: AgentEvent | CompactionEvent;
|
||||
}
|
||||
```
|
||||
|
||||
In Gateway mode, these arrive as Socket.io messages with `action = "stream"`. In Desktop IPC mode, they arrive as `localChat:event` messages with the same structure.
|
||||
|
||||
## Event Types
|
||||
|
||||
### 1. Message Lifecycle Events (AgentEvent)
|
||||
|
||||
These events represent an LLM response being generated in real time.
|
||||
|
||||
#### `message_start`
|
||||
|
||||
A new assistant message has begun streaming.
|
||||
|
||||
```json
|
||||
{
|
||||
"streamId": "019abc12-...",
|
||||
"agentId": "019def34-...",
|
||||
"event": {
|
||||
"type": "message_start",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Client action:** Create a new empty assistant message bubble. Use `streamId` as the message ID for subsequent updates.
|
||||
|
||||
#### `message_update`
|
||||
|
||||
Partial content has arrived for the current message.
|
||||
|
||||
```json
|
||||
{
|
||||
"streamId": "019abc12-...",
|
||||
"agentId": "019def34-...",
|
||||
"event": {
|
||||
"type": "message_update",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{ "type": "text", "text": "Here is the partial response so far..." },
|
||||
{ "type": "thinking", "thinking": "Let me consider..." }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Client action:** Replace the message's `content` array with the new snapshot. Each update contains the full accumulated content, not a delta.
|
||||
|
||||
#### `message_end`
|
||||
|
||||
The assistant message is complete.
|
||||
|
||||
```json
|
||||
{
|
||||
"streamId": "019abc12-...",
|
||||
"agentId": "019def34-...",
|
||||
"event": {
|
||||
"type": "message_end",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{ "type": "text", "text": "Final complete response." }
|
||||
],
|
||||
"stopReason": "end_turn"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Client action:** Finalize the message. Mark streaming as complete. Extract `stopReason` if needed.
|
||||
|
||||
### 2. Tool Execution Events (AgentEvent)
|
||||
|
||||
These events track tool calls made by the assistant during a turn.
|
||||
|
||||
#### `tool_execution_start`
|
||||
|
||||
The agent has begun executing a tool.
|
||||
|
||||
```json
|
||||
{
|
||||
"streamId": "019abc12-...",
|
||||
"agentId": "019def34-...",
|
||||
"event": {
|
||||
"type": "tool_execution_start",
|
||||
"toolCallId": "toolu_01ABC...",
|
||||
"toolName": "Bash",
|
||||
"args": { "command": "ls -la" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Client action:** Create a tool result message with `toolStatus: "running"`. Display a spinner or loading indicator.
|
||||
|
||||
#### `tool_execution_end`
|
||||
|
||||
The tool has finished executing.
|
||||
|
||||
```json
|
||||
{
|
||||
"streamId": "019abc12-...",
|
||||
"agentId": "019def34-...",
|
||||
"event": {
|
||||
"type": "tool_execution_end",
|
||||
"toolCallId": "toolu_01ABC...",
|
||||
"result": "file1.txt\nfile2.txt\n",
|
||||
"isError": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Client action:** Update the matching tool result message. Set `toolStatus` to `"success"` or `"error"` based on `isError`. Render `result` as the tool output.
|
||||
|
||||
### 3. Compaction Events (CompactionEvent)
|
||||
|
||||
These events notify the client when context window compaction occurs. They use a synthetic `streamId` of `compaction:{agentId}` and do not belong to any message stream.
|
||||
|
||||
#### `compaction_start`
|
||||
|
||||
Context compaction has begun. The agent is removing old messages to free up context window space.
|
||||
|
||||
```json
|
||||
{
|
||||
"streamId": "compaction:019def34-...",
|
||||
"agentId": "019def34-...",
|
||||
"event": {
|
||||
"type": "compaction_start"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Client action:** Show a compaction indicator (e.g., "Compacting context...").
|
||||
|
||||
#### `compaction_end`
|
||||
|
||||
Compaction is complete. Includes statistics about what was removed.
|
||||
|
||||
```json
|
||||
{
|
||||
"streamId": "compaction:019def34-...",
|
||||
"agentId": "019def34-...",
|
||||
"event": {
|
||||
"type": "compaction_end",
|
||||
"removed": 24,
|
||||
"kept": 8,
|
||||
"tokensRemoved": 45000,
|
||||
"tokensKept": 12000,
|
||||
"reason": "tokens"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `removed` | `number` | Number of messages removed |
|
||||
| `kept` | `number` | Number of messages retained |
|
||||
| `tokensRemoved` | `number?` | Estimated tokens freed (absent in count mode) |
|
||||
| `tokensKept` | `number?` | Estimated tokens remaining (absent in count mode) |
|
||||
| `reason` | `string` | What triggered compaction: `"tokens"`, `"count"`, or `"summary"` |
|
||||
|
||||
**Client action:** Hide the compaction indicator. Optionally display a toast or inline notice with the stats.
|
||||
|
||||
## Content Block Types
|
||||
|
||||
Message content is an array of `ContentBlock`, which is a union of:
|
||||
|
||||
```ts
|
||||
// Plain text
|
||||
interface TextContent {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
|
||||
// LLM reasoning (extended thinking)
|
||||
interface ThinkingContent {
|
||||
type: "thinking";
|
||||
thinking: string;
|
||||
}
|
||||
|
||||
// Tool invocation (appears in assistant messages)
|
||||
interface ToolCall {
|
||||
type: "toolCall";
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Image content (appears in user messages)
|
||||
interface ImageContent {
|
||||
type: "image";
|
||||
source: { type: "base64"; media_type: string; data: string };
|
||||
}
|
||||
```
|
||||
|
||||
## Client-Side Store Structure
|
||||
|
||||
The recommended Zustand store shape for rendering:
|
||||
|
||||
```ts
|
||||
interface Message {
|
||||
id: string;
|
||||
role: "user" | "assistant" | "toolResult";
|
||||
content: ContentBlock[];
|
||||
agentId: string;
|
||||
stopReason?: string;
|
||||
// Tool result fields (role === "toolResult" only)
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
toolArgs?: Record<string, unknown>;
|
||||
toolStatus?: "running" | "success" | "error" | "interrupted";
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
interface CompactionStats {
|
||||
removed: number;
|
||||
kept: number;
|
||||
tokensRemoved?: number;
|
||||
tokensKept?: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
interface MessagesState {
|
||||
messages: Message[];
|
||||
streamingIds: Set<string>; // IDs of messages currently streaming
|
||||
compacting: boolean; // true while compaction is in progress
|
||||
lastCompaction: CompactionStats | null; // stats from most recent compaction
|
||||
}
|
||||
```
|
||||
|
||||
## Event Routing Pseudocode
|
||||
|
||||
```ts
|
||||
function handleStreamEvent(payload: StreamPayload) {
|
||||
const { streamId, agentId, event } = payload;
|
||||
|
||||
switch (event.type) {
|
||||
case "message_start":
|
||||
store.startStream(streamId, agentId);
|
||||
break;
|
||||
case "message_update":
|
||||
store.appendStream(streamId, event.message.content);
|
||||
break;
|
||||
case "message_end":
|
||||
store.endStream(streamId, event.message.content, event.message.stopReason);
|
||||
break;
|
||||
case "tool_execution_start":
|
||||
store.startToolExecution(agentId, event.toolCallId, event.toolName, event.args);
|
||||
break;
|
||||
case "tool_execution_end":
|
||||
store.endToolExecution(event.toolCallId, event.result, event.isError);
|
||||
break;
|
||||
case "compaction_start":
|
||||
store.startCompaction();
|
||||
break;
|
||||
case "compaction_end":
|
||||
store.endCompaction({
|
||||
removed: event.removed,
|
||||
kept: event.kept,
|
||||
tokensRemoved: event.tokensRemoved,
|
||||
tokensKept: event.tokensKept,
|
||||
reason: event.reason,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Message History via RPC
|
||||
|
||||
Clients can also fetch historical messages using the `getAgentMessages` RPC method. See [rpc.md](./rpc.md) for details.
|
||||
|
||||
The response returns `AgentMessage[]` which must be normalized into the `Message` format above. Key differences from streaming:
|
||||
|
||||
- Historical messages don't have `toolStatus` — infer it from `isError` (`"error"` or `"success"`).
|
||||
- Historical messages may have `content` as a plain `string` instead of `ContentBlock[]` — normalize by wrapping in `[{ type: "text", text: content }]`.
|
||||
- Tool arguments are not stored on `toolResult` messages — build a lookup map from assistant `ToolCall` blocks by `toolCallId` to reconstruct `toolArgs`.
|
||||
|
||||
## SDK Imports
|
||||
|
||||
All types are available from `@multica/sdk`:
|
||||
|
||||
```ts
|
||||
import {
|
||||
StreamAction,
|
||||
type StreamPayload,
|
||||
type AgentEvent,
|
||||
type CompactionEvent,
|
||||
type CompactionStartEvent,
|
||||
type CompactionEndEvent,
|
||||
type ContentBlock,
|
||||
type TextContent,
|
||||
type ThinkingContent,
|
||||
type ToolCall,
|
||||
type ImageContent,
|
||||
} from "@multica/sdk";
|
||||
```
|
||||
|
||||
Store types are available from `@multica/store`:
|
||||
|
||||
```ts
|
||||
import {
|
||||
useMessagesStore,
|
||||
type Message,
|
||||
type CompactionStats,
|
||||
type ToolStatus,
|
||||
} from "@multica/store";
|
||||
```
|
||||
|
|
@ -1,347 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Super Multica 代码贡献统计</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b0d10;
|
||||
--panel: #14181d;
|
||||
--panel-2: #1a2027;
|
||||
--line: #2a3440;
|
||||
--text: #e8edf3;
|
||||
--muted: #98a7b7;
|
||||
--ok: #2fbf71;
|
||||
--danger: #ef4444;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial;
|
||||
background: radial-gradient(circle at 20% -10%, #1a2430 0%, #0b0d10 45%) fixed;
|
||||
color: var(--text);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.wrap { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
||||
h1 { margin: 0 0 8px; font-size: 28px; }
|
||||
.sub { color: var(--muted); margin-bottom: 20px; }
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.card {
|
||||
background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
.k { color: var(--muted); font-size: 12px; margin-bottom: 8px; }
|
||||
.v { font-size: 24px; font-weight: 700; letter-spacing: 0.3px; }
|
||||
.section { margin-top: 14px; }
|
||||
.section h2 { margin: 0 0 10px; font-size: 16px; color: #d4dde7; }
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 9px 10px; border-bottom: 1px solid var(--line); font-size: 13px; }
|
||||
th { background: #11161c; text-align: left; color: #c5d0db; position: sticky; top: 0; }
|
||||
tr:last-child td { border-bottom: 0; }
|
||||
.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.bar-wrap { background: #0f1318; border-radius: 999px; height: 8px; width: 180px; border: 1px solid #273241; }
|
||||
.bar { height: 100%; border-radius: 999px; background: linear-gradient(90deg, #3f7ef7, #58a6ff); }
|
||||
.ok { color: var(--ok); }
|
||||
.danger { color: var(--danger); }
|
||||
.foot { margin-top: 16px; color: var(--muted); font-size: 12px; }
|
||||
.scroll { max-height: 420px; overflow: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>Super Multica 代码贡献统计</h1>
|
||||
<div class="sub" id="subtitle"></div>
|
||||
|
||||
<div class="grid" id="summary"></div>
|
||||
|
||||
<div class="section">
|
||||
<h2>代码量分布(按扩展名)</h2>
|
||||
<div class="panel scroll"><table id="extTable"></table></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>人员贡献(人工口径)</h2>
|
||||
<div class="panel scroll"><table id="authorTable"></table></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>每日贡献(人工口径)</h2>
|
||||
<div class="panel scroll"><table id="dayTable"></table></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>小时段贡献(人工口径)</h2>
|
||||
<div class="panel scroll"><table id="hourTable"></table></div>
|
||||
</div>
|
||||
|
||||
<div class="foot">数据来源:git log --numstat 与当前工作树文件统计。人工口径排除 checkpointer / dependabot。</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const RAW = {
|
||||
locTotals: String.raw`
|
||||
files 669
|
||||
lines 137510
|
||||
source_files 500
|
||||
source_lines 75894
|
||||
doc_files 47
|
||||
doc_lines 11701
|
||||
config_files 82
|
||||
config_lines 42562
|
||||
`,
|
||||
locByExt: String.raw`
|
||||
mjs 3 25
|
||||
js 8 357
|
||||
production 1 11
|
||||
dockerignore 1 45
|
||||
xml 5 15
|
||||
gitignore 4 126
|
||||
tsx 109 12206
|
||||
ts 337 55198
|
||||
json 38 1025
|
||||
yaml 2 21654
|
||||
yml 2 66
|
||||
css 4 412
|
||||
sh 5 259
|
||||
icns 1 1992
|
||||
example 2 115
|
||||
json5 1 87
|
||||
svg 1 75
|
||||
sql 1 17
|
||||
html 5 2363
|
||||
md 47 11701
|
||||
development 1 10
|
||||
npmrc 1 1
|
||||
xsd 39 19730
|
||||
ico 2 315
|
||||
[noext] 1 44
|
||||
cjs 1 19
|
||||
py 28 5055
|
||||
png 18 4154
|
||||
drawio 1 433
|
||||
`,
|
||||
authorHuman: String.raw`
|
||||
Jiayuan Zhang <forrestchang7@gmail.com> 233 110606 55597 55009 49.11% 27.67%
|
||||
Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> 253 61422 24246 37176 27.27% 30.05%
|
||||
Jiang Bohan <bhjiang@outlook.com> 203 30764 4944 25820 13.66% 24.11%
|
||||
yushen <ldnvnbl@gmail.com> 128 21171 2185 18986 9.40% 15.20%
|
||||
yushen <linyushen@proton.me> 25 1279 341 938 0.57% 2.97%
|
||||
`,
|
||||
dayHuman: String.raw`
|
||||
2026-01-28 4 3403 13 3390 14 17
|
||||
2026-01-29 2 9990 676 9314 16 16
|
||||
2026-01-30 173 39220 4907 34313 01 23
|
||||
2026-01-31 28 3353 1117 2236 01 21
|
||||
2026-02-01 17 4093 539 3554 02 23
|
||||
2026-02-02 42 3993 2331 1662 00 17
|
||||
2026-02-03 52 22784 6410 16374 02 21
|
||||
2026-02-04 64 8893 1733 7160 03 20
|
||||
2026-02-05 78 12776 5203 7573 02 22
|
||||
2026-02-06 42 6074 521 5553 12 22
|
||||
2026-02-09 44 4458 941 3517 07 19
|
||||
2026-02-10 50 9474 4410 5064 02 23
|
||||
2026-02-11 58 7069 3222 3847 00 20
|
||||
2026-02-12 60 79852 4003 75849 09 23
|
||||
2026-02-13 66 6194 1427 4767 01 23
|
||||
2026-02-14 30 1479 1049 430 00 23
|
||||
2026-02-15 32 2137 48811 -46674 00 04
|
||||
`,
|
||||
hourHuman: String.raw`
|
||||
00 18 1236 248 988
|
||||
01 32 5718 45407 -39689
|
||||
02 34 4449 2948 1501
|
||||
03 27 3861 3587 274
|
||||
04 24 4591 1047 3544
|
||||
05 7 2752 69 2683
|
||||
06 0 0 0 0
|
||||
07 7 809 54 755
|
||||
08 17 3605 2099 1506
|
||||
09 19 5612 1801 3811
|
||||
10 17 2907 1784 1123
|
||||
11 22 1673 530 1143
|
||||
12 12 1974 72 1902
|
||||
13 49 8470 1911 6559
|
||||
14 67 8581 2303 6278
|
||||
15 64 10866 2467 8399
|
||||
16 84 35691 6756 28935
|
||||
17 111 97435 7908 89527
|
||||
18 73 8936 2276 6660
|
||||
19 36 2716 723 1993
|
||||
20 22 3916 1223 2693
|
||||
21 24 2296 497 1799
|
||||
22 48 3545 1016 2529
|
||||
23 28 3603 587 3016
|
||||
`,
|
||||
dayPeak: String.raw`
|
||||
2026-01-28 16 1 2237 5
|
||||
2026-01-29 16 2 9990 676
|
||||
2026-01-30 13 16 4675 55
|
||||
2026-01-31 02 9 1277 144
|
||||
2026-02-01 23 7 3036 270
|
||||
2026-02-02 16 13 1218 280
|
||||
2026-02-03 16 7 14253 4890
|
||||
2026-02-04 17 10 2766 143
|
||||
2026-02-05 17 17 4345 1657
|
||||
2026-02-06 17 8 2300 120
|
||||
2026-02-09 19 3 1095 335
|
||||
2026-02-10 17 5 5778 3625
|
||||
2026-02-11 17 17 2520 1018
|
||||
2026-02-12 17 13 73452 132
|
||||
2026-02-13 12 7 1378 56
|
||||
2026-02-14 00 8 601 212
|
||||
2026-02-15 04 4 657 364
|
||||
`
|
||||
};
|
||||
|
||||
const fmt = (n) => Number(n).toLocaleString("en-US");
|
||||
const tsv = (txt) => txt.trim().split(/\n+/).map((line) => line.split("\t"));
|
||||
const toNum = (v) => Number(v || 0);
|
||||
|
||||
const locTotalsRows = tsv(RAW.locTotals);
|
||||
const locTotals = Object.fromEntries(locTotalsRows.map(([k, v]) => [k, toNum(v)]));
|
||||
|
||||
const extRows = tsv(RAW.locByExt).map(([ext, files, lines]) => ({
|
||||
ext,
|
||||
files: toNum(files),
|
||||
lines: toNum(lines),
|
||||
})).sort((a, b) => b.lines - a.lines);
|
||||
|
||||
const authors = tsv(RAW.authorHuman).map(([name, commits, add, del, net, addPct, commitPct]) => ({
|
||||
name,
|
||||
commits: toNum(commits),
|
||||
add: toNum(add),
|
||||
del: toNum(del),
|
||||
net: toNum(net),
|
||||
addPct,
|
||||
commitPct,
|
||||
})).sort((a, b) => b.add - a.add);
|
||||
|
||||
const dayPeaks = Object.fromEntries(tsv(RAW.dayPeak).map(([d, h, c, a, del]) => [d, {
|
||||
hour: h,
|
||||
commits: toNum(c),
|
||||
add: toNum(a),
|
||||
del: toNum(del),
|
||||
}]));
|
||||
|
||||
const days = tsv(RAW.dayHuman).map(([date, commits, add, del, net, startHour, endHour]) => ({
|
||||
date,
|
||||
commits: toNum(commits),
|
||||
add: toNum(add),
|
||||
del: toNum(del),
|
||||
net: toNum(net),
|
||||
startHour,
|
||||
endHour,
|
||||
peak: dayPeaks[date] || null,
|
||||
})).sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
const hours = tsv(RAW.hourHuman).map(([hour, commits, add, del, net]) => ({
|
||||
hour,
|
||||
commits: toNum(commits),
|
||||
add: toNum(add),
|
||||
del: toNum(del),
|
||||
net: toNum(net),
|
||||
})).sort((a, b) => a.hour.localeCompare(b.hour));
|
||||
|
||||
const totalHumanCommits = authors.reduce((sum, x) => sum + x.commits, 0);
|
||||
const totalHumanAdd = authors.reduce((sum, x) => sum + x.add, 0);
|
||||
const totalHumanDel = authors.reduce((sum, x) => sum + x.del, 0);
|
||||
const topHour = [...hours].sort((a, b) => b.add - a.add)[0] || { hour: "--", add: 0 };
|
||||
const startDate = days[0]?.date || "--";
|
||||
const endDate = days[days.length - 1]?.date || "--";
|
||||
|
||||
document.getElementById("subtitle").textContent = `${startDate} ~ ${endDate}`;
|
||||
|
||||
const summaryItems = [
|
||||
["总文件数", fmt(locTotals.files || 0)],
|
||||
["总行数", fmt(locTotals.lines || 0)],
|
||||
["源码行数", fmt(locTotals.source_lines || 0)],
|
||||
["贡献人数", fmt(authors.length)],
|
||||
["人工提交数", fmt(totalHumanCommits)],
|
||||
["人工新增", fmt(totalHumanAdd)],
|
||||
["人工删除", fmt(totalHumanDel)],
|
||||
["最高产小时", `${topHour.hour}:00 (${fmt(topHour.add)})`],
|
||||
];
|
||||
|
||||
document.getElementById("summary").innerHTML = summaryItems.map(([k, v]) => (
|
||||
`<div class="card"><div class="k">${k}</div><div class="v">${v}</div></div>`
|
||||
)).join("");
|
||||
|
||||
const maxExtLines = Math.max(...extRows.map((x) => x.lines), 1);
|
||||
document.getElementById("extTable").innerHTML = `
|
||||
<thead><tr><th>扩展名</th><th class="num">文件数</th><th class="num">行数</th><th>占比</th><th>可视化</th></tr></thead>
|
||||
<tbody>
|
||||
${extRows.map((r) => {
|
||||
const pct = ((r.lines / (locTotals.lines || 1)) * 100).toFixed(2);
|
||||
const w = ((r.lines / maxExtLines) * 100).toFixed(1);
|
||||
return `<tr>
|
||||
<td class="mono">${r.ext}</td>
|
||||
<td class="num">${fmt(r.files)}</td>
|
||||
<td class="num">${fmt(r.lines)}</td>
|
||||
<td class="num">${pct}%</td>
|
||||
<td><div class="bar-wrap"><div class="bar" style="width:${w}%"></div></div></td>
|
||||
</tr>`;
|
||||
}).join("")}
|
||||
</tbody>`;
|
||||
|
||||
document.getElementById("authorTable").innerHTML = `
|
||||
<thead><tr><th>作者</th><th class="num">提交</th><th class="num">新增</th><th class="num">删除</th><th class="num">净新增</th><th class="num">新增占比</th><th class="num">提交占比</th></tr></thead>
|
||||
<tbody>
|
||||
${authors.map((a) => `<tr>
|
||||
<td>${a.name}</td>
|
||||
<td class="num">${fmt(a.commits)}</td>
|
||||
<td class="num">${fmt(a.add)}</td>
|
||||
<td class="num">${fmt(a.del)}</td>
|
||||
<td class="num ${a.net >= 0 ? "ok" : "danger"}">${fmt(a.net)}</td>
|
||||
<td class="num">${a.addPct}</td>
|
||||
<td class="num">${a.commitPct}</td>
|
||||
</tr>`).join("")}
|
||||
</tbody>`;
|
||||
|
||||
document.getElementById("dayTable").innerHTML = `
|
||||
<thead><tr><th>日期</th><th class="num">提交</th><th class="num">新增</th><th class="num">删除</th><th class="num">净新增</th><th>活跃时段</th><th>峰值小时</th></tr></thead>
|
||||
<tbody>
|
||||
${days.map((d) => `<tr>
|
||||
<td class="mono">${d.date}</td>
|
||||
<td class="num">${fmt(d.commits)}</td>
|
||||
<td class="num">${fmt(d.add)}</td>
|
||||
<td class="num">${fmt(d.del)}</td>
|
||||
<td class="num ${d.net >= 0 ? "ok" : "danger"}">${fmt(d.net)}</td>
|
||||
<td class="mono">${d.startHour}:00 - ${d.endHour}:59</td>
|
||||
<td class="mono">${d.peak ? `${d.peak.hour}:00 (${fmt(d.peak.add)})` : "--"}</td>
|
||||
</tr>`).join("")}
|
||||
</tbody>`;
|
||||
|
||||
const maxHourAdd = Math.max(...hours.map((h) => h.add), 1);
|
||||
document.getElementById("hourTable").innerHTML = `
|
||||
<thead><tr><th>小时</th><th class="num">提交</th><th class="num">新增</th><th class="num">删除</th><th class="num">净新增</th><th>可视化</th></tr></thead>
|
||||
<tbody>
|
||||
${hours.map((h) => {
|
||||
const w = ((h.add / maxHourAdd) * 100).toFixed(1);
|
||||
return `<tr>
|
||||
<td class="mono">${h.hour}:00</td>
|
||||
<td class="num">${fmt(h.commits)}</td>
|
||||
<td class="num">${fmt(h.add)}</td>
|
||||
<td class="num">${fmt(h.del)}</td>
|
||||
<td class="num ${h.net >= 0 ? "ok" : "danger"}">${fmt(h.net)}</td>
|
||||
<td><div class="bar-wrap"><div class="bar" style="width:${w}%"></div></div></td>
|
||||
</tr>`;
|
||||
}).join("")}
|
||||
</tbody>`;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,64 +1,76 @@
|
|||
# Credentials & LLM Providers
|
||||
# Credentials Guide
|
||||
|
||||
## Setup
|
||||
## Initialize
|
||||
|
||||
```bash
|
||||
multica credentials init
|
||||
pnpm multica credentials init
|
||||
```
|
||||
|
||||
Creates:
|
||||
- `~/.super-multica/credentials.json5` — LLM providers + tools
|
||||
This creates:
|
||||
|
||||
Example `credentials.json5`:
|
||||
- `~/.super-multica/credentials.json5`
|
||||
|
||||
## Path Resolution
|
||||
|
||||
Credential file lookup order:
|
||||
|
||||
1. `SMC_CREDENTIALS_PATH` (explicit override)
|
||||
2. `SMC_DATA_DIR/credentials.json5` (or default data dir)
|
||||
3. `~/.super-multica/credentials.json5` fallback
|
||||
|
||||
## Minimal Template
|
||||
|
||||
```json5
|
||||
{
|
||||
version: 1,
|
||||
llm: {
|
||||
provider: "openai",
|
||||
provider: "kimi-coding",
|
||||
providers: {
|
||||
openai: { apiKey: "sk-xxx", model: "gpt-4o" }
|
||||
}
|
||||
"kimi-coding": {
|
||||
apiKey: "your-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
brave: { apiKey: "brv-..." }
|
||||
}
|
||||
// tool-specific keys
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Skill API Keys
|
||||
## Multi-Key Rotation (Per Provider)
|
||||
|
||||
Skill-specific API keys are stored in `.env` files within each skill's directory:
|
||||
You can define multiple keys under one provider namespace:
|
||||
|
||||
```
|
||||
~/.super-multica/skills/<skill-id>/.env
|
||||
```json5
|
||||
{
|
||||
llm: {
|
||||
providers: {
|
||||
"anthropic": { apiKey: "primary" },
|
||||
"anthropic:backup": { apiKey: "backup" },
|
||||
},
|
||||
order: {
|
||||
anthropic: ["anthropic", "anthropic:backup"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Example for the `earnings-analysis` skill:
|
||||
## OAuth Providers
|
||||
|
||||
```bash
|
||||
# ~/.super-multica/skills/earnings-analysis/.env
|
||||
FINANCIAL_DATASETS_API_KEY=your-key-here
|
||||
```
|
||||
- `claude-code`: run `claude login`
|
||||
- `openai-codex`: run `codex login`
|
||||
|
||||
Skills declare their required environment variables in `SKILL.md` frontmatter:
|
||||
API-key providers are configured directly in `credentials.json5`.
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
requires:
|
||||
env:
|
||||
- FINANCIAL_DATASETS_API_KEY
|
||||
```
|
||||
## Tool Credentials
|
||||
|
||||
The `.env` file is preserved across skill upgrades and is never committed to version control.
|
||||
Tool credentials are read from:
|
||||
|
||||
## LLM Providers
|
||||
- `credentials.json5` under `tools`
|
||||
- skill-level `.env` files under skill directories
|
||||
|
||||
**OAuth Providers** (external CLI login):
|
||||
- `claude-code` — requires `claude login`
|
||||
- `openai-codex` — requires `codex login`
|
||||
## Security
|
||||
|
||||
**API Key Providers** (configure in `credentials.json5`):
|
||||
- `anthropic`, `openai`, `kimi-coding`, `google`, `groq`, `mistral`, `xai`, `openrouter`
|
||||
|
||||
Check status: `/provider` in interactive mode
|
||||
- Keep credentials file mode private (`600` on Unix-like systems).
|
||||
- Do not commit keys into the repository.
|
||||
- Prefer isolated data dirs (`SMC_DATA_DIR`) for test/dev environments.
|
||||
|
|
|
|||
|
|
@ -1,82 +1,109 @@
|
|||
# Development Guide
|
||||
|
||||
## Dev Commands
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- pnpm 10+
|
||||
- macOS/Linux/Windows
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pnpm dev # Desktop app (recommended)
|
||||
pnpm dev:desktop # Same as above
|
||||
pnpm dev:gateway # Gateway only
|
||||
pnpm dev:web # Web app only
|
||||
pnpm dev:all # Gateway + Web
|
||||
|
||||
pnpm build # Production build (turbo-orchestrated)
|
||||
pnpm typecheck # Type check all packages
|
||||
pnpm test # Run tests
|
||||
pnpm test:watch # Watch mode
|
||||
pnpm test:coverage # With v8 coverage
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Local Full-Stack Development
|
||||
`.npmrc` must keep:
|
||||
|
||||
`pnpm dev:local` starts Gateway + Desktop + Web together with isolated data directories.
|
||||
```ini
|
||||
shamefully-hoist=true
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
## Main Dev Entry Points
|
||||
|
||||
1. Copy `.env.example` to `.env` at the repo root
|
||||
2. Fill in `TELEGRAM_BOT_TOKEN` (get from [@BotFather](https://t.me/BotFather))
|
||||
```bash
|
||||
# Recommended local desktop workflow
|
||||
pnpm dev
|
||||
|
||||
# Service-specific
|
||||
pnpm dev:desktop
|
||||
pnpm dev:gateway
|
||||
pnpm dev:web
|
||||
|
||||
# Full local stack with isolated dev data
|
||||
pnpm dev:local
|
||||
pnpm dev:local:archive
|
||||
```
|
||||
|
||||
## What Each Command Does
|
||||
|
||||
- `pnpm dev`: builds shared packages, then runs `types + utils + core + desktop` watch flow.
|
||||
- `pnpm dev:desktop`: Electron desktop only.
|
||||
- `pnpm dev:gateway`: NestJS WebSocket gateway (`PORT`, default `3000`).
|
||||
- `pnpm dev:web`: Next.js web app (`3000` by script).
|
||||
- `pnpm dev:local`: gateway + web + desktop with dev-safe env defaults.
|
||||
- `pnpm dev:local:archive`: archive dev data and start fresh.
|
||||
|
||||
## Important Environment Variables
|
||||
|
||||
- `SMC_DATA_DIR`: override runtime data root (default `~/.super-multica`)
|
||||
- `GATEWAY_URL`: gateway endpoint for desktop/CLI hub connection
|
||||
- `MULTICA_API_URL`: required by web/data tools
|
||||
- `PORT`: gateway/server port
|
||||
- `MULTICA_WORKSPACE_DIR`: override workspace root
|
||||
- `MULTICA_RUN_LOG=1`: enable structured run-log output
|
||||
|
||||
## Local Full-Stack Notes (`pnpm dev:local`)
|
||||
|
||||
`pnpm dev:local` is the recommended way to run the full local stack for integration work.
|
||||
|
||||
Setup:
|
||||
|
||||
1. `cp .env.example .env`
|
||||
2. Set `TELEGRAM_BOT_TOKEN` in root `.env`
|
||||
3. Run `pnpm dev:local`
|
||||
|
||||
Services started by the script:
|
||||
|
||||
| Service | Address | Notes |
|
||||
|---------|---------|-------|
|
||||
| Gateway | `http://localhost:4000` | Telegram long-polling mode |
|
||||
| Web | `http://localhost:3000` | OAuth login flow |
|
||||
| Desktop | — | Connects to local Gateway + Web |
|
||||
| Gateway | `http://localhost:4000` | Telegram long-polling mode (`PORT=4000`) |
|
||||
| Web | `http://localhost:3000` | OAuth login / frontend |
|
||||
| Desktop | — | Uses `GATEWAY_URL=http://localhost:4000` and local web URL |
|
||||
|
||||
Data is stored in `~/.super-multica-dev` and `~/Documents/Multica-dev`, isolated from production.
|
||||
Data/workspace isolation used by the script:
|
||||
|
||||
- `SMC_DATA_DIR=~/.super-multica-dev`
|
||||
- `MULTICA_WORKSPACE_DIR=~/Documents/Multica-dev`
|
||||
|
||||
Why this matters:
|
||||
|
||||
- avoids polluting production data under `~/.super-multica`
|
||||
- provides a stable local target for auth/session debugging
|
||||
|
||||
Common follow-up:
|
||||
|
||||
```bash
|
||||
pnpm dev:local:archive # Archive dev data and start fresh
|
||||
pnpm dev:local:archive
|
||||
```
|
||||
|
||||
## Environment Configuration
|
||||
This archives prior dev data before starting fresh local runs.
|
||||
|
||||
**Desktop** (`apps/desktop/.env.*`):
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MAIN_VITE_GATEWAY_URL` | WebSocket Gateway URL for remote device pairing |
|
||||
| `MAIN_VITE_WEB_URL` | Web app URL for OAuth login redirect |
|
||||
|
||||
**Web** (`apps/web/next.config.ts`):
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MULTICA_API_URL` | Backend API URL (required, no default) |
|
||||
|
||||
**Build for different environments:**
|
||||
## Build / Quality
|
||||
|
||||
```bash
|
||||
# Desktop
|
||||
pnpm --filter @multica/desktop build # Production (.env.production)
|
||||
pnpm --filter @multica/desktop build:staging # Staging (.env.staging)
|
||||
|
||||
# Web (Vercel)
|
||||
# Set MULTICA_API_URL in Vercel Dashboard → Settings → Environment Variables
|
||||
pnpm build
|
||||
pnpm typecheck
|
||||
pnpm test
|
||||
pnpm test:coverage
|
||||
```
|
||||
|
||||
See `apps/desktop/.env.example` for the full variable reference.
|
||||
## Useful Reset Commands
|
||||
|
||||
## Monorepo Workflow
|
||||
```bash
|
||||
# Reset default + dev data dirs used by desktop scripts
|
||||
pnpm dev:desktop:reset
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `pnpm dev` | Full dev mode — watches `core`, `types`, `utils` packages |
|
||||
| `pnpm dev:desktop` | Desktop only — skip package watching |
|
||||
|
||||
**When modifying packages:**
|
||||
|
||||
1. Edit code in `packages/core`, `packages/types`, or `packages/utils`
|
||||
2. Terminal shows `[core] ESM ⚡️ Build success` (~100ms)
|
||||
3. Restart Desktop to apply changes (Ctrl+C, then `pnpm dev`)
|
||||
|
||||
> **Why restart?** Electron main process does not support hot reload — this is an Electron limitation, not ours.
|
||||
# Reset and relaunch desktop onboarding flow
|
||||
pnpm dev:desktop:fresh
|
||||
pnpm dev:desktop:onboarding
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,235 +0,0 @@
|
|||
# Exec Approval Protocol
|
||||
|
||||
Human-in-the-loop command execution approval for the `exec` tool. When an agent attempts to run a shell command that doesn't pass safety checks, the Hub requests approval from the connected client before proceeding.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
Agent (exec tool) Hub Gateway Client (UI)
|
||||
| | | |
|
||||
|-- onApprovalNeeded -->| | |
|
||||
| |-- evaluateCommandSafety() |
|
||||
| |-- requiresApproval()? |
|
||||
| | | |
|
||||
| |== exec-approval-request =============> |
|
||||
| | | |-- show UI
|
||||
| | | |-- user decides
|
||||
| | <== resolveExecApproval RPC ==========|
|
||||
| | | |
|
||||
| <-- approved/denied -| | |
|
||||
| | | |
|
||||
```
|
||||
|
||||
1. The **Agent** calls the `exec` tool with a shell command.
|
||||
2. The `exec` tool invokes the `onApprovalNeeded` callback (injected by the Hub).
|
||||
3. The **Hub** evaluates the command through a 4-layer safety engine.
|
||||
4. If approval is needed, the Hub sends an `exec-approval-request` message to the Client via the Gateway.
|
||||
5. The **Client** displays the approval UI and the user makes a decision.
|
||||
6. The Client calls the `resolveExecApproval` RPC with the decision.
|
||||
7. The Hub resolves the pending promise and the command is either executed or denied.
|
||||
|
||||
## Safety Evaluation
|
||||
|
||||
Before requesting approval, the Hub evaluates the command through 4 layers:
|
||||
|
||||
| Layer | Description | Example |
|
||||
|-------|-------------|---------|
|
||||
| **Allowlist** | Glob patterns of pre-approved commands | `git **`, `pnpm **` |
|
||||
| **Shell syntax** | Detects dangerous shell constructs | `\|&`, `` ` ` ``, `$()`, `;` |
|
||||
| **Safe binaries** | ~40 known-safe commands (no file-path args) | `ls`, `cat`, `git status` |
|
||||
| **Dangerous patterns** | 25+ regex patterns for risky commands | `rm -rf`, `sudo`, `curl \| sh` |
|
||||
|
||||
The result is a risk level: `"safe"`, `"needs-review"`, or `"dangerous"`.
|
||||
|
||||
### Configuration
|
||||
|
||||
Stored in profile config (`~/.super-multica/agent-profiles/{profileId}/config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"execApproval": {
|
||||
"security": "allowlist",
|
||||
"ask": "on-miss",
|
||||
"timeoutMs": 60000,
|
||||
"askFallback": "deny",
|
||||
"allowlist": [
|
||||
{ "pattern": "git **" },
|
||||
{ "pattern": "pnpm **" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Values | Default | Description |
|
||||
|-------|--------|---------|-------------|
|
||||
| `security` | `"deny"` \| `"allowlist"` \| `"full"` | `"allowlist"` | `deny` blocks all exec, `full` allows all, `allowlist` requires matching |
|
||||
| `ask` | `"off"` \| `"on-miss"` \| `"always"` | `"on-miss"` | `off` never asks, `on-miss` asks when allowlist misses, `always` always asks |
|
||||
| `timeoutMs` | number (ms) | `60000` | Time before auto-deny |
|
||||
| `askFallback` | `"deny"` \| `"allowlist"` \| `"full"` | `"deny"` | What happens on timeout |
|
||||
| `allowlist` | array of entries | `[]` | Pre-approved command patterns |
|
||||
|
||||
## WebSocket Protocol
|
||||
|
||||
### Step 1: Approval Request (Hub → Client)
|
||||
|
||||
When a command requires approval, the Hub sends a push message with action `exec-approval-request`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "019444a0-0000-7000-8000-000000000001",
|
||||
"from": "<hubDeviceId>",
|
||||
"to": "<clientDeviceId>",
|
||||
"action": "exec-approval-request",
|
||||
"payload": {
|
||||
"approvalId": "019444a0-1234-7abc-8000-abcdef123456",
|
||||
"agentId": "019444a0-5678-7def-8000-123456abcdef",
|
||||
"command": "rm -rf /tmp/test-data",
|
||||
"cwd": "/Users/alice/projects/my-app",
|
||||
"riskLevel": "dangerous",
|
||||
"riskReasons": [
|
||||
"Matches dangerous pattern: rm with -r or -f flags",
|
||||
"Uses recursive/force deletion flags"
|
||||
],
|
||||
"expiresAtMs": 1738700060000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Payload Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `approvalId` | `string` | Unique ID for this approval request (UUIDv7). Must be included in the response. |
|
||||
| `agentId` | `string` | Session ID of the agent that initiated the command. |
|
||||
| `command` | `string` | The shell command to be executed. |
|
||||
| `cwd` | `string?` | Working directory for the command. Optional. |
|
||||
| `riskLevel` | `"safe" \| "needs-review" \| "dangerous"` | Evaluated risk level. |
|
||||
| `riskReasons` | `string[]` | Human-readable reasons for the risk assessment. |
|
||||
| `expiresAtMs` | `number` | Unix timestamp (ms) when this request expires. After this time, the Hub auto-resolves based on `askFallback`. |
|
||||
|
||||
### Step 2: User Decision (Client → Hub)
|
||||
|
||||
The client sends a standard RPC request with method `resolveExecApproval`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "019444a0-0000-7000-8000-000000000002",
|
||||
"from": "<clientDeviceId>",
|
||||
"to": "<hubDeviceId>",
|
||||
"action": "request",
|
||||
"payload": {
|
||||
"requestId": "client-req-001",
|
||||
"method": "resolveExecApproval",
|
||||
"params": {
|
||||
"approvalId": "019444a0-1234-7abc-8000-abcdef123456",
|
||||
"decision": "allow-once"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Decision Values
|
||||
|
||||
| Decision | Effect |
|
||||
|----------|--------|
|
||||
| `"allow-once"` | Allow this command to execute. No persistent change. |
|
||||
| `"allow-always"` | Allow and add the command's binary to the profile allowlist (e.g., `rm **`). Future commands from the same binary will auto-approve. |
|
||||
| `"deny"` | Block the command. The agent receives a denial message. |
|
||||
|
||||
### Step 3: RPC Response (Hub → Client)
|
||||
|
||||
**Success** — the approval was found and resolved:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "019444a0-0000-7000-8000-000000000003",
|
||||
"from": "<hubDeviceId>",
|
||||
"to": "<clientDeviceId>",
|
||||
"action": "response",
|
||||
"payload": {
|
||||
"requestId": "client-req-001",
|
||||
"ok": true,
|
||||
"payload": {
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error** — the approval was not found (already resolved or expired):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "019444a0-0000-7000-8000-000000000004",
|
||||
"from": "<hubDeviceId>",
|
||||
"to": "<clientDeviceId>",
|
||||
"action": "response",
|
||||
"payload": {
|
||||
"requestId": "client-req-001",
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Approval request not found or already resolved"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Timeout Behavior
|
||||
|
||||
If the client does not respond within `timeoutMs` (default: 60 seconds), the Hub resolves the approval automatically based on the `askFallback` configuration:
|
||||
|
||||
| `askFallback` | Behavior on timeout |
|
||||
|---------------|---------------------|
|
||||
| `"deny"` (default) | Command is denied (fail-closed). |
|
||||
| `"full"` | Command is allowed. |
|
||||
| `"allowlist"` | Command is allowed only if it matched the allowlist; otherwise denied. |
|
||||
|
||||
## SDK Types
|
||||
|
||||
All protocol types are exported from `@multica/sdk`:
|
||||
|
||||
```ts
|
||||
import {
|
||||
ExecApprovalRequestAction, // "exec-approval-request"
|
||||
type ApprovalDecision, // "allow-once" | "allow-always" | "deny"
|
||||
type ExecApprovalRequestPayload,
|
||||
type ResolveExecApprovalParams,
|
||||
type ResolveExecApprovalResult,
|
||||
} from "@multica/sdk";
|
||||
```
|
||||
|
||||
## Client Implementation Guide
|
||||
|
||||
A minimal client handling exec approvals:
|
||||
|
||||
```ts
|
||||
import { GatewayClient, ExecApprovalRequestAction } from "@multica/sdk";
|
||||
import type { ExecApprovalRequestPayload, ApprovalDecision } from "@multica/sdk";
|
||||
|
||||
// Listen for approval requests
|
||||
client.onMessage((msg) => {
|
||||
if (msg.action === ExecApprovalRequestAction) {
|
||||
const payload = msg.payload as ExecApprovalRequestPayload;
|
||||
showApprovalUI(payload);
|
||||
}
|
||||
});
|
||||
|
||||
// When user makes a decision
|
||||
async function respondToApproval(approvalId: string, decision: ApprovalDecision) {
|
||||
const result = await client.request(hubDeviceId, "resolveExecApproval", {
|
||||
approvalId,
|
||||
decision,
|
||||
});
|
||||
// result.ok === true if resolved successfully
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The system is designed to be **fail-closed**:
|
||||
|
||||
- If sending the approval request to the client fails → command is denied.
|
||||
- If the client disconnects before responding → timeout fires, command follows `askFallback` (default: deny).
|
||||
- If the RPC response references an unknown `approvalId` → `NOT_FOUND` error returned, no side effects.
|
||||
- If the agent is closed while an approval is pending → all pending approvals for that agent are auto-denied.
|
||||
265
docs/memo.md
265
docs/memo.md
|
|
@ -1,265 +0,0 @@
|
|||
# Multica Memo
|
||||
|
||||
**Multiplexed Information & Computing Agent**
|
||||
|
||||
---
|
||||
|
||||
## What is Multica
|
||||
|
||||
Multica is an always-on AI agent that pulls real data, runs real computation, and takes real action on behalf of users.
|
||||
|
||||
It is not a chatbot. It is not a search engine. It is not an analytics dashboard. It is an **autonomous employee** that works 24/7 — monitoring, analyzing, and acting within user-defined authorization boundaries.
|
||||
|
||||
Users interact with Multica through natural conversation. They can ask for immediate analysis, or tell the agent to run recurring tasks in the background. The same interface handles both modes — no separate workflow builder, no configuration forms. You talk to it like you'd talk to a team member.
|
||||
|
||||
---
|
||||
|
||||
## Core Insight
|
||||
|
||||
The value chain of knowledge work is: **Data → Analysis → Decision → Action**.
|
||||
|
||||
Existing AI products truncate this chain. ChatGPT and Claude stop at conversation. Perplexity stops at search. BI dashboards stop at visualization. Each one hands the remaining work back to the human.
|
||||
|
||||
Multica completes the full chain:
|
||||
|
||||
- **Data**: Pulls structured data from multiple sources through a unified `data` tool, backed by Multica's centralized data infrastructure. Users never configure API keys or deal with data providers.
|
||||
- **Analysis**: Runs actual computation — Python, statistical models, charts — not just text summaries. The agent writes and executes code to derive quantitative insights.
|
||||
- **Decision**: Applies domain-specific analytical frameworks encoded as Skills to evaluate the data and form actionable conclusions.
|
||||
- **Action**: Executes real-world actions (trade, send email, update records) within a tiered authorization model that the user controls.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### One Tool, Infinite Domains
|
||||
|
||||
Multica's extensibility model is designed for horizontal scaling across verticals without agent-side complexity growth.
|
||||
|
||||
```
|
||||
Finance Legal Medical ...
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
Skills │ Earnings │ │ Case │ │ Literature│
|
||||
(Markdown) │ Screening │ │ Contract │ │ Drug │
|
||||
│ Macro │ │ Compliance│ │ Clinical │
|
||||
└─────┬─────┘ └─────┬─────┘ └─────┬────┘
|
||||
│ │ │
|
||||
┌─────┴───────────────┴────────────────┴────┐
|
||||
Tool │ data(query, domain) │
|
||||
(single) └─────────────────┬──────────────────────────┘
|
||||
│
|
||||
┌─────────────────┴──────────────────────────┐
|
||||
Backend │ Multica Data Service │
|
||||
│ routing / caching / normalization │
|
||||
├─────────┬───────────┬───────────┬──────────┤
|
||||
│ Polygon │ FRED │ PubMed │ Court- │
|
||||
│ SEC │ NewsAPI │ OpenFDA │ listener │
|
||||
└─────────┴───────────┴───────────┴──────────┘
|
||||
```
|
||||
|
||||
**One `data` tool** serves all verticals. Adding a new domain means adding backend source adapters and writing Skill markdown files. The agent engine, tool set, and product surface remain unchanged.
|
||||
|
||||
**Skills encode domain expertise, not data plumbing.** A Skill is a Markdown file that teaches the agent an analytical workflow: what data to request, how to process it, what to look for, how to present findings. Domain experts can author Skills without writing code.
|
||||
|
||||
**Multica proxies all data access.** Users never register for third-party data APIs. Multica's backend handles authentication, rate limiting, caching, and normalization. This simplifies the user experience and creates a natural monetization layer.
|
||||
|
||||
### Foreground + Background, One Interface
|
||||
|
||||
```
|
||||
User in conversation:
|
||||
|
||||
"Analyze TSLA" → Immediate execution
|
||||
"Send me a market briefing every morning" → Agent schedules cron task
|
||||
"Alert me if NVDA drops below 100" → Agent sets event trigger
|
||||
"Cancel the morning briefing" → Agent removes cron task
|
||||
```
|
||||
|
||||
The agent manages its own background tasks through existing tools (`cron`, `exec`). There is no separate workflow configuration UI. Conversation is the control plane.
|
||||
|
||||
Background tasks run persistently, independent of the app being open. Results are delivered through the user's preferred channel (email, Slack, Telegram, push notification, or in-app).
|
||||
|
||||
### Tiered Action Authorization
|
||||
|
||||
The agent's ability to take action is governed by a user-controlled trust gradient:
|
||||
|
||||
| Level | Behavior | Example |
|
||||
|-------|----------|---------|
|
||||
| 0 — Read-only | Pull data, analyze, report | Generate earnings analysis |
|
||||
| 1 — Notify | Detect signal, alert user | "TSLA broke your stop-loss level" |
|
||||
| 2 — Confirm | Propose action, wait for approval | "Sell 50% TSLA position? [Confirm]" |
|
||||
| 3 — Autonomous | Execute within preset rules, notify after | Auto-rebalance portfolio within mandate |
|
||||
|
||||
Each action type can be independently configured. Users start conservative and escalate trust as they build confidence in the agent. Authorization constraints include per-action limits, daily caps, and scope restrictions.
|
||||
|
||||
---
|
||||
|
||||
## Product
|
||||
|
||||
### Form Factor
|
||||
|
||||
**Web-first** for distribution, with desktop and mobile for persistent background operation.
|
||||
|
||||
The primary interface is conversational — but output is structured. When the agent produces an analysis, it renders as a formatted report with charts, tables, and data citations, not a chat bubble. Reports are exportable (PDF, Excel).
|
||||
|
||||
The secondary interface is **the user's inbox**. Background tasks deliver results via email or messaging. Many users will interact with Multica more through their email than through the app itself.
|
||||
|
||||
### User Experience
|
||||
|
||||
A new user's first 24 hours:
|
||||
|
||||
1. Sign up (web, 30 seconds)
|
||||
2. Tell the agent which stocks/sectors they follow
|
||||
3. Next morning: first market briefing arrives in their inbox
|
||||
4. Open the app, ask a follow-up question about something in the briefing
|
||||
5. Tell the agent "do this every morning"
|
||||
|
||||
**Time to first value: < 24 hours, zero configuration, zero learning curve.**
|
||||
|
||||
### Cross-Domain Composition
|
||||
|
||||
The most powerful use cases combine multiple domains in a single workflow:
|
||||
|
||||
> "We're evaluating an acquisition of a gene-editing company. Give me a full due diligence report."
|
||||
>
|
||||
> Agent combines:
|
||||
> - `data(query, "finance")` → Target's financials, valuation comps
|
||||
> - `data(query, "legal")` → Patent portfolio, regulatory filings
|
||||
> - `data(query, "medical")` → Clinical pipeline, trial results
|
||||
> - `exec` → Python analysis, charts, risk scoring
|
||||
> - Output: Integrated due diligence report spanning finance + IP + science
|
||||
|
||||
One `data` tool, three domains, agent orchestrates autonomously.
|
||||
|
||||
---
|
||||
|
||||
## Go-to-Market
|
||||
|
||||
### First Vertical: Finance
|
||||
|
||||
Finance is the right starting point because:
|
||||
|
||||
- **Data accessibility**: Abundant free and commercial APIs (market data, filings, macro indicators)
|
||||
- **Willingness to pay**: Finance professionals value time; current tools (Bloomberg terminal: $24k/year) prove the market pays for information advantage
|
||||
- **Quantitative output**: The agent's ability to compute (not just chat) is most visible in finance — ratios, models, charts, backtests
|
||||
- **Recurring workflows**: Daily briefings, portfolio monitoring, earnings tracking — these drive retention naturally
|
||||
|
||||
### Target User
|
||||
|
||||
Individual investors, independent financial advisors, small fund analysts (< $50M AUM). They currently cobble together Yahoo Finance + SEC EDGAR + Excel + maybe Python scripts. A full company analysis takes them half a day.
|
||||
|
||||
Multica does it in 2 minutes.
|
||||
|
||||
### Distribution
|
||||
|
||||
| Channel | Approach |
|
||||
|---------|----------|
|
||||
| Twitter/X FinTwit | Real analysis examples as content — the output IS the demo |
|
||||
| YouTube | "AI analyst built my morning briefing in 2 minutes" |
|
||||
| Finance newsletters (Substack) | Weekly analysis pieces generated by Multica, attributed |
|
||||
| Reddit (r/investing, r/SecurityAnalysis) | High-quality analysis posts, organic |
|
||||
| Finance KOLs | Free Pro accounts, let them showcase their own output |
|
||||
|
||||
### Growth Loop
|
||||
|
||||
```
|
||||
Free daily briefing (user signs up, picks stocks)
|
||||
↓
|
||||
Briefing arrives next morning (immediate value)
|
||||
↓
|
||||
User shares briefing excerpt on social media
|
||||
↓
|
||||
Report footer: "Generated with Multica"
|
||||
↓
|
||||
New user sees it → signs up
|
||||
```
|
||||
|
||||
The output is inherently shareable. Every analysis report is a marketing asset.
|
||||
|
||||
### Pricing
|
||||
|
||||
| Tier | Price | Includes |
|
||||
|------|-------|---------|
|
||||
| Free | $0/mo | 5 analyses/month, 1 daily briefing, delayed data |
|
||||
| Pro | $29/mo | Unlimited analyses, custom briefings, real-time data, export, action (Level 0-2) |
|
||||
| Team | $79/user/mo | Shared workspace, collaborative Skills, API access |
|
||||
| Enterprise | Custom | Private deployment, custom data sources, autonomous actions (Level 3), SLA |
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 0→1: Finance MVP (8 weeks)
|
||||
|
||||
| Week | Deliverable |
|
||||
|------|-------------|
|
||||
| 1-2 | `data` tool backend + 2 sources (market data, macro) |
|
||||
| 3-4 | 3 finance Skills (company analysis, screening, macro briefing) |
|
||||
| 5-6 | Email channel (agent sends results, receives instructions) |
|
||||
| 7-8 | Web app (conversation + report rendering + task management) |
|
||||
|
||||
**Launch artifact**: "Sign up, pick 3 stocks, get your first AI briefing tomorrow morning."
|
||||
|
||||
### Phase 1→10: Deepen and Expand (months 3-12)
|
||||
|
||||
**Months 3-6 — Deepen finance:**
|
||||
- More data sources (SEC filings, alternative data, earnings call transcripts)
|
||||
- More Skills (DCF modeling, options analysis, sector comparison, portfolio review)
|
||||
- Portfolio binding (user connects brokerage, agent gives personalized analysis)
|
||||
- Event triggers (price alerts, earnings surprises, insider trading signals)
|
||||
- Action capability (Level 1-2: trade proposals with confirmation)
|
||||
|
||||
**Months 6-12 — Adjacent verticals:**
|
||||
- Finance + Legal (M&A due diligence, SEC compliance, patent analysis)
|
||||
- Finance + Macro (policy impact, central bank analysis, geopolitical risk)
|
||||
- Open Skill authoring (users create and share their own Skills)
|
||||
|
||||
### Phase 10→100: Platform (year 2+)
|
||||
|
||||
**Skill Ecosystem:**
|
||||
|
||||
```
|
||||
multica.ai/skills/
|
||||
├── @multica/ Official Skills (free)
|
||||
├── @analyst-pro/ Community contributor (free/paid)
|
||||
├── @hedgefund-x/ Enterprise private Skills
|
||||
└── @lawfirm-y/ Vertical-specific paid Skills
|
||||
```
|
||||
|
||||
- Anyone can publish a Skill (it's a Markdown file)
|
||||
- Enterprises deploy private Skills for their teams
|
||||
- Paid Skills: creator sets price, Multica takes platform fee
|
||||
|
||||
**Data Marketplace:**
|
||||
- Third-party data providers plug into Multica's backend
|
||||
- Premium data sources available to paying users
|
||||
- Multica becomes the distribution channel for data providers
|
||||
|
||||
**Multi-vertical expansion:**
|
||||
- Each new vertical = backend source adapters + domain Skills
|
||||
- Agent engine unchanged
|
||||
- Same authorization model, same product surface
|
||||
|
||||
---
|
||||
|
||||
## Defensibility
|
||||
|
||||
| Layer | Moat |
|
||||
|-------|------|
|
||||
| Data infrastructure | Aggregated, normalized, cached — hard to replicate per-source |
|
||||
| Skill ecosystem | Network effects: more Skills → more users → more Skill creators |
|
||||
| User data | Portfolio history, preference patterns, analysis history — switching cost |
|
||||
| Trust calibration | User's authorization levels and constraints are personalized over time |
|
||||
| Domain compounding | Cross-vertical composition (finance + legal + medical) is uniquely enabled by the unified `data` tool architecture |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Multica is an always-on AI agent that completes the full knowledge work chain: data → analysis → decision → action.
|
||||
|
||||
It starts in finance — where data is accessible, users pay, and quantitative output is the clearest differentiator — with a daily briefing that delivers value in < 24 hours.
|
||||
|
||||
It scales horizontally through a unified `data` tool + Skill architecture that adds new verticals without changing the agent engine.
|
||||
|
||||
It builds a platform moat through a Skill ecosystem where domain experts encode their workflows as shareable, composable Markdown files.
|
||||
|
||||
The product is not a tool you open. It's an employee that works while you sleep.
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
# Message Paths — Desktop / Web / Channel
|
||||
|
||||
Three independent paths deliver messages to and from the Hub's agent.
|
||||
All three share the same `AsyncAgent` instance — they are just different I/O surfaces.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
Desktop (Electron IPC) Web (WebSocket via Gateway) Channel (Bot API, e.g. Telegram)
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
localChat:send IPC client.send → Gateway WS plugin.gateway (polling/webhook)
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
hub.ts / ipc/hub.ts hub.ts / onMessage manager.ts / routeIncoming
|
||||
clearLastRoute() clearLastRoute() set lastRoute
|
||||
│ │ │
|
||||
└────────────────► agent.write(text) ◄──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
AsyncAgent.run()
|
||||
│
|
||||
┌────────────┴────────────────┐
|
||||
▼ ▼
|
||||
agent.subscribe() agent.read()
|
||||
(multi-consumer) (single-consumer iterable)
|
||||
│ │
|
||||
┌────────┴────────┐ ▼
|
||||
▼ ▼ hub.ts / consumeAgent()
|
||||
Desktop IPC Channel Manager │
|
||||
(ipc/hub.ts) (manager.ts) ▼
|
||||
│ │ Gateway WS → Web client
|
||||
▼ ▼
|
||||
localChat:event Bot API reply
|
||||
→ renderer (via lastRoute)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Path 1: Desktop (Electron IPC)
|
||||
|
||||
### Send (User → Agent)
|
||||
|
||||
```
|
||||
Renderer: sendMessage(text)
|
||||
→ IPC: localChat:send
|
||||
→ ipc/hub.ts handler
|
||||
→ hub.channelManager.clearLastRoute() // reply stays in desktop
|
||||
→ agent.write(text)
|
||||
```
|
||||
|
||||
**File**: `apps/desktop/electron/ipc/hub.ts` — `localChat:send` handler (line ~373)
|
||||
|
||||
### Receive (Agent → User)
|
||||
|
||||
```
|
||||
Agent runs LLM
|
||||
→ pi-agent-core fires AgentEvent
|
||||
→ Agent.subscribeAll() → AsyncAgent channel + subscribers
|
||||
→ agent.subscribe() callback in ipc/hub.ts
|
||||
→ Filter: assistant messages + tool_execution + passthrough (compaction, agent_error)
|
||||
→ IPC: mainWindow.webContents.send('localChat:event', { agentId, streamId, event })
|
||||
→ Renderer: use-local-chat.ts onEvent callback
|
||||
→ chat.handleStream(payload)
|
||||
```
|
||||
|
||||
**Files**:
|
||||
- `apps/desktop/electron/ipc/hub.ts` — `localChat:subscribe` handler (line ~248)
|
||||
- `apps/desktop/src/hooks/use-local-chat.ts` — `onEvent` listener (line ~54)
|
||||
- `packages/hooks/src/use-chat.ts` — `handleStream()` (line ~133)
|
||||
|
||||
### Error Handling
|
||||
|
||||
```
|
||||
Agent.run() throws / returns error
|
||||
→ AsyncAgent.write() catch block
|
||||
→ channel.send(legacy Message) // for read() consumers (Web)
|
||||
→ agent.emitMulticaEvent({ type: "agent_error", error }) // for subscribe() consumers
|
||||
→ ipc/hub.ts subscriber → passthrough event → localChat:event
|
||||
→ use-local-chat.ts → chat.setError() + setIsLoading(false)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Path 2: Web (WebSocket via Gateway)
|
||||
|
||||
### Send (User → Agent)
|
||||
|
||||
```
|
||||
Web app: sendMessage(text)
|
||||
→ GatewayClient.send(hubId, "message", { agentId, content })
|
||||
→ Socket.io → Gateway server → routes to Hub device
|
||||
→ hub.ts / onMessage handler
|
||||
→ channelManager.clearLastRoute() // reply stays in gateway
|
||||
→ agentSenders.set(agentId, deviceId)
|
||||
→ agent.write(content)
|
||||
```
|
||||
|
||||
**File**: `src/hub/hub.ts` — `onMessage` handler (line ~154)
|
||||
|
||||
### Receive (Agent → User)
|
||||
|
||||
```
|
||||
Agent runs LLM
|
||||
→ pi-agent-core fires AgentEvent
|
||||
→ Agent.subscribeAll() → AsyncAgent channel + subscribers
|
||||
→ agent.read() consumed by hub.ts / consumeAgent()
|
||||
→ Filter: assistant messages + tool_execution + passthrough (compaction, agent_error)
|
||||
→ client.send(targetDeviceId, StreamAction, { streamId, agentId, event })
|
||||
→ Socket.io → Gateway → routes to Web client device
|
||||
→ GatewayClient.onMessage callback
|
||||
→ use-gateway-chat.ts → chat.handleStream(payload)
|
||||
```
|
||||
|
||||
**Files**:
|
||||
- `src/hub/hub.ts` — `consumeAgent()` (line ~314)
|
||||
- `packages/hooks/src/use-gateway-chat.ts` — `onMessage` listener (line ~50)
|
||||
- `packages/hooks/src/use-chat.ts` — `handleStream()` (line ~133)
|
||||
|
||||
### Error Handling
|
||||
|
||||
```
|
||||
Agent.run() throws / returns error
|
||||
→ AsyncAgent.write() catch block
|
||||
→ channel.send(legacy Message) // consumed by consumeAgent() → sent as "message" action
|
||||
→ agent.emitMulticaEvent({ type: "agent_error", error })
|
||||
→ read() → consumeAgent() → passthrough event → StreamAction
|
||||
→ GatewayClient → use-gateway-chat.ts → chat.setError() + setIsLoading(false)
|
||||
```
|
||||
|
||||
**Note**: Legacy error Messages also reach the Web client as `"message"` action (a plain text fallback). The `agent_error` event provides structured error info for proper UI rendering.
|
||||
|
||||
---
|
||||
|
||||
## Path 3: Channel (Bot API, e.g. Telegram)
|
||||
|
||||
### Send (User → Agent)
|
||||
|
||||
```
|
||||
User sends message in Telegram
|
||||
→ grammy long-polling receives Update
|
||||
→ plugin.gateway.start() callback: onMessage(channelMessage)
|
||||
→ ChannelManager.routeIncoming()
|
||||
→ Set lastRoute = { plugin, deliveryCtx } // reply goes back to Telegram
|
||||
→ agent.write(text) // same as desktop/web
|
||||
```
|
||||
|
||||
**File**: `src/channels/manager.ts` — `routeIncoming()` (line ~233)
|
||||
|
||||
### Receive (Agent → User)
|
||||
|
||||
```
|
||||
Agent runs LLM
|
||||
→ pi-agent-core fires AgentEvent
|
||||
→ Agent.subscribeAll() → AsyncAgent channel + subscribers
|
||||
→ agent.subscribe() callback in ChannelManager.subscribeToAgent()
|
||||
→ Check: if (!lastRoute) return // no active channel route, skip
|
||||
→ Filter: only assistant messages
|
||||
→ message_start → createAggregator() // MessageAggregator buffers/chunks text
|
||||
→ message_update → aggregator.handleEvent()
|
||||
→ message_end → aggregator.handleEvent() → null aggregator
|
||||
→ Aggregator emits text blocks
|
||||
→ Block 0: plugin.outbound.replyText(deliveryCtx, text) // Telegram reply
|
||||
→ Block N: plugin.outbound.sendText(deliveryCtx, text) // follow-up messages
|
||||
```
|
||||
|
||||
**Files**:
|
||||
- `src/channels/manager.ts` — `subscribeToAgent()` (line ~151), `createAggregator()` (line ~205)
|
||||
- `src/hub/message-aggregator.ts` — text chunking/buffering logic
|
||||
|
||||
### Error Handling
|
||||
|
||||
```
|
||||
Agent.run() throws / returns error
|
||||
→ AsyncAgent.write() catch block
|
||||
→ agent.emitMulticaEvent({ type: "agent_error", error })
|
||||
→ subscribe() → ChannelManager subscriber
|
||||
→ if lastRoute exists:
|
||||
→ plugin.outbound.sendText(deliveryCtx, "[Error] ${errorMsg}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comparison Table
|
||||
|
||||
| Aspect | Desktop (IPC) | Web (WebSocket) | Channel (Bot API) |
|
||||
|---------------------|------------------------|---------------------------|--------------------------|
|
||||
| **Transport** | Electron IPC | Socket.io via Gateway | Bot API (HTTP) |
|
||||
| **Send entry** | `localChat:send` | `client.send` → Gateway | `routeIncoming` |
|
||||
| **Receive method** | `agent.subscribe()` | `agent.read()` (iterable) | `agent.subscribe()` |
|
||||
| **Consumer** | ipc/hub.ts subscriber | hub.ts `consumeAgent()` | manager.ts subscriber |
|
||||
| **Frontend hook** | `use-local-chat.ts` | `use-gateway-chat.ts` | N/A (Bot API) |
|
||||
| **State hook** | `use-chat.ts` | `use-chat.ts` | N/A |
|
||||
| **Reply routing** | Always (IPC channel) | `agentSenders` Map | `lastRoute` pattern |
|
||||
| **clearLastRoute** | Yes (on send) | Yes (on send) | No (sets lastRoute) |
|
||||
| **Error display** | `agent_error` → UI | `agent_error` → UI | `agent_error` → Bot text |
|
||||
| **Tool results** | Rendered in UI | Rendered in UI | Skipped (text only) |
|
||||
| **Text chunking** | No (full stream) | No (full stream) | Yes (MessageAggregator) |
|
||||
|
||||
---
|
||||
|
||||
## lastRoute Pattern
|
||||
|
||||
The `lastRoute` tracks which channel last sent a message. When the agent replies:
|
||||
- If `lastRoute` is set → reply goes to that channel (e.g. Telegram)
|
||||
- If `lastRoute` is null → reply goes to Desktop/Web only (via their own mechanisms)
|
||||
|
||||
**Clearing**: Desktop and Web both call `channelManager.clearLastRoute()` before `agent.write()`, so channel replies stop when the user switches to desktop/web.
|
||||
|
||||
**Setting**: `routeIncoming()` sets `lastRoute` when a channel message arrives.
|
||||
|
||||
Desktop and Web always receive agent events regardless of `lastRoute` — they use their own independent delivery mechanisms (IPC subscribe / Gateway read).
|
||||
|
||||
---
|
||||
|
||||
## Event Filtering
|
||||
|
||||
All three paths filter raw agent events. Only these are forwarded to consumers:
|
||||
|
||||
| Event Type | Desktop | Web | Channel |
|
||||
|-------------------------|---------|-----|---------|
|
||||
| `message_start` | assistant only | assistant only | assistant only |
|
||||
| `message_update` | assistant only | assistant only | assistant only |
|
||||
| `message_end` | assistant only | assistant only | assistant only |
|
||||
| `tool_execution_start` | Yes | Yes | No |
|
||||
| `tool_execution_end` | Yes | Yes | No |
|
||||
| `compaction_start` | Yes (passthrough) | Yes (passthrough) | No |
|
||||
| `compaction_end` | Yes (passthrough) | Yes (passthrough) | No |
|
||||
| `agent_error` | Yes (passthrough) | Yes (passthrough) | Yes (→ text) |
|
||||
| User message events | Filtered out | Filtered out | Filtered out |
|
||||
|
|
@ -1,497 +0,0 @@
|
|||
# Mobile Development Guide
|
||||
|
||||
Complete lifecycle guide for developing, testing, and publishing the Expo React Native app — from first line of code to App Store / Google Play.
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
Phase 1: Environment Setup You are here if starting fresh
|
||||
↓
|
||||
Phase 2: Development & Testing Daily work loop
|
||||
↓
|
||||
Phase 3: Pre-Release Preparation Before your first submission
|
||||
↓
|
||||
Phase 4: Build & Submit Ship to stores
|
||||
↓
|
||||
Phase 5: Post-Launch Maintain and update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Environment Setup
|
||||
|
||||
### 1.1 Required Software
|
||||
|
||||
| Tool | Purpose | Install |
|
||||
|------|---------|---------|
|
||||
| **Node.js** (LTS) | JS runtime | `brew install node` or [nodejs.org](https://nodejs.org) |
|
||||
| **pnpm** | Package manager | `corepack enable && corepack prepare pnpm@latest --activate` |
|
||||
| **Xcode** | iOS build toolchain | Mac App Store (free) |
|
||||
| **Xcode Command Line Tools** | Compilers, simulators | `xcode-select --install` |
|
||||
| **CocoaPods** | iOS dependency manager | `sudo gem install cocoapods` |
|
||||
| **Android Studio** | Android emulator + SDK (optional, iOS-first) | [developer.android.com](https://developer.android.com/studio) |
|
||||
| **EAS CLI** | Expo build & submit | `npm install -g eas-cli` |
|
||||
| **Expo CLI** | Dev server | Bundled with `npx expo` |
|
||||
|
||||
### 1.2 Xcode First-Time Setup
|
||||
|
||||
1. Open Xcode at least once to accept the license and install components
|
||||
2. **Add your Apple ID** (free account is enough for development):
|
||||
- Xcode → Settings → Accounts → `+` → Apple ID
|
||||
- This creates a "Personal Team" for free code signing
|
||||
3. Verify simulators are installed:
|
||||
- Xcode → Settings → Components → download an iOS Simulator runtime
|
||||
|
||||
### 1.3 iPhone First-Time Setup (for Real Device Testing)
|
||||
|
||||
1. **Enable Developer Mode** (required on iOS 16+):
|
||||
- Settings → Privacy & Security → Developer Mode → ON
|
||||
- Device will restart
|
||||
2. Connect iPhone to Mac via USB/USB-C cable
|
||||
3. When prompted "Trust This Computer?" → tap Trust
|
||||
|
||||
### 1.4 Project Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Generate native project files (creates ios/ and android/ directories)
|
||||
npx expo prebuild
|
||||
|
||||
# Initialize EAS configuration (creates eas.json)
|
||||
eas build:configure
|
||||
```
|
||||
|
||||
### 1.5 Expo Account
|
||||
|
||||
```bash
|
||||
# Create account at expo.dev, then:
|
||||
eas login
|
||||
eas whoami # verify
|
||||
```
|
||||
|
||||
**No paid accounts needed at this stage.** Free Apple ID + free Expo account is enough for development.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Development & Testing
|
||||
|
||||
### 2.1 Running on iOS Simulator
|
||||
|
||||
```bash
|
||||
# Start the app in iOS simulator (no real device needed)
|
||||
npx expo run:ios
|
||||
```
|
||||
|
||||
- Fastest iteration loop — code changes hot-reload instantly
|
||||
- Good for: UI layout, navigation, business logic, API calls
|
||||
- **Cannot test**: camera, barcode scanner, real push notifications, biometrics
|
||||
|
||||
### 2.2 Running on Real iPhone
|
||||
|
||||
```bash
|
||||
# Connect iPhone via USB, then:
|
||||
npx expo run:ios --device
|
||||
```
|
||||
|
||||
Expo CLI will:
|
||||
1. Detect your connected device
|
||||
2. Sign the app with your Personal Team (free Apple ID)
|
||||
3. Build, install, and launch the app
|
||||
|
||||
**First time only**: After installation, go to:
|
||||
- Settings → General → VPN & Device Management → Trust your developer certificate
|
||||
|
||||
#### Free Signing Limitations
|
||||
|
||||
| Limitation | Detail |
|
||||
|-----------|--------|
|
||||
| 7-day expiry | App stops launching after 7 days — just re-run `npx expo run:ios --device` |
|
||||
| 3 devices max | Can register up to 3 test devices per Apple ID |
|
||||
| Some entitlements unavailable | Push notifications, Apple Pay, iCloud require paid account |
|
||||
| Cannot distribute to others | Only works on your own registered devices |
|
||||
|
||||
**Camera, barcode scanner, GPS, sensors all work fine with free signing.**
|
||||
|
||||
### 2.3 Daily Development Workflow
|
||||
|
||||
```
|
||||
First time (or after native config changes):
|
||||
npx expo prebuild Generate/update native projects
|
||||
npx expo run:ios --device Build and install on device
|
||||
|
||||
Every day after that:
|
||||
npx expo start --dev-client Start dev server only (no rebuild)
|
||||
→ Open the app on device It connects automatically
|
||||
→ Edit code, save Hot-reload updates instantly
|
||||
```
|
||||
|
||||
**When do you need to rebuild?**
|
||||
|
||||
| Change | Rebuild needed? |
|
||||
|--------|----------------|
|
||||
| JS/TS code, React components | No — hot-reload |
|
||||
| Styles, images, assets | No — hot-reload |
|
||||
| Added new Expo SDK module | **Yes** — `npx expo prebuild && npx expo run:ios --device` |
|
||||
| Changed `app.json` permissions | **Yes** — rebuild |
|
||||
| Updated native dependency | **Yes** — rebuild |
|
||||
| Upgraded Expo SDK version | **Yes** — rebuild |
|
||||
|
||||
### 2.4 Testing Native Features (Camera, Scanner)
|
||||
|
||||
| Feature | Simulator | Real Device |
|
||||
|---------|-----------|-------------|
|
||||
| Camera preview | Not available | Works |
|
||||
| Barcode / QR scan | Not available | Works |
|
||||
| GPS location | Simulated location via Xcode menu | Real GPS |
|
||||
| Push notifications | Not available | Requires paid Apple Developer account |
|
||||
| Haptic feedback | Not available | Works |
|
||||
| Device sensors (accelerometer, gyroscope) | Not available | Works |
|
||||
|
||||
For camera/scanner features, **always test on a real device**.
|
||||
|
||||
### 2.5 Debugging Tools
|
||||
|
||||
#### Developer Menu
|
||||
|
||||
Press `m` in the terminal (or shake the device) to open:
|
||||
- Toggle Performance Monitor
|
||||
- Toggle Element Inspector
|
||||
- Open React Native DevTools
|
||||
|
||||
#### React Native DevTools
|
||||
|
||||
The primary debugging tool (replaced Chrome DevTools since RN 0.76):
|
||||
|
||||
| Tab | Use |
|
||||
|-----|-----|
|
||||
| Console | View logs, execute JS in app context |
|
||||
| Sources | Set breakpoints, step through code |
|
||||
| Network | Inspect API requests (Expo only) |
|
||||
| Components | Inspect React component tree and props |
|
||||
| Profiler | Measure render performance |
|
||||
|
||||
#### VS Code Integration
|
||||
|
||||
Install the **Expo Tools** extension for:
|
||||
- Breakpoint debugging directly in VS Code
|
||||
- `app.json` / `app.config.ts` IntelliSense
|
||||
|
||||
#### Native Crash Debugging
|
||||
|
||||
For crashes in native modules (not JS):
|
||||
- **iOS**: Open Xcode → Window → Devices and Simulators → View Device Logs
|
||||
- **Android**: `adb logcat` in terminal
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Pre-Release Preparation
|
||||
|
||||
**This is when you need to start spending money.**
|
||||
|
||||
### 3.1 Accounts & Fees
|
||||
|
||||
| Platform | Cost | Registration Time | Required For |
|
||||
|----------|------|-------------------|--------------|
|
||||
| **Apple Developer Program** | $99/year | 1-2 days review | App Store distribution |
|
||||
| **Google Play Console** | $25 one-time | Days to weeks review | Play Store distribution |
|
||||
| **Expo Account** | Free tier sufficient | Instant | EAS Build & Submit |
|
||||
|
||||
Register early — account review takes time, especially Google.
|
||||
|
||||
### 3.2 App Configuration
|
||||
|
||||
Update `app.json` or `app.config.ts`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "Multica",
|
||||
"slug": "multica",
|
||||
"version": "1.0.0",
|
||||
"ios": {
|
||||
"bundleIdentifier": "com.multica.app",
|
||||
"buildNumber": "1", // increment each submission
|
||||
"infoPlist": {
|
||||
"NSCameraUsageDescription": "Used to scan QR codes and take photos",
|
||||
"NSPhotoLibraryUsageDescription": "Used to save scanned images"
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"package": "com.multica.app",
|
||||
"versionCode": 1, // increment each submission
|
||||
"permissions": ["CAMERA"]
|
||||
},
|
||||
"icon": "./assets/icon.png", // 1024x1024 PNG, no transparency
|
||||
"splash": {
|
||||
"image": "./assets/splash.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 EAS Build Profiles
|
||||
|
||||
`eas.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"cli": { "version": ">= 10.0.0" },
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 App Signing & Credentials
|
||||
|
||||
#### iOS
|
||||
|
||||
EAS auto-manages credentials (recommended):
|
||||
- Distribution Certificate
|
||||
- Provisioning Profile
|
||||
- Or create manually in [Apple Developer Portal](https://developer.apple.com)
|
||||
|
||||
#### Android
|
||||
|
||||
- EAS auto-generates Keystore, stored securely on EAS servers
|
||||
- **Back up your Keystore** — losing it means you can never update the published app
|
||||
- Play Store requires AAB (Android App Bundle) format
|
||||
|
||||
### 3.5 Required Assets
|
||||
|
||||
| Asset | Spec |
|
||||
|-------|------|
|
||||
| **App Icon** | 1024x1024 PNG, no alpha/transparency (iOS) |
|
||||
| **Splash Screen** | Platform-appropriate sizes |
|
||||
| **iOS Screenshots** | 6.7", 6.5", 5.5" iPhone sizes + iPad (if universal) |
|
||||
| **Android Screenshots** | 2-8 screenshots |
|
||||
|
||||
### 3.6 Required Metadata
|
||||
|
||||
#### Both Platforms
|
||||
|
||||
| Item | Notes |
|
||||
|------|-------|
|
||||
| **Privacy Policy URL** | Publicly accessible. Must disclose data collection, third-party sharing, AI usage, deletion rights |
|
||||
| **App Description** | Short (≤80 chars for Google) + full description |
|
||||
| **Support URL** | Where users can get help |
|
||||
| **Account Deletion** | If app has registration, must support in-app account + data deletion |
|
||||
|
||||
#### Apple App Store Connect
|
||||
|
||||
| Item | Details |
|
||||
|------|---------|
|
||||
| Privacy Nutrition Labels | Data collection practices per category |
|
||||
| App Review Information | Reviewer contact info, demo/test account |
|
||||
| Content Rating | Age classification |
|
||||
| Export Compliance | Encryption usage declaration |
|
||||
| Info.plist Permission Strings | Clear purpose description for each permission |
|
||||
|
||||
#### Google Play Console
|
||||
|
||||
| Item | Details |
|
||||
|------|---------|
|
||||
| Data Safety Form | Required even if no data is collected |
|
||||
| Content Rating Questionnaire | IARC rating |
|
||||
| Target Audience | Must declare if targeting children |
|
||||
| First Upload | Must upload AAB manually (Google API limitation) |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Build & Submit
|
||||
|
||||
### 4.1 Production Build
|
||||
|
||||
```bash
|
||||
# iOS
|
||||
eas build --platform ios --profile production
|
||||
|
||||
# Android
|
||||
eas build --platform android --profile production
|
||||
|
||||
# Both platforms
|
||||
eas build --platform all --profile production
|
||||
```
|
||||
|
||||
Builds run in Expo cloud — no local Xcode or Android Studio needed for production builds.
|
||||
|
||||
### 4.2 Submit to Apple App Store
|
||||
|
||||
```bash
|
||||
eas submit --platform ios
|
||||
```
|
||||
|
||||
This uploads the build to **App Store Connect / TestFlight**. Then:
|
||||
|
||||
1. Log into [App Store Connect](https://appstoreconnect.apple.com)
|
||||
2. Select the uploaded build
|
||||
3. Associate it with a version
|
||||
4. Fill in all metadata, screenshots, privacy nutrition labels
|
||||
5. Submit for App Review
|
||||
|
||||
### 4.3 Submit to Google Play Store
|
||||
|
||||
```bash
|
||||
eas submit --platform android
|
||||
```
|
||||
|
||||
**First time**: Must upload AAB manually in [Play Console](https://play.google.com/console).
|
||||
|
||||
After initial upload:
|
||||
1. Navigate to Production → Create new release
|
||||
2. Upload AAB or use the EAS-submitted build
|
||||
3. Fill in description, screenshots, data safety form
|
||||
4. Submit for review
|
||||
|
||||
### 4.4 Auto-Submit (Optional)
|
||||
|
||||
Build and submit in one step:
|
||||
|
||||
```bash
|
||||
eas build --platform all --profile production --auto-submit
|
||||
```
|
||||
|
||||
### 4.5 App Review
|
||||
|
||||
| | Apple | Google |
|
||||
|---|---|---|
|
||||
| Review time | Typically 24-48 hours | Hours to 7 days |
|
||||
| Common rejections | Incomplete features, misleading screenshots, missing privacy policy, unclear permission strings | Data safety form mismatch, policy violations |
|
||||
| After rejection | Fix issues, resubmit | Fix issues, resubmit |
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Post-Launch
|
||||
|
||||
### 5.1 OTA Updates (No Re-Review)
|
||||
|
||||
For JS/asset-only changes, push updates without going through App Review:
|
||||
|
||||
```bash
|
||||
eas update --branch production
|
||||
```
|
||||
|
||||
- Instant delivery to users — no store review
|
||||
- Only works for JavaScript and asset changes
|
||||
- **Native code changes still require a new build + review**
|
||||
|
||||
### 5.2 Version Bumping
|
||||
|
||||
For each new store submission:
|
||||
- iOS: increment `buildNumber` in `app.json`
|
||||
- Android: increment `versionCode` in `app.json`
|
||||
- Bump `version` for user-visible version changes
|
||||
|
||||
### 5.3 CI/CD Automation
|
||||
|
||||
Create `.eas/workflows/build-and-submit.yml` to auto-build and submit on push to main.
|
||||
|
||||
#### Google Service Account Key (Automated Android Submissions)
|
||||
|
||||
1. EAS dashboard → Credentials → Android
|
||||
2. Click Application identifier → Service Credentials
|
||||
3. Add Google Service Account Key
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npx expo prebuild # Generate native projects
|
||||
npx expo run:ios # Run on iOS simulator
|
||||
npx expo run:ios --device # Run on connected iPhone
|
||||
npx expo start --dev-client # Start dev server (after initial install)
|
||||
|
||||
# Building
|
||||
eas build --platform ios --profile development # Dev build (for device testing)
|
||||
eas build --platform ios --profile production # Production build
|
||||
eas build --platform all --profile production # Both platforms
|
||||
|
||||
# Submitting
|
||||
eas submit --platform ios # Submit to App Store
|
||||
eas submit --platform android # Submit to Play Store
|
||||
|
||||
# OTA Updates
|
||||
eas update --branch production # Push JS update to users
|
||||
```
|
||||
|
||||
### Cost Summary
|
||||
|
||||
| Phase | Cost |
|
||||
|-------|------|
|
||||
| Development + local testing | **Free** (free Apple ID + Xcode) |
|
||||
| EAS cloud builds | Free tier: 30 iOS + 30 Android builds/month |
|
||||
| App Store submission | **$99/year** (Apple Developer Program) |
|
||||
| Play Store submission | **$25 one-time** (Google Play Console) |
|
||||
|
||||
---
|
||||
|
||||
## Master Checklist
|
||||
|
||||
### Development Phase
|
||||
- [ ] Install Node.js, pnpm, Xcode, EAS CLI
|
||||
- [ ] Add Apple ID to Xcode (Settings → Accounts)
|
||||
- [ ] Enable Developer Mode on iPhone
|
||||
- [ ] Run `npx expo prebuild`
|
||||
- [ ] Test on simulator: `npx expo run:ios`
|
||||
- [ ] Test on real device: `npx expo run:ios --device`
|
||||
- [ ] Trust developer certificate on device
|
||||
- [ ] Verify camera/scanner functionality on real device
|
||||
|
||||
### Pre-Release Phase
|
||||
- [ ] Register Apple Developer Program ($99/year)
|
||||
- [ ] Register Google Play Console ($25)
|
||||
- [ ] Configure `app.json` (bundleIdentifier, permissions, icon, splash)
|
||||
- [ ] Configure `eas.json` build profiles
|
||||
- [ ] Prepare app icon (1024x1024 PNG)
|
||||
- [ ] Prepare splash screen
|
||||
- [ ] Take App Store screenshots (all required sizes)
|
||||
- [ ] Write and host privacy policy URL
|
||||
- [ ] Write app description (short + full)
|
||||
- [ ] Set up support URL
|
||||
- [ ] Implement in-app account deletion (if registration exists)
|
||||
|
||||
### Submission Phase
|
||||
- [ ] Run `eas build --platform all --profile production`
|
||||
- [ ] iOS: `eas submit --platform ios`
|
||||
- [ ] iOS: Fill metadata + privacy labels in App Store Connect
|
||||
- [ ] iOS: Submit for App Review
|
||||
- [ ] Android: Upload first AAB manually in Play Console
|
||||
- [ ] Android: `eas submit --platform android`
|
||||
- [ ] Android: Fill data safety form + metadata in Play Console
|
||||
- [ ] Android: Submit for review
|
||||
- [ ] Wait for review approval → app goes live
|
||||
|
||||
### Post-Launch Phase
|
||||
- [ ] Set up `eas update` for OTA updates
|
||||
- [ ] Set up CI/CD workflow (optional)
|
||||
- [ ] Configure Google Service Account Key for automated Android submissions (optional)
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Expo: Getting Started](https://docs.expo.dev/get-started/introduction/)
|
||||
- [Expo: Development Builds](https://docs.expo.dev/develop/development-builds/introduction/)
|
||||
- [Expo: Local App Development](https://docs.expo.dev/guides/local-app-development/)
|
||||
- [Expo: Debugging Tools](https://docs.expo.dev/debugging/tools/)
|
||||
- [Expo: Submit to App Stores](https://docs.expo.dev/deploy/submit-to-app-stores/)
|
||||
- [Expo: EAS Submit](https://docs.expo.dev/submit/introduction/)
|
||||
- [Expo: EAS Update](https://docs.expo.dev/eas-update/introduction/)
|
||||
- [Apple App Review Guidelines](https://developer.apple.com/app-store/review/guidelines/)
|
||||
- [Apple App Privacy Details](https://developer.apple.com/app-store/app-privacy-details/)
|
||||
- [Google Play Data Safety](https://support.google.com/googleplay/android-developer/answer/10787469)
|
||||
- [Google Play Developer Policy Center](https://play.google/developer-content-policy/)
|
||||
|
|
@ -1,315 +1,48 @@
|
|||
# Package Management Guide
|
||||
# Package Management
|
||||
|
||||
## Overview
|
||||
## Workspace
|
||||
|
||||
Super Multica uses **pnpm workspaces** for monorepo management. This document covers package management, dependency handling, and merge conflict resolution.
|
||||
- Package manager: `pnpm` (workspace mode)
|
||||
- Build orchestrator: `turbo`
|
||||
|
||||
---
|
||||
## Required `.npmrc`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
super-multica/
|
||||
├── apps/ # Deployable applications
|
||||
│ ├── cli/ # @multica/cli
|
||||
│ ├── desktop/ # @multica/desktop (Electron)
|
||||
│ ├── gateway/ # @multica/gateway (NestJS WebSocket)
|
||||
│ ├── server/ # @multica/server (NestJS REST)
|
||||
│ ├── web/ # @multica/web (Next.js)
|
||||
│ └── mobile/ # @multica/mobile (React Native)
|
||||
│
|
||||
├── packages/ # Shared libraries
|
||||
│ ├── core/ # @multica/core (agent, hub, channels)
|
||||
│ ├── sdk/ # @multica/sdk (gateway client)
|
||||
│ ├── ui/ # @multica/ui (shared components)
|
||||
│ ├── store/ # @multica/store (Zustand)
|
||||
│ ├── hooks/ # @multica/hooks (React hooks)
|
||||
│ ├── types/ # @multica/types (TypeScript types)
|
||||
│ └── utils/ # @multica/utils (utility functions)
|
||||
│
|
||||
├── skills/ # Bundled agent skills
|
||||
├── pnpm-workspace.yaml # Workspace definition
|
||||
├── pnpm-lock.yaml # Lockfile (auto-generated)
|
||||
└── .npmrc # pnpm configuration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Configuration Files
|
||||
|
||||
### pnpm-workspace.yaml
|
||||
|
||||
Defines which directories are workspace packages:
|
||||
|
||||
```yaml
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
```
|
||||
|
||||
### .npmrc
|
||||
|
||||
**Required configuration for Electron packaging:**
|
||||
Keep this in repo root:
|
||||
|
||||
```ini
|
||||
shamefully-hoist=true
|
||||
```
|
||||
|
||||
**Why?** electron-builder requires all dependencies to be hoisted to the root `node_modules`. Without this, Electron builds will fail with "Cannot find module" errors.
|
||||
This is required for Electron packaging compatibility in this monorepo.
|
||||
|
||||
### pnpm-lock.yaml
|
||||
|
||||
- Auto-generated lockfile
|
||||
- **Never manually edit**
|
||||
- Always regenerate on conflicts
|
||||
|
||||
---
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Install Dependencies
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Install all workspace dependencies
|
||||
pnpm install
|
||||
```
|
||||
|
||||
# Clean install (after changing .npmrc or major updates)
|
||||
## Clean Reinstall (When Needed)
|
||||
|
||||
Use this when lockfile/hoist state is corrupted or after major package-manager config changes:
|
||||
|
||||
```bash
|
||||
rm -rf node_modules apps/*/node_modules packages/*/node_modules
|
||||
rm pnpm-lock.yaml
|
||||
rm -f pnpm-lock.yaml
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Add Dependencies
|
||||
## Build / Check
|
||||
|
||||
```bash
|
||||
# Add to root (shared dev tools)
|
||||
pnpm add -D typescript -w
|
||||
|
||||
# Add to specific package
|
||||
pnpm add lodash --filter @multica/core
|
||||
|
||||
# Add dev dependency to specific package
|
||||
pnpm add -D vitest --filter @multica/core
|
||||
|
||||
# Add workspace dependency (internal package)
|
||||
pnpm add @multica/utils --filter @multica/core --workspace
|
||||
```
|
||||
|
||||
### Update Dependencies
|
||||
|
||||
```bash
|
||||
# Update all
|
||||
pnpm update --recursive
|
||||
|
||||
# Update specific package
|
||||
pnpm update lodash --filter @multica/core
|
||||
|
||||
# Interactive update
|
||||
pnpm update --interactive --recursive
|
||||
```
|
||||
|
||||
### Run Scripts
|
||||
|
||||
```bash
|
||||
# Run script in specific package
|
||||
pnpm --filter @multica/desktop dev
|
||||
pnpm --filter @multica/core build
|
||||
|
||||
# Run script in all packages
|
||||
pnpm --recursive run build
|
||||
|
||||
# Run script in root
|
||||
pnpm multica --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workspace Dependencies
|
||||
|
||||
### Internal References
|
||||
|
||||
Use `workspace:*` for internal dependencies:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@multica/desktop",
|
||||
"dependencies": {
|
||||
"@multica/core": "workspace:*",
|
||||
"@multica/ui": "workspace:*",
|
||||
"@multica/utils": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Direction
|
||||
|
||||
```
|
||||
apps/ → depends on → packages/
|
||||
packages/ui → depends on → packages/core
|
||||
packages/core → depends on → packages/types, packages/utils
|
||||
|
||||
❌ Circular dependencies are forbidden
|
||||
```
|
||||
|
||||
### Catalog (Shared Versions)
|
||||
|
||||
`pnpm-workspace.yaml` defines shared versions:
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
react: "19.2.3"
|
||||
typescript: "^5.9.3"
|
||||
```
|
||||
|
||||
Use in package.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"react": "catalog:"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Branch Merge & Conflicts
|
||||
|
||||
### High-Conflict Files
|
||||
|
||||
| File | Conflict Type | Resolution Strategy |
|
||||
|------|---------------|---------------------|
|
||||
| `pnpm-lock.yaml` | Auto-generated | **Always regenerate** |
|
||||
| `*/package.json` | Version/deps | Manual merge |
|
||||
| `pnpm-workspace.yaml` | Catalog versions | Manual merge |
|
||||
| `turbo.json` | Pipeline config | Manual merge |
|
||||
|
||||
### Resolving pnpm-lock.yaml Conflicts
|
||||
|
||||
**Never manually resolve `pnpm-lock.yaml` conflicts.** It's a machine-generated file with complex checksums.
|
||||
|
||||
```bash
|
||||
# 1. Accept either version (doesn't matter which)
|
||||
git checkout --theirs pnpm-lock.yaml
|
||||
# or
|
||||
git checkout --ours pnpm-lock.yaml
|
||||
|
||||
# 2. Delete and regenerate
|
||||
rm pnpm-lock.yaml
|
||||
pnpm install
|
||||
|
||||
# 3. Stage the new lockfile
|
||||
git add pnpm-lock.yaml
|
||||
|
||||
# 4. Continue with merge
|
||||
git merge --continue
|
||||
# or
|
||||
git commit
|
||||
```
|
||||
|
||||
### Standard Merge Workflow
|
||||
|
||||
```bash
|
||||
# 1. Fetch and merge
|
||||
git fetch origin main
|
||||
git merge origin/main
|
||||
|
||||
# 2. If conflicts in pnpm-lock.yaml:
|
||||
git checkout --theirs pnpm-lock.yaml
|
||||
rm pnpm-lock.yaml
|
||||
pnpm install
|
||||
git add pnpm-lock.yaml
|
||||
|
||||
# 3. Resolve other conflicts manually
|
||||
# Edit conflicted files...
|
||||
git add <resolved-files>
|
||||
|
||||
# 4. Complete merge
|
||||
git commit
|
||||
|
||||
# 5. Verify build
|
||||
pnpm build
|
||||
pnpm typecheck
|
||||
pnpm test
|
||||
```
|
||||
|
||||
### After Major Merges
|
||||
|
||||
Always verify:
|
||||
## Targeted Commands
|
||||
|
||||
```bash
|
||||
pnpm install # Ensure deps are correct
|
||||
pnpm build # Verify build works
|
||||
pnpm test # Run tests
|
||||
pnpm typecheck # Check types
|
||||
pnpm --filter @multica/desktop build
|
||||
pnpm --filter @multica/core build
|
||||
pnpm --filter @multica/web dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Cannot find module" in Electron Build
|
||||
|
||||
**Cause:** electron-builder can't find hoisted dependencies.
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Ensure .npmrc has:
|
||||
echo 'shamefully-hoist=true' > .npmrc
|
||||
|
||||
# Clean reinstall
|
||||
rm -rf node_modules apps/*/node_modules packages/*/node_modules
|
||||
rm pnpm-lock.yaml
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Workspace Protocol Not Resolved
|
||||
|
||||
**Cause:** workspace:* not resolving correctly.
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Check pnpm-workspace.yaml includes the package
|
||||
# Ensure package name matches exactly
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Peer Dependency Warnings
|
||||
|
||||
**Cause:** Missing peer dependencies.
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Usually safe to ignore, but if causing issues:
|
||||
pnpm add <missing-peer> --filter <package>
|
||||
```
|
||||
|
||||
### Build Order Issues
|
||||
|
||||
**Cause:** Turborepo not building dependencies first.
|
||||
|
||||
**Solution:** Check `turbo.json` has correct `dependsOn`:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use pnpm** — Don't mix npm/yarn
|
||||
2. **Commit lockfile** — Always commit `pnpm-lock.yaml` changes
|
||||
3. **Don't edit lockfile manually** — Regenerate on conflicts
|
||||
4. **Use workspace:*** — For internal dependencies
|
||||
5. **Use catalog:** — For shared version management
|
||||
6. **Clean install after .npmrc changes** — Delete node_modules and lockfile
|
||||
7. **Verify after merge** — Run build and tests
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
365
docs/rpc.md
365
docs/rpc.md
|
|
@ -1,365 +0,0 @@
|
|||
# Hub RPC Protocol
|
||||
|
||||
The Hub exposes an RPC (Remote Procedure Call) interface over the Gateway WebSocket transport. Clients can invoke methods on the Hub and receive structured responses, all routed through the same Gateway message layer used for regular chat.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
Client (SDK) Gateway (WebSocket) Hub
|
||||
| | |
|
||||
|-- send(RequestAction) ------->|-- route to Hub ----------->|
|
||||
| | |-- dispatch(method, params)
|
||||
| | |-- handler executes
|
||||
|<-- receive(ResponseAction) ---|<-- route to Client --------|
|
||||
| | |
|
||||
```
|
||||
|
||||
1. The **Client** calls `client.request(hubDeviceId, method, params)`.
|
||||
2. The SDK generates a `requestId` (UUIDv7), wraps it into a `RequestPayload`, and sends a message with `action = "request"` to the Hub via the Gateway.
|
||||
3. The **Gateway** routes the message to the Hub's socket (standard device-to-device routing).
|
||||
4. The **Hub** detects `action === "request"` in its `onMessage` handler and delegates to `RpcDispatcher.dispatch()`.
|
||||
5. The dispatcher looks up the registered handler for the given `method` and invokes it.
|
||||
6. The Hub sends back a message with `action = "response"` containing either a success or error payload, addressed to the original sender.
|
||||
7. The **Client SDK** intercepts incoming `"response"` messages in its `RECEIVE` listener, matches by `requestId`, and resolves (or rejects) the corresponding `Promise`.
|
||||
|
||||
## Message Format
|
||||
|
||||
All RPC messages use the standard `RoutedMessage` envelope:
|
||||
|
||||
```ts
|
||||
interface RoutedMessage<T> {
|
||||
id: string; // UUIDv7 message ID
|
||||
uid: string | null;
|
||||
from: string; // sender deviceId
|
||||
to: string; // recipient deviceId
|
||||
action: string; // "request" or "response"
|
||||
payload: T;
|
||||
}
|
||||
```
|
||||
|
||||
### Request Payload
|
||||
|
||||
```ts
|
||||
interface RequestPayload<T = unknown> {
|
||||
requestId: string; // UUIDv7, generated by the SDK
|
||||
method: string; // RPC method name
|
||||
params?: T; // method-specific parameters
|
||||
}
|
||||
```
|
||||
|
||||
### Response Payload (Success)
|
||||
|
||||
```ts
|
||||
interface ResponseSuccessPayload<T = unknown> {
|
||||
requestId: string; // matches the request
|
||||
ok: true;
|
||||
payload: T; // method-specific result
|
||||
}
|
||||
```
|
||||
|
||||
### Response Payload (Error)
|
||||
|
||||
```ts
|
||||
interface ResponseErrorPayload {
|
||||
requestId: string; // matches the request
|
||||
ok: false;
|
||||
error: {
|
||||
code: string; // machine-readable error code
|
||||
message: string; // human-readable description
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Code | Description |
|
||||
|---|---|
|
||||
| `METHOD_NOT_FOUND` | The requested RPC method does not exist. |
|
||||
| `INVALID_PARAMS` | Missing or malformed parameters. |
|
||||
| `AGENT_NOT_FOUND` | No session file found for the given agent ID. |
|
||||
| `RPC_ERROR` | Catch-all for unexpected errors. |
|
||||
|
||||
## Client SDK Usage
|
||||
|
||||
The `GatewayClient` provides a `request()` method that handles the full request/response lifecycle:
|
||||
|
||||
```ts
|
||||
request<T = unknown>(
|
||||
to: string, // target deviceId (Hub's deviceId)
|
||||
method: string, // RPC method name
|
||||
params?: unknown, // method parameters
|
||||
timeout?: number, // timeout in ms (default: 10000)
|
||||
): Promise<T>
|
||||
```
|
||||
|
||||
The method:
|
||||
- Generates a `requestId` internally.
|
||||
- Sends a `RequestPayload` via the Gateway.
|
||||
- Returns a `Promise` that resolves with the response payload on success, or rejects with an `Error` on failure or timeout.
|
||||
- Automatically cleans up pending requests on disconnect.
|
||||
|
||||
### Example
|
||||
|
||||
```ts
|
||||
import { GatewayClient, type GetAgentMessagesResult } from "@multica/sdk";
|
||||
|
||||
const client = new GatewayClient({
|
||||
url: "http://localhost:3000",
|
||||
deviceId: "my-client",
|
||||
deviceType: "client",
|
||||
});
|
||||
|
||||
client.connect();
|
||||
|
||||
client.onRegistered(async () => {
|
||||
try {
|
||||
const result = await client.request<GetAgentMessagesResult>(
|
||||
"hub-device-id",
|
||||
"getAgentMessages",
|
||||
{ agentId: "019abc12-...", offset: 0, limit: 20 },
|
||||
);
|
||||
console.log(`Total: ${result.total}, returned: ${result.messages.length}`);
|
||||
} catch (err) {
|
||||
console.error("RPC failed:", err.message);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Available RPC Methods
|
||||
|
||||
### `getAgentMessages`
|
||||
|
||||
Retrieves the message history for a given agent session. Works for both active and closed agents as long as the session file exists on disk.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
```ts
|
||||
interface GetAgentMessagesParams {
|
||||
agentId: string; // required - the agent/session ID
|
||||
offset?: number; // starting index (default: 0)
|
||||
limit?: number; // max messages to return (default: 50)
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```ts
|
||||
interface GetAgentMessagesResult {
|
||||
messages: AgentMessage[]; // array of messages
|
||||
total: number; // total message count in the session
|
||||
offset: number; // the offset used
|
||||
limit: number; // the limit used
|
||||
}
|
||||
```
|
||||
|
||||
Each `AgentMessage` in the array is one of:
|
||||
|
||||
- **UserMessage** (`role: "user"`) - User input (text or multimodal content).
|
||||
- **AssistantMessage** (`role: "assistant"`) - LLM response, may contain `TextContent`, `ThinkingContent`, or `ToolCall` blocks. Includes `usage` (token counts and costs), `model`, `provider`, and `stopReason`.
|
||||
- **ToolResultMessage** (`role: "toolResult"`) - Result of a tool invocation, with `toolCallId`, `toolName`, `content`, and `isError`.
|
||||
|
||||
**Example request:**
|
||||
|
||||
```ts
|
||||
const result = await client.request<GetAgentMessagesResult>(
|
||||
hubDeviceId,
|
||||
"getAgentMessages",
|
||||
{ agentId: "019abc12-3def-7000-8000-000000000001", offset: 0, limit: 10 },
|
||||
);
|
||||
```
|
||||
|
||||
**Example success response payload:**
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "019abc12-...",
|
||||
"ok": true,
|
||||
"payload": {
|
||||
"messages": [
|
||||
{ "role": "user", "content": "Hello", "timestamp": 1700000000000 },
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{ "type": "text", "text": "Hi! How can I help?" }],
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"provider": "anthropic",
|
||||
"usage": { "input": 10, "output": 15, "totalTokens": 25 },
|
||||
"stopReason": "end_turn",
|
||||
"timestamp": 1700000001000
|
||||
}
|
||||
],
|
||||
"total": 42,
|
||||
"offset": 0,
|
||||
"limit": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example error response payload:**
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "019abc12-...",
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "AGENT_NOT_FOUND",
|
||||
"message": "No session found for agent: 019abc12-bad-id"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `getHubInfo`
|
||||
|
||||
Returns Hub status information. No parameters required.
|
||||
|
||||
**Response:**
|
||||
|
||||
```ts
|
||||
interface GetHubInfoResult {
|
||||
hubId: string; // Hub device ID
|
||||
url: string; // Current Gateway URL
|
||||
connectionState: string; // "disconnected" | "connecting" | "connected" | "registered"
|
||||
agentCount: number; // Number of active agents
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
const info = await client.request<GetHubInfoResult>(hubDeviceId, "getHubInfo");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `listAgents`
|
||||
|
||||
Lists all active agents. No parameters required.
|
||||
|
||||
**Response:**
|
||||
|
||||
```ts
|
||||
interface ListAgentsResult {
|
||||
agents: { id: string; closed: boolean }[];
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
const result = await client.request<ListAgentsResult>(hubDeviceId, "listAgents");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `createAgent`
|
||||
|
||||
Creates a new agent or restores an existing one.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
```ts
|
||||
interface CreateAgentParams {
|
||||
id?: string; // optional - reuse existing session ID
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```ts
|
||||
interface CreateAgentResult {
|
||||
id: string; // the created/restored agent session ID
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
const result = await client.request<CreateAgentResult>(hubDeviceId, "createAgent");
|
||||
// or with specific ID:
|
||||
const result = await client.request<CreateAgentResult>(hubDeviceId, "createAgent", { id: "existing-id" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `deleteAgent`
|
||||
|
||||
Closes and removes an agent.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
```ts
|
||||
interface DeleteAgentParams {
|
||||
id: string; // required - agent ID to delete
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```ts
|
||||
interface DeleteAgentResult {
|
||||
ok: boolean; // true if agent was found and deleted
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
const result = await client.request<DeleteAgentResult>(hubDeviceId, "deleteAgent", { id: "019abc12-..." });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `updateGateway`
|
||||
|
||||
Reconnects the Hub to a different Gateway URL.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
```ts
|
||||
interface UpdateGatewayParams {
|
||||
url: string; // required - new Gateway URL
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```ts
|
||||
interface UpdateGatewayResult {
|
||||
url: string; // the new URL
|
||||
connectionState: string; // connection state after reconnect
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
const result = await client.request<UpdateGatewayResult>(hubDeviceId, "updateGateway", { url: "http://localhost:4000" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding New RPC Methods
|
||||
|
||||
1. Create a handler file in `src/hub/rpc/handlers/`:
|
||||
|
||||
```ts
|
||||
// src/hub/rpc/handlers/my-method.ts
|
||||
import { RpcError, type RpcHandler } from "../dispatcher.js";
|
||||
|
||||
export function createMyMethodHandler(): RpcHandler {
|
||||
return (params: unknown) => {
|
||||
if (!params || typeof params !== "object") {
|
||||
throw new RpcError("INVALID_PARAMS", "params must be an object");
|
||||
}
|
||||
// ... validate and handle
|
||||
return { /* result */ };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
2. Register it in `src/hub/hub.ts` constructor:
|
||||
|
||||
```ts
|
||||
this.rpc.register("myMethod", createMyMethodHandler());
|
||||
```
|
||||
|
||||
3. (Optional) Add typed params/result interfaces in `packages/sdk/src/actions/rpc.ts` and export them from `packages/sdk/src/actions/index.ts` for client-side type safety.
|
||||
|
|
@ -1,19 +1,86 @@
|
|||
# Skills & Tools
|
||||
# Skills and Tools
|
||||
|
||||
## Skills
|
||||
## Skills Loading Model
|
||||
|
||||
Skills extend agent functionality via `SKILL.md` files. See [Skills Documentation](../packages/core/src/agent/skills/README.md).
|
||||
Skills are loaded from two sources with precedence:
|
||||
|
||||
1. Managed skills: `~/.super-multica/skills/`
|
||||
2. Profile skills: `~/.super-multica/agent-profiles/<profile-id>/skills/`
|
||||
|
||||
Profile skills override managed skills when IDs conflict.
|
||||
|
||||
## Skill File Contract
|
||||
|
||||
A valid skill directory must include:
|
||||
|
||||
- `SKILL.md`
|
||||
|
||||
Optional runtime files:
|
||||
|
||||
- `.env`
|
||||
- helper scripts/assets
|
||||
|
||||
## Current Repo Note
|
||||
|
||||
This repository intentionally keeps docs and bundled skill metadata minimal.
|
||||
If a directory under `skills/` does not contain `SKILL.md`, it will not be loaded as a skill.
|
||||
|
||||
## Skills CLI
|
||||
|
||||
```bash
|
||||
multica skills list # List skills
|
||||
multica skills add owner/repo # Install from GitHub
|
||||
multica skills status # Check status
|
||||
multica skills list
|
||||
multica skills status [id]
|
||||
multica skills install <id>
|
||||
multica skills add <owner/repo[/skill]>
|
||||
multica skills remove <name>
|
||||
```
|
||||
|
||||
Built-in: `commit`, `code-review`, `skill-creator`
|
||||
## Tool System
|
||||
|
||||
## Tools
|
||||
`@multica/core` composes:
|
||||
|
||||
Available tools: `read`, `write`, `edit`, `glob`, `exec`, `process`, `web_fetch`, `web_search`, `memory_search`, `sessions_spawn`
|
||||
- base coding tools (`read/write/edit/...`)
|
||||
- extended tools (`exec`, `process`, `glob`, `web_fetch`, `web_search`, `data`, `cron`, `delegate`)
|
||||
- conditional tools (`memory_search`, `send_file`)
|
||||
|
||||
See [Tools Documentation](../packages/core/src/agent/tools/README.md) for details.
|
||||
Tool errors are wrapped into structured tool results instead of crashing runs.
|
||||
|
||||
## Tool Groups
|
||||
|
||||
Supported group aliases:
|
||||
|
||||
- `group:fs` -> `read, write, edit, glob`
|
||||
- `group:runtime` -> `exec, process`
|
||||
- `group:web` -> `web_search, web_fetch`
|
||||
- `group:memory` -> `memory_search`
|
||||
- `group:subagent` -> `delegate`
|
||||
- `group:cron` -> `cron`
|
||||
- `group:data` -> `data`
|
||||
- `group:core` -> core local/web/data set
|
||||
|
||||
## Tool Policy Example
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
allow: ["group:fs", "web_search", "web_fetch"],
|
||||
deny: ["exec"],
|
||||
byProvider: {
|
||||
"openai": {
|
||||
deny: ["data"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`deny` always has priority over `allow`.
|
||||
|
||||
## Inspect Effective Tools
|
||||
|
||||
```bash
|
||||
multica tools list
|
||||
multica tools list --allow group:fs,web_fetch
|
||||
multica tools list --deny exec
|
||||
multica tools groups
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,884 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Subagent Orchestration Architecture</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--border: #30363d;
|
||||
--text: #e6edf3;
|
||||
--text-muted: #8b949e;
|
||||
--accent: #58a6ff;
|
||||
--green: #3fb950;
|
||||
--orange: #d29922;
|
||||
--red: #f85149;
|
||||
--purple: #bc8cff;
|
||||
--cyan: #39d2c0;
|
||||
--pink: #f778ba;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
padding: 40px 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--purple));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
margin: 48px 0 24px;
|
||||
color: var(--accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
h2::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ── Architecture Diagram ── */
|
||||
.arch-diagram {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.arch-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.arch-box {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 20px 24px;
|
||||
min-width: 200px;
|
||||
max-width: 320px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.arch-box.wide { min-width: 500px; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.arch-box.wide { min-width: 100%; }
|
||||
}
|
||||
|
||||
.arch-box .title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.arch-box .desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.arch-box .file {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-family: 'SF Mono', Consolas, monospace;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.badge-blue { background: rgba(88,166,255,0.15); color: var(--accent); }
|
||||
.badge-green { background: rgba(63,185,80,0.15); color: var(--green); }
|
||||
.badge-purple { background: rgba(188,140,255,0.15); color: var(--purple); }
|
||||
.badge-orange { background: rgba(210,153,34,0.15); color: var(--orange); }
|
||||
.badge-cyan { background: rgba(57,210,192,0.15); color: var(--cyan); }
|
||||
.badge-pink { background: rgba(247,120,186,0.15); color: var(--pink); }
|
||||
|
||||
/* ── SVG Arrows ── */
|
||||
.arrow-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.arrow-section svg {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* ── Call Chain ── */
|
||||
.call-chain {
|
||||
position: relative;
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.call-chain::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: linear-gradient(to bottom, var(--accent), var(--purple), var(--green));
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.chain-step {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px 20px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.chain-step:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.chain-step::before {
|
||||
content: attr(data-step);
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
top: 16px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
.chain-step.phase-spawn::before { background: var(--accent); }
|
||||
.chain-step.phase-watch::before { background: var(--purple); }
|
||||
.chain-step.phase-complete::before { background: var(--green); }
|
||||
.chain-step.phase-cleanup::before { background: var(--orange); }
|
||||
|
||||
.chain-step .step-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.chain-step .step-detail {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.chain-step code {
|
||||
background: rgba(88,166,255,0.1);
|
||||
color: var(--accent);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-family: 'SF Mono', Consolas, monospace;
|
||||
}
|
||||
|
||||
.chain-step .arrow-label {
|
||||
font-size: 11px;
|
||||
color: var(--cyan);
|
||||
font-family: 'SF Mono', Consolas, monospace;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* ── Sequence Diagram ── */
|
||||
.sequence-container {
|
||||
overflow-x: auto;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.sequence-diagram {
|
||||
min-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ── Module Map ── */
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.module-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.module-card .mod-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-family: 'SF Mono', Consolas, monospace;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.module-card .mod-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.module-card .mod-exports {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-family: 'SF Mono', Consolas, monospace;
|
||||
}
|
||||
|
||||
.module-card .mod-exports span {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
/* ── State Machine ── */
|
||||
.state-diagram {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.state-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.state-arrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.state-arrow svg { margin: 0 4px; }
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<h1>Subagent Orchestration Architecture</h1>
|
||||
<p class="subtitle">Super Multica — Parent-child agent spawning, lifecycle management, and result announcement</p>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
<h2>System Architecture</h2>
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
|
||||
<div class="arch-diagram">
|
||||
|
||||
<!-- Row 1: Parent Agent -->
|
||||
<div class="arch-row">
|
||||
<div class="arch-box wide" style="border-color: var(--accent);">
|
||||
<div class="title">
|
||||
<span class="badge badge-blue">Agent</span>
|
||||
Parent Agent (Interactive Session)
|
||||
</div>
|
||||
<div class="desc">
|
||||
User-facing agent with full tool access. Can spawn child agents via <code>sessions_spawn</code> tool.
|
||||
Receives announcement messages when child agents complete.
|
||||
</div>
|
||||
<div class="file">src/agent/runner.ts → tools: sessions_spawn, exec, glob, web_fetch, ...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<div class="arrow-section">
|
||||
<svg width="400" height="48">
|
||||
<defs>
|
||||
<marker id="arrow1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#58a6ff"/>
|
||||
</marker>
|
||||
<marker id="arrow2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#3fb950"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<!-- spawn arrow (down-left) -->
|
||||
<line x1="140" y1="4" x2="80" y2="40" stroke="#58a6ff" stroke-width="2" marker-end="url(#arrow1)"/>
|
||||
<text x="70" y="24" fill="#58a6ff" font-size="11" font-family="monospace">spawn</text>
|
||||
<!-- announce arrow (up-right) -->
|
||||
<line x1="320" y1="40" x2="260" y2="4" stroke="#3fb950" stroke-width="2" stroke-dasharray="6 3" marker-end="url(#arrow2)"/>
|
||||
<text x="282" y="24" fill="#3fb950" font-size="11" font-family="monospace">announce</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Hub + Registry -->
|
||||
<div class="arch-row">
|
||||
<div class="arch-box" style="border-color: var(--purple);">
|
||||
<div class="title">
|
||||
<span class="badge badge-purple">Singleton</span>
|
||||
Hub
|
||||
</div>
|
||||
<div class="desc">
|
||||
Central coordinator. Creates & manages all agents. Provides <code>createSubagent()</code>,
|
||||
<code>getAgent()</code>, <code>closeAgent()</code>. Calls registry init on startup, shutdown on exit.
|
||||
</div>
|
||||
<div class="file">src/hub/hub.ts + hub-singleton.ts</div>
|
||||
</div>
|
||||
|
||||
<div class="arch-box" style="border-color: var(--orange);">
|
||||
<div class="title">
|
||||
<span class="badge badge-orange">Module</span>
|
||||
Subagent Registry
|
||||
</div>
|
||||
<div class="desc">
|
||||
In-memory Map + JSON persistence. Tracks run lifecycle (created → started → ended).
|
||||
Archive sweeper cleans old runs every 60s. Handles crash recovery on restart.
|
||||
</div>
|
||||
<div class="file">src/agent/subagent/registry.ts</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<div class="arrow-section">
|
||||
<svg width="400" height="48">
|
||||
<defs>
|
||||
<marker id="arrow3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#bc8cff"/>
|
||||
</marker>
|
||||
<marker id="arrow4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d29922"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<line x1="120" y1="4" x2="120" y2="40" stroke="#bc8cff" stroke-width="2" marker-end="url(#arrow3)"/>
|
||||
<text x="132" y="26" fill="#bc8cff" font-size="11" font-family="monospace">createSubagent()</text>
|
||||
<line x1="280" y1="4" x2="280" y2="40" stroke="#d29922" stroke-width="2" stroke-dasharray="6 3" marker-end="url(#arrow4)"/>
|
||||
<text x="292" y="26" fill="#d29922" font-size="11" font-family="monospace">watchChildAgent()</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: Child Agent + Announce + Store -->
|
||||
<div class="arch-row">
|
||||
<div class="arch-box" style="border-color: var(--cyan);">
|
||||
<div class="title">
|
||||
<span class="badge badge-cyan">Agent</span>
|
||||
Child AsyncAgent
|
||||
</div>
|
||||
<div class="desc">
|
||||
Isolated agent with <code>isSubagent: true</code>. Restricted tools (no <code>sessions_spawn</code>).
|
||||
Custom system prompt: stay focused, no user messaging, no nested spawning.
|
||||
</div>
|
||||
<div class="file">src/agent/async-agent.ts</div>
|
||||
</div>
|
||||
|
||||
<div class="arch-box" style="border-color: var(--green);">
|
||||
<div class="title">
|
||||
<span class="badge badge-green">Flow</span>
|
||||
Announce Module
|
||||
</div>
|
||||
<div class="desc">
|
||||
Reads child's last assistant reply from session JSONL. Formats announcement message with findings, duration, status.
|
||||
Delivers to parent via <code>parentAgent.write()</code>.
|
||||
</div>
|
||||
<div class="file">src/agent/subagent/announce.ts</div>
|
||||
</div>
|
||||
|
||||
<div class="arch-box" style="border-color: var(--text-muted);">
|
||||
<div class="title">
|
||||
<span class="badge" style="background:rgba(139,148,158,0.15);color:var(--text-muted);">Store</span>
|
||||
Registry Store
|
||||
</div>
|
||||
<div class="desc">
|
||||
JSON file persistence at <code>~/.super-multica/subagents/runs.json</code>.
|
||||
Schema: <code>{ version: 1, runs: {...} }</code>. Survives process restarts.
|
||||
</div>
|
||||
<div class="file">src/agent/subagent/registry-store.ts</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
<h2>Call Chain — Spawn & Lifecycle</h2>
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
|
||||
<div class="call-chain">
|
||||
|
||||
<div class="chain-step phase-spawn" data-step="1">
|
||||
<div class="step-title">Parent Agent invokes <code>sessions_spawn</code> tool</div>
|
||||
<div class="step-detail">
|
||||
Agent calls tool with <code>{ task, label?, model?, cleanup?, timeoutSeconds? }</code>.
|
||||
Guard rejects if <code>isSubagent === true</code>.
|
||||
</div>
|
||||
<div class="arrow-label">sessions-spawn.ts → execute()</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-step phase-spawn" data-step="2">
|
||||
<div class="step-title">Generate IDs & build system prompt</div>
|
||||
<div class="step-detail">
|
||||
<code>runId</code> = UUIDv7, <code>childSessionId</code> = UUIDv7.
|
||||
<code>buildSubagentSystemPrompt()</code> creates prompt with task context, rules (no nested spawn, stay focused).
|
||||
</div>
|
||||
<div class="arrow-label">announce.ts → buildSubagentSystemPrompt()</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-step phase-spawn" data-step="3">
|
||||
<div class="step-title">Hub creates child AsyncAgent</div>
|
||||
<div class="step-detail">
|
||||
<code>hub.createSubagent(childSessionId, { systemPrompt, model })</code>
|
||||
creates an <code>AsyncAgent</code> with <code>isSubagent: true</code>. Not persisted to agent store (ephemeral).
|
||||
</div>
|
||||
<div class="arrow-label">hub.ts → createSubagent()</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-step phase-watch" data-step="4">
|
||||
<div class="step-title">Write task to child (non-blocking)</div>
|
||||
<div class="step-detail">
|
||||
<code>childAgent.write(task)</code> enqueues the task to the serial queue.
|
||||
This happens before registration so <code>waitForIdle()</code> observes queued work.
|
||||
</div>
|
||||
<div class="arrow-label">async-agent.ts → write() (enqueues to serial queue)</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-step phase-spawn" data-step="5">
|
||||
<div class="step-title">Register run in registry</div>
|
||||
<div class="step-detail">
|
||||
<code>registerSubagentRun()</code> saves record to in-memory Map + JSON file.
|
||||
Sets <code>createdAt</code>, starts archive sweeper.
|
||||
</div>
|
||||
<div class="arrow-label">registry.ts → registerSubagentRun()</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-step phase-watch" data-step="6">
|
||||
<div class="step-title">Start lifecycle watcher & return to parent</div>
|
||||
<div class="step-detail">
|
||||
<code>watchChildAgent()</code> sets <code>startedAt</code>.
|
||||
Attaches <code>childAgent.waitForIdle()</code> (promise resolves when task queue drained)
|
||||
and <code>childAgent.onClose()</code> callback. Optionally sets timeout timer.
|
||||
Tool returns <code>{ status: "accepted", childSessionId, runId }</code> immediately.
|
||||
</div>
|
||||
<div class="arrow-label">registry.ts → watchChildAgent() → AsyncAgent.waitForIdle() + onClose()</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-step phase-watch" data-step="7">
|
||||
<div class="step-title">Child agent processes task autonomously</div>
|
||||
<div class="step-detail">
|
||||
Child runs LLM inference with restricted tools. Uses its own session.
|
||||
May call <code>exec</code>, <code>glob</code>, <code>web_fetch</code> etc. but NOT <code>sessions_spawn</code>.
|
||||
</div>
|
||||
<div class="arrow-label">runner.ts → Agent.run() (within AsyncAgent queue)</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-step phase-complete" data-step="8">
|
||||
<div class="step-title">Child completes → <code>waitForIdle()</code> resolves</div>
|
||||
<div class="step-detail">
|
||||
Task queue drains. Watcher's cleanup callback fires: sets <code>endedAt</code>, <code>outcome: { status: "ok" }</code>.
|
||||
Persists updated record to JSON.
|
||||
</div>
|
||||
<div class="arrow-label">registry.ts → cleanup() → handleRunCompletion()</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-step phase-complete" data-step="9">
|
||||
<div class="step-title">Announce flow: read child reply & deliver to parent</div>
|
||||
<div class="step-detail">
|
||||
<code>readLatestAssistantReply(childSessionId)</code> reads session JSONL, extracts last assistant text.
|
||||
<code>formatAnnouncementMessage()</code> builds summary with task, status, findings, runtime.
|
||||
<code>parentAgent.write(message)</code> delivers to parent.
|
||||
</div>
|
||||
<div class="arrow-label">announce.ts → runSubagentAnnounceFlow() → hub.getAgent(parentId).write()</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-step phase-cleanup" data-step="10">
|
||||
<div class="step-title">Session cleanup & archive</div>
|
||||
<div class="step-detail">
|
||||
If <code>cleanup === "delete"</code>: removes child session directory + closes agent in Hub.
|
||||
Schedules archive at <code>now + 60min</code>. Sweeper removes from registry after TTL.
|
||||
</div>
|
||||
<div class="arrow-label">registry.ts → deleteChildSession() + sweep()</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
<h2>Sequence Diagram</h2>
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
|
||||
<div class="sequence-container">
|
||||
<svg class="sequence-diagram" viewBox="0 0 920 620" width="920" height="620">
|
||||
<style>
|
||||
.seq-text { font-family: -apple-system, sans-serif; font-size: 12px; fill: #e6edf3; }
|
||||
.seq-mono { font-family: 'SF Mono', Consolas, monospace; font-size: 11px; }
|
||||
.seq-label { font-size: 13px; font-weight: 600; }
|
||||
.seq-line { stroke: #30363d; stroke-width: 1; }
|
||||
.seq-lifeline { stroke: #30363d; stroke-width: 1; stroke-dasharray: 6 4; }
|
||||
</style>
|
||||
|
||||
<!-- Column headers -->
|
||||
<rect x="40" y="10" width="130" height="36" rx="6" fill="#161b22" stroke="#58a6ff"/>
|
||||
<text x="105" y="33" text-anchor="middle" class="seq-text seq-label" fill="#58a6ff">Parent Agent</text>
|
||||
|
||||
<rect x="230" y="10" width="130" height="36" rx="6" fill="#161b22" stroke="#d29922"/>
|
||||
<text x="295" y="33" text-anchor="middle" class="seq-text seq-label" fill="#d29922">sessions_spawn</text>
|
||||
|
||||
<rect x="420" y="10" width="100" height="36" rx="6" fill="#161b22" stroke="#bc8cff"/>
|
||||
<text x="470" y="33" text-anchor="middle" class="seq-text seq-label" fill="#bc8cff">Hub</text>
|
||||
|
||||
<rect x="580" y="10" width="120" height="36" rx="6" fill="#161b22" stroke="#d29922"/>
|
||||
<text x="640" y="33" text-anchor="middle" class="seq-text seq-label" fill="#d29922">Registry</text>
|
||||
|
||||
<rect x="760" y="10" width="120" height="36" rx="6" fill="#161b22" stroke="#39d2c0"/>
|
||||
<text x="820" y="33" text-anchor="middle" class="seq-text seq-label" fill="#39d2c0">Child Agent</text>
|
||||
|
||||
<!-- Lifelines -->
|
||||
<line x1="105" y1="46" x2="105" y2="610" class="seq-lifeline"/>
|
||||
<line x1="295" y1="46" x2="295" y2="610" class="seq-lifeline"/>
|
||||
<line x1="470" y1="46" x2="470" y2="610" class="seq-lifeline"/>
|
||||
<line x1="640" y1="46" x2="640" y2="610" class="seq-lifeline"/>
|
||||
<line x1="820" y1="46" x2="820" y2="610" class="seq-lifeline"/>
|
||||
|
||||
<!-- 1. Parent → Tool: call sessions_spawn -->
|
||||
<line x1="105" y1="80" x2="290" y2="80" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#seq-arrow-blue)"/>
|
||||
<text x="198" y="74" text-anchor="middle" class="seq-text seq-mono" fill="#58a6ff">execute({ task, label })</text>
|
||||
|
||||
<!-- 2. Tool → Hub: createSubagent -->
|
||||
<line x1="295" y1="115" x2="465" y2="115" stroke="#bc8cff" stroke-width="1.5" marker-end="url(#seq-arrow-purple)"/>
|
||||
<text x="380" y="109" text-anchor="middle" class="seq-text seq-mono" fill="#bc8cff">createSubagent(id, opts)</text>
|
||||
|
||||
<!-- 3. Hub → Child: new AsyncAgent -->
|
||||
<line x1="470" y1="150" x2="815" y2="150" stroke="#39d2c0" stroke-width="1.5" marker-end="url(#seq-arrow-cyan)"/>
|
||||
<text x="640" y="144" text-anchor="middle" class="seq-text seq-mono" fill="#39d2c0">new AsyncAgent({ isSubagent: true })</text>
|
||||
|
||||
<!-- 4. Tool → Child: write(task) -->
|
||||
<line x1="295" y1="190" x2="815" y2="190" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#seq-arrow-blue)"/>
|
||||
<text x="555" y="184" text-anchor="middle" class="seq-text seq-mono" fill="#58a6ff">childAgent.write(task)</text>
|
||||
|
||||
<!-- 5. Tool → Registry: registerSubagentRun -->
|
||||
<line x1="295" y1="225" x2="635" y2="225" stroke="#d29922" stroke-width="1.5" marker-end="url(#seq-arrow-orange)"/>
|
||||
<text x="465" y="219" text-anchor="middle" class="seq-text seq-mono" fill="#d29922">registerSubagentRun(params)</text>
|
||||
|
||||
<!-- 6. Registry → Child: watchChildAgent (waitForIdle) -->
|
||||
<line x1="640" y1="260" x2="815" y2="260" stroke="#d29922" stroke-width="1.5" stroke-dasharray="6 3" marker-end="url(#seq-arrow-orange)"/>
|
||||
<text x="727" y="254" text-anchor="middle" class="seq-text seq-mono" fill="#d29922">waitForIdle() + onClose()</text>
|
||||
|
||||
<!-- 7. Tool → Parent: return accepted -->
|
||||
<line x1="290" y1="295" x2="110" y2="295" stroke="#3fb950" stroke-width="1.5" marker-end="url(#seq-arrow-green)"/>
|
||||
<text x="200" y="289" text-anchor="middle" class="seq-text seq-mono" fill="#3fb950">{ status: "accepted", runId }</text>
|
||||
|
||||
<!-- Async boundary -->
|
||||
<line x1="20" y1="330" x2="900" y2="330" stroke="#30363d" stroke-width="1" stroke-dasharray="3 3"/>
|
||||
<rect x="390" y="320" width="140" height="20" rx="4" fill="#161b22" stroke="#30363d"/>
|
||||
<text x="460" y="335" text-anchor="middle" class="seq-text" fill="#8b949e" style="font-size:11px">async (non-blocking)</text>
|
||||
|
||||
<!-- 8. Child processes task -->
|
||||
<rect x="790" y="355" width="60" height="60" rx="4" fill="rgba(57,210,192,0.1)" stroke="#39d2c0" stroke-dasharray="4 2"/>
|
||||
<text x="820" y="380" text-anchor="middle" class="seq-text" fill="#39d2c0" style="font-size:10px">LLM</text>
|
||||
<text x="820" y="395" text-anchor="middle" class="seq-text" fill="#39d2c0" style="font-size:10px">inference</text>
|
||||
|
||||
<!-- 9. Child → Registry: waitForIdle resolves -->
|
||||
<line x1="815" y1="440" x2="645" y2="440" stroke="#3fb950" stroke-width="1.5" stroke-dasharray="6 3" marker-end="url(#seq-arrow-green)"/>
|
||||
<text x="730" y="434" text-anchor="middle" class="seq-text seq-mono" fill="#3fb950">idle (resolved)</text>
|
||||
|
||||
<!-- 10. Registry: handleRunCompletion -->
|
||||
<rect x="610" y="460" width="60" height="30" rx="4" fill="rgba(210,153,34,0.1)" stroke="#d29922" stroke-dasharray="4 2"/>
|
||||
<text x="640" y="480" text-anchor="middle" class="seq-text" fill="#d29922" style="font-size:10px">announce</text>
|
||||
|
||||
<!-- 11. Registry → Parent: announcement -->
|
||||
<line x1="635" y1="510" x2="110" y2="510" stroke="#3fb950" stroke-width="1.5" marker-end="url(#seq-arrow-green)"/>
|
||||
<text x="372" y="504" text-anchor="middle" class="seq-text seq-mono" fill="#3fb950">parentAgent.write(announcement)</text>
|
||||
|
||||
<!-- 12. Registry: cleanup -->
|
||||
<line x1="640" y1="545" x2="815" y2="545" stroke="#f85149" stroke-width="1.5" marker-end="url(#seq-arrow-red)"/>
|
||||
<text x="727" y="539" text-anchor="middle" class="seq-text seq-mono" fill="#f85149">deleteChildSession()</text>
|
||||
|
||||
<!-- 13. Registry: schedule archive -->
|
||||
<rect x="610" y="565" width="60" height="25" rx="4" fill="rgba(210,153,34,0.1)" stroke="#d29922" stroke-dasharray="4 2"/>
|
||||
<text x="640" y="582" text-anchor="middle" class="seq-text" fill="#d29922" style="font-size:9px">archive 60m</text>
|
||||
|
||||
<!-- Arrow markers -->
|
||||
<defs>
|
||||
<marker id="seq-arrow-blue" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#58a6ff"/>
|
||||
</marker>
|
||||
<marker id="seq-arrow-green" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#3fb950"/>
|
||||
</marker>
|
||||
<marker id="seq-arrow-purple" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#bc8cff"/>
|
||||
</marker>
|
||||
<marker id="seq-arrow-orange" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d29922"/>
|
||||
</marker>
|
||||
<marker id="seq-arrow-cyan" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#39d2c0"/>
|
||||
</marker>
|
||||
<marker id="seq-arrow-red" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#f85149"/>
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
<h2>Run State Machine</h2>
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
|
||||
<div class="state-diagram" style="flex-wrap: wrap; gap: 12px;">
|
||||
<div class="state-node" style="background:rgba(88,166,255,0.12); border:1px solid var(--accent);">
|
||||
<svg width="10" height="10"><circle cx="5" cy="5" r="5" fill="#58a6ff"/></svg>
|
||||
created
|
||||
</div>
|
||||
<div class="state-arrow">
|
||||
<svg width="24" height="12">
|
||||
<defs>
|
||||
<marker id="sm-arrow-1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">
|
||||
<path d="M0 0 L10 5 L0 10z" fill="#8b949e"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<path d="M0 6 L20 6" stroke="#8b949e" stroke-width="1.5" marker-end="url(#sm-arrow-1)"/>
|
||||
</svg>
|
||||
startedAt
|
||||
</div>
|
||||
<div class="state-node" style="background:rgba(188,140,255,0.12); border:1px solid var(--purple);">
|
||||
<svg width="10" height="10"><circle cx="5" cy="5" r="5" fill="#bc8cff"/></svg>
|
||||
started
|
||||
</div>
|
||||
<div class="state-arrow">
|
||||
<svg width="24" height="12">
|
||||
<defs>
|
||||
<marker id="sm-arrow-2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">
|
||||
<path d="M0 0 L10 5 L0 10z" fill="#8b949e"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<path d="M0 6 L20 6" stroke="#8b949e" stroke-width="1.5" marker-end="url(#sm-arrow-2)"/>
|
||||
</svg>
|
||||
endedAt
|
||||
</div>
|
||||
<div class="state-node" style="background:rgba(63,185,80,0.12); border:1px solid var(--green);">
|
||||
<svg width="10" height="10"><circle cx="5" cy="5" r="5" fill="#3fb950"/></svg>
|
||||
ended
|
||||
</div>
|
||||
<div class="state-arrow">
|
||||
<svg width="24" height="12">
|
||||
<defs>
|
||||
<marker id="sm-arrow-3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">
|
||||
<path d="M0 0 L10 5 L0 10z" fill="#8b949e"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<path d="M0 6 L20 6" stroke="#8b949e" stroke-width="1.5" marker-end="url(#sm-arrow-3)"/>
|
||||
</svg>
|
||||
announce
|
||||
</div>
|
||||
<div class="state-node" style="background:rgba(210,153,34,0.12); border:1px solid var(--orange);">
|
||||
<svg width="10" height="10"><circle cx="5" cy="5" r="5" fill="#d29922"/></svg>
|
||||
cleanup done
|
||||
</div>
|
||||
<div class="state-arrow">
|
||||
<svg width="24" height="12">
|
||||
<defs>
|
||||
<marker id="sm-arrow-4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">
|
||||
<path d="M0 0 L10 5 L0 10z" fill="#8b949e"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<path d="M0 6 L20 6" stroke="#8b949e" stroke-width="1.5" marker-end="url(#sm-arrow-4)"/>
|
||||
</svg>
|
||||
60 min
|
||||
</div>
|
||||
<div class="state-node" style="background:rgba(139,148,158,0.12); border:1px solid var(--text-muted);">
|
||||
<svg width="10" height="10"><circle cx="5" cy="5" r="5" fill="#8b949e"/></svg>
|
||||
archived
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; padding: 16px 20px; background: var(--surface); border: 1px solid var(--border); border-radius: 10px;">
|
||||
<div style="font-size: 13px; font-weight: 600; margin-bottom: 8px;">Outcome Status Values</div>
|
||||
<div style="display: flex; gap: 24px; flex-wrap: wrap; font-size: 12px;">
|
||||
<span><code style="color:var(--green)">ok</code> — Task completed normally (waitForIdle resolved)</span>
|
||||
<span><code style="color:var(--red)">error</code> — Child agent threw an error</span>
|
||||
<span><code style="color:var(--orange)">timeout</code> — Exceeded timeoutSeconds limit</span>
|
||||
<span><code style="color:var(--text-muted)">unknown</code> — Process crash or Hub shutdown</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
<h2>Module Map</h2>
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
|
||||
<div class="module-grid">
|
||||
<div class="module-card" style="border-left: 3px solid var(--cyan);">
|
||||
<div class="mod-name">src/agent/subagent/types.ts</div>
|
||||
<div class="mod-desc">Core type definitions</div>
|
||||
<div class="mod-exports">
|
||||
<span>export</span> SubagentRunOutcome<br>
|
||||
<span>export</span> SubagentRunRecord<br>
|
||||
<span>export</span> RegisterSubagentRunParams<br>
|
||||
<span>export</span> SubagentAnnounceParams<br>
|
||||
<span>export</span> SubagentSystemPromptParams
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="module-card" style="border-left: 3px solid var(--orange);">
|
||||
<div class="mod-name">src/agent/subagent/registry.ts</div>
|
||||
<div class="mod-desc">In-memory registry + lifecycle watcher</div>
|
||||
<div class="mod-exports">
|
||||
<span>export</span> initSubagentRegistry()<br>
|
||||
<span>export</span> registerSubagentRun()<br>
|
||||
<span>export</span> listSubagentRuns()<br>
|
||||
<span>export</span> releaseSubagentRun()<br>
|
||||
<span>export</span> getSubagentRun()<br>
|
||||
<span>export</span> shutdownSubagentRegistry()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="module-card" style="border-left: 3px solid var(--text-muted);">
|
||||
<div class="mod-name">src/agent/subagent/registry-store.ts</div>
|
||||
<div class="mod-desc">JSON file persistence</div>
|
||||
<div class="mod-exports">
|
||||
<span>export</span> loadSubagentRuns()<br>
|
||||
<span>export</span> saveSubagentRuns()<br>
|
||||
<span>export</span> getSubagentStorePath()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="module-card" style="border-left: 3px solid var(--green);">
|
||||
<div class="mod-name">src/agent/subagent/announce.ts</div>
|
||||
<div class="mod-desc">Result propagation child → parent</div>
|
||||
<div class="mod-exports">
|
||||
<span>export</span> buildSubagentSystemPrompt()<br>
|
||||
<span>export</span> readLatestAssistantReply()<br>
|
||||
<span>export</span> formatAnnouncementMessage()<br>
|
||||
<span>export</span> runSubagentAnnounceFlow()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="module-card" style="border-left: 3px solid var(--accent);">
|
||||
<div class="mod-name">src/agent/tools/sessions-spawn.ts</div>
|
||||
<div class="mod-desc">Tool definition for parent agents</div>
|
||||
<div class="mod-exports">
|
||||
<span>export</span> createSessionsSpawnTool()<br>
|
||||
schema: { task, label?, model?,<br>
|
||||
cleanup?, timeoutSeconds? }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="module-card" style="border-left: 3px solid var(--purple);">
|
||||
<div class="mod-name">src/hub/hub-singleton.ts</div>
|
||||
<div class="mod-desc">Global Hub access for tools & registry</div>
|
||||
<div class="mod-exports">
|
||||
<span>export</span> setHub(hub)<br>
|
||||
<span>export</span> getHub()<br>
|
||||
<span>export</span> isHubInitialized()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
<h2>Key Design Decisions</h2>
|
||||
<!-- ══════════════════════════════════════════════════════ -->
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 16px;">
|
||||
<div class="module-card">
|
||||
<div class="mod-name" style="color: var(--accent); font-family: inherit;">waitForIdle() vs stream consumption</div>
|
||||
<div class="mod-desc" style="font-size: 13px; margin-top: 8px;">
|
||||
Channel is single-reader. <code>Hub.consumeAgent()</code> already reads the stream for forwarding events.
|
||||
Registry uses <code>waitForIdle()</code> (promise on internal task queue) to detect completion without competing for the stream.
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="mod-name" style="color: var(--accent); font-family: inherit;">Singleton Hub access</div>
|
||||
<div class="mod-desc" style="font-size: 13px; margin-top: 8px;">
|
||||
Tools and registry modules cannot receive Hub via constructor injection (tools are created before Hub exists).
|
||||
A module-level singleton (<code>setHub</code>/<code>getHub</code>) bridges this gap, with <code>isHubInitialized()</code> guard for test safety.
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="mod-name" style="color: var(--accent); font-family: inherit;">Crash recovery</div>
|
||||
<div class="mod-desc" style="font-size: 13px; margin-top: 8px;">
|
||||
Runs are persisted to JSON after every state change. On restart, <code>initSubagentRegistry()</code>
|
||||
loads persisted runs: completed-but-unannounced runs trigger announce flow; unfinished runs are marked as
|
||||
<code>status: "unknown"</code>.
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="mod-name" style="color: var(--accent); font-family: inherit;">Subagent isolation</div>
|
||||
<div class="mod-desc" style="font-size: 13px; margin-top: 8px;">
|
||||
Child agents have <code>isSubagent: true</code> which applies tool deny-list (blocks <code>sessions_spawn</code>).
|
||||
System prompt explicitly forbids nested spawning, direct user communication, and off-topic work.
|
||||
Sessions are ephemeral — deleted after announce unless <code>cleanup: "keep"</code>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; color: var(--text-muted); font-size: 12px; margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--border);">
|
||||
Super Multica — Subagent Orchestration System — Branch: subagent-orchestration
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
# SWE-bench: Agent Coding Benchmark
|
||||
|
||||
Run and evaluate the Multica agent against [SWE-bench](https://www.swebench.com/), the standard benchmark for AI coding agents. SWE-bench tasks are real GitHub issues from open-source Python projects — the agent must read the issue, explore the codebase, and produce a patch that fixes the bug.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Download dataset (requires: pip install datasets)
|
||||
python scripts/swe-bench/download-dataset.py --dataset lite --limit 5
|
||||
|
||||
# 2. Run the agent
|
||||
npx tsx scripts/swe-bench/run.ts --limit 5
|
||||
|
||||
# 3. Analyze results
|
||||
npx tsx scripts/swe-bench/analyze.ts
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
```
|
||||
scripts/swe-bench/
|
||||
├── download-dataset.py # Download from HuggingFace → JSONL
|
||||
├── run.ts # Core runner: Agent API → git diff → predictions
|
||||
├── evaluate.sh # Official Docker evaluation harness wrapper
|
||||
├── analyze.ts # Summarize run results
|
||||
└── .gitignore # Ignores downloaded datasets and output files
|
||||
```
|
||||
|
||||
## Pipeline
|
||||
|
||||
```
|
||||
┌──────────────────┐
|
||||
HuggingFace ──download──► JSONL ──┤ For each task: │
|
||||
│ 1. git clone │
|
||||
│ 2. git checkout │
|
||||
│ 3. Agent.run() │
|
||||
│ 4. git diff │
|
||||
└────────┬─────────┘
|
||||
│
|
||||
predictions.jsonl (SWE-bench format)
|
||||
│
|
||||
┌───────────────┴───────────────┐
|
||||
│ swebench.harness (Docker) │
|
||||
│ Apply patch → run tests │
|
||||
│ → pass/fail verdict │
|
||||
└───────────────────────────────┘
|
||||
```
|
||||
|
||||
## Dataset Variants
|
||||
|
||||
| Variant | Size | HuggingFace ID | Recommended For |
|
||||
|---------|------|----------------|-----------------|
|
||||
| **Lite** | 300 tasks | `princeton-nlp/SWE-bench_Lite` | Quick iteration, development |
|
||||
| **Verified** | 500 tasks | `princeton-nlp/SWE-bench_Verified` | Official benchmarking, leaderboard |
|
||||
| **Full** | ~2294 tasks | `princeton-nlp/SWE-bench` | Comprehensive evaluation |
|
||||
|
||||
```bash
|
||||
# Download specific variant
|
||||
python scripts/swe-bench/download-dataset.py --dataset verified
|
||||
python scripts/swe-bench/download-dataset.py --dataset lite --limit 20
|
||||
```
|
||||
|
||||
## Runner Options
|
||||
|
||||
```bash
|
||||
npx tsx scripts/swe-bench/run.ts [options]
|
||||
|
||||
Options:
|
||||
--dataset PATH JSONL dataset path (default: scripts/swe-bench/lite.jsonl)
|
||||
--provider NAME LLM provider (default: kimi-coding)
|
||||
--model NAME Model override
|
||||
--limit N Max tasks to run (default: all)
|
||||
--offset N Skip first N tasks (default: 0)
|
||||
--output PATH Output predictions JSONL (default: scripts/swe-bench/predictions.jsonl)
|
||||
--workdir PATH Repo clone directory (default: /tmp/swe-bench)
|
||||
--timeout MS Per-task timeout (default: 300000 = 5min)
|
||||
--instance ID Run a single instance
|
||||
--debug Enable debug logging
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Run 10 tasks with Anthropic Claude
|
||||
npx tsx scripts/swe-bench/run.ts --limit 10 --provider anthropic
|
||||
|
||||
# Run a specific instance
|
||||
npx tsx scripts/swe-bench/run.ts --instance "django__django-16379"
|
||||
|
||||
# Resume from task 50 with longer timeout
|
||||
npx tsx scripts/swe-bench/run.ts --offset 50 --limit 10 --timeout 600000
|
||||
|
||||
# Compare providers (run separately, different output files)
|
||||
npx tsx scripts/swe-bench/run.ts --provider kimi-coding --output scripts/swe-bench/pred-kimi.jsonl
|
||||
npx tsx scripts/swe-bench/run.ts --provider anthropic --output scripts/swe-bench/pred-claude.jsonl
|
||||
```
|
||||
|
||||
## How the Agent Solves Tasks
|
||||
|
||||
For each task, the runner:
|
||||
|
||||
1. **Clones the repository** to `/tmp/swe-bench/<instance_id>/` and checks out `base_commit`
|
||||
2. **Creates an Agent** with a focused system prompt and restricted tools (coding only — no web, no cron, no sessions)
|
||||
3. **Runs the agent** with the issue description as the prompt
|
||||
4. **Collects `git diff`** as the patch after the agent finishes
|
||||
5. **Appends** the prediction to `predictions.jsonl` in SWE-bench format
|
||||
|
||||
The agent has access to:
|
||||
- `read`, `write`, `edit` — file operations
|
||||
- `exec`, `process` — shell commands (for exploring code, running tests)
|
||||
- `glob` — file search
|
||||
|
||||
Tools explicitly denied: `web_fetch`, `web_search`, `cron`, `data`, `sessions_spawn`, `sessions_list`, `memory_search`, `send_file`.
|
||||
|
||||
## Output Files
|
||||
|
||||
After a run, two files are produced:
|
||||
|
||||
### `predictions.jsonl` — SWE-bench format
|
||||
|
||||
```json
|
||||
{"instance_id": "astropy__astropy-12907", "model_patch": "diff --git a/...", "model_name_or_path": "multica-kimi-coding"}
|
||||
```
|
||||
|
||||
This file is the input to the official evaluation harness.
|
||||
|
||||
### `predictions.results.jsonl` — detailed run metrics
|
||||
|
||||
```json
|
||||
{
|
||||
"instance_id": "astropy__astropy-12907",
|
||||
"success": true,
|
||||
"patch": "diff --git a/...",
|
||||
"error": null,
|
||||
"duration_ms": 141892,
|
||||
"session_id": "019c60c7-52ac-702a-9b9c-dc53c0daea6b"
|
||||
}
|
||||
```
|
||||
|
||||
## Analyzing Results
|
||||
|
||||
```bash
|
||||
# Summary report
|
||||
npx tsx scripts/swe-bench/analyze.ts
|
||||
|
||||
# Or specify a results file
|
||||
npx tsx scripts/swe-bench/analyze.ts scripts/swe-bench/pred-kimi.results.jsonl
|
||||
```
|
||||
|
||||
Output includes:
|
||||
- Patch rate (how many tasks produced a diff)
|
||||
- Duration statistics (avg/min/max)
|
||||
- Error breakdown
|
||||
- Per-repository stats
|
||||
- Slowest tasks
|
||||
|
||||
### Run-Log Analysis
|
||||
|
||||
Each agent session writes a structured `run-log.jsonl` to `~/.super-multica/sessions/<session-id>/`. This captures every LLM call, tool invocation, and timing:
|
||||
|
||||
```bash
|
||||
# Find a session's run log
|
||||
cat ~/.super-multica/sessions/<session-id>/run-log.jsonl | head -5
|
||||
|
||||
# Quick stats from a run log
|
||||
cat ~/.super-multica/sessions/<session-id>/run-log.jsonl | python3 -c "
|
||||
import json, sys
|
||||
events = [json.loads(l) for l in sys.stdin if l.strip()]
|
||||
tools = [e for e in events if e['event'] == 'tool_start']
|
||||
llm_ms = sum(e.get('duration_ms', 0) for e in events if e['event'] == 'llm_result')
|
||||
print(f'LLM time: {llm_ms/1000:.1f}s | Tool calls: {len(tools)}')
|
||||
"
|
||||
```
|
||||
|
||||
## Official Evaluation (Docker)
|
||||
|
||||
The runner produces patches, but **only the official SWE-bench harness determines pass/fail** by applying the patch and running the project's test suite.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker running (at least 120GB storage, 16GB RAM, 8 CPU cores)
|
||||
- `pip install swebench`
|
||||
|
||||
### Run Evaluation
|
||||
|
||||
```bash
|
||||
# Using the wrapper script
|
||||
bash scripts/swe-bench/evaluate.sh
|
||||
|
||||
# Or directly
|
||||
python -m swebench.harness.run_evaluation \
|
||||
--dataset_name princeton-nlp/SWE-bench_Lite \
|
||||
--predictions_path scripts/swe-bench/predictions.jsonl \
|
||||
--max_workers 4 \
|
||||
--run_id multica
|
||||
```
|
||||
|
||||
Results are written to `logs/` and `evaluation_results/`.
|
||||
|
||||
## Known Limitations and Improvements
|
||||
|
||||
### Current Limitations
|
||||
|
||||
1. **No Docker isolation for agent execution**: The agent runs on the host, so `pip install` and other commands affect the system Python. SWE-bench standard practice is to run each task in a Docker container.
|
||||
|
||||
2. **`SMC_DATA_DIR` timing**: Setting `SMC_DATA_DIR` at runtime doesn't affect `DATA_DIR` (resolved at module import time). Sessions currently write to `~/.super-multica/sessions/`. To isolate, set the env var before the process starts:
|
||||
```bash
|
||||
SMC_DATA_DIR=~/.swe-bench-eval npx tsx scripts/swe-bench/run.ts --limit 5
|
||||
```
|
||||
|
||||
3. **Sequential execution**: Tasks run one at a time. For large-scale runs, launch multiple processes with `--offset`/`--limit` to parallelize:
|
||||
```bash
|
||||
# Run 4 workers in parallel
|
||||
npx tsx scripts/swe-bench/run.ts --offset 0 --limit 75 --output pred-0.jsonl &
|
||||
npx tsx scripts/swe-bench/run.ts --offset 75 --limit 75 --output pred-1.jsonl &
|
||||
npx tsx scripts/swe-bench/run.ts --offset 150 --limit 75 --output pred-2.jsonl &
|
||||
npx tsx scripts/swe-bench/run.ts --offset 225 --limit 75 --output pred-3.jsonl &
|
||||
wait
|
||||
cat pred-*.jsonl > predictions.jsonl
|
||||
```
|
||||
|
||||
4. **Repo cloning per instance**: Each instance clones the full repo. For repos with many tasks (e.g., astropy, django), a shared clone with `git worktree` would be faster.
|
||||
|
||||
### Potential Improvements
|
||||
|
||||
- **Docker-per-task**: Run each agent in a Docker container matching the SWE-bench environment spec (correct Python version, pre-installed dependencies)
|
||||
- **Shared repo pool**: Clone each unique repo once, use `git worktree` for per-task isolation
|
||||
- **Cost tracking**: Parse run-log token counts for per-task and aggregate cost estimates
|
||||
- **Multi-turn retries**: If the agent produces no patch, retry with feedback
|
||||
- **System prompt tuning**: The current prompt is minimal; more detailed guidance (e.g., "search for related test files to understand expected behavior") could improve solve rate
|
||||
|
||||
## Related Benchmarks
|
||||
|
||||
| Benchmark | Focus | Notes |
|
||||
|-----------|-------|-------|
|
||||
| [SWE-bench Verified](https://openai.com/index/introducing-swe-bench-verified/) | Bug fixing (Python) | Gold standard, 500 human-verified tasks |
|
||||
| [SWE-bench Multilingual](https://github.com/SWE-bench/SWE-bench) | Bug fixing (7 languages) | Java, TS, JS, Go, Rust, C, C++ |
|
||||
| [Terminal-Bench](https://www.swebench.com/) | CLI workflows | Multi-step sandboxed terminal tasks |
|
||||
| [Aider Polyglot](https://aider.chat/docs/leaderboards/) | Code editing | 225 Exercism exercises, 6 languages |
|
||||
| [DPAI Arena](https://www.jetbrains.com/) | Full dev workflow | JetBrains: patch, test, review, analysis |
|
||||
| [HumanEval](https://github.com/openai/human-eval) | Function generation | 164 Python function tasks, largely saturated |
|
||||
|
||||
## Initial Results (kimi-coding, 3 tasks)
|
||||
|
||||
First run on 3 SWE-bench Lite tasks (all astropy):
|
||||
|
||||
| Task | Status | Duration | LLM Time | Tools | Fix |
|
||||
|------|--------|----------|----------|-------|-----|
|
||||
| `astropy__astropy-12907` | PATCHED | 141.9s | 125.1s | 30 | `_cstack`: `= 1` → `= right` |
|
||||
| `astropy__astropy-14182` | PATCHED | 192.0s | 166.9s | 56 | Added `header_rows` param to RST writer |
|
||||
| `astropy__astropy-14365` | PATCHED | 65.7s | 49.6s | 23 | `re.compile()` + `re.IGNORECASE` |
|
||||
|
||||
3/3 tasks produced patches. Formal evaluation pending (requires Docker harness).
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# Time Injection Design
|
||||
|
||||
Super Multica uses **message-level timestamp injection** for time awareness.
|
||||
Instead of placing dynamic time text in the system prompt, user turns are stamped at runtime.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Incoming turn] --> B{Entry point}
|
||||
B -->|Desktop/Gateway/Cron/Subagent| C[AsyncAgent.write]
|
||||
B -->|Heartbeat poll| D[AsyncAgent.write injectTimestamp=false]
|
||||
C --> E{Already stamped or has 'Current time:'?}
|
||||
E -->|Yes| F[Keep original message]
|
||||
E -->|No| G[Prefix: [DOW YYYY-MM-DD HH:mm TZ]]
|
||||
D --> H[Keep original heartbeat prompt]
|
||||
F --> I[Agent.run]
|
||||
G --> I
|
||||
H --> I
|
||||
I --> J[LLM receives final turn text]
|
||||
```
|
||||
|
||||
## Injection Matrix
|
||||
|
||||
| Path | Runtime call | Timestamp injected? | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Desktop direct chat | `agent.write(content)` | Yes | Default behavior |
|
||||
| Gateway/remote chat | `agent.write(content)` | Yes | Same entry path as desktop |
|
||||
| `sessions_spawn` child task | `childAgent.write(task)` | Yes | Child turn gets current time context |
|
||||
| Cron `agent-turn` payload | `agent.write(cronMessage)` | Yes (guarded) | Skips if message already carries `Current time:` |
|
||||
| Heartbeat runner | `agent.write(prompt, { injectTimestamp: false })` | No | Prevents heartbeat prompt matching from breaking |
|
||||
| Internal orchestration | `writeInternal(...)` | No | Uses separate internal run path |
|
||||
|
||||
## Why This Design
|
||||
|
||||
- Keeps system prompt cache-stable (no per-turn date churn in system prompt text)
|
||||
- Gives the model an explicit "now" reference on each user turn
|
||||
- Uses guardrails to avoid double-stamping and heartbeat regressions
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
# Agent Profile System
|
||||
|
||||
The Agent Profile system allows you to define and manage agent personalities, capabilities, and configurations. Each profile is a collection of markdown files and a JSON configuration file stored in a directory.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
~/.super-multica/agent-profiles/
|
||||
└── <profile-id>/
|
||||
├── soul.md # Personality constraints and behavior style
|
||||
├── identity.md # Agent's name and self-awareness
|
||||
├── tools.md # Custom tool usage instructions
|
||||
├── memory.md # Persistent knowledge base
|
||||
├── bootstrap.md # Guidance for each conversation start
|
||||
└── config.json # Profile configuration (tools, provider, model)
|
||||
```
|
||||
|
||||
## Profile Files
|
||||
|
||||
### soul.md
|
||||
Defines the agent's personality constraints and behavior boundaries.
|
||||
|
||||
```markdown
|
||||
# Soul
|
||||
|
||||
You are a helpful AI assistant. Follow these guidelines:
|
||||
|
||||
- Be concise and direct in your responses
|
||||
- Ask clarifying questions when requirements are ambiguous
|
||||
- Admit when you don't know something
|
||||
```
|
||||
|
||||
### identity.md
|
||||
Contains the agent's identity information.
|
||||
|
||||
```markdown
|
||||
# Identity
|
||||
|
||||
- Name: CodeBot
|
||||
- Role: Software development assistant
|
||||
```
|
||||
|
||||
### tools.md
|
||||
Custom instructions for tool usage (appended to the system prompt).
|
||||
|
||||
### memory.md
|
||||
Persistent knowledge base that survives across conversations.
|
||||
|
||||
### bootstrap.md
|
||||
Guidance information provided at the start of each conversation.
|
||||
|
||||
### config.json
|
||||
JSON configuration for the profile:
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"profile": "coding",
|
||||
"allow": ["web_fetch"],
|
||||
"deny": ["exec"]
|
||||
},
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"thinkingLevel": "medium"
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### tools
|
||||
Tool policy configuration. See [Tools README](../tools/README.md) for details.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `profile` | string | Base profile: `minimal`, `coding`, `web`, `full` |
|
||||
| `allow` | string[] | Additional tools to allow (supports `group:*` syntax) |
|
||||
| `deny` | string[] | Tools to block (takes precedence over allow) |
|
||||
| `byProvider` | object | Provider-specific tool rules |
|
||||
|
||||
Example configurations:
|
||||
|
||||
```json
|
||||
// Minimal - only file operations
|
||||
{
|
||||
"tools": {
|
||||
"profile": "minimal",
|
||||
"allow": ["group:fs"]
|
||||
}
|
||||
}
|
||||
|
||||
// Coding without web access
|
||||
{
|
||||
"tools": {
|
||||
"profile": "coding",
|
||||
"deny": ["group:web"]
|
||||
}
|
||||
}
|
||||
|
||||
// Full access except shell execution
|
||||
{
|
||||
"tools": {
|
||||
"deny": ["exec", "process"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### provider
|
||||
Default LLM provider for this profile.
|
||||
|
||||
### model
|
||||
Default model ID for this profile.
|
||||
|
||||
### thinkingLevel
|
||||
Default thinking level: `none`, `low`, `medium`, `high`.
|
||||
|
||||
## Usage
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Use a specific profile
|
||||
pnpm agent:cli --profile my-agent "Hello"
|
||||
|
||||
# Profile with custom base directory
|
||||
pnpm agent:cli --profile my-agent --profile-dir /path/to/profiles "Hello"
|
||||
```
|
||||
|
||||
### Programmatic
|
||||
|
||||
```typescript
|
||||
import { ProfileManager } from "./profile/index.js";
|
||||
|
||||
// Load existing profile
|
||||
const manager = new ProfileManager({
|
||||
profileId: "my-agent",
|
||||
baseDir: "/custom/path", // optional
|
||||
});
|
||||
|
||||
// Get profile (returns undefined if not exists)
|
||||
const profile = manager.getProfile();
|
||||
|
||||
// Get or create with defaults
|
||||
const profile = manager.getOrCreateProfile(true); // useTemplates
|
||||
|
||||
// Build system prompt from profile
|
||||
const systemPrompt = manager.buildSystemPrompt();
|
||||
|
||||
// Get tools configuration
|
||||
const toolsConfig = manager.getToolsConfig();
|
||||
|
||||
// Get full profile config
|
||||
const config = manager.getProfileConfig();
|
||||
```
|
||||
|
||||
## Config Priority
|
||||
|
||||
When using a profile, configurations are merged with CLI options:
|
||||
|
||||
1. **Profile config.json** - Base configuration
|
||||
2. **CLI options** - Override profile settings
|
||||
|
||||
```bash
|
||||
# Profile has tools.profile = "coding"
|
||||
# CLI adds --tools-deny exec
|
||||
# Result: coding profile without exec tool
|
||||
pnpm agent:cli --profile my-agent --tools-deny exec "list files"
|
||||
```
|
||||
|
||||
The merge behavior:
|
||||
- `profile`: CLI wins if specified
|
||||
- `allow`: Union of both lists
|
||||
- `deny`: Union of both lists
|
||||
- `byProvider`: Deep merge with CLI taking precedence
|
||||
|
||||
## Creating a Profile
|
||||
|
||||
### Manual Creation
|
||||
|
||||
1. Create directory: `mkdir -p ~/.super-multica/agent-profiles/my-agent`
|
||||
2. Create markdown files (soul.md, identity.md, etc.)
|
||||
3. Create config.json with your settings
|
||||
|
||||
### Programmatic Creation
|
||||
|
||||
```typescript
|
||||
import { createAgentProfile } from "./profile/index.js";
|
||||
|
||||
// Create with default templates
|
||||
const profile = createAgentProfile("my-agent", {
|
||||
useTemplates: true, // Fill with default content
|
||||
});
|
||||
|
||||
// Create empty profile
|
||||
const profile = createAgentProfile("minimal-agent", {
|
||||
useTemplates: false,
|
||||
});
|
||||
```
|
||||
|
|
@ -1,438 +0,0 @@
|
|||
# Skills System
|
||||
|
||||
[English](./README.md) | [中文](./README.zh-CN.md)
|
||||
|
||||
Skills extend agent capabilities through `SKILL.md` definition files.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [SKILL.md Specification](#skillmd-specification)
|
||||
- [Skill Invocation](#skill-invocation)
|
||||
- [Loading & Precedence](#loading--precedence)
|
||||
- [CLI Commands](#cli-commands)
|
||||
|
||||
---
|
||||
|
||||
## SKILL.md Specification
|
||||
|
||||
Each skill is a directory containing a `SKILL.md` file with YAML frontmatter + Markdown content.
|
||||
|
||||
### Basic Structure
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: My Skill
|
||||
version: 1.0.0
|
||||
description: What this skill does
|
||||
metadata:
|
||||
emoji: "🔧"
|
||||
requires:
|
||||
bins: [git]
|
||||
---
|
||||
|
||||
# Instructions
|
||||
|
||||
Detailed instructions injected into the agent's system prompt...
|
||||
```
|
||||
|
||||
### Frontmatter Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | string | Yes | Display name |
|
||||
| `version` | string | No | Version number |
|
||||
| `description` | string | No | Short description |
|
||||
| `homepage` | string | No | Homepage URL |
|
||||
| `metadata` | object | No | See below |
|
||||
| `config` | object | No | See below |
|
||||
| `install` | array | No | See below |
|
||||
|
||||
### metadata.requires
|
||||
|
||||
Defines eligibility requirements:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
emoji: "📝"
|
||||
requires:
|
||||
bins: [git, node] # All must exist
|
||||
anyBins: [npm, pnpm] # At least one must exist
|
||||
env: [API_KEY] # All must be set
|
||||
platforms: [darwin, linux] # Current OS must match
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `bins` | Required binaries (all must exist in PATH) |
|
||||
| `anyBins` | Alternative binaries (at least one must exist) |
|
||||
| `env` | Required environment variables |
|
||||
| `platforms` | Supported platforms: `darwin`, `linux`, `win32` |
|
||||
|
||||
### config
|
||||
|
||||
Runtime configuration options:
|
||||
|
||||
```yaml
|
||||
config:
|
||||
enabled: true
|
||||
requiresConfig: ["skills.myskill.apiKey"]
|
||||
options:
|
||||
timeout: 30000
|
||||
```
|
||||
|
||||
### install
|
||||
|
||||
Dependency installation specifications:
|
||||
|
||||
```yaml
|
||||
install:
|
||||
- kind: brew
|
||||
package: jq
|
||||
|
||||
- kind: npm
|
||||
package: typescript
|
||||
global: true
|
||||
|
||||
- kind: uv
|
||||
package: requests
|
||||
|
||||
- kind: go
|
||||
package: github.com/example/tool@latest
|
||||
|
||||
- kind: download
|
||||
url: https://example.com/tool.tar.gz
|
||||
archiveType: tar.gz
|
||||
stripComponents: 1
|
||||
```
|
||||
|
||||
**Supported install kinds:**
|
||||
|
||||
| Kind | Description | Key Fields |
|
||||
|------|-------------|------------|
|
||||
| `brew` | Homebrew | `package`, `cask` |
|
||||
| `npm` | npm/pnpm/yarn | `package`, `global` |
|
||||
| `uv` | Python uv | `package` |
|
||||
| `go` | Go install | `package` |
|
||||
| `download` | Download & extract | `url`, `archiveType` |
|
||||
|
||||
**Common fields:** `id`, `label`, `platforms`, `when`
|
||||
|
||||
---
|
||||
|
||||
## Skill Invocation
|
||||
|
||||
Skills can be invoked by users via slash commands (`/skill-name`) or automatically by the AI model.
|
||||
|
||||
### User Invocation
|
||||
|
||||
In the interactive CLI, type `/` followed by a skill name to invoke it:
|
||||
|
||||
```
|
||||
You: /pdf analyze report.pdf
|
||||
```
|
||||
|
||||
**Tab completion**: Type `/p` then press Tab to see matching skills like `/pdf`.
|
||||
|
||||
**List available skills**: Type `/help` to see all available skill commands.
|
||||
|
||||
### Invocation Control
|
||||
|
||||
Control how skills can be invoked using frontmatter fields:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: My Skill
|
||||
user-invocable: true # Can be invoked via /command (default: true)
|
||||
disable-model-invocation: false # Include in AI prompt (default: false)
|
||||
---
|
||||
```
|
||||
|
||||
| Field | Default | Description |
|
||||
|-------|---------|-------------|
|
||||
| `user-invocable` | `true` | Enable `/command` invocation in CLI |
|
||||
| `disable-model-invocation` | `false` | If `true`, skill is hidden from AI's system prompt |
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- **User-only skill** (`disable-model-invocation: true`): User can invoke via `/command`, but AI won't use it automatically
|
||||
- **AI-only skill** (`user-invocable: false`): AI can use it, but no `/command` available
|
||||
- **Disabled skill** (both `false`): Hidden from both user and AI
|
||||
|
||||
### Command Dispatch
|
||||
|
||||
For advanced integrations, skills can dispatch directly to tools:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: PDF Tool
|
||||
command-dispatch: tool
|
||||
command-tool: pdf-processor
|
||||
command-arg-mode: raw
|
||||
---
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `command-dispatch` | Set to `tool` to enable tool dispatch |
|
||||
| `command-tool` | Name of the tool to invoke |
|
||||
| `command-arg-mode` | How arguments are passed (`raw` = as-is) |
|
||||
|
||||
### Command Name Normalization
|
||||
|
||||
Skill names are normalized for command use:
|
||||
|
||||
- Converted to lowercase
|
||||
- Special characters replaced with underscores
|
||||
- Truncated to 32 characters max
|
||||
- Duplicate names get numeric suffixes (e.g., `pdf_2`)
|
||||
|
||||
---
|
||||
|
||||
## Loading & Precedence
|
||||
|
||||
Skills load from two sources with precedence (lowest to highest):
|
||||
|
||||
| Priority | Source | Path | Description |
|
||||
|----------|--------|------|-------------|
|
||||
| 1 | managed | `~/.super-multica/skills/` | Global skills (CLI-installed + bundled) |
|
||||
| 2 | profile | `~/.super-multica/agent-profiles/<id>/skills/` | Profile-specific skills |
|
||||
|
||||
Higher priority sources override skills with the same ID.
|
||||
|
||||
### Initialization
|
||||
|
||||
On first run, bundled skills are automatically copied to the managed directory (`~/.super-multica/skills/`). This makes them editable and allows users to customize or remove them.
|
||||
|
||||
### Adding Profile-Specific Skills
|
||||
|
||||
You can install skills directly to a profile using the `--profile` option:
|
||||
|
||||
```bash
|
||||
# Install skill to a specific profile
|
||||
multica skills add owner/repo --profile my-agent
|
||||
|
||||
# Install with force overwrite
|
||||
multica skills add owner/repo/skill-name --profile my-agent --force
|
||||
```
|
||||
|
||||
Alternatively, create them manually:
|
||||
|
||||
```bash
|
||||
# Create profile skills directory
|
||||
mkdir -p ~/.super-multica/agent-profiles/<profile-id>/skills/<skill-name>
|
||||
|
||||
# Create the SKILL.md file
|
||||
cat > ~/.super-multica/agent-profiles/<profile-id>/skills/<skill-name>/SKILL.md << 'EOF'
|
||||
---
|
||||
name: My Profile Skill
|
||||
version: 1.0.0
|
||||
description: A skill specific to this profile
|
||||
---
|
||||
|
||||
# Instructions
|
||||
|
||||
Your skill instructions here...
|
||||
EOF
|
||||
```
|
||||
|
||||
Profile skills automatically override managed skills with the same ID, allowing per-profile customization.
|
||||
|
||||
### Eligibility Filtering
|
||||
|
||||
After loading, skills are filtered by:
|
||||
|
||||
1. Platform check (`platforms`)
|
||||
2. Binary check (`bins`, `anyBins`)
|
||||
3. Environment check (`env`)
|
||||
4. Config check (`requiresConfig`)
|
||||
5. Enabled check (`config.enabled`)
|
||||
|
||||
Only skills passing all checks are marked as eligible.
|
||||
|
||||
---
|
||||
|
||||
## CLI Commands
|
||||
|
||||
All commands use the unified `multica` CLI (or `pnpm multica` during development).
|
||||
|
||||
### List Skills
|
||||
|
||||
```bash
|
||||
multica skills list # List all skills
|
||||
multica skills list -v # Verbose mode
|
||||
multica skills status # Summary status
|
||||
multica skills status <id> # Specific skill status
|
||||
```
|
||||
|
||||
### Install from GitHub
|
||||
|
||||
**Example: Installing from [anthropics/skills](https://github.com/anthropics/skills)**
|
||||
|
||||
The repository structure:
|
||||
```
|
||||
anthropics/skills/
|
||||
├── skills/
|
||||
│ ├── algorithmic-art/
|
||||
│ │ └── SKILL.md
|
||||
│ ├── brand-guidelines/
|
||||
│ │ └── SKILL.md
|
||||
│ ├── pdf/
|
||||
│ │ └── SKILL.md
|
||||
│ └── ... (16 skills total)
|
||||
```
|
||||
|
||||
Install the entire repository (all 16 skills):
|
||||
```bash
|
||||
multica skills add anthropics/skills
|
||||
# Installs to: ~/.super-multica/skills/skills/
|
||||
# All skills available: algorithmic-art, brand-guidelines, pdf, etc.
|
||||
```
|
||||
|
||||
Install a single skill only:
|
||||
```bash
|
||||
multica skills add anthropics/skills/skills/pdf
|
||||
# Installs to: ~/.super-multica/skills/pdf/
|
||||
# Only the pdf skill is installed
|
||||
```
|
||||
|
||||
Install from a specific branch or tag:
|
||||
```bash
|
||||
multica skills add anthropics/skills@main
|
||||
```
|
||||
|
||||
Using full URL:
|
||||
```bash
|
||||
multica skills add https://github.com/anthropics/skills
|
||||
multica skills add https://github.com/anthropics/skills/tree/main/skills/pdf
|
||||
```
|
||||
|
||||
Force overwrite existing:
|
||||
```bash
|
||||
multica skills add anthropics/skills --force
|
||||
```
|
||||
|
||||
**Supported formats:**
|
||||
|
||||
| Format | Example | Description |
|
||||
|--------|---------|-------------|
|
||||
| `owner/repo` | `anthropics/skills` | Clone entire repository |
|
||||
| `owner/repo/path` | `anthropics/skills/skills/pdf` | Single directory (sparse checkout) |
|
||||
| `owner/repo@ref` | `anthropics/skills@v1.0.0` | Specific branch or tag |
|
||||
| Full URL | `https://github.com/anthropics/skills` | GitHub URL |
|
||||
| Full URL + path | `https://github.com/.../tree/main/skills/pdf` | URL with specific path |
|
||||
|
||||
### Remove Skills
|
||||
|
||||
```bash
|
||||
multica skills remove <name> # Remove installed skill
|
||||
multica skills remove # List installed skills
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
```bash
|
||||
multica skills install <id> # Install skill dependencies
|
||||
multica skills install <id> <install-id> # Specific install option
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Status Diagnostics
|
||||
|
||||
The `status` command provides detailed diagnostics for understanding why skills are or aren't eligible.
|
||||
|
||||
### Summary Status
|
||||
|
||||
```bash
|
||||
multica skills status # Show summary with grouping by issue type
|
||||
multica skills status -v # Verbose mode with hints
|
||||
```
|
||||
|
||||
Output shows:
|
||||
- Total/eligible/ineligible counts
|
||||
- Ineligible skills grouped by issue type (binary, env, platform, etc.)
|
||||
|
||||
### Detailed Skill Status
|
||||
|
||||
```bash
|
||||
multica skills status <skill-id>
|
||||
```
|
||||
|
||||
Output includes:
|
||||
- Basic skill info (name, version, source, path)
|
||||
- **Eligibility status** with detailed diagnostics
|
||||
- **Requirements checklist** showing which binaries/env vars are present
|
||||
- **Install options** with availability status
|
||||
- **Quick actions** with actionable hints to resolve issues
|
||||
|
||||
### Diagnostic Types
|
||||
|
||||
| Type | Description | Example Hint |
|
||||
|------|-------------|--------------|
|
||||
| `disabled` | Skill disabled in config | Enable via `skills.<id>.enabled: true` |
|
||||
| `not_in_allowlist` | Bundled skill not allowed | Add to `config.allowBundled` array |
|
||||
| `platform` | Platform mismatch | "Only works on: darwin, linux" |
|
||||
| `binary` | Missing required binary | "brew install git" |
|
||||
| `any_binary` | No alternative binary found | "Install any of: npm, pnpm, yarn" |
|
||||
| `env` | Missing environment variable | "export OPENAI_API_KEY=..." |
|
||||
| `config` | Missing config value | "Set config path: browser.enabled" |
|
||||
|
||||
---
|
||||
|
||||
## Async Serialization
|
||||
|
||||
The skills system uses async serialization to prevent concurrent operations from corrupting files or causing race conditions.
|
||||
|
||||
### How It Works
|
||||
|
||||
Operations with the same key are executed sequentially:
|
||||
|
||||
```typescript
|
||||
import { serialize, SerializeKeys } from "./skills/index.js";
|
||||
|
||||
// These will execute sequentially, not in parallel
|
||||
const p1 = serialize(SerializeKeys.skillAdd("my-skill"), () => addSkill(...));
|
||||
const p2 = serialize(SerializeKeys.skillAdd("my-skill"), () => addSkill(...));
|
||||
|
||||
// This runs in parallel (different key)
|
||||
const p3 = serialize(SerializeKeys.skillAdd("other-skill"), () => addSkill(...));
|
||||
```
|
||||
|
||||
### Built-in Serialization
|
||||
|
||||
The following operations are automatically serialized:
|
||||
- `addSkill()` - by skill name
|
||||
- `removeSkill()` - by skill name
|
||||
- `installSkill()` - by skill ID
|
||||
|
||||
### Utility Functions
|
||||
|
||||
```typescript
|
||||
import {
|
||||
isProcessing, // Check if key is being processed
|
||||
getQueueLength, // Get pending operations count
|
||||
getActiveKeys, // Get all active operation keys
|
||||
waitForKey, // Wait for key operations to complete
|
||||
waitForAll, // Wait for all operations
|
||||
} from "./skills/index.js";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Skill not showing as eligible?**
|
||||
|
||||
Run `pnpm skills:cli status <skill-id>` to see detailed diagnostics with actionable hints.
|
||||
|
||||
**Override a bundled skill?**
|
||||
|
||||
Create a skill with the same ID in `~/.super-multica/skills/` or profile skills directory.
|
||||
|
||||
**Hot reload not working?**
|
||||
|
||||
Ensure `chokidar` is installed: `pnpm add chokidar`
|
||||
|
||||
**Concurrent operations causing issues?**
|
||||
|
||||
All add/remove/install operations are automatically serialized. If you're building custom integrations, use the `serialize()` function with appropriate keys.
|
||||
|
|
@ -1,438 +0,0 @@
|
|||
# Skills 系统
|
||||
|
||||
[English](./README.md) | [中文](./README.zh-CN.md)
|
||||
|
||||
Skills 通过 `SKILL.md` 定义文件扩展 Agent 的能力。
|
||||
|
||||
## 目录
|
||||
|
||||
- [SKILL.md 规范](#skillmd-规范)
|
||||
- [Skill 调用](#skill-调用)
|
||||
- [加载与优先级](#加载与优先级)
|
||||
- [CLI 命令](#cli-命令)
|
||||
|
||||
---
|
||||
|
||||
## SKILL.md 规范
|
||||
|
||||
每个 skill 是一个包含 `SKILL.md` 文件的目录,文件包含 YAML frontmatter 和 Markdown 内容。
|
||||
|
||||
### 基本结构
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: My Skill
|
||||
version: 1.0.0
|
||||
description: 这个 skill 的功能描述
|
||||
metadata:
|
||||
emoji: "🔧"
|
||||
requires:
|
||||
bins: [git]
|
||||
---
|
||||
|
||||
# 说明
|
||||
|
||||
注入到 agent 系统提示词中的详细说明...
|
||||
```
|
||||
|
||||
### Frontmatter 字段
|
||||
|
||||
| 字段 | 类型 | 必需 | 描述 |
|
||||
|------|------|------|------|
|
||||
| `name` | string | 是 | 显示名称 |
|
||||
| `version` | string | 否 | 版本号 |
|
||||
| `description` | string | 否 | 简短描述 |
|
||||
| `homepage` | string | 否 | 主页 URL |
|
||||
| `metadata` | object | 否 | 见下文 |
|
||||
| `config` | object | 否 | 见下文 |
|
||||
| `install` | array | 否 | 见下文 |
|
||||
|
||||
### metadata.requires
|
||||
|
||||
定义资格要求:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
emoji: "📝"
|
||||
requires:
|
||||
bins: [git, node] # 全部必须存在
|
||||
anyBins: [npm, pnpm] # 至少一个必须存在
|
||||
env: [API_KEY] # 全部必须设置
|
||||
platforms: [darwin, linux] # 当前操作系统必须匹配
|
||||
```
|
||||
|
||||
| 字段 | 描述 |
|
||||
|------|------|
|
||||
| `bins` | 必需的二进制文件(全部必须存在于 PATH 中) |
|
||||
| `anyBins` | 备选二进制文件(至少一个必须存在) |
|
||||
| `env` | 必需的环境变量 |
|
||||
| `platforms` | 支持的平台:`darwin`、`linux`、`win32` |
|
||||
|
||||
### config
|
||||
|
||||
运行时配置选项:
|
||||
|
||||
```yaml
|
||||
config:
|
||||
enabled: true
|
||||
requiresConfig: ["skills.myskill.apiKey"]
|
||||
options:
|
||||
timeout: 30000
|
||||
```
|
||||
|
||||
### install
|
||||
|
||||
依赖安装规范:
|
||||
|
||||
```yaml
|
||||
install:
|
||||
- kind: brew
|
||||
package: jq
|
||||
|
||||
- kind: npm
|
||||
package: typescript
|
||||
global: true
|
||||
|
||||
- kind: uv
|
||||
package: requests
|
||||
|
||||
- kind: go
|
||||
package: github.com/example/tool@latest
|
||||
|
||||
- kind: download
|
||||
url: https://example.com/tool.tar.gz
|
||||
archiveType: tar.gz
|
||||
stripComponents: 1
|
||||
```
|
||||
|
||||
**支持的安装类型:**
|
||||
|
||||
| 类型 | 描述 | 关键字段 |
|
||||
|------|------|----------|
|
||||
| `brew` | Homebrew | `package`、`cask` |
|
||||
| `npm` | npm/pnpm/yarn | `package`、`global` |
|
||||
| `uv` | Python uv | `package` |
|
||||
| `go` | Go install | `package` |
|
||||
| `download` | 下载并解压 | `url`、`archiveType` |
|
||||
|
||||
**通用字段:** `id`、`label`、`platforms`、`when`
|
||||
|
||||
---
|
||||
|
||||
## Skill 调用
|
||||
|
||||
用户可以通过斜杠命令(`/skill-name`)调用 skills,AI 模型也可以自动调用。
|
||||
|
||||
### 用户调用
|
||||
|
||||
在交互式 CLI 中,输入 `/` 加上 skill 名称来调用:
|
||||
|
||||
```
|
||||
You: /pdf analyze report.pdf
|
||||
```
|
||||
|
||||
**Tab 补全**:输入 `/p` 然后按 Tab 键查看匹配的 skills,如 `/pdf`。
|
||||
|
||||
**列出可用 skills**:输入 `/help` 查看所有可用的 skill 命令。
|
||||
|
||||
### 调用控制
|
||||
|
||||
使用 frontmatter 字段控制 skill 的调用方式:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: My Skill
|
||||
user-invocable: true # 可通过 /command 调用(默认:true)
|
||||
disable-model-invocation: false # 包含在 AI 提示词中(默认:false)
|
||||
---
|
||||
```
|
||||
|
||||
| 字段 | 默认值 | 描述 |
|
||||
|------|--------|------|
|
||||
| `user-invocable` | `true` | 在 CLI 中启用 `/command` 调用 |
|
||||
| `disable-model-invocation` | `false` | 如果为 `true`,skill 对 AI 的系统提示词隐藏 |
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- **仅用户 skill**(`disable-model-invocation: true`):用户可通过 `/command` 调用,但 AI 不会自动使用
|
||||
- **仅 AI skill**(`user-invocable: false`):AI 可使用,但没有 `/command` 可用
|
||||
- **禁用 skill**(两者都为 `false`):对用户和 AI 都隐藏
|
||||
|
||||
### 命令分发
|
||||
|
||||
对于高级集成,skills 可以直接分发到工具:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: PDF Tool
|
||||
command-dispatch: tool
|
||||
command-tool: pdf-processor
|
||||
command-arg-mode: raw
|
||||
---
|
||||
```
|
||||
|
||||
| 字段 | 描述 |
|
||||
|------|------|
|
||||
| `command-dispatch` | 设置为 `tool` 启用工具分发 |
|
||||
| `command-tool` | 要调用的工具名称 |
|
||||
| `command-arg-mode` | 参数传递方式(`raw` = 原样传递) |
|
||||
|
||||
### 命令名称规范化
|
||||
|
||||
Skill 名称会被规范化以用作命令:
|
||||
|
||||
- 转换为小写
|
||||
- 特殊字符替换为下划线
|
||||
- 截断至最多 32 个字符
|
||||
- 重复名称添加数字后缀(如 `pdf_2`)
|
||||
|
||||
---
|
||||
|
||||
## 加载与优先级
|
||||
|
||||
Skills 从两个来源加载,优先级从低到高:
|
||||
|
||||
| 优先级 | 来源 | 路径 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| 1 | managed | `~/.super-multica/skills/` | 全局 skills(CLI 安装 + 内置) |
|
||||
| 2 | profile | `~/.super-multica/agent-profiles/<id>/skills/` | Profile 专属 skills |
|
||||
|
||||
高优先级来源会覆盖具有相同 ID 的 skills。
|
||||
|
||||
### 初始化
|
||||
|
||||
首次运行时,内置 skills 会自动复制到 managed 目录(`~/.super-multica/skills/`)。这使得用户可以编辑或删除它们。
|
||||
|
||||
### 添加 Profile 专属 Skills
|
||||
|
||||
可以使用 `--profile` 选项直接安装 skills 到特定 profile:
|
||||
|
||||
```bash
|
||||
# 安装 skill 到特定 profile
|
||||
multica skills add owner/repo --profile my-agent
|
||||
|
||||
# 强制覆盖安装
|
||||
multica skills add owner/repo/skill-name --profile my-agent --force
|
||||
```
|
||||
|
||||
也可以手动创建:
|
||||
|
||||
```bash
|
||||
# 创建 profile skills 目录
|
||||
mkdir -p ~/.super-multica/agent-profiles/<profile-id>/skills/<skill-name>
|
||||
|
||||
# 创建 SKILL.md 文件
|
||||
cat > ~/.super-multica/agent-profiles/<profile-id>/skills/<skill-name>/SKILL.md << 'EOF'
|
||||
---
|
||||
name: My Profile Skill
|
||||
version: 1.0.0
|
||||
description: 此 profile 专属的 skill
|
||||
---
|
||||
|
||||
# 说明
|
||||
|
||||
你的 skill 说明内容...
|
||||
EOF
|
||||
```
|
||||
|
||||
Profile skills 会自动覆盖同 ID 的 managed skills,允许按 profile 自定义。
|
||||
|
||||
### 资格过滤
|
||||
|
||||
加载后,skills 会按以下条件过滤:
|
||||
|
||||
1. 平台检查(`platforms`)
|
||||
2. 二进制文件检查(`bins`、`anyBins`)
|
||||
3. 环境变量检查(`env`)
|
||||
4. 配置检查(`requiresConfig`)
|
||||
5. 启用检查(`config.enabled`)
|
||||
|
||||
只有通过所有检查的 skills 才会被标记为符合条件。
|
||||
|
||||
---
|
||||
|
||||
## CLI 命令
|
||||
|
||||
所有命令使用统一的 `multica` CLI(开发时使用 `pnpm multica`)。
|
||||
|
||||
### 列出 Skills
|
||||
|
||||
```bash
|
||||
multica skills list # 列出所有 skills
|
||||
multica skills list -v # 详细模式
|
||||
multica skills status # 汇总状态
|
||||
multica skills status <id> # 特定 skill 状态
|
||||
```
|
||||
|
||||
### 从 GitHub 安装
|
||||
|
||||
**示例:从 [anthropics/skills](https://github.com/anthropics/skills) 安装**
|
||||
|
||||
仓库结构:
|
||||
```
|
||||
anthropics/skills/
|
||||
├── skills/
|
||||
│ ├── algorithmic-art/
|
||||
│ │ └── SKILL.md
|
||||
│ ├── brand-guidelines/
|
||||
│ │ └── SKILL.md
|
||||
│ ├── pdf/
|
||||
│ │ └── SKILL.md
|
||||
│ └── ... (共 16 个 skills)
|
||||
```
|
||||
|
||||
安装整个仓库(所有 16 个 skills):
|
||||
```bash
|
||||
multica skills add anthropics/skills
|
||||
# 安装到:~/.super-multica/skills/skills/
|
||||
# 所有 skills 可用:algorithmic-art、brand-guidelines、pdf 等
|
||||
```
|
||||
|
||||
只安装单个 skill:
|
||||
```bash
|
||||
multica skills add anthropics/skills/skills/pdf
|
||||
# 安装到:~/.super-multica/skills/pdf/
|
||||
# 只安装 pdf skill
|
||||
```
|
||||
|
||||
从特定分支或标签安装:
|
||||
```bash
|
||||
multica skills add anthropics/skills@main
|
||||
```
|
||||
|
||||
使用完整 URL:
|
||||
```bash
|
||||
multica skills add https://github.com/anthropics/skills
|
||||
multica skills add https://github.com/anthropics/skills/tree/main/skills/pdf
|
||||
```
|
||||
|
||||
强制覆盖现有:
|
||||
```bash
|
||||
multica skills add anthropics/skills --force
|
||||
```
|
||||
|
||||
**支持的格式:**
|
||||
|
||||
| 格式 | 示例 | 描述 |
|
||||
|------|------|------|
|
||||
| `owner/repo` | `anthropics/skills` | 克隆整个仓库 |
|
||||
| `owner/repo/path` | `anthropics/skills/skills/pdf` | 单个目录(稀疏检出) |
|
||||
| `owner/repo@ref` | `anthropics/skills@v1.0.0` | 特定分支或标签 |
|
||||
| 完整 URL | `https://github.com/anthropics/skills` | GitHub URL |
|
||||
| 完整 URL + 路径 | `https://github.com/.../tree/main/skills/pdf` | 带特定路径的 URL |
|
||||
|
||||
### 移除 Skills
|
||||
|
||||
```bash
|
||||
multica skills remove <name> # 移除已安装的 skill
|
||||
multica skills remove # 列出已安装的 skills
|
||||
```
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
multica skills install <id> # 安装 skill 依赖
|
||||
multica skills install <id> <install-id> # 特定安装选项
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 状态诊断
|
||||
|
||||
`status` 命令提供详细的诊断信息,帮助了解 skills 为何符合或不符合条件。
|
||||
|
||||
### 汇总状态
|
||||
|
||||
```bash
|
||||
multica skills status # 显示按问题类型分组的汇总
|
||||
multica skills status -v # 详细模式带提示
|
||||
```
|
||||
|
||||
输出显示:
|
||||
- 总计/符合条件/不符合条件计数
|
||||
- 按问题类型分组的不符合条件 skills(binary、env、platform 等)
|
||||
|
||||
### 详细 Skill 状态
|
||||
|
||||
```bash
|
||||
multica skills status <skill-id>
|
||||
```
|
||||
|
||||
输出包括:
|
||||
- 基本 skill 信息(名称、版本、来源、路径)
|
||||
- **资格状态**及详细诊断
|
||||
- **要求检查表**显示哪些二进制文件/环境变量存在
|
||||
- **安装选项**及可用性状态
|
||||
- **快速操作**及可操作的提示
|
||||
|
||||
### 诊断类型
|
||||
|
||||
| 类型 | 描述 | 示例提示 |
|
||||
|------|------|----------|
|
||||
| `disabled` | Skill 在配置中禁用 | 通过 `skills.<id>.enabled: true` 启用 |
|
||||
| `not_in_allowlist` | 内置 skill 不在允许列表中 | 添加到 `config.allowBundled` 数组 |
|
||||
| `platform` | 平台不匹配 | "仅支持:darwin、linux" |
|
||||
| `binary` | 缺少必需的二进制文件 | "brew install git" |
|
||||
| `any_binary` | 未找到备选二进制文件 | "安装任一:npm、pnpm、yarn" |
|
||||
| `env` | 缺少环境变量 | "export OPENAI_API_KEY=..." |
|
||||
| `config` | 缺少配置值 | "设置配置路径:browser.enabled" |
|
||||
|
||||
---
|
||||
|
||||
## 异步序列化
|
||||
|
||||
Skills 系统使用异步序列化来防止并发操作损坏文件或导致竞态条件。
|
||||
|
||||
### 工作原理
|
||||
|
||||
具有相同键的操作按顺序执行:
|
||||
|
||||
```typescript
|
||||
import { serialize, SerializeKeys } from "./skills/index.js";
|
||||
|
||||
// 这些将按顺序执行,而非并行
|
||||
const p1 = serialize(SerializeKeys.skillAdd("my-skill"), () => addSkill(...));
|
||||
const p2 = serialize(SerializeKeys.skillAdd("my-skill"), () => addSkill(...));
|
||||
|
||||
// 这个并行运行(不同的键)
|
||||
const p3 = serialize(SerializeKeys.skillAdd("other-skill"), () => addSkill(...));
|
||||
```
|
||||
|
||||
### 内置序列化
|
||||
|
||||
以下操作自动序列化:
|
||||
- `addSkill()` - 按 skill 名称
|
||||
- `removeSkill()` - 按 skill 名称
|
||||
- `installSkill()` - 按 skill ID
|
||||
|
||||
### 工具函数
|
||||
|
||||
```typescript
|
||||
import {
|
||||
isProcessing, // 检查键是否正在处理
|
||||
getQueueLength, // 获取待处理操作数量
|
||||
getActiveKeys, // 获取所有活动操作键
|
||||
waitForKey, // 等待键操作完成
|
||||
waitForAll, // 等待所有操作
|
||||
} from "./skills/index.js";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
**Skill 未显示为符合条件?**
|
||||
|
||||
运行 `multica skills status <skill-id>` 查看详细诊断及可操作的提示。
|
||||
|
||||
**覆盖内置 skill?**
|
||||
|
||||
在 `~/.super-multica/skills/` 或配置文件 skills 目录中创建具有相同 ID 的 skill。
|
||||
|
||||
**热重载不工作?**
|
||||
|
||||
确保安装了 `chokidar`:`pnpm add chokidar`
|
||||
|
||||
**并发操作导致问题?**
|
||||
|
||||
所有 add/remove/install 操作都会自动序列化。如果你在构建自定义集成,请使用 `serialize()` 函数并使用适当的键。
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
# Tools System
|
||||
|
||||
[中文文档](./README.zh-CN.md)
|
||||
|
||||
The tools system provides LLM agents with capabilities to interact with the external world. Tools are the "hands and feet" of an agent - without tools, an LLM can only generate text responses.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Tool Definition │
|
||||
│ (AgentTool from @mariozechner/pi-agent-core) │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ name │ │ description │ │ parameters │ │
|
||||
│ │ label │ │ execute │ │ (TypeBox) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3-Layer Policy Filter │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Layer 1: Global Allow/Deny │ │
|
||||
│ │ User customization via CLI or config │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Layer 2: Provider-Specific │ │
|
||||
│ │ Different rules for different LLM providers │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Layer 3: Subagent Restrictions │ │
|
||||
│ │ Limited tools for spawned child agents │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Filtered Tools │
|
||||
│ (passed to pi-agent-core) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Name | Description |
|
||||
| -------------- | ---------------- | ----------------------------------- |
|
||||
| Read | `read` | Read file contents |
|
||||
| Write | `write` | Write content to files |
|
||||
| Edit | `edit` | Edit existing files |
|
||||
| Glob | `glob` | Find files by pattern |
|
||||
| Exec | `exec` | Execute shell commands |
|
||||
| Process | `process` | Manage long-running processes |
|
||||
| Web Fetch | `web_fetch` | Fetch and extract content from URLs |
|
||||
| Web Search | `web_search` | Search the web via Devv Search |
|
||||
| Sessions Spawn | `sessions_spawn` | Spawn a sub-agent session |
|
||||
|
||||
> **Note**: Agents use file-based memory (`memory.md`, `memory/*.md`) via `read` and `edit` tools instead of dedicated memory tools.
|
||||
|
||||
## Tool Groups
|
||||
|
||||
Groups provide shortcuts for allowing/denying multiple tools at once:
|
||||
|
||||
| Group | Tools |
|
||||
| ---------------- | ------------------------------------ |
|
||||
| `group:fs` | read, write, edit, glob |
|
||||
| `group:runtime` | exec, process |
|
||||
| `group:web` | web_search, web_fetch |
|
||||
| `group:subagent` | sessions_spawn |
|
||||
| `group:core` | All fs, runtime, and web tools |
|
||||
|
||||
## Usage
|
||||
|
||||
### CLI Usage
|
||||
|
||||
All commands use the unified `multica` CLI (or `pnpm multica` during development).
|
||||
|
||||
```bash
|
||||
# Allow only specific tools
|
||||
multica run --tools-allow group:fs,group:runtime "list files"
|
||||
|
||||
# Deny specific tools
|
||||
multica run --tools-deny exec,process "read file.txt"
|
||||
|
||||
# Use tool groups
|
||||
multica run --tools-allow group:fs "read config.json"
|
||||
```
|
||||
|
||||
### Programmatic Usage
|
||||
|
||||
```typescript
|
||||
import { Agent } from './runner.js';
|
||||
|
||||
const agent = new Agent({
|
||||
tools: {
|
||||
// Layer 1: Global allow/deny
|
||||
allow: ['group:fs', 'group:runtime', 'web_fetch'],
|
||||
deny: ['exec'],
|
||||
|
||||
// Layer 2: Provider-specific rules
|
||||
byProvider: {
|
||||
google: {
|
||||
deny: ['exec', 'process'], // Google models can't use runtime tools
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Layer 3: Subagent mode
|
||||
isSubagent: false,
|
||||
});
|
||||
```
|
||||
|
||||
### Inspecting Tool Configuration
|
||||
|
||||
Use the tools CLI to inspect and test configurations:
|
||||
|
||||
```bash
|
||||
# List all available tools
|
||||
multica tools list
|
||||
|
||||
# List tools with allow rules
|
||||
multica tools list --allow group:fs,group:runtime
|
||||
|
||||
# List tools with deny rules
|
||||
multica tools list --deny exec
|
||||
|
||||
# Show all tool groups
|
||||
multica tools groups
|
||||
```
|
||||
|
||||
## Policy System Details
|
||||
|
||||
### Layer 1: Global Allow/Deny
|
||||
|
||||
User-specified allow/deny lists:
|
||||
|
||||
- `allow`: Only these tools are available (supports group:\* syntax)
|
||||
- `deny`: These tools are blocked (takes precedence over allow)
|
||||
|
||||
If no `allow` list is specified, all tools are available by default.
|
||||
|
||||
### Layer 2: Provider-Specific
|
||||
|
||||
Different LLM providers may have different capabilities or restrictions:
|
||||
|
||||
```typescript
|
||||
{
|
||||
byProvider: {
|
||||
google: { deny: ["exec"] }, // Gemini can't execute commands
|
||||
anthropic: { allow: ["*"] }, // Claude has full access
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 3: Subagent Restrictions
|
||||
|
||||
When `isSubagent: true`, additional restrictions are applied to prevent spawned agents from accessing sensitive tools like session management.
|
||||
|
||||
## Adding New Tools
|
||||
|
||||
1. Create a new file in `src/agent/tools/` (e.g., `my-tool.ts`)
|
||||
|
||||
2. Define the tool using TypeBox for the schema:
|
||||
|
||||
```typescript
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { AgentTool } from '@mariozechner/pi-agent-core';
|
||||
|
||||
const MyToolSchema = Type.Object({
|
||||
param1: Type.String({ description: 'Parameter description' }),
|
||||
param2: Type.Optional(Type.Number()),
|
||||
});
|
||||
|
||||
export function createMyTool(): AgentTool<typeof MyToolSchema> {
|
||||
return {
|
||||
name: 'my_tool',
|
||||
label: 'My Tool',
|
||||
description: 'What this tool does',
|
||||
parameters: MyToolSchema,
|
||||
execute: async (toolCallId, args) => {
|
||||
// Implementation
|
||||
return { result: 'success' };
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
3. Register the tool in `src/agent/tools.ts`:
|
||||
|
||||
```typescript
|
||||
import { createMyTool } from './tools/my-tool.js';
|
||||
|
||||
export function createAllTools(cwd: string): AgentTool<any>[] {
|
||||
// ... existing tools
|
||||
const myTool = createMyTool();
|
||||
|
||||
return [
|
||||
...baseTools,
|
||||
myTool as AgentTool<any>,
|
||||
// ...
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
4. Add the tool to appropriate groups in `groups.ts`:
|
||||
|
||||
```typescript
|
||||
export const TOOL_GROUPS: Record<string, string[]> = {
|
||||
'group:my_category': ['my_tool', 'other_tool'],
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the policy system tests:
|
||||
|
||||
```bash
|
||||
pnpm test src/agent/tools/policy.test.ts
|
||||
```
|
||||
|
||||
## Agent Profile Integration
|
||||
|
||||
Tools configuration can be defined in Agent Profile's `config.json`, allowing different agents to have different tool capabilities:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Super Multica Hub │
|
||||
│ │
|
||||
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
||||
│ │ Agent A │ │ Agent B │ │ Agent C │ │
|
||||
│ │ Profile: │ │ Profile: │ │ Profile: │ │
|
||||
│ │ coder │ │ reviewer │ │ devops │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ tools: │ │ tools: │ │ tools: │ │
|
||||
│ │ allow:fs │ │ deny:* │ │ allow:* │ │
|
||||
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
|
||||
│ │ │ │ │
|
||||
└─────────┼────────────────┼────────────────┼─────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Client │ │ Client │ │ Client │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
Each Agent's Profile can define its own tools configuration in `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"allow": ["group:fs", "group:runtime"],
|
||||
"deny": ["exec"]
|
||||
},
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet-4-20250514"
|
||||
}
|
||||
```
|
||||
|
||||
See [Profile README](../profile/README.md) for full documentation.
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
# 工具系统
|
||||
|
||||
[English](./README.md)
|
||||
|
||||
工具系统为 LLM Agent 提供与外部世界交互的能力。工具是 Agent 的"手和脚"——没有工具,LLM 只能生成文本响应。
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 工具定义 │
|
||||
│ (AgentTool from @mariozechner/pi-agent-core) │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ name │ │ description │ │ parameters │ │
|
||||
│ │ label │ │ execute │ │ (TypeBox) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3 层策略过滤器 │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ 第 1 层: 全局 Allow/Deny │ │
|
||||
│ │ 通过 CLI 或配置文件进行用户自定义 │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ 第 2 层: Provider 特定规则 │ │
|
||||
│ │ 不同 LLM Provider 有不同的规则 │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ 第 3 层: Subagent 限制 │ │
|
||||
│ │ 子 Agent 的工具访问受限 │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 过滤后的工具 │
|
||||
│ (传递给 pi-agent-core) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 可用工具
|
||||
|
||||
| 工具 | 名称 | 描述 |
|
||||
| -------------- | ---------------- | ------------------------------ |
|
||||
| Read | `read` | 读取文件内容 |
|
||||
| Write | `write` | 写入文件内容 |
|
||||
| Edit | `edit` | 编辑现有文件 |
|
||||
| Glob | `glob` | 按模式查找文件 |
|
||||
| Exec | `exec` | 执行 Shell 命令 |
|
||||
| Process | `process` | 管理长时间运行的进程 |
|
||||
| Web Fetch | `web_fetch` | 从 URL 获取并提取内容 |
|
||||
| Web Search | `web_search` | 搜索网络(需要 API Key) |
|
||||
| Memory Search | `memory_search` | 搜索 memory 文件(需要 Profile)|
|
||||
| Sessions Spawn | `sessions_spawn` | 创建子 Agent 会话 |
|
||||
|
||||
> **注意**: `memory_search` 工具通过关键词搜索 `memory.md` 和 `memory/*.md` 文件。Agent 通过 `read` 和 `edit` 工具操作 memory 文件内容。
|
||||
|
||||
## 工具组
|
||||
|
||||
工具组提供了一次性允许/禁止多个工具的快捷方式:
|
||||
|
||||
| 组 | 工具 |
|
||||
| ---------------- | ------------------------------ |
|
||||
| `group:fs` | read, write, edit, glob |
|
||||
| `group:runtime` | exec, process |
|
||||
| `group:web` | web_search, web_fetch |
|
||||
| `group:memory` | memory_search |
|
||||
| `group:subagent` | sessions_spawn |
|
||||
| `group:core` | 所有 fs、runtime 和 web 工具 |
|
||||
|
||||
## 使用方法
|
||||
|
||||
### CLI 使用
|
||||
|
||||
所有命令使用统一的 `multica` CLI(开发时使用 `pnpm multica`)。
|
||||
|
||||
```bash
|
||||
# 只允许特定工具
|
||||
multica run --tools-allow group:fs,group:runtime "list files"
|
||||
|
||||
# 禁止特定工具
|
||||
multica run --tools-deny exec,process "read file.txt"
|
||||
|
||||
# 使用工具组
|
||||
multica run --tools-allow group:fs "read config.json"
|
||||
```
|
||||
|
||||
### 编程使用
|
||||
|
||||
```typescript
|
||||
import { Agent } from './runner.js';
|
||||
|
||||
const agent = new Agent({
|
||||
tools: {
|
||||
// 第 1 层: 全局 allow/deny
|
||||
allow: ['group:fs', 'group:runtime', 'web_fetch'],
|
||||
deny: ['exec'],
|
||||
|
||||
// 第 2 层: Provider 特定规则
|
||||
byProvider: {
|
||||
google: {
|
||||
deny: ['exec', 'process'], // Google 模型不能使用运行时工具
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 第 3 层: Subagent 模式
|
||||
isSubagent: false,
|
||||
});
|
||||
```
|
||||
|
||||
### 检查工具配置
|
||||
|
||||
使用 tools CLI 检查和测试配置:
|
||||
|
||||
```bash
|
||||
# 列出所有可用工具
|
||||
multica tools list
|
||||
|
||||
# 列出带有允许规则的工具
|
||||
multica tools list --allow group:fs,group:runtime
|
||||
|
||||
# 列出带有禁止规则的工具
|
||||
multica tools list --deny exec
|
||||
|
||||
# 显示所有工具组
|
||||
multica tools groups
|
||||
```
|
||||
|
||||
## 策略系统详情
|
||||
|
||||
### 第 1 层: 全局 Allow/Deny
|
||||
|
||||
用户指定的 allow/deny 列表:
|
||||
|
||||
- `allow`: 只有这些工具可用(支持 group:\* 语法)
|
||||
- `deny`: 这些工具被阻止(优先于 allow)
|
||||
|
||||
如果未指定 `allow` 列表,默认所有工具都可用。
|
||||
|
||||
### 第 2 层: Provider 特定规则
|
||||
|
||||
不同的 LLM Provider 可能有不同的能力或限制:
|
||||
|
||||
```typescript
|
||||
{
|
||||
byProvider: {
|
||||
google: { deny: ["exec"] }, // Gemini 不能执行命令
|
||||
anthropic: { allow: ["*"] }, // Claude 有完全访问权限
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 第 3 层: Subagent 限制
|
||||
|
||||
当 `isSubagent: true` 时,会应用额外的限制,防止子 Agent 访问敏感工具(如会话管理)。
|
||||
|
||||
## 添加新工具
|
||||
|
||||
1. 在 `src/agent/tools/` 中创建新文件(例如 `my-tool.ts`)
|
||||
|
||||
2. 使用 TypeBox 定义工具的 Schema:
|
||||
|
||||
```typescript
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { AgentTool } from '@mariozechner/pi-agent-core';
|
||||
|
||||
const MyToolSchema = Type.Object({
|
||||
param1: Type.String({ description: '参数描述' }),
|
||||
param2: Type.Optional(Type.Number()),
|
||||
});
|
||||
|
||||
export function createMyTool(): AgentTool<typeof MyToolSchema> {
|
||||
return {
|
||||
name: 'my_tool',
|
||||
label: 'My Tool',
|
||||
description: '这个工具做什么',
|
||||
parameters: MyToolSchema,
|
||||
execute: async (toolCallId, args) => {
|
||||
// 实现
|
||||
return { result: 'success' };
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
3. 在 `src/agent/tools.ts` 中注册工具:
|
||||
|
||||
```typescript
|
||||
import { createMyTool } from './tools/my-tool.js';
|
||||
|
||||
export function createAllTools(cwd: string): AgentTool<any>[] {
|
||||
// ... 现有工具
|
||||
const myTool = createMyTool();
|
||||
|
||||
return [
|
||||
...baseTools,
|
||||
myTool as AgentTool<any>,
|
||||
// ...
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
4. 在 `groups.ts` 中将工具添加到适当的组:
|
||||
|
||||
```typescript
|
||||
export const TOOL_GROUPS: Record<string, string[]> = {
|
||||
'group:my_category': ['my_tool', 'other_tool'],
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
运行策略系统测试:
|
||||
|
||||
```bash
|
||||
pnpm test src/agent/tools/policy.test.ts
|
||||
```
|
||||
|
||||
## Agent Profile 集成
|
||||
|
||||
工具配置可以在 Agent Profile 的 `config.json` 中定义,允许不同的 Agent 拥有不同的工具能力:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Super Multica Hub │
|
||||
│ │
|
||||
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
||||
│ │ Agent A │ │ Agent B │ │ Agent C │ │
|
||||
│ │ Profile: │ │ Profile: │ │ Profile: │ │
|
||||
│ │ coder │ │ reviewer │ │ devops │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ tools: │ │ tools: │ │ tools: │ │
|
||||
│ │ allow:fs │ │ deny:* │ │ allow:* │ │
|
||||
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
|
||||
│ │ │ │ │
|
||||
└─────────┼────────────────┼────────────────┼─────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Client │ │ Client │ │ Client │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
每个 Agent 的 Profile 可以在 `config.json` 中定义自己的工具配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"allow": ["group:fs", "group:runtime"],
|
||||
"deny": ["exec"]
|
||||
},
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet-4-20250514"
|
||||
}
|
||||
```
|
||||
|
||||
详见 [Profile README](../profile/README.md)。
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# @multica/store
|
||||
|
||||
Zustand state management for Multica apps.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
// From barrel
|
||||
import { useHubStore, useMessagesStore, useGatewayStore } from '@multica/store'
|
||||
|
||||
// Per-file subpath import
|
||||
import { useGatewayStore } from '@multica/store/gateway'
|
||||
import { useHubStore } from '@multica/store/hub'
|
||||
import { useMessagesStore } from '@multica/store/messages'
|
||||
import { useHubInit } from '@multica/store/hub-init'
|
||||
import { useDeviceId } from '@multica/store/device-id'
|
||||
```
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# @multica/ui
|
||||
|
||||
Shared UI component library. Shadcn + Tailwind CSS v4.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
// UI components — subpath imports, no barrel
|
||||
import { Button } from '@multica/ui/components/ui/button'
|
||||
import { Card, CardContent } from '@multica/ui/components/ui/card'
|
||||
|
||||
// Feature components
|
||||
import { ThemeProvider } from '@multica/ui/components/theme-provider'
|
||||
import { Chat } from '@multica/ui/components/chat'
|
||||
import { Markdown } from '@multica/ui/components/markdown'
|
||||
|
||||
// Hooks
|
||||
import { useIsMobile } from '@multica/ui/hooks/use-mobile'
|
||||
import { useAutoScroll } from '@multica/ui/hooks/use-auto-scroll'
|
||||
|
||||
// Utilities
|
||||
import { cn } from '@multica/ui/lib/utils'
|
||||
|
||||
// Styles (app entry point)
|
||||
import '@multica/ui/globals.css'
|
||||
```
|
||||
|
||||
## Adding Components
|
||||
|
||||
```bash
|
||||
pnpm --filter @multica/ui dlx shadcn@latest add <component>
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue