From 5cdc6974f70d68171904335583f4131ed5adc455 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Thu, 5 Feb 2026 02:41:29 +0800 Subject: [PATCH 1/5] refactor(profile): implement progressive disclosure for profile loading - Only inject workspace.md into system prompt (not soul, user, memory) - Add profile directory path to workspace section for on-demand file access - Agent now reads soul.md, user.md, memory.md on first session using tools - Reduces system prompt size and improves token efficiency - Aligns with workspace.md template instructions ("Read soul.md first") Co-Authored-By: Claude Opus 4.5 --- src/agent/system-prompt/builder.test.ts | 24 ++++-- src/agent/system-prompt/builder.ts | 2 +- src/agent/system-prompt/sections.test.ts | 66 ++++++++------- src/agent/system-prompt/sections.ts | 102 ++++++++++++++--------- 4 files changed, 114 insertions(+), 80 deletions(-) diff --git a/src/agent/system-prompt/builder.test.ts b/src/agent/system-prompt/builder.test.ts index e9657a5e..09a29467 100644 --- a/src/agent/system-prompt/builder.test.ts +++ b/src/agent/system-prompt/builder.test.ts @@ -15,12 +15,13 @@ const TOOLS = ["read", "write", "edit", "glob", "exec", "memory_get", "memory_se describe("buildSystemPrompt", () => { // ── Full mode ───────────────────────────────────────────────────────── - it("full mode includes all profile sections", () => { + it("full mode includes workspace section only (progressive disclosure)", () => { + // Soul, user, memory are read on-demand by the agent const result = buildSystemPrompt({ mode: "full", profile: PROFILE }); - expect(result).toContain("# Soul"); - expect(result).toContain("# User"); + expect(result).not.toContain("# Soul"); + expect(result).not.toContain("# User"); expect(result).toContain("# Workspace"); - expect(result).toContain("# Memory"); + expect(result).not.toContain("# Memory"); }); it("full mode includes safety constitution", () => { @@ -76,13 +77,17 @@ describe("buildSystemPrompt", () => { expect(result).toContain("os=darwin (arm64)"); }); - it("full mode includes profile directory", () => { + it("full mode includes profile info in workspace section", () => { const result = buildSystemPrompt({ mode: "full", profileDir: "/home/user/.super-multica/agent-profiles/test", + profile: { workspace: "Workspace rules" }, }); - expect(result).toContain("## Profile Directory"); + expect(result).toContain("## Profile"); expect(result).toContain("/home/user/.super-multica/agent-profiles/test"); + expect(result).toContain("soul.md"); + expect(result).toContain("user.md"); + expect(result).toContain("memory.md"); }); it("full mode excludes subagent section", () => { @@ -242,8 +247,13 @@ describe("buildSystemPromptWithReport", () => { it("report marks excluded sections correctly in minimal mode", () => { const { report } = buildSystemPromptWithReport({ mode: "minimal" }); + // Identity is now included in all modes (just a one-liner) const identity = report.sections.find((s) => s.name === "identity"); - expect(identity?.included).toBe(false); + expect(identity?.included).toBe(true); + + // User and memory are excluded (progressive disclosure) + const user = report.sections.find((s) => s.name === "user"); + expect(user?.included).toBe(false); const safety = report.sections.find((s) => s.name === "safety"); expect(safety?.included).toBe(true); diff --git a/src/agent/system-prompt/builder.ts b/src/agent/system-prompt/builder.ts index b0f1f45f..619b05f8 100644 --- a/src/agent/system-prompt/builder.ts +++ b/src/agent/system-prompt/builder.ts @@ -56,7 +56,7 @@ export function buildSystemPromptWithReport(options: SystemPromptOptions): { const candidates: Array<{ name: string; lines: string[] }> = [ { name: "identity", lines: buildIdentitySection(profile, mode) }, { name: "user", lines: buildUserSection(profile, mode) }, - { name: "workspace", lines: buildWorkspaceSection(profile, mode) }, + { name: "workspace", lines: buildWorkspaceSection(profile, mode, profileDir) }, { name: "memory", lines: buildMemoryFileSection(profile, mode) }, { name: "safety", lines: buildSafetySection(includeSafety) }, { name: "tooling", lines: buildToolingSummary(tools, mode) }, diff --git a/src/agent/system-prompt/sections.test.ts b/src/agent/system-prompt/sections.test.ts index f98ccabd..cfd25f6f 100644 --- a/src/agent/system-prompt/sections.test.ts +++ b/src/agent/system-prompt/sections.test.ts @@ -15,9 +15,15 @@ import { } from "./sections.js"; describe("buildIdentitySection", () => { - it("returns soul content in full mode", () => { + it("returns identity line in full mode (progressive disclosure)", () => { + // Soul content is no longer injected - agent reads soul.md on demand + const result = buildIdentitySection({ soul: "You are helpful.", config: { name: "Cleo" } }, "full"); + expect(result).toEqual(["You are Cleo, a Super Multica agent."]); + }); + + it("returns generic identity line in full mode without name", () => { const result = buildIdentitySection({ soul: "You are helpful." }, "full"); - expect(result).toEqual(["You are helpful."]); + expect(result).toEqual(["You are a Super Multica agent."]); }); it("returns identity line with name in none mode", () => { @@ -30,47 +36,48 @@ describe("buildIdentitySection", () => { expect(result).toEqual(["You are a Super Multica agent."]); }); - it("returns empty in minimal mode", () => { - const result = buildIdentitySection({ soul: "data" }, "minimal"); - expect(result).toEqual([]); + it("returns identity line in minimal mode", () => { + const result = buildIdentitySection({ soul: "data", config: { name: "Cleo" } }, "minimal"); + expect(result).toEqual(["You are Cleo, a Super Multica agent."]); }); }); describe("buildUserSection", () => { - it("returns user content in full mode", () => { - const result = buildUserSection({ user: "Name: Bob" }, "full"); - expect(result).toEqual(["Name: Bob"]); - }); - - it("returns empty in minimal mode", () => { - const result = buildUserSection({ user: "data" }, "minimal"); - expect(result).toEqual([]); - }); - - it("returns empty when no user content", () => { - const result = buildUserSection({}, "full"); - expect(result).toEqual([]); + it("returns empty in all modes (progressive disclosure)", () => { + // User content is no longer injected - agent reads user.md on demand + expect(buildUserSection({ user: "Name: Bob" }, "full")).toEqual([]); + expect(buildUserSection({ user: "data" }, "minimal")).toEqual([]); + expect(buildUserSection({}, "full")).toEqual([]); }); }); describe("buildWorkspaceSection", () => { - it("returns workspace content in full mode", () => { + it("returns workspace content with profile info in full mode", () => { + const result = buildWorkspaceSection({ workspace: "Rules here" }, "full", "/path/to/profile"); + const text = result.join("\n"); + expect(text).toContain("## Profile"); + expect(text).toContain("/path/to/profile"); + expect(text).toContain("soul.md"); + expect(text).toContain("user.md"); + expect(text).toContain("memory.md"); + expect(text).toContain("Rules here"); + }); + + it("returns workspace content without profile dir", () => { const result = buildWorkspaceSection({ workspace: "Rules here" }, "full"); expect(result).toEqual(["Rules here"]); }); it("returns empty in minimal mode", () => { expect(buildWorkspaceSection({ workspace: "data" }, "minimal")).toEqual([]); + expect(buildWorkspaceSection({ workspace: "data" }, "minimal", "/path")).toEqual([]); }); }); describe("buildMemoryFileSection", () => { - it("returns memory content in full mode", () => { - const result = buildMemoryFileSection({ memory: "Key facts" }, "full"); - expect(result).toEqual(["Key facts"]); - }); - - it("returns empty in minimal mode", () => { + it("returns empty in all modes (progressive disclosure)", () => { + // Memory content is no longer injected - agent reads memory.md on demand + expect(buildMemoryFileSection({ memory: "Key facts" }, "full")).toEqual([]); expect(buildMemoryFileSection({ memory: "data" }, "minimal")).toEqual([]); }); }); @@ -227,12 +234,9 @@ describe("buildRuntimeSection", () => { }); describe("buildProfileDirSection", () => { - it("includes path in full mode", () => { - const result = buildProfileDirSection("/path/to/profile", "full"); - expect(result.join("\n")).toContain("/path/to/profile"); - }); - - it("returns empty in minimal mode", () => { + it("returns empty in all modes (merged into workspace section)", () => { + // Profile directory info is now part of buildWorkspaceSection + expect(buildProfileDirSection("/path/to/profile", "full")).toEqual([]); expect(buildProfileDirSection("/path", "minimal")).toEqual([]); }); }); diff --git a/src/agent/system-prompt/sections.ts b/src/agent/system-prompt/sections.ts index af1569d4..a0126d56 100644 --- a/src/agent/system-prompt/sections.ts +++ b/src/agent/system-prompt/sections.ts @@ -47,59 +47,85 @@ const TOOL_ORDER = [ // ─── Section builders ─────────────────────────────────────────────────────── /** - * Identity section — soul.md in full mode, single line in none mode, nothing in minimal. + * Identity section — brief identity line only. + * Full profile content (soul.md) is loaded on-demand by the agent. */ export function buildIdentitySection( profile: ProfileContent | undefined, mode: SystemPromptMode, ): string[] { - if (mode === "none") { - const name = profile?.config?.name; + const name = profile?.config?.name; + if (mode === "none" || mode === "minimal") { return name ? [`You are ${name}, a Super Multica agent.`] : ["You are a Super Multica agent."]; } - if (mode === "minimal") { - return []; - } - // full mode - if (profile?.soul) { - return [profile.soul]; - } + // full mode - just identity line, agent reads soul.md on demand + return name + ? [`You are ${name}, a Super Multica agent.`] + : ["You are a Super Multica agent."]; +} + +/** + * User section — no longer injected into system prompt. + * Agent reads user.md on demand from profile directory. + */ +export function buildUserSection( + _profile: ProfileContent | undefined, + _mode: SystemPromptMode, +): string[] { + // Progressive disclosure: agent reads user.md on demand return []; } /** - * User section — user.md content (full mode only). - */ -export function buildUserSection( - profile: ProfileContent | undefined, - mode: SystemPromptMode, -): string[] { - if (mode !== "full" || !profile?.user) return []; - return [profile.user]; -} - -/** - * Workspace section — workspace.md content (full mode only). + * Workspace section — workspace.md content with profile directory path. + * This is the primary profile content injected into system prompt. + * Other profile files (soul.md, user.md, memory.md) are read on demand. */ export function buildWorkspaceSection( profile: ProfileContent | undefined, mode: SystemPromptMode, + profileDir?: string, ): string[] { - if (mode !== "full" || !profile?.workspace) return []; - return [profile.workspace]; + if (mode !== "full") return []; + + const lines: string[] = []; + + // Add profile directory context first + if (profileDir) { + lines.push( + "## Profile", + "", + `Your profile directory: \`${profileDir}\``, + "", + "Profile files:", + "- `soul.md` — Your identity and values", + "- `user.md` — Information about your user", + "- `workspace.md` — Guidelines and conventions (below)", + "- `memory.md` — Persistent knowledge", + "", + ); + } + + // Add workspace.md content + if (profile?.workspace) { + lines.push(profile.workspace); + } + + return lines; } /** - * Memory section — memory.md content (full mode only). + * Memory section — no longer injected into system prompt. + * Agent reads memory.md on demand from profile directory. */ export function buildMemoryFileSection( - profile: ProfileContent | undefined, - mode: SystemPromptMode, + _profile: ProfileContent | undefined, + _mode: SystemPromptMode, ): string[] { - if (mode !== "full" || !profile?.memory) return []; - return [profile.memory]; + // Progressive disclosure: agent reads memory.md on demand + return []; } /** @@ -265,21 +291,15 @@ export function buildRuntimeSection( } /** - * Profile directory section — tells agent where its files live. - * Full mode only. + * Profile directory section — now merged into buildWorkspaceSection. + * Kept for backwards compatibility but returns empty. */ export function buildProfileDirSection( - profileDir: string | undefined, - mode: SystemPromptMode, + _profileDir: string | undefined, + _mode: SystemPromptMode, ): string[] { - if (mode !== "full" || !profileDir) return []; - return [ - "## Profile Directory", - "", - `Your profile files are located at: \`${profileDir}\``, - "", - "Use `edit` or `write` tools to update these files when needed.", - ]; + // Profile directory info is now part of workspace section + return []; } /** From 9b16001e0eeb75e76f3618b5796758f44ad1cb46 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Thu, 5 Feb 2026 02:58:08 +0800 Subject: [PATCH 2/5] feat(subagent): pass tools to subagent system prompt Resolve tools before building subagent system prompt so the "## Tooling" section is included, matching OpenClaw's pattern. Co-Authored-By: Claude Opus 4.5 --- src/agent/subagent/announce.ts | 1 + src/agent/subagent/types.ts | 2 ++ src/agent/tools/sessions-spawn.ts | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/src/agent/subagent/announce.ts b/src/agent/subagent/announce.ts index 7fa27d65..fbfa9800 100644 --- a/src/agent/subagent/announce.ts +++ b/src/agent/subagent/announce.ts @@ -29,6 +29,7 @@ export function buildSubagentSystemPrompt(params: SubagentSystemPromptParams): s label: params.label, task: params.task, }, + tools: params.tools, }); } diff --git a/src/agent/subagent/types.ts b/src/agent/subagent/types.ts index dfbf62bb..d4043572 100644 --- a/src/agent/subagent/types.ts +++ b/src/agent/subagent/types.ts @@ -71,4 +71,6 @@ export type SubagentSystemPromptParams = { childSessionId: string; label?: string | undefined; task: string; + /** Tool names available to the subagent (for tooling summary in system prompt) */ + tools?: string[] | undefined; }; diff --git a/src/agent/tools/sessions-spawn.ts b/src/agent/tools/sessions-spawn.ts index 9ae2cc69..fc93199e 100644 --- a/src/agent/tools/sessions-spawn.ts +++ b/src/agent/tools/sessions-spawn.ts @@ -11,6 +11,7 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import { getHub } from "../../hub/hub-singleton.js"; import { buildSubagentSystemPrompt } from "../subagent/announce.js"; import { registerSubagentRun } from "../subagent/registry.js"; +import { resolveTools } from "../tools.js"; const SessionsSpawnSchema = Type.Object({ task: Type.String({ description: "The task for the subagent to perform.", minLength: 1 }), @@ -84,12 +85,17 @@ export function createSessionsSpawnTool( const runId = uuidv7(); const childSessionId = uuidv7(); + // Resolve tools for the subagent (with isSubagent=true for policy filtering) + const subagentTools = resolveTools({ isSubagent: true }); + const toolNames = subagentTools.map((t) => t.name); + // Build system prompt for the child const systemPrompt = buildSubagentSystemPrompt({ requesterSessionId, childSessionId, label, task, + tools: toolNames, }); // Spawn child agent via Hub From 087d1a8653b61444b63cd84d4298cc2cda1b44aa Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Thu, 5 Feb 2026 02:58:15 +0800 Subject: [PATCH 3/5] refactor(tools): remove tool profile layer from policy system Simplify 4-layer policy to 3-layer: - Layer 1: Global allow/deny (user config) - Layer 2: Provider-specific rules - Layer 3: Subagent restrictions Removed: - ToolProfileId type (minimal/coding/web/full) - TOOL_PROFILES constant - getProfilePolicy function - profile field from ToolsConfig Users can achieve the same effect using allow/deny with group:* syntax. Co-Authored-By: Claude Opus 4.5 --- src/agent/tools/README.md | 151 ++++++++++---------------------- src/agent/tools/README.zh-CN.md | 151 ++++++++++---------------------- src/agent/tools/groups.ts | 49 +---------- src/agent/tools/index.ts | 5 +- src/agent/tools/policy.test.ts | 72 +++++---------- src/agent/tools/policy.ts | 61 ++++--------- 6 files changed, 128 insertions(+), 361 deletions(-) diff --git a/src/agent/tools/README.md b/src/agent/tools/README.md index 80087453..db5b5266 100644 --- a/src/agent/tools/README.md +++ b/src/agent/tools/README.md @@ -19,28 +19,22 @@ The tools system provides LLM agents with capabilities to interact with the exte │ ▼ ┌─────────────────────────────────────────────────────────────────┐ -│ 4-Layer Policy Filter │ +│ 3-Layer Policy Filter │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Layer 1: Profile │ │ -│ │ Base tool set: minimal | coding | web | full │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Layer 2: Global Allow/Deny │ │ +│ │ Layer 1: Global Allow/Deny │ │ │ │ User customization via CLI or config │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Layer 3: Provider-Specific │ │ +│ │ Layer 2: Provider-Specific │ │ │ │ Different rules for different LLM providers │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Layer 4: Subagent Restrictions │ │ +│ │ Layer 3: Subagent Restrictions │ │ │ │ Limited tools for spawned child agents │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ @@ -55,20 +49,20 @@ The tools system provides LLM agents with capabilities to interact with the exte ## 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 (requires API key) | -| Memory Get | `memory_get` | Retrieve a value from persistent memory | -| Memory Set | `memory_set` | Store a value in persistent memory | -| Memory Delete | `memory_delete` | Delete a value from persistent memory | -| Memory List | `memory_list` | List all keys in persistent memory | +| 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 (requires API key) | +| Memory Get | `memory_get` | Retrieve a value from persistent memory | +| Memory Set | `memory_set` | Store a value in persistent memory | +| Memory Delete | `memory_delete` | Delete a value from persistent memory | +| Memory List | `memory_list` | List all keys in persistent memory | > **Note**: Memory tools require a `profileId` to be specified. They store data in the profile's memory directory. @@ -76,24 +70,13 @@ The tools system provides LLM agents with capabilities to interact with the exte 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:memory` | memory_get, memory_set, memory_delete, memory_list| -| `group:core` | All of the above (excluding memory) | - -## Tool Profiles - -Profiles are predefined tool sets for common use cases: - -| Profile | Description | Tools | -| --------- | ----------------------- | ---------------------------------- | -| `minimal` | No tools (chat-only) | None | -| `coding` | File system + execution | group:fs, group:runtime | -| `web` | Coding + web access | group:fs, group:runtime, group:web | -| `full` | No restrictions | All tools | +| Group | Tools | +| --------------- | -------------------------------------------------- | +| `group:fs` | read, write, edit, glob | +| `group:runtime` | exec, process | +| `group:web` | web_search, web_fetch | +| `group:memory` | memory_get, memory_set, memory_delete, memory_list | +| `group:core` | All of the above (excluding memory) | ## Usage @@ -102,11 +85,8 @@ Profiles are predefined tool sets for common use cases: All commands use the unified `multica` CLI (or `pnpm multica` during development). ```bash -# Use a specific profile -multica run --tools-profile coding "list files" - -# Minimal profile with specific tools allowed -multica run --tools-profile minimal --tools-allow exec "run ls" +# 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" @@ -122,14 +102,11 @@ import { Agent } from './runner.js'; const agent = new Agent({ tools: { - // Layer 1: Base profile - profile: 'coding', + // Layer 1: Global allow/deny + allow: ['group:fs', 'group:runtime', 'web_fetch'], + deny: ['exec'], - // Layer 2: Global customization - allow: ['web_fetch'], // Add web_fetch to coding profile - deny: ['exec'], // But deny exec - - // Layer 3: Provider-specific rules + // Layer 2: Provider-specific rules byProvider: { google: { deny: ['exec', 'process'], // Google models can't use runtime tools @@ -137,7 +114,7 @@ const agent = new Agent({ }, }, - // Layer 4: Subagent mode + // Layer 3: Subagent mode isSubagent: false, }); ``` @@ -150,43 +127,28 @@ Use the tools CLI to inspect and test configurations: # List all available tools multica tools list -# List tools after applying a profile -multica tools list --profile coding +# List tools with allow rules +multica tools list --allow group:fs,group:runtime # List tools with deny rules -multica tools list --profile coding --deny exec +multica tools list --deny exec # Show all tool groups multica tools groups - -# Show all profiles -multica tools profiles ``` ## Policy System Details -### Layer 1: Profile +### Layer 1: Global Allow/Deny -The profile determines the base set of available tools. If not specified, all tools are available. +User-specified allow/deny lists: -```typescript -// In groups.ts -export const TOOL_PROFILES = { - minimal: { allow: [] }, // No tools - coding: { allow: ['group:fs', 'group:runtime'] }, // FS + execution - web: { allow: ['group:fs', 'group:runtime', 'group:web'] }, // + web - full: {}, // No restrictions -}; -``` +- `allow`: Only these tools are available (supports group:\* syntax) +- `deny`: These tools are blocked (takes precedence over allow) -### Layer 2: Global Allow/Deny +If no `allow` list is specified, all tools are available by default. -User-specified allow/deny lists that modify the profile's tool set: - -- `allow`: Only these tools are available (additive to profile) -- `deny`: These tools are blocked (takes precedence over allow) - -### Layer 3: Provider-Specific +### Layer 2: Provider-Specific Different LLM providers may have different capabilities or restrictions: @@ -199,7 +161,7 @@ Different LLM providers may have different capabilities or restrictions: } ``` -### Layer 4: Subagent Restrictions +### Layer 3: Subagent Restrictions When `isSubagent: true`, additional restrictions are applied to prevent spawned agents from accessing sensitive tools like session management. @@ -280,7 +242,7 @@ Tools configuration can be defined in Agent Profile's `config.json`, allowing di │ │ coder │ │ reviewer │ │ devops │ │ │ │ │ │ │ │ │ │ │ │ tools: │ │ tools: │ │ tools: │ │ -│ │ coding │ │ minimal │ │ full │ │ +│ │ allow:fs │ │ deny:* │ │ allow:* │ │ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │ │ │ │ └─────────┼────────────────┼────────────────┼─────────────────────┘ @@ -296,7 +258,7 @@ Each Agent's Profile can define its own tools configuration in `config.json`: ```json { "tools": { - "profile": "coding", + "allow": ["group:fs", "group:runtime"], "deny": ["exec"] }, "provider": "anthropic", @@ -305,28 +267,3 @@ Each Agent's Profile can define its own tools configuration in `config.json`: ``` See [Profile README](../profile/README.md) for full documentation. - -### Config Priority - -When both Profile config and CLI options are provided: - -1. **Profile `config.json`** - Base configuration -2. **CLI options** - Override/extend profile settings - -```bash -# Profile has tools.profile = "coding" -# CLI adds --tools-deny exec -# Result: coding profile without exec tool -multica run --profile my-agent --tools-deny exec "list files" -``` - -## Future Tools - -The following tools are planned for future implementation: - -- **Browser** - Simplified web automation (screenshot, click, type) -- **Session Management** - `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` -- **Image** - Image generation and manipulation -- **Cron** - Scheduled task execution -- **Message** - Inter-agent communication -- **Canvas** - Visual output generation diff --git a/src/agent/tools/README.zh-CN.md b/src/agent/tools/README.zh-CN.md index 5ac1a99e..80d84815 100644 --- a/src/agent/tools/README.zh-CN.md +++ b/src/agent/tools/README.zh-CN.md @@ -19,28 +19,22 @@ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ -│ 4 层策略过滤器 │ +│ 3 层策略过滤器 │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 第 1 层: Profile │ │ -│ │ 基础工具集: minimal | coding | web | full │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 第 2 层: 全局 Allow/Deny │ │ +│ │ 第 1 层: 全局 Allow/Deny │ │ │ │ 通过 CLI 或配置文件进行用户自定义 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 第 3 层: Provider 特定规则 │ │ +│ │ 第 2 层: Provider 特定规则 │ │ │ │ 不同 LLM Provider 有不同的规则 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ -│ │ 第 4 层: Subagent 限制 │ │ +│ │ 第 3 层: Subagent 限制 │ │ │ │ 子 Agent 的工具访问受限 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ @@ -55,20 +49,20 @@ ## 可用工具 -| 工具 | 名称 | 描述 | -| ------------- | --------------- | --------------------------------------------- | -| 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 Get | `memory_get` | 从持久化内存中获取值 | -| Memory Set | `memory_set` | 向持久化内存中存储值 | -| Memory Delete | `memory_delete` | 从持久化内存中删除值 | -| Memory List | `memory_list` | 列出持久化内存中的所有键 | +| 工具 | 名称 | 描述 | +| ------------- | --------------- | ------------------------ | +| 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 Get | `memory_get` | 从持久化内存中获取值 | +| Memory Set | `memory_set` | 向持久化内存中存储值 | +| Memory Delete | `memory_delete` | 从持久化内存中删除值 | +| Memory List | `memory_list` | 列出持久化内存中的所有键 | > **注意**: Memory 工具需要指定 `profileId`。数据存储在 Profile 的 memory 目录中。 @@ -76,24 +70,13 @@ 工具组提供了一次性允许/禁止多个工具的快捷方式: -| 组 | 工具 | -| --------------- | ------------------------------------------------- | -| `group:fs` | read, write, edit, glob | -| `group:runtime` | exec, process | -| `group:web` | web_search, web_fetch | -| `group:memory` | memory_get, memory_set, memory_delete, memory_list| -| `group:core` | 以上所有(不包括 memory) | - -## 工具配置文件 - -配置文件是为常见用例预定义的工具集: - -| Profile | 描述 | 工具 | -| --------- | ------------------- | ---------------------------------- | -| `minimal` | 无工具(仅聊天) | 无 | -| `coding` | 文件系统 + 执行 | group:fs, group:runtime | -| `web` | 编码 + 网络访问 | group:fs, group:runtime, group:web | -| `full` | 无限制 | 所有工具 | +| 组 | 工具 | +| --------------- | -------------------------------------------------- | +| `group:fs` | read, write, edit, glob | +| `group:runtime` | exec, process | +| `group:web` | web_search, web_fetch | +| `group:memory` | memory_get, memory_set, memory_delete, memory_list | +| `group:core` | 以上所有(不包括 memory) | ## 使用方法 @@ -102,11 +85,8 @@ 所有命令使用统一的 `multica` CLI(开发时使用 `pnpm multica`)。 ```bash -# 使用特定配置文件 -multica run --tools-profile coding "list files" - -# 最小配置文件 + 允许特定工具 -multica run --tools-profile minimal --tools-allow exec "run ls" +# 只允许特定工具 +multica run --tools-allow group:fs,group:runtime "list files" # 禁止特定工具 multica run --tools-deny exec,process "read file.txt" @@ -122,14 +102,11 @@ import { Agent } from './runner.js'; const agent = new Agent({ tools: { - // 第 1 层: 基础配置文件 - profile: 'coding', + // 第 1 层: 全局 allow/deny + allow: ['group:fs', 'group:runtime', 'web_fetch'], + deny: ['exec'], - // 第 2 层: 全局自定义 - allow: ['web_fetch'], // 在 coding 配置文件基础上添加 web_fetch - deny: ['exec'], // 但禁止 exec - - // 第 3 层: Provider 特定规则 + // 第 2 层: Provider 特定规则 byProvider: { google: { deny: ['exec', 'process'], // Google 模型不能使用运行时工具 @@ -137,7 +114,7 @@ const agent = new Agent({ }, }, - // 第 4 层: Subagent 模式 + // 第 3 层: Subagent 模式 isSubagent: false, }); ``` @@ -150,43 +127,28 @@ const agent = new Agent({ # 列出所有可用工具 multica tools list -# 列出应用配置文件后的工具 -multica tools list --profile coding +# 列出带有允许规则的工具 +multica tools list --allow group:fs,group:runtime # 列出带有禁止规则的工具 -multica tools list --profile coding --deny exec +multica tools list --deny exec # 显示所有工具组 multica tools groups - -# 显示所有配置文件 -multica tools profiles ``` ## 策略系统详情 -### 第 1 层: Profile +### 第 1 层: 全局 Allow/Deny -配置文件决定了可用工具的基础集合。如果未指定,则所有工具都可用。 +用户指定的 allow/deny 列表: -```typescript -// 在 groups.ts 中 -export const TOOL_PROFILES = { - minimal: { allow: [] }, // 无工具 - coding: { allow: ['group:fs', 'group:runtime'] }, // 文件系统 + 执行 - web: { allow: ['group:fs', 'group:runtime', 'group:web'] }, // + 网络 - full: {}, // 无限制 -}; -``` +- `allow`: 只有这些工具可用(支持 group:\* 语法) +- `deny`: 这些工具被阻止(优先于 allow) -### 第 2 层: 全局 Allow/Deny +如果未指定 `allow` 列表,默认所有工具都可用。 -用户指定的 allow/deny 列表,用于修改配置文件的工具集: - -- `allow`: 只有这些工具可用(在配置文件基础上添加) -- `deny`: 这些工具被阻止(优先于 allow) - -### 第 3 层: Provider 特定规则 +### 第 2 层: Provider 特定规则 不同的 LLM Provider 可能有不同的能力或限制: @@ -199,7 +161,7 @@ export const TOOL_PROFILES = { } ``` -### 第 4 层: Subagent 限制 +### 第 3 层: Subagent 限制 当 `isSubagent: true` 时,会应用额外的限制,防止子 Agent 访问敏感工具(如会话管理)。 @@ -280,7 +242,7 @@ pnpm test src/agent/tools/policy.test.ts │ │ coder │ │ reviewer │ │ devops │ │ │ │ │ │ │ │ │ │ │ │ tools: │ │ tools: │ │ tools: │ │ -│ │ coding │ │ minimal │ │ full │ │ +│ │ allow:fs │ │ deny:* │ │ allow:* │ │ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │ │ │ │ └─────────┼────────────────┼────────────────┼─────────────────────┘ @@ -296,7 +258,7 @@ pnpm test src/agent/tools/policy.test.ts ```json { "tools": { - "profile": "coding", + "allow": ["group:fs", "group:runtime"], "deny": ["exec"] }, "provider": "anthropic", @@ -305,28 +267,3 @@ pnpm test src/agent/tools/policy.test.ts ``` 详见 [Profile README](../profile/README.md)。 - -### 配置优先级 - -当同时提供 Profile 配置和 CLI 选项时: - -1. **Profile `config.json`** - 基础配置 -2. **CLI 选项** - 覆盖/扩展 Profile 设置 - -```bash -# Profile 有 tools.profile = "coding" -# CLI 添加 --tools-deny exec -# 结果: coding 配置文件但没有 exec 工具 -multica run --profile my-agent --tools-deny exec "list files" -``` - -## 未来工具 - -以下工具计划在未来实现: - -- **Browser** - 简化的网页自动化(截图、点击、输入) -- **Session Management** - `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` -- **Image** - 图像生成和处理 -- **Cron** - 定时任务执行 -- **Message** - Agent 间通信 -- **Canvas** - 可视化输出生成 diff --git a/src/agent/tools/groups.ts b/src/agent/tools/groups.ts index 1e9edf6c..dde6b5a0 100644 --- a/src/agent/tools/groups.ts +++ b/src/agent/tools/groups.ts @@ -1,12 +1,10 @@ /** - * Tool groups and profiles for policy-based filtering. + * Tool groups for policy-based filtering. * * Groups provide shortcuts for allowing/denying multiple tools at once. - * Profiles are predefined tool sets for common use cases. + * Use "group:name" in allow/deny lists. */ -export type ToolProfileId = "minimal" | "coding" | "web" | "full"; - /** * Tool name aliases for compatibility. * Maps alternative names to canonical tool names. @@ -51,29 +49,6 @@ export const TOOL_GROUPS: Record = { ], }; -/** - * Tool profiles - predefined tool sets. - */ -export const TOOL_PROFILES: Record = { - // Minimal: no tools (useful for chat-only agents) - minimal: { - allow: [], - }, - - // Coding: file system + execution (default for coding tasks) - coding: { - allow: ["group:fs", "group:runtime"], - }, - - // Web: coding + web access - web: { - allow: ["group:fs", "group:runtime", "group:web"], - }, - - // Full: no restrictions - full: {}, -}; - /** * Default tools denied for subagents. * Subagents should not have access to session management or system tools. @@ -118,23 +93,3 @@ export function expandToolGroups(list?: string[]): string[] { return Array.from(new Set(expanded)); } - -/** - * Get the policy for a profile. - */ -export function getProfilePolicy( - profile?: ToolProfileId, -): { allow?: string[]; deny?: string[] } | undefined { - if (!profile) return undefined; - const resolved = TOOL_PROFILES[profile]; - if (!resolved) return undefined; - if (!resolved.allow && !resolved.deny) return undefined; - const result: { allow?: string[]; deny?: string[] } = {}; - if (resolved.allow) { - result.allow = [...resolved.allow]; - } - if (resolved.deny) { - result.deny = [...resolved.deny]; - } - return result; -} diff --git a/src/agent/tools/index.ts b/src/agent/tools/index.ts index 1e6f6334..70b616f4 100644 --- a/src/agent/tools/index.ts +++ b/src/agent/tools/index.ts @@ -8,17 +8,14 @@ export { createProcessTool } from "./process.js"; export { createGlobTool } from "./glob.js"; export { createWebFetchTool, createWebSearchTool } from "./web/index.js"; -// Tool groups and profiles +// Tool groups export { - type ToolProfileId, TOOL_NAME_ALIASES, TOOL_GROUPS, - TOOL_PROFILES, DEFAULT_SUBAGENT_TOOL_DENY, normalizeToolName, normalizeToolList, expandToolGroups, - getProfilePolicy, } from "./groups.js"; // Tool policy system diff --git a/src/agent/tools/policy.test.ts b/src/agent/tools/policy.test.ts index e0902708..3cfa5276 100644 --- a/src/agent/tools/policy.test.ts +++ b/src/agent/tools/policy.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { filterTools } from "./policy.js"; -import { TOOL_PROFILES, expandToolGroups } from "./groups.js"; +import { expandToolGroups } from "./groups.js"; // Mock tools for testing const mockTools = [ @@ -36,58 +36,12 @@ describe("tool groups", () => { }); }); -describe("tool profiles", () => { - it("minimal has empty allow", () => { - expect(TOOL_PROFILES.minimal.allow).toEqual([]); - }); - - it("coding has fs and runtime", () => { - expect(TOOL_PROFILES.coding.allow).toEqual(["group:fs", "group:runtime"]); - }); - - it("full has no restrictions", () => { - expect(TOOL_PROFILES.full.allow).toBeUndefined(); - expect(TOOL_PROFILES.full.deny).toBeUndefined(); - }); -}); - describe("filterTools", () => { it("no config returns all tools", () => { const filtered = filterTools(mockTools, {}); expect(filtered.length).toBe(mockTools.length); }); - it("minimal profile returns no tools", () => { - const filtered = filterTools(mockTools, { config: { profile: "minimal" } }); - expect(filtered.length).toBe(0); - }); - - it("coding profile returns fs and runtime", () => { - const filtered = filterTools(mockTools, { config: { profile: "coding" } }); - const names = filtered.map((t) => t.name).sort(); - expect(names).toEqual(["edit", "exec", "glob", "process", "read", "write"]); - }); - - it("web profile returns all", () => { - const filtered = filterTools(mockTools, { config: { profile: "web" } }); - const names = filtered.map((t) => t.name).sort(); - expect(names).toEqual([ - "edit", - "exec", - "glob", - "process", - "read", - "web_fetch", - "web_search", - "write", - ]); - }); - - it("full profile returns all tools", () => { - const filtered = filterTools(mockTools, { config: { profile: "full" } }); - expect(filtered.length).toBe(mockTools.length); - }); - it("deny specific tool", () => { const filtered = filterTools(mockTools, { config: { deny: ["exec"] } }); const names = filtered.map((t) => t.name); @@ -110,6 +64,22 @@ describe("filterTools", () => { const names = filtered.map((t) => t.name).sort(); expect(names).toEqual(["read", "write"]); }); + + it("allow with group:* syntax", () => { + const filtered = filterTools(mockTools, { + config: { allow: ["group:fs", "group:runtime"] }, + }); + const names = filtered.map((t) => t.name).sort(); + expect(names).toEqual(["edit", "exec", "glob", "process", "read", "write"]); + }); + + it("deny with group:* syntax", () => { + const filtered = filterTools(mockTools, { + config: { deny: ["group:web"] }, + }); + const names = filtered.map((t) => t.name).sort(); + expect(names).toEqual(["edit", "exec", "glob", "process", "read", "write"]); + }); }); describe("provider-specific filtering", () => { @@ -149,10 +119,10 @@ describe("subagent restrictions", () => { }); describe("combined filtering", () => { - it("profile + deny", () => { + it("allow + deny", () => { const filtered = filterTools(mockTools, { config: { - profile: "coding", + allow: ["group:fs", "group:runtime"], deny: ["exec"], }, }); @@ -160,10 +130,10 @@ describe("combined filtering", () => { expect(names).toEqual(["edit", "glob", "process", "read", "write"]); }); - it("profile + provider deny", () => { + it("allow + provider deny", () => { const filtered = filterTools(mockTools, { config: { - profile: "web", + allow: ["group:fs", "group:runtime", "group:web"], byProvider: { google: { deny: ["exec"] }, }, diff --git a/src/agent/tools/policy.ts b/src/agent/tools/policy.ts index 5b8c2fc0..3e1468fe 100644 --- a/src/agent/tools/policy.ts +++ b/src/agent/tools/policy.ts @@ -1,18 +1,15 @@ /** * Tool policy system for filtering tools based on configuration. * - * Supports 4 layers of filtering: - * 1. Profile - base tool set (minimal/coding/web/full) - * 2. Global allow/deny - user customization - * 3. Provider-specific - different rules for different LLM providers - * 4. Subagent restrictions - limited tools for spawned agents + * Supports 3 layers of filtering: + * 1. Global allow/deny - user customization + * 2. Provider-specific - different rules for different LLM providers + * 3. Subagent restrictions - limited tools for spawned agents */ import type { AgentTool } from "@mariozechner/pi-agent-core"; import { - type ToolProfileId, expandToolGroups, - getProfilePolicy, normalizeToolName, DEFAULT_SUBAGENT_TOOL_DENY, } from "./groups.js"; @@ -31,11 +28,9 @@ export interface ToolPolicy { * Full tool configuration from config file. */ export interface ToolsConfig { - /** Base profile (minimal/coding/web/full) */ - profile?: ToolProfileId; - /** Additional tools to allow */ + /** Tools to allow (supports group:* syntax) */ allow?: string[]; - /** Tools to deny */ + /** Tools to deny (takes precedence over allow) */ deny?: string[]; /** Provider-specific overrides */ byProvider?: Record; @@ -191,12 +186,11 @@ export interface FilterToolsOptions { } /** - * Filter tools through the 4-layer policy system. + * Filter tools through the 3-layer policy system. * - * Layer 1: Profile (base tool set) - * Layer 2: Global allow/deny - * Layer 3: Provider-specific - * Layer 4: Subagent restrictions + * Layer 1: Global allow/deny + * Layer 2: Provider-specific + * Layer 3: Subagent restrictions */ export function filterTools( tools: AgentTool[], @@ -206,15 +200,7 @@ export function filterTools( let filtered = tools; - // Layer 1: Profile - if (config?.profile) { - const profilePolicy = getProfilePolicy(config.profile); - if (profilePolicy) { - filtered = filterToolsByPolicy(filtered, profilePolicy); - } - } - - // Layer 2: Global allow/deny + // Layer 1: Global allow/deny if (config?.allow || config?.deny) { const globalPolicy: ToolPolicy = {}; if (config.allow) { @@ -226,7 +212,7 @@ export function filterTools( filtered = filterToolsByPolicy(filtered, globalPolicy); } - // Layer 3: Provider-specific + // Layer 2: Provider-specific if (provider && config?.byProvider) { const providerPolicy = resolveProviderPolicy(config.byProvider, provider); if (providerPolicy) { @@ -234,7 +220,7 @@ export function filterTools( } } - // Layer 4: Subagent restrictions + // Layer 3: Subagent restrictions if (isSubagent) { const subagentPolicy = getSubagentPolicy(); filtered = filterToolsByPolicy(filtered, subagentPolicy); @@ -246,7 +232,6 @@ export function filterTools( /** * Merge two ToolsConfig objects. * The override config takes precedence: - * - profile: override wins if set * - allow: union of both * - deny: union of both * - byProvider: deep merge with override taking precedence @@ -261,12 +246,6 @@ export function mergeToolsConfig( const result: ToolsConfig = {}; - // profile: override wins - const profile = override.profile ?? base.profile; - if (profile) { - result.profile = profile; - } - // allow: union const allow = mergeAllow(base.allow, override.allow); if (allow) { @@ -321,15 +300,7 @@ export function wouldToolBeAllowed( ): boolean { const { config, provider, isSubagent } = options; - // Layer 1: Profile - if (config?.profile) { - const profilePolicy = getProfilePolicy(config.profile); - if (profilePolicy && !isToolAllowed(toolName, profilePolicy)) { - return false; - } - } - - // Layer 2: Global allow/deny + // Layer 1: Global allow/deny if (config?.allow || config?.deny) { const globalPolicy: ToolPolicy = {}; if (config.allow) { @@ -343,7 +314,7 @@ export function wouldToolBeAllowed( } } - // Layer 3: Provider-specific + // Layer 2: Provider-specific if (provider && config?.byProvider) { const providerPolicy = resolveProviderPolicy(config.byProvider, provider); if (providerPolicy && !isToolAllowed(toolName, providerPolicy)) { @@ -351,7 +322,7 @@ export function wouldToolBeAllowed( } } - // Layer 4: Subagent restrictions + // Layer 3: Subagent restrictions if (isSubagent) { const subagentPolicy = getSubagentPolicy(); if (!isToolAllowed(toolName, subagentPolicy)) { From a3e639f8f598b6103a7e55155fde94adc2992763 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Thu, 5 Feb 2026 03:14:44 +0800 Subject: [PATCH 4/5] refactor(cli): make desktop app the default dev target - Change `multica dev` default from gateway+console+web to desktop app - Remove console from dev command options (use embedded Hub in desktop) - Update package.json scripts to reflect new defaults - Update README.md and CLAUDE.md architecture documentation The desktop app has an embedded Hub, so no separate console/gateway is needed for local development. Gateway is now optional, only needed for remote client access. Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 29 +++++++++--------- README.md | 55 ++++++++++++++++++++--------------- package.json | 6 ++-- src/agent/cli/commands/dev.ts | 53 ++++++++++++--------------------- 4 files changed, 70 insertions(+), 73 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 32d8a9d7..267369b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,13 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Super Multica is a distributed AI agent framework with a monorepo architecture. It includes an agent engine with multi-provider LLM support, a WebSocket gateway, a console hub for multi-agent coordination, and frontend apps (Next.js web, Electron desktop). +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. ## Monorepo Structure -- **`src/`** — Core modules (agent engine, gateway, console, client, shared types) +- **`src/`** — Core modules (agent engine, gateway, hub, shared types) +- **`apps/desktop`** — Electron + Vite + React desktop app (`@multica/desktop`) — **primary development target** - **`apps/web`** — Next.js 16 web app (`@multica/web`, port 3001) -- **`apps/desktop`** — Electron + Vite + React desktop app (`@multica/desktop`) - **`packages/ui`** — Shared UI component library (`@multica/ui`, Shadcn/Tailwind CSS v4) - **`packages/sdk`** — Gateway client SDK (`@multica/sdk`, Socket.io) - **`packages/store`** — Zustand state management (`@multica/store`) @@ -31,15 +31,14 @@ multica profile list # List profiles multica skills list # List skills multica tools list # List tools multica credentials init # Initialize credentials -multica dev # Start all dev services +multica dev # Start desktop app (default) multica help # Show help # Development servers -multica dev # All services (gateway:3000, console:4000, web:3001) -multica dev gateway # WebSocket gateway only -multica dev console # NestJS console with agent +multica dev # Desktop app (default, recommended) +multica dev gateway # WebSocket gateway only (for remote clients) multica dev web # Next.js web app -multica dev desktop # Electron desktop app +multica dev all # Gateway + web app # Build (turbo-orchestrated) pnpm build @@ -56,18 +55,22 @@ pnpm test:coverage # With v8 coverage ## Architecture ``` -Frontend (web:3001 / desktop) +Desktop App (standalone, recommended) + └─ Hub (embedded) + └─ Agent Engine (LLM runner, sessions, skills, tools) + └─ (Optional) Gateway connection for remote access + +Web App (requires Gateway) → @multica/sdk (GatewayClient, Socket.io) → Gateway (NestJS, WebSocket, port 3000) - → Console Hub (multi-agent coordination) - → Agent Engine (LLM runner, sessions, skills, tools) + → Hub + Agent Engine ``` **Agent Engine** (`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 (compaction modes: tokens, count, summary). Unified CLI in `src/agent/cli/index.ts` with subcommands in `src/agent/cli/commands/`. -**Gateway** (`src/gateway/`): NestJS WebSocket server with Socket.io for real-time message passing, RPC request/response, and streaming. +**Hub** (`src/hub/`): Manages agents and communication channels. Embedded in desktop app, or runs standalone for web clients. -**Console** (`src/console/`): NestJS hub for multi-agent coordination with a web dashboard. +**Gateway** (`src/gateway/`): NestJS WebSocket server with Socket.io for remote client access, message routing, and device verification. ## Tech Stack & Config diff --git a/README.md b/README.md index 5aee5c5e..c6fd6d28 100644 --- a/README.md +++ b/README.md @@ -13,18 +13,18 @@ src/ │ ├── skills/ # Modular skill system │ └── tools/ # Agent tools │ └── web/ # Web fetch and search tools -├── gateway/ # WebSocket gateway for distributed communication -├── hub/ # Multi-agent coordination hub -├── client/ # Client library -├── console/ # NestJS console application -└── shared/ # Shared types and gateway SDK - └── gateway-sdk/ # Gateway client SDK +├── gateway/ # WebSocket gateway for remote access +├── hub/ # Agent coordination hub +└── shared/ # Shared types apps/ +├── desktop/ # Electron desktop app (recommended) └── web/ # Next.js web application packages/ -└── sdk/ # SDK package for external use +├── sdk/ # Gateway client SDK +├── store/ # Zustand state management +└── ui/ # Shared UI components skills/ # Bundled skills (commit, code-review) ``` @@ -85,9 +85,8 @@ Example `skills.env.json5` (dynamic keys): Start services directly (no `source .env`): ```bash -multica dev console -multica run "hello" -multica dev gateway +multica dev # Start desktop app +multica run "hello" # Run CLI mode ``` Optional overrides: @@ -194,10 +193,10 @@ multica chat --profile my-agent multica run --thinking high "solve this complex problem" # Development servers -multica dev # Start all services -multica dev gateway # Gateway only (:3000) -multica dev console # Console only (:4000) +multica dev # Start desktop app (default) +multica dev gateway # Gateway only (:3000) - for remote clients multica dev web # Web app only (:3001) +multica dev all # Start gateway + web # Help multica help @@ -383,23 +382,33 @@ web_search({ }) ``` -## Distributed Architecture +## Architecture + +### Desktop App (Recommended) + +The Electron desktop app runs a standalone Hub with embedded Agent Engine: + +- **No Gateway required** for local development +- Direct IPC communication for optimal performance +- QR code pairing for mobile remote access +- Optional Gateway connection for web/remote clients ### Gateway -The WebSocket gateway enables distributed multi-agent communication: +The WebSocket gateway enables remote client access: -- Real-time message passing between agents +- Real-time message routing between clients and Hub - Streaming support for long-running operations - RPC-style request/response patterns +- Device verification and authentication ### Hub -The Hub manages multiple agents and gateway connections: +The Hub manages agents and communication: - Agent lifecycle management -- Communication channel coordination -- Device identification and tracking +- Multi-subscriber event distribution +- Device whitelist and token-based verification ## Scripts @@ -418,11 +427,11 @@ The Hub manages multiple agents and gateway connections: ### Development (shortcuts) -- `pnpm dev` - Run full stack (gateway + console + web) -- `pnpm dev:gateway` - Run gateway only -- `pnpm dev:console` - Run console only -- `pnpm dev:web` - Run web app only +- `pnpm dev` - Run desktop app (default, recommended) - `pnpm dev:desktop` - Run desktop app +- `pnpm dev:gateway` - Run gateway only (for remote clients) +- `pnpm dev:web` - Run web app only +- `pnpm dev:all` - Run gateway + web ### Build & Test diff --git a/package.json b/package.json index 1396639c..38c7ec1a 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "multica": "tsx src/agent/cli/index.ts", "mu": "tsx src/agent/cli/index.ts", "dev": "tsx src/agent/cli/index.ts dev", - "dev:gateway": "tsx src/agent/cli/index.ts dev gateway", - "dev:console": "tsx src/agent/cli/index.ts dev console", - "dev:web": "tsx src/agent/cli/index.ts dev web", "dev:desktop": "tsx src/agent/cli/index.ts dev desktop", + "dev:gateway": "tsx src/agent/cli/index.ts dev gateway", + "dev:web": "tsx src/agent/cli/index.ts dev web", + "dev:all": "tsx src/agent/cli/index.ts dev all", "build": "turbo build", "build:sdk": "pnpm --filter @multica/sdk build", "build:cli": "node scripts/build-cli.js", diff --git a/src/agent/cli/commands/dev.ts b/src/agent/cli/commands/dev.ts index 88d608ca..edd06cd6 100644 --- a/src/agent/cli/commands/dev.ts +++ b/src/agent/cli/commands/dev.ts @@ -2,44 +2,46 @@ * Dev command - Start development servers * * Usage: - * multica dev Start all services (gateway + console + web) - * multica dev gateway Start gateway only (:3000) - * multica dev console Start console only (:4000) + * multica dev Start desktop app (with embedded Hub) + * multica dev gateway Start gateway only (:3000) - for remote clients * multica dev web Start web app only (:3001) - * multica dev desktop Start desktop app + * multica dev all Start all services (gateway + web) */ import { spawn } from "node:child_process"; import { cyan, yellow, green, dim, red } from "../colors.js"; -type Service = "all" | "gateway" | "console" | "web" | "desktop" | "help"; +type Service = "all" | "gateway" | "web" | "desktop" | "help"; function printHelp() { console.log(` ${cyan("Usage:")} multica dev [service] ${cyan("Services:")} - ${yellow("(default)")} Start all services (gateway + console + web) - ${yellow("gateway")} Start Gateway server (:3000) - ${yellow("console")} Start Console server (:4000) + ${yellow("(default)")} Start Desktop app (with embedded Hub) + ${yellow("gateway")} Start Gateway server (:3000) - for remote clients ${yellow("web")} Start Web app (:3001) - ${yellow("desktop")} Start Desktop app + ${yellow("all")} Start all services (gateway + web) ${yellow("help")} Show this help ${cyan("Architecture:")} - Frontend (web:3001 / desktop) + Desktop App (standalone) + └─ Embedded Hub + Agent Engine + └─ (Optional) Gateway connection for remote access + + Web App (requires Gateway) → Gateway (WebSocket, :3000) - → Console Hub (multi-agent coordination, :4000) - → Agent Engine + → Hub + Agent Engine ${cyan("Examples:")} - ${dim("# Start all services")} + ${dim("# Start desktop app (recommended for local development)")} multica dev - ${dim("# Start only the gateway")} + ${dim("# Start desktop with remote Gateway for mobile access")} + GATEWAY_URL=http://localhost:3000 multica dev & multica dev gateway - ${dim("# Start web and gateway separately")} + ${dim("# Start web app with gateway")} multica dev gateway & multica dev web `); @@ -52,7 +54,7 @@ interface DevOptions { function parseArgs(argv: string[]): DevOptions { const args = [...argv]; - let service: Service = "all"; + let service: Service = "desktop"; let watch = true; while (args.length > 0) { @@ -68,7 +70,7 @@ function parseArgs(argv: string[]): DevOptions { } // Service name - if (["gateway", "console", "web", "desktop", "all", "help"].includes(arg)) { + if (["gateway", "web", "desktop", "all", "help"].includes(arg)) { service = arg as Service; } } @@ -105,14 +107,6 @@ async function startGateway(watch: boolean) { }); } -async function startConsole(watch: boolean) { - const watchFlag = watch ? "--watch" : ""; - return runCommand("tsx", [watchFlag, "src/console/main.ts"].filter(Boolean), { - name: "console", - color: "\x1b[33m", // yellow - }); -} - async function startWeb() { return runCommand("pnpm", ["--filter", "@multica/web", "dev"], { name: "web", @@ -130,20 +124,17 @@ async function startDesktop() { async function startAll(watch: boolean) { console.log(`\n${cyan("Starting all services...")}\n`); console.log(` ${"\x1b[34m"}Gateway${"\x1b[0m"} → http://localhost:3000`); - console.log(` ${"\x1b[33m"}Console${"\x1b[0m"} → http://localhost:4000`); console.log(` ${"\x1b[32m"}Web${"\x1b[0m"} → http://localhost:3001`); console.log(""); // Start all services const gateway = await startGateway(watch); - const console_ = await startConsole(watch); const web = await startWeb(); // Handle Ctrl+C const cleanup = () => { console.log(`\n${dim("Stopping all services...")}`); gateway.kill(); - console_.kill(); web.kill(); process.exit(0); }; @@ -154,7 +145,6 @@ async function startAll(watch: boolean) { // Wait for all to exit await Promise.all([ new Promise((resolve) => gateway.on("exit", resolve)), - new Promise((resolve) => console_.on("exit", resolve)), new Promise((resolve) => web.on("exit", resolve)), ]); } @@ -168,11 +158,6 @@ export async function devCommand(args: string[]): Promise { await startGateway(opts.watch); break; - case "console": - console.log(`\n${cyan("Starting Console...")} → http://localhost:4000\n`); - await startConsole(opts.watch); - break; - case "web": console.log(`\n${cyan("Starting Web App...")} → http://localhost:3001\n`); await startWeb(); From cdc64f9c837bfc32be6089bc611a84d001eb45d3 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Thu, 5 Feb 2026 03:28:33 +0800 Subject: [PATCH 5/5] feat(profile): enforce file-based memory with stronger guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update workspace.md template to prevent agents from claiming to "remember" things without actually editing files. The new guidance: - Uses "No Mental Notes!" as a strong warning - Lists specific trigger phrases (记住, Remember, I prefer, etc.) - Maps each trigger to the appropriate file to edit - Explicitly forbids saying "I'll remember" without file edits This addresses the issue where agents would acknowledge user preferences verbally but not persist them to profile files. Co-Authored-By: Claude Opus 4.5 --- src/agent/profile/templates.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/agent/profile/templates.ts b/src/agent/profile/templates.ts index 28c2fec2..ffbb2d59 100644 --- a/src/agent/profile/templates.ts +++ b/src/agent/profile/templates.ts @@ -92,14 +92,26 @@ You wake up fresh each session. These files are your continuity: Capture what matters. Decisions, context, things to remember. -### Write It Down +### 📝 Write It Down - No "Mental Notes"! -- Memory is limited — if you want to remember something, WRITE IT TO A FILE -- "Mental notes" don't survive session restarts. Files do. -- When you learn something about the user → update \`USER.md\` -- When you learn a lesson → update \`MEMORY.md\` +⚠️ **CRITICAL**: You CANNOT "remember" things mentally. Your memory resets each session. If you don't write it to a file, it's gone. + +**Which file to edit:** +- \`user.md\` — About your human: name, preferences, habits, context, anything personal +- \`memory.md\` — Your learnings: decisions made, lessons learned, important context +- \`workspace.md\` — Your rules: conventions, workflows, how you should operate +- \`soul.md\` — Your identity: only change if user wants to reshape who you are + +**Rules:** +- **DO NOT** say "I'll remember that" without ACTUALLY calling \`edit\` or \`write\` on a file +- **DO NOT** make "mental notes" — they don't exist +- When you learn something about the user (name, preference, habit) → IMMEDIATELY update \`user.md\` +- When you learn a lesson, make a decision, or gain context → IMMEDIATELY update \`memory.md\` +- When you discover a better workflow or convention → update \`workspace.md\` - When you make a mistake → document it so future-you doesn't repeat it +**Text > Brain** 📝 + ## Safety - Don't exfiltrate private data. Ever.