Skip to content

feat!: Multi-agent/multi-workspace architecture#1375

Merged
xieyxclack merged 28 commits intoagentscope-ai:mainfrom
rayrayraykk:weirui/dev/multi_workspacev2
Mar 16, 2026
Merged

feat!: Multi-agent/multi-workspace architecture#1375
xieyxclack merged 28 commits intoagentscope-ai:mainfrom
rayrayraykk:weirui/dev/multi_workspacev2

Conversation

@rayrayraykk
Copy link
Copy Markdown
Member

@rayrayraykk rayrayraykk commented Mar 12, 2026

Multi-Agent/Multi-Workspace Architecture

Description

This PR introduces a complete multi-agent/multi-workspace architecture that enables running multiple isolated AI agents simultaneously, each with their own configuration, memory, skills, and tools. This is a significant architectural overhaul to support true multi-tenancy at the agent level.

🚨 Breaking Changes

Configuration Structure:

  • The root config.json structure has changed significantly
  • agents section now contains only references (profiles) to workspace directories
  • Agent-specific configs moved from root to ~/.copaw/workspaces/{agent_id}/agent.json
  • Migration logic is provided but users should backup their configs before upgrading

Data Directory Layout:

~/.copaw/
├── config.json              # Global config (providers, active_agent)
└── workspaces/
    ├── default/             # Default agent workspace
    │   ├── agent.json       # Agent-specific config
    │   ├── chats.json
    │   ├── jobs.json
    │   ├── AGENTS.md
    │   ├── PROFILE.md
    │   └── skills/
    └── agent2/              # Another agent workspace
        └── ...

⚠️ Known Limitations & TODOs

This is a work-in-progress preview for community feedback. The following are NOT yet implemented:

  • CLI Commands: Most copaw CLI commands still operate on global/default agent only

    • copaw init - needs update for multi-agent setup
    • copaw chat - needs agent selection flag
    • copaw skills, copaw channels, copaw cron - need agent context
    • copaw daemon - partially updated but needs refinement
  • ⚠️ Agent Management UI: Frontend agent switcher exists but needs polish

    • Bulk agent creation/import
    • Agent templates/presets
    • Advanced workspace management
  • ⚠️ Documentation: Architecture docs and migration guide in progress

  • ⚠️ Testing: Core functionality tested manually, but needs comprehensive test suite

🎯 What's Working

  • Complete workspace isolation: Each agent has its own files, memory, chats, jobs
  • Tool & file operations: Automatically scoped to agent's workspace
  • System prompts: Each agent loads from its own workspace directory
  • Backend API: All agent-scoped endpoints functional
  • Frontend: Agent switching, config, skills, tools, workspace pages
  • Channels: Messages routed to correct agent workspace
  • Memory & Context: Per-agent memory compaction and management
  • Configuration: Dependency injection pattern, no global state leakage

Related Issue: N/A (new feature)

Security Considerations:

  • Each agent workspace is isolated at filesystem level
  • Tool guard rules and security configs are per-agent

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Refactoring

Component(s) Affected

  • Core / Backend (app, agents, config, providers, utils, local_models)
  • Console (frontend web UI)
  • Channels (DingTalk, Feishu, QQ, Discord, iMessage, etc.)
  • Skills
  • CLI (partial - needs more work)
  • Documentation (website)
  • Tests (minimal coverage)
  • CI/CD
  • Scripts / Deploy

Checklist

  • I ran pre-commit run --all-files locally and it passes
  • If pre-commit auto-fixed files, I committed those changes and reran checks
  • I ran tests locally (pytest or as relevant) and they pass ⚠️ Many tests need updates
  • Documentation updated (if needed) ⚠️ TODO
  • Ready for review ⚠️ Preview only - feedback requested

Testing

Manual Testing Done

  1. Multi-agent creation: Created 3 agents via frontend
  2. Workspace isolation: Verified each agent has separate files/configs
  3. Tool operations: Confirmed write_file, read_file, execute_shell_command use correct workspace
  4. Frontend switching: Tested all pages refresh correctly when switching agents
  5. Chat persistence: Messages saved to correct agent's chats.json
  6. Memory: Verified memory compaction uses agent-specific thresholds

What Needs Testing

  • CLI commands with multi-agent setup
  • Migration from old config structure
  • Concurrent agent operations under load
  • Channel message routing edge cases
  • Cron jobs with multiple agents
  • MCP client isolation (if enabled)

Local Verification Evidence

# Pre-commit passed (with some acceptable warnings in legacy code)
pre-commit run --all-files
# Result: All hooks passed except legacy mypy warnings in init_cmd.py

# Backend starts successfully
python -m copaw.app.main
# INFO: Application startup complete.
# INFO: Uvicorn running on http://0.0.0.0:7860

# Frontend builds without errors
cd console && npm run build
# ✓ built in 45s

# Core functionality smoke tested:
# - Created agents via UI
# - Switched between agents
# - Sent messages (saved to correct workspace)
# - File operations (wrote to correct workspace)
# - Config pages (loaded per-agent settings)

Additional Notes

Why Release This Now?

This PR represents days of architectural work and we want to:

  1. Get community feedback early on the approach
  2. Enable parallel work on CLI/docs by other contributors
  3. Demonstrate progress toward true multi-agent support
  4. Avoid massive PR shock - better to review incrementally

Migration Path

Users on main branch will need to:

  1. Backup ~/.copaw/ directory
  2. Pull this branch
  3. First run will auto-migrate to new structure
  4. Verify migrated config at ~/.copaw/workspaces/default/agent.json
  5. Test basic functionality before creating new agents

What's Next

Priority TODO list:

  1. Update CLI commands to support --agent-id flag
  2. Add agent import/export functionality
  3. Comprehensive test suite
  4. Architecture documentation

This is a preview release. Please test in a non-production environment first. Feedback, bug reports, and contributions are highly appreciated! 🙏

Resolved conflicts in:
- console/src/layouts/MainLayout/index.tsx: merged Chat optimization (display:none) with /agents route
- console/src/locales/{en,zh}.json: merged "help", "actions", "total" keys
- console/src/pages/Agent/Config/index.tsx: adopted upstream refactoring with useAgentConfig hook
- console/src/pages/Settings/Security/index.tsx: adopted upstream refactoring with useToolGuard hook
- src/copaw/app/routers/skills.py: preserved local workspace-aware SkillService implementation

Upstream changes merged:
- feat(skills): AI skill optimization with streaming support
- feat(skills): show metadata description in unified card layout
- feat(console): preserve chat status when navigating away
- refactor(console): Config & Security page refactoring
- refactor(session): sync session state saving
- refactor(model): integrate TokenRecordingModelWrapper
- fix(provider): avoid triggering fetch models every time
- fix(config): align channel config types
- perf(console): optimize multiple chat requests

All tests passing (58/58).

Made-with: Cursor
Copilot AI review requested due to automatic review settings March 12, 2026 13:30
@rayrayraykk rayrayraykk requested a deployment to maintainer-approved March 12, 2026 13:30 — with GitHub Actions Waiting
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a WIP preview of a breaking multi-agent, multi-workspace architecture for CoPaw, moving agent-specific state/config into per-agent workspace directories while keeping a smaller global config.json and updating backend APIs + console UI to support agent switching.

Changes:

  • Introduces Workspace + MultiAgentManager to run multiple isolated agent runtimes concurrently, with per-agent agent.json, chats, jobs, memory, and skills directories.
  • Refactors backend routers and agent runtime (runner/agent/tools/skills/mcp/channels) to resolve configuration and filesystem operations in an agent-scoped way.
  • Adds console UI support for listing/creating/updating/activating agents and propagates X-Agent-Id across requests.

Reviewed changes

Copilot reviewed 63 out of 63 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/copaw/config/context.py Adds a ContextVar to carry the active workspace directory into tool execution.
src/copaw/config/config.py Adds agent profile models, root-agent reference structure, agent.json load/save helpers, and legacy migration logic.
src/copaw/cli/skills_cmd.py Updates CLI skills operations to use a workspace dir (still global WORKING_DIR for now).
src/copaw/cli/init_cmd.py Passes explicit workspace_dir into skill sync and handles optional agent language.
src/copaw/app/workspace_restart.py Adds workspace-scoped restart helper for hot reload.
src/copaw/app/workspace.py Implements the per-agent Workspace runtime (runner/chat/memory/mcp/channels/cron).
src/copaw/app/runner/runner.py Makes AgentRunner agent-aware (agent_id/workspace_dir) and loads agent config per query.
src/copaw/app/runner/repo/base.py Adds debug logging around chat lookup.
src/copaw/app/runner/manager.py Adds verbose logging for chat auto-registration flow.
src/copaw/app/runner/command_dispatch.py Moves ReMe import to lazy import to avoid module import-time dependency issues.
src/copaw/app/runner/api.py Resolves chat manager/session via active agent workspace instead of global app state.
src/copaw/app/routers/workspace.py Scopes workspace download/upload to the active agent workspace directory.
src/copaw/app/routers/tools.py Makes tools listing/toggling agent-scoped and triggers background reload.
src/copaw/app/routers/skills.py Makes skills endpoints agent-scoped and routes skill hub install into a workspace.
src/copaw/app/routers/mcp.py Makes MCP CRUD operations agent-scoped and triggers background reload.
src/copaw/app/routers/config.py Makes channel + heartbeat configuration agent-scoped (partially keeps some global settings).
src/copaw/app/routers/agents.py Adds REST API for multi-agent CRUD + activation + workspace file listing/reading/writing.
src/copaw/app/routers/agent_scoped.py Adds a wrapper router intended to mount existing routers under /agents/{agentId}/....
src/copaw/app/routers/agent.py Refactors agent file/memory + running-config + system-prompt endpoints to use active agent workspace.
src/copaw/app/routers/init.py Registers the new agents router and exports agent-scoped router factory.
src/copaw/app/multi_agent_manager.py Adds manager to lazily start/stop/reload workspaces by agent_id.
src/copaw/app/migration.py Adds migration to create default agent workspace and update root config structure.
src/copaw/app/crons/repo/json_repo.py Allows cron repo path to be provided as `Path
src/copaw/app/crons/api.py Resolves cron manager via active agent workspace.
src/copaw/app/channels/manager.py Skips channel startup when channel config indicates it’s disabled.
src/copaw/app/agent_context.py Adds helper to resolve workspace for a request (header/path/active agent).
src/copaw/app/_app.py Reworks app startup to migrate/init multi-agent manager + dynamic runner + mounts agent-scoped router.
src/copaw/agents/tools/shell.py Resolves shell tool cwd via workspace ContextVar fallback to WORKING_DIR.
src/copaw/agents/tools/file_search.py Resolves search root via workspace ContextVar fallback to WORKING_DIR.
src/copaw/agents/tools/file_io.py Resolves relative file paths via workspace ContextVar fallback to WORKING_DIR.
src/copaw/agents/skills_manager.py Refactors skill management to be workspace-dir scoped (SkillService becomes instance-based).
src/copaw/agents/skills_hub.py Updates skill hub install to write/enable skills in a specific workspace.
src/copaw/agents/react_agent.py Makes agent runtime use agent config + workspace dir for prompt/skills/tools/memory compaction hooks.
src/copaw/agents/prompt.py Allows system prompt builder to read from a specified working/workspace directory.
src/copaw/agents/memory/memory_manager.py Adds configurable memory behavior, reduces reliance on global config, and attempts degraded mode when reme missing.
src/copaw/agents/hooks/memory_compaction.py Makes compaction thresholds explicit inputs instead of reading global config.
src/copaw/agents/command_handler.py Makes history token limits explicit input rather than reading global config.
console/src/stores/agentStore.ts Adds persisted Zustand store for active agent + agent list.
console/src/pages/Settings/Models/index.tsx Simplifies models page layout (removes ModelsSection).
console/src/pages/Settings/Agents/index.tsx Adds Agent Management settings page (CRUD + activation).
console/src/pages/Settings/Agents/index.module.less Styles for agent management page.
console/src/pages/Control/Heartbeat/index.tsx Reloads heartbeat settings when active agent changes.
console/src/pages/Control/CronJobs/useCronJobs.ts Reloads cron jobs when active agent changes.
console/src/pages/Control/Channels/useChannels.ts Reloads channel config when active agent changes.
console/src/pages/Chat/index.tsx Adds agent switching refresh + propagates X-Agent-Id in chat request.
console/src/pages/Agent/Workspace/components/useAgentsData.ts Uses agent-specific endpoints to list/read workspace files.
console/src/pages/Agent/Tools/useTools.ts Reloads tools on agent change + optimistic toggling behavior.
console/src/pages/Agent/Skills/useSkills.ts Reloads skills when active agent changes.
console/src/pages/Agent/MCP/useMCP.ts Reloads MCP clients when active agent changes.
console/src/pages/Agent/MCP/index.tsx Minor TS cleanup (const for clientsToCreate).
console/src/locales/zh.json Adds agent-management i18n strings and common table strings.
console/src/locales/en.json Adds agent-management i18n strings and common table strings.
console/src/layouts/Sidebar.tsx Adds “Agent Management” nav item.
console/src/layouts/MainLayout/index.tsx Adds /agents route.
console/src/layouts/Header.tsx Adds AgentSelector to header.
console/src/components/AgentSelector/index.tsx Adds agent switcher dropdown that activates agents and updates store.
console/src/components/AgentSelector/index.module.less Styles for agent selector.
console/src/api/types/index.ts Exports new agents API types.
console/src/api/types/agents.ts Adds typed contracts for agents API responses.
console/src/api/request.ts Adds global X-Agent-Id header injection based on agent store persistence.
console/src/api/modules/agents.ts Adds agents API client module.
console/src/api/index.ts Exposes agentsApi.
SKILL_MULTI_WORKSPACE_FIX.md Notes/guide describing the skills multi-workspace refactor.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a fundamental architectural shift towards a multi-agent system, allowing for robust isolation and independent operation of multiple AI agents. The changes enable each agent to maintain its own distinct environment, configurations, and operational data, significantly enhancing the platform's capability for multi-tenancy and specialized agent deployments. The refactoring touches both the backend API and the frontend user interface to support this new paradigm, while also providing a migration path for existing single-agent setups.

Highlights

  • Multi-Agent/Multi-Workspace Architecture: Introduced a complete multi-agent/multi-workspace architecture, enabling multiple isolated AI agents to run simultaneously, each with their own configuration, memory, skills, and tools. This is a significant architectural overhaul to support true multi-tenancy at the agent level.
  • Configuration Restructuring and Migration: The root config.json structure has been significantly changed. Agent-specific configurations (channels, MCP, heartbeat, running, LLM routing, system prompts, tools, security) are now moved from the root to ~/.copaw/workspaces/{agent_id}/agent.json. Migration logic is provided to automatically convert legacy single-agent setups to the new default agent workspace structure.
  • Frontend Agent Management and Switching: Added a new 'Agent Management' page in the console for listing, creating, editing, deleting, and activating agents. A new AgentSelector component has been integrated into the header, allowing users to switch between active agents. All relevant frontend pages (MCP, Skills, Tools, Workspace, Chat) now react to the active agent, ensuring data isolation.
  • Backend Agent Context and Isolation: Implemented a MultiAgentManager to lazily load and manage Workspace instances, each encapsulating a full agent runtime (runner, channel manager, memory manager, cron manager, MCP manager). API endpoints have been updated to use X-Agent-Id headers or URL parameters to route requests to the correct agent's workspace, ensuring isolated operations for files, skills, tools, and configurations.
  • Skill Management Refactoring: The SkillService class and related helper functions have been refactored from static methods to instance methods, now requiring a workspace_dir to manage skills on a per-agent basis. This ensures each agent has its own set of active and customized skills.
  • Tool and Memory Contextualization: Built-in tools (file I/O, file search, shell commands) and memory management now correctly resolve paths and operate within the context of the active agent's workspace, preventing cross-agent data leakage. Memory compaction hooks have been updated to use agent-specific configuration values.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • SKILL_MULTI_WORKSPACE_FIX.md
    • Added documentation detailing the refactoring of SkillService to an instance class and the updates to API routes and auxiliary functions to support multi-workspace skill management.
  • console/src/api/index.ts
    • Imported and exported the new agentsApi module.
  • console/src/api/modules/agents.ts
    • Added a new module defining API calls for multi-agent management, including listing, getting, creating, updating, deleting, and activating agents, as well as agent-specific file and memory access.
  • console/src/api/request.ts
    • Modified buildHeaders to include an X-Agent-Id header in all requests, dynamically retrieved from local storage for multi-agent support.
  • console/src/api/types/agents.ts
    • Added new TypeScript interfaces for AgentSummary, AgentListResponse, AgentProfileConfig, and AgentProfileRef to support multi-agent data structures.
  • console/src/api/types/index.ts
    • Exported the new agents types.
  • console/src/components/AgentSelector/index.module.less
    • Added Less CSS styles for the new AgentSelector component.
  • console/src/components/AgentSelector/index.tsx
    • Added a new React component for selecting and switching between active agents, integrating with the agentStore and agentsApi.
  • console/src/layouts/Header.tsx
    • Integrated the new AgentSelector component into the application's header.
  • console/src/layouts/MainLayout/index.tsx
    • Added a new route /agents to display the AgentsPage.
  • console/src/layouts/Sidebar.tsx
    • Added a new 'Agent Management' item to the sidebar navigation, linking to the /agents route.
  • console/src/locales/en.json
    • Added new localization strings for agent management, including actions, statuses, and form fields.
  • console/src/locales/zh.json
    • Added new Chinese localization strings for agent management.
  • console/src/pages/Agent/MCP/index.tsx
    • Changed clientsToCreate variable declaration from let to const.
  • console/src/pages/Agent/MCP/useMCP.ts
    • Updated the useEffect dependency array to include activeAgent, ensuring MCP clients reload when the active agent changes.
  • console/src/pages/Agent/Skills/useSkills.ts
    • Updated the useEffect dependency array to include activeAgent, ensuring skills reload when the active agent changes.
  • console/src/pages/Agent/Tools/useTools.ts
    • Updated the useEffect dependency array to include activeAgent, ensuring tools reload when the active agent changes. Implemented optimistic updates for tool toggling and batch operations.
  • console/src/pages/Agent/Workspace/components/useAgentsData.ts
    • Updated the useEffect dependency array to include activeAgent, ensuring workspace files and memory reload. Modified file fetching to use the new agentsApi and added logic to re-select previously opened files after an agent switch.
  • console/src/pages/Chat/index.tsx
    • Integrated useAgentStore to refresh chat sessions when the active agent changes and added X-Agent-Id to chat request headers for agent context.
  • console/src/pages/Control/Channels/useChannels.ts
    • Updated the useEffect dependency array to include activeAgent, ensuring channels reload when the active agent changes.
  • console/src/pages/Control/CronJobs/useCronJobs.ts
    • Updated the useEffect dependency array to include activeAgent, ensuring cron jobs reload when the active agent changes.
  • console/src/pages/Control/Heartbeat/index.tsx
    • Updated the useEffect dependency array to include activeAgent, ensuring heartbeat configuration reloads when the active agent changes.
  • console/src/pages/Settings/Agents/index.module.less
    • Added Less CSS styles for the new Agent Management page.
  • console/src/pages/Settings/Agents/index.tsx
    • Added a new React page for managing agents, including functionalities to list, create, edit, delete, and activate agent profiles.
  • console/src/pages/Settings/Models/index.tsx
    • Removed the ModelsSection component, streamlining the models settings page.
  • console/src/stores/agentStore.ts
    • Added a new Zustand store (useAgentStore) to manage the active agent and a list of all agents, with persistence to local storage.
  • src/copaw/agents/command_handler.py
    • Removed global config loading and introduced max_input_length as an instance variable, ensuring agent-specific context window limits for history processing.
  • src/copaw/agents/hooks/memory_compaction.py
    • Removed global config loading and passed memory compaction parameters (memory_compact_threshold, memory_compact_reserve, enable_tool_result_compact, tool_result_compact_keep_n) directly to the MemoryCompactionHook constructor for agent-specific configuration.
  • src/copaw/agents/memory/memory_manager.py
    • Removed global config loading and passed memory configuration parameters (max_input_length, memory_compact_ratio, memory_reserve_ratio, language) to the MemoryManager constructor, enabling agent-specific memory settings. Added a no-op start method to ReMeLight for consistency.
  • src/copaw/agents/prompt.py
    • Modified build_system_prompt_from_working_dir to accept an optional working_dir and enabled_files parameter, allowing agent-specific system prompts to be loaded from their respective workspaces.
  • src/copaw/agents/react_agent.py
    • Updated the CoPawAgent constructor to accept agent-specific memory and language parameters, and a workspace_dir. Refactored tool loading to use load_agent_config for agent-specific tool enablement. Modified skill registration and system prompt building to use the agent's workspace_dir. Implemented set_current_workspace_dir in the reply method to set the context for tool functions.
  • src/copaw/agents/skills_hub.py
    • Modified install_skill_from_hub to accept a workspace_dir parameter and now uses an instance of SkillService for skill operations, ensuring skills are installed into the correct agent's workspace.
  • src/copaw/agents/skills_manager.py
    • Refactored SkillService from a static class to an instance class, requiring a workspace_dir in its constructor. All skill management functions (get_customized_skills_dir, get_active_skills_dir, get_working_skills_dir, sync_skills_to_working_dir, sync_skills_from_active_to_customized, list_available_skills, ensure_skills_initialized, and all SkillService methods) were updated to operate within the specified workspace_dir.
  • src/copaw/agents/tools/file_io.py
    • Modified _resolve_file_path to use get_current_workspace_dir from the context, ensuring file operations are scoped to the active agent's workspace.
  • src/copaw/agents/tools/file_search.py
    • Modified grep_search and glob_search to use get_current_workspace_dir for determining the search root, ensuring file searches are scoped to the active agent's workspace.
  • src/copaw/agents/tools/shell.py
    • Modified execute_shell_command to use get_current_workspace_dir for setting the working directory, ensuring shell commands are executed within the active agent's workspace.
  • src/copaw/app/_app.py
    • Removed previous single-agent lifecycle management components (ConfigWatcher, MCPClientManager, CronManager, ChatManager, AgentRunner). Introduced DynamicMultiAgentRunner to dynamically route requests to the correct agent's workspace based on X-Agent-Id header. Integrated MultiAgentManager for overall agent lifecycle. Added migration functions (migrate_legacy_workspace_to_default_agent, ensure_default_agent_exists) to the startup process. Included create_agent_scoped_router for agent-specific API routes.
  • src/copaw/app/agent_context.py
    • Added a new module to provide utilities for determining the active agent for a given request (get_agent_for_request) and retrieving the global active agent ID (get_active_agent_id).
  • src/copaw/app/channels/manager.py
    • Added a check to ensure channels are enabled before processing them, preventing disabled channels from being started.
  • src/copaw/app/crons/api.py
    • Modified get_cron_manager to retrieve the CronManager from the active agent's Workspace instance, ensuring cron jobs are managed per agent.
  • src/copaw/app/crons/repo/json_repo.py
    • Updated the JsonJobRepository constructor to accept path as either Path or str.
  • src/copaw/app/migration.py
    • Added a new module containing functions (migrate_legacy_workspace_to_default_agent, ensure_default_agent_exists) to handle the migration of old single-agent configurations and workspace files to the new multi-agent structure, creating a default agent if necessary.
  • src/copaw/app/multi_agent_manager.py
    • Added a new module defining MultiAgentManager, responsible for lazy loading, starting, stopping, reloading, and preloading individual Workspace instances in a thread-safe manner.
  • src/copaw/app/routers/init.py
    • Imported and included the new agents_router for global agent management and create_agent_scoped_router to mount agent-specific API routes under /agents/{agentId}/.
  • src/copaw/app/routers/agent.py
    • Modified all endpoints (list_working_files, read_working_file, write_working_file, list_memory_files, read_memory_file, write_memory_file, get_agents_running_config, put_agents_running_config, get_system_prompt_files, put_system_prompt_files) to use get_agent_for_request and agent-specific AgentMdManager or configuration, ensuring operations are scoped to the active agent. Added background tasks for hot-reloading agent configurations.
  • src/copaw/app/routers/agent_scoped.py
    • Added a new module to create an agent-scoped router that wraps existing API routes. It includes AgentContextMiddleware to extract agentId from the URL path and inject it into the request state for downstream processing.
  • src/copaw/app/routers/agents.py
    • Added a new module defining API endpoints for comprehensive multi-agent management, including listing, retrieving, creating, updating, deleting, and activating agents, along with agent-specific file and memory access. Includes helper functions for workspace initialization.
  • src/copaw/app/routers/config.py
    • Modified all configuration-related endpoints (list_channels, put_channels, get_channel, put_channel, get_heartbeat, put_heartbeat, get_agents_llm_routing) to retrieve and update configurations specific to the active agent's workspace. Added background tasks for hot-reloading agent configurations.
  • src/copaw/app/routers/mcp.py
    • Modified all MCP-related endpoints (list_mcp_clients, get_mcp_client, create_mcp_client, update_mcp_client, toggle_mcp_client, delete_mcp_client) to operate on the active agent's MCP configuration. Added background tasks for hot-reloading agent configurations.
  • src/copaw/app/routers/skills.py
    • Modified all skill-related endpoints (list_skills, get_available_skills, install_from_hub, batch_disable_skills, batch_enable_skills, create_skill, disable_skill, enable_skill, delete_skill, load_skill_file) to use an agent-specific SkillService instance, ensuring skill operations are scoped to the active agent's workspace. Added background tasks for hot-reloading agent configurations.
  • src/copaw/app/routers/tools.py
    • Modified list_tools and toggle_tool endpoints to operate on the active agent's tool configuration. Added background tasks for hot-reloading agent configurations.
  • src/copaw/app/routers/workspace.py
    • Modified download_workspace and upload_workspace endpoints to operate on the active agent's specific workspace_dir, ensuring workspace archives are agent-isolated.
  • src/copaw/app/runner/api.py
    • Modified get_chat_manager and get_session to retrieve the respective managers/sessions from the active agent's Workspace instance, ensuring chat history is managed per agent.
  • src/copaw/app/runner/command_dispatch.py
    • Updated to lazy import ReMeInMemoryMemory to avoid module-level dependency issues.
  • src/copaw/app/runner/manager.py
    • Added more detailed logging for chat creation and retrieval operations within the ChatManager.
  • src/copaw/app/runner/repo/base.py
    • Added more detailed logging for chat repository operations, specifically when searching for existing chats.
  • src/copaw/app/runner/runner.py
    • Updated AgentRunner to store agent_id and workspace_dir. Modified query_handler to load agent-specific configurations and pass them to CoPawAgent, ensuring agent-specific runtime parameters. Added logging for _chat_manager status.
  • src/copaw/app/workspace.py
    • Added a new module defining the Workspace class, which encapsulates a complete, independent agent runtime environment. This includes its own AgentRunner, ChannelManager, MemoryManager, MCPClientManager, CronManager, and ChatManager, along with start, stop, and reload methods for lifecycle management.
  • src/copaw/app/workspace_restart.py
    • Added a new module providing workspace-level restart logic, including restart_workspace and create_restart_callback functions, allowing individual workspaces to be reloaded independently.
  • src/copaw/cli/init_cmd.py
    • Modified init_cmd to pass WORKING_DIR to sync_skills_to_working_dir and set a default language of 'zh' if not specified in the config.
  • src/copaw/cli/skills_cmd.py
    • Modified configure_skills_interactive and list_cmd to use an instance of SkillService and pass WORKING_DIR, ensuring CLI skill operations are consistent with the new multi-workspace model.
  • src/copaw/config/config.py
    • Introduced new Pydantic models AgentProfileRef and AgentProfileConfig to define the new multi-agent configuration structure. AgentsConfig now manages references to agent profiles. Added load_agent_config and save_agent_config functions for managing agent-specific agent.json files. Included migrate_legacy_config_to_multi_agent for backward compatibility, moving legacy settings into a default agent profile.
  • src/copaw/config/context.py
    • Added a new module defining a ContextVar (current_workspace_dir) and helper functions (get_current_workspace_dir, set_current_workspace_dir) to manage the active agent's workspace directory in a thread-safe manner, enabling tools to resolve paths correctly.
Activity
  • This pull request is a work-in-progress preview, primarily for gathering community feedback on the proposed multi-agent architecture.
  • Manual testing has been performed to verify core functionalities, including multi-agent creation, workspace isolation, tool operations within correct workspaces, frontend agent switching, chat persistence, and memory management.
  • Known limitations include incomplete CLI commands for multi-agent setup, unpolished agent management UI features, and ongoing documentation and comprehensive testing efforts.
  • The author has provided detailed instructions for local verification and a migration path for existing users.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Generative AI Prohibited Use Policy, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive multi-agent support by refactoring the backend to manage agent-specific workspaces, each with its own runner, channel, memory, MCP, and cron managers. Key changes include making SkillService an instance class bound to a workspace, updating various agent components and API routes to use workspace-specific directories and configurations, and implementing a MultiAgentManager for lazy loading and lifecycle management of agent instances. The frontend is updated to include an agent selector, new API endpoints for agent management, and ensures that all requests are routed with the active agent's ID. A migration path for legacy single-agent configurations to the new multi-agent structure is also included. A review comment highlights a code duplication issue in the frontend where logic for retrieving the active agent ID from localStorage is duplicated and suggests extracting it into a shared utility function for improved maintainability and consistency.

@rayrayraykk rayrayraykk requested a deployment to maintainer-approved March 12, 2026 14:04 — with GitHub Actions Waiting
Copilot AI review requested due to automatic review settings March 12, 2026 14:21
@rayrayraykk rayrayraykk requested a deployment to maintainer-approved March 12, 2026 14:21 — with GitHub Actions Waiting
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 64 out of 64 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@lijiajun1997
Copy link
Copy Markdown

Thanks for this monumental effort This multi-agent/multi-workspace architecture is exactly what the project needs to evolve. The focus on true filesystem-level isolation and multi-tenancy is impressive and highly appreciated. It’s a huge architectural improvement that makes Copaw much more robust for complex use cases. Looking forward to seeing this merged!

@rayrayraykk rayrayraykk requested a deployment to maintainer-approved March 16, 2026 11:39 — with GitHub Actions Waiting
@rayrayraykk
Copy link
Copy Markdown
Member Author

CoPaw Multi-Agent Architecture Refactor – Technical Overview

Why the Refactor?

The previous architecture was single-agent with all configuration crammed into a root config.json. This caused several problems:

  1. You couldn’t run multiple agent instances at the same time.
  2. Configurations for different projects/scenarios interfered with each other.
  3. File operations and chat logs were globally shared, causing data leakage and cross-contamination.

Refactor goal: each agent should have its own fully isolated workspace.


Core Design: Workspace Isolation

New Directory Layout

~/.copaw/
├── config.json              # global config only (providers, active_agent)
└── workspaces/
    ├── default/             
    │   ├── agent.json       # per-agent configuration
    │   ├── chats.json
    │   ├── jobs.json
    │   ├── AGENTS.md
    │   └── skills/
    └── agent2/
        └── ...

All agent-specific config previously in the root (channels, MCP, tools, system_prompt, etc.) is now moved into workspace_dir/agent.json for each agent.

Workspace Class

A new Workspace class (src/copaw/app/workspace.py) encapsulates a full agent runtime:

class Workspace:
    def __init__(self, agent_id: str, workspace_dir: Path, ...):
        self.agent_id = agent_id
        self.workspace_dir = workspace_dir
        
        # Each workspace has its own managers
        self.runner = AgentRunner(agent_id, workspace_dir)
        self.channel_manager = ChannelManager(...)
        self.memory_manager = MemoryManager(...)
        self.mcp_manager = MCPClientManager(...)
        self.cron_manager = CronManager(...)
        self.chat_manager = ChatManager(...)

Each workspace is independent; state and resources no longer bleed across agents.


Implementation Details

1. MultiAgentManager: Lazy Loading + Thread Safety

To avoid loading all agents on startup, the manager uses lazy loading:

class MultiAgentManager:
    def __init__(self):
        self._workspaces: Dict[str, Workspace] = {}
        self._lock = asyncio.Lock()
    
    async def get_or_load(self, agent_id: str) -> Workspace:
        async with self._lock:
            if agent_id not in self._workspaces:
                # Load only on first access
                workspace = await self._load_workspace(agent_id)
                self._workspaces[agent_id] = workspace
            return self._workspaces[agent_id]

This keeps startup fast and ensures concurrent access is safe.


2. Key Technique: Middleware-Mounted Multi-Agent API

The core trick is enabling all APIs to be aware of “which agent” they are operating on.

Approach: Agent-Scoped Router + Middleware

We introduce an AgentContextMiddleware (src/copaw/app/routers/agent_scoped.py):

class AgentContextMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # Extract agent_id from path
        # e.g., /agents/{agentId}/skills -> agentId
        path = request.url.path
        match = re.match(r'/agents/([^/]+)/', path)
        
        if match:
            agent_id = match.group(1)
            # Inject into request.state for downstream use
            request.state.agent_id = agent_id
        
        return await call_next(request)

Then a factory wraps existing routers:

def create_agent_scoped_router(original_router: APIRouter) -> APIRouter:
    """
    Mount an existing router under /agents/{agentId}/
    """
    scoped = APIRouter()
    
    # Attach middleware
    scoped.add_middleware(AgentContextMiddleware)
    
    # Mount original routes under /{agentId}/
    scoped.include_router(
        original_router, 
        prefix="/{agentId}"
    )
    
    return scoped

Usage

In _app.py:

from copaw.app.routers import skills, tools, mcp, config

agent_scoped_router = APIRouter(prefix="/agents")
agent_scoped_router.include_router(
    create_agent_scoped_router(skills.router)
)
agent_scoped_router.include_router(
    create_agent_scoped_router(tools.router)
)
# ... other routers

app.include_router(agent_scoped_router)

This gives you:

  • /api/skills – operate on the current active agent.
  • /api/agents/{agentId}/skills – operate on a specific agent.

Internally, agent_id is carried via middleware/request.state, and then mapped to the correct Workspace instance.


3. SkillService Refactor: From Static to Instance-Based

Previously, SkillService was a global static class with @classmethods and hardcoded paths:

# Old
class SkillService:
    @classmethod
    def list_skills(cls):
        skills_dir = WORKING_DIR / "skills"
        ...

Now it’s instance-based and workspace-aware:

# New
class SkillService:
    def __init__(self, workspace_dir: Path):
        self.workspace_dir = workspace_dir
    
    def list_skills(self):
        skills_dir = self.workspace_dir / "active_skills"
        ...

In the API router:

@router.get("/skills")
async def list_skills(request: Request):
    agent_id = get_agent_for_request(request)
    workspace = get_workspace(agent_id)
    
    service = SkillService(workspace.workspace_dir)
    return service.list_skills()

This pattern eliminates global state and keeps operations scoped to a single agent’s workspace.


4. Frontend: Global agent_id Injection

The frontend uses Zustand to manage the active agent (console/src/stores/agentStore.ts):

export const useAgentStore = create<AgentStore>()(
  persist(
    (set) => ({
      activeAgent: 'default',
      agents: [],
      setActiveAgent: (id) => set({ activeAgent: id }),
    }),
    { name: 'agent-storage' }
  )
);

Axios interceptors automatically attach the active agent to all requests (console/src/api/request.ts):

const buildHeaders = () => {
  const agentStore = localStorage.getItem('agent-storage');
  const activeAgent = agentStore 
    ? JSON.parse(agentStore).state.activeAgent 
    : 'default';
  
  return {
    'Content-Type': 'application/json',
    'X-Agent-Id': activeAgent,  // sent with every request
  };
};

The backend reads X-Agent-Id and dispatches the request to the appropriate workspace.


5. CLI: Multi-Agent Support

Most CLI commands now accept an --agent-id option:

@click.option('--agent-id', default='default', help='Agent ID')
def skills_cmd(agent_id: str):
    workspace_dir = get_workspace_dir(agent_id)
    service = SkillService(workspace_dir)
    # ...

This mirrors the backend behavior: each CLI command runs in the context of a specific agent/workspace.


6. Config Hot Reload per Workspace

Each workspace has its own ConfigWatcher, watching its own agent.json:

class Workspace:
    async def start(self):
        self.config_watcher = ConfigWatcher(
            config_path=self.workspace_dir / "agent.json",
            on_change=self._on_config_change
        )

Changing config for one agent only reloads that agent’s workspace. Others keep running unaffected.


Migration Logic

On first startup, migration runs automatically (src/copaw/app/migration.py):

  1. Detect old layout (e.g., root JSON has agents.system_prompt etc.).
  2. Create workspaces/default/.
  3. Move legacy agent configuration into workspaces/default/agent.json.
  4. Migrate existing chats.json, jobs.json into the default workspace.
  5. Rewrite root config.json into the new global-only format.

This is transparent to users; no manual steps required.


Current Limitations & Known Issues

Some areas are still to be improved:

  1. Channel reuse
    Each workspace currently has its own ChannelManager. If multiple agents share the same bot token, they clash. A better design is a global Channel pool with routing to workspaces.

  2. MCP/Skill process duplication
    With N agents × M MCPs, you get N×M MCP processes. We plan to move toward globally shared MCP/skills with per-agent whitelisting and permission checks.

These will be refined in upcoming PRs.


Summary

The crux of this refactor is:

  • Middleware extracts agent_id from the request.
  • agent_id is used to resolve a Workspace instance.
  • All services (skills, MCP, memory, etc.) become workspace-scoped, not global.

This enables multiple agents to run concurrently, with clean isolation of config, data, and runtime.

CoPaw 多智能体架构重构技术分享

为什么要重构

之前的架构是单智能体的,所有配置都堆在根目录的 config.json 里。问题很明显:

  1. 没法同时跑多个智能体实例
  2. 不同项目/场景的配置会互相干扰
  3. 文件操作、聊天记录都是全局共享的,容易出现数据串台

所以这次重构的目标就是:让每个智能体有自己独立的工作区,完全隔离

核心设计:Workspace 隔离

新的目录结构

~/.copaw/
├── config.json              # 只保留全局配置(providers、active_agent)
└── workspaces/
    ├── default/             
    │   ├── agent.json       # 智能体自己的配置
    │   ├── chats.json
    │   ├── jobs.json
    │   ├── AGENTS.md
    │   └── skills/
    └── agent2/
        └── ...

把原来在根目录的智能体配置(channels、mcp、tools、system_prompt等)全部挪到各自的 workspace_dir/agent.json 里。

Workspace 类

新建了一个 Workspace 类(src/copaw/app/workspace.py),封装了一个完整的智能体运行时:

class Workspace:
    def __init__(self, agent_id: str, workspace_dir: Path, ...):
        self.agent_id = agent_id
        self.workspace_dir = workspace_dir
        
        # 每个 workspace 有自己的管理器实例
        self.runner = AgentRunner(agent_id, workspace_dir)
        self.channel_manager = ChannelManager(...)
        self.memory_manager = MemoryManager(...)
        self.mcp_manager = MCPClientManager(...)
        self.cron_manager = CronManager(...)
        self.chat_manager = ChatManager(...)

每个 workspace 都是独立的,不会互相影响。

技术实现细节

1. MultiAgentManager:懒加载 + 线程安全

为了避免启动时把所有智能体都加载进来(太慢),用了懒加载策略:

class MultiAgentManager:
    def __init__(self):
        self._workspaces: Dict[str, Workspace] = {}
        self._lock = asyncio.Lock()
    
    async def get_or_load(self, agent_id: str) -> Workspace:
        async with self._lock:
            if agent_id not in self._workspaces:
                # 第一次访问才加载
                workspace = await self._load_workspace(agent_id)
                self._workspaces[agent_id] = workspace
            return self._workspaces[agent_id]

2. 关键点:Middleware 挂载多智能体 API

这是整个重构的核心技巧。我们需要让所有 API 都能感知到当前操作的是哪个智能体。

方案:agent-scoped router + middleware

创建了一个 AgentContextMiddlewaresrc/copaw/app/routers/agent_scoped.py):

class AgentContextMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # 从路径中提取 agent_id
        # 例如:/agents/{agentId}/skills -> agentId
        path = request.url.path
        match = re.match(r'/agents/([^/]+)/', path)
        
        if match:
            agent_id = match.group(1)
            # 注入到 request.state,后续 endpoint 就能拿到
            request.state.agent_id = agent_id
        
        return await call_next(request)

然后用一个工厂函数包装现有的 router:

def create_agent_scoped_router(original_router: APIRouter) -> APIRouter:
    """
    把原有的 router 挂到 /agents/{agentId}/ 下
    """
    scoped = APIRouter()
    
    # 添加 middleware
    scoped.add_middleware(AgentContextMiddleware)
    
    # 把原有路由挂载到 /{agentId}/ 下
    scoped.include_router(
        original_router, 
        prefix="/{agentId}"
    )
    
    return scoped

实际应用

_app.py 里:

# 原有的 router
from copaw.app.routers import skills, tools, mcp, config

# 创建智能体 scoped 版本
agent_scoped_router = APIRouter(prefix="/agents")
agent_scoped_router.include_router(
    create_agent_scoped_router(skills.router)
)
agent_scoped_router.include_router(
    create_agent_scoped_router(tools.router)
)
# ... 其他 router

app.include_router(agent_scoped_router)

这样就实现了:

  • /api/skills - 操作当前活动智能体
  • /api/agents/{agentId}/skills - 操作指定智能体

3. SkillService 重构:从静态类改成实例

之前 SkillService 是全局静态类,所有方法都是 @classmethod。现在改成实例化:

# 旧方式
class SkillService:
    @classmethod
    def list_skills(cls):
        # 硬编码路径
        skills_dir = WORKING_DIR / "skills"
        ...

# 新方式
class SkillService:
    def __init__(self, workspace_dir: Path):
        self.workspace_dir = workspace_dir
    
    def list_skills(self):
        skills_dir = self.workspace_dir / "active_skills"
        ...

API router 里这样用:

@router.get("/skills")
async def list_skills(request: Request):
    agent_id = get_agent_for_request(request)
    workspace = get_workspace(agent_id)
    
    service = SkillService(workspace.workspace_dir)
    return service.list_skills()

5. 前端:全局 agent_id 注入

前端用 Zustand 管理活动智能体(console/src/stores/agentStore.ts):

export const useAgentStore = create<AgentStore>()(
  persist(
    (set) => ({
      activeAgent: 'default',
      agents: [],
      setActiveAgent: (id) => set({ activeAgent: id }),
    }),
    { name: 'agent-storage' }
  )
);

在 axios 请求拦截器里自动加 header(console/src/api/request.ts):

const buildHeaders = () => {
  const agentStore = localStorage.getItem('agent-storage');
  const activeAgent = agentStore 
    ? JSON.parse(agentStore).state.activeAgent 
    : 'default';
  
  return {
    'Content-Type': 'application/json',
    'X-Agent-Id': activeAgent,  // 所有请求都带上
  };
};

后端拿到 header 后路由到对应 workspace。

6. CLI 多智能体支持

大部分 CLI 命令加了 --agent-id 参数:

@click.option('--agent-id', default='default', help='Agent ID')
def skills_cmd(agent_id: str):
    workspace_dir = get_workspace_dir(agent_id)
    service = SkillService(workspace_dir)
    # ...

7. 配置热重载

每个 workspace 有独立的 ConfigWatcher,监听自己的 agent.json

class Workspace:
    async def start(self):
        # 监听 workspace_dir/agent.json
        self.config_watcher = ConfigWatcher(
            config_path=self.workspace_dir / "agent.json",
            on_change=self._on_config_change
        )

修改某个智能体的配置,只会重载那个 workspace,不影响其他的。

迁移逻辑

首次启动时自动迁移(src/copaw/app/migration.py):

  1. 检查是否有旧结构(根目录有 agents.system_prompt 等配置)
  2. 创建 workspaces/default/ 目录
  3. 把旧配置搬到 workspaces/default/agent.json
  4. 迁移现有的 chats.json、jobs.json
  5. 更新根 config.json 结构

用户无感知,自动完成。

目前的限制和已知问题

还有一些地方可以优化:

  1. Channel 复用问题:现在每个 workspace 有自己的 ChannelManager,如果多个智能体用同一个 bot token 会冲突。可能需要改成全局 Channel + 路由到不同智能体
  2. MCP/Skill 重复加载:3个智能体 × 4个MCP = 12个进程。考虑改成全局共享 + 智能体级别的白名单过滤

这些在后续 PR 里会继续优化。

总结

这次重构的核心思路就是:用 middleware 拦截请求提取 agent_id,用 ContextVar 传递 workspace 上下文,让所有操作自动路由到正确的 workspace

代码改动比较大,但逻辑其实不复杂。希望这个说明能帮大家快速理解架构变化。

有问题随时在 PR 里讨论。

Copilot AI review requested due to automatic review settings March 16, 2026 11:44
@rayrayraykk rayrayraykk temporarily deployed to maintainer-approved March 16, 2026 11:44 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 98 out of 98 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 43 to 80
# Normalize the path: expand ~ and fix Unicode normalization differences
# (e.g. macOS stores filenames as NFD but paths from the LLM arrive as NFC,
# causing os.path.exists to return False for files that do exist).
file_path = os.path.expanduser(unicodedata.normalize("NFC", file_path))

if not os.path.exists(file_path):
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error: The file {file_path} does not exist.",
),
],
)

if not os.path.isfile(file_path):
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error: The path {file_path} is not a file.",
),
],
)

# Detect MIME type
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type is None:
# Default to application/octet-stream for unknown types
mime_type = "application/octet-stream"
as_type = _auto_as_type(mime_type)

try:
# Use local file URL instead of base64
absolute_path = os.path.abspath(file_path)

if not _is_allowed_media_path(absolute_path):
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error: Media file outside allowed directory: {os.path.basename(file_path)}",
),
],
)

file_url = f"file://{absolute_path}"
source = {"type": "url", "url": file_url}

Comment on lines +71 to +83
try:
workspace = await manager.get_agent(target_agent_id)
if not workspace:
raise HTTPException(
status_code=404,
detail=f"Agent '{target_agent_id}' not found",
)
return workspace
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get agent: {str(e)}",
) from e
Comment on lines +24 to +55
import logging
from ..agent_context import set_current_agent_id

logger = logging.getLogger(__name__)
agent_id = None

# Priority 1: Extract agentId from path: /api/agents/{agentId}/...
path_parts = request.url.path.split("/")
if len(path_parts) >= 4 and path_parts[1] == "api":
if path_parts[2] == "agents":
agent_id = path_parts[3]
request.state.agent_id = agent_id
logger.debug(
f"AgentContextMiddleware: agent_id={agent_id} "
f"from path={request.url.path}",
)

# Priority 2: Check X-Agent-Id header
if not agent_id:
agent_id = request.headers.get("X-Agent-Id")
if agent_id:
logger.debug(
f"AgentContextMiddleware: agent_id={agent_id} "
f"from X-Agent-Id header",
)

# Set agent_id in context variable for use by runners
if agent_id:
set_current_agent_id(agent_id)

response = await call_next(request)
return response
Comment on lines 63 to 70
with zipfile.ZipFile(io.BytesIO(data)) as zf:
for name in zf.namelist():
resolved = (WORKING_DIR / name).resolve()
if not str(resolved).startswith(str(WORKING_DIR)):
resolved = (workspace_dir / name).resolve()
if not str(resolved).startswith(str(workspace_dir)):
raise HTTPException(
status_code=400,
detail=f"Zip contains unsafe path: {name}",
)
Comment on lines 68 to +73
@router.patch("/{tool_name}/toggle", response_model=ToolInfo)
async def toggle_tool(tool_name: str = Path(...)) -> ToolInfo:
"""Toggle tool enabled status.
async def toggle_tool(
tool_name: str = Path(...),
request: Request = None,
) -> ToolInfo:
"""Toggle tool enabled status for active agent.
Comment on lines 44 to 99
@@ -48,33 +66,61 @@ async def list_tools() -> List[ToolInfo]:


@router.patch("/{tool_name}/toggle", response_model=ToolInfo)
async def toggle_tool(tool_name: str = Path(...)) -> ToolInfo:
"""Toggle tool enabled status.
async def toggle_tool(
tool_name: str = Path(...),
request: Request = None,
) -> ToolInfo:
"""Toggle tool enabled status for active agent.

Args:
tool_name: Tool function name
request: FastAPI request

Returns:
Updated tool information

Raises:
HTTPException: If tool not found
"""
config = load_config()
from ..agent_context import get_agent_for_request
from ...config.config import load_agent_config, save_agent_config

workspace = await get_agent_for_request(request)
agent_config = load_agent_config(workspace.agent_id)

if tool_name not in config.tools.builtin_tools:
if (
not agent_config.tools
or tool_name not in agent_config.tools.builtin_tools
):
raise HTTPException(
status_code=404,
detail=f"Tool '{tool_name}' not found",
)

@rayrayraykk rayrayraykk changed the title feat!: Multi-agent/multi-workspace architecture (WIP - preview for feedback) feat!: Multi-agent/multi-workspace architecture Mar 16, 2026
Copilot AI review requested due to automatic review settings March 16, 2026 12:50
@rayrayraykk rayrayraykk requested a deployment to maintainer-approved March 16, 2026 12:50 — with GitHub Actions Waiting
@rayrayraykk rayrayraykk requested a deployment to maintainer-approved March 16, 2026 12:52 — with GitHub Actions Waiting
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 102 out of 102 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +221 to 223
**多智能体支持:** 所有命令都支持 `--agent-id` 参数(默认为 `default`)。

```bash
Comment on lines +410 to 436
"""Set active model for current agent."""
# Validate provider and model exist
try:
await manager.activate_model(body.provider_id, body.model)
except ValueError as exc:
message = str(exc)
lower_msg = message.lower()
if "provider" in lower_msg and "not found" in lower_msg:
# Missing provider
raise HTTPException(status_code=404, detail=message) from exc
# Invalid model, unreachable provider, or other configuration error
raise HTTPException(status_code=400, detail=message) from exc

# Save to agent config
try:
workspace = await get_agent_for_request(request)
agent_config = load_agent_config(workspace.agent_id)
agent_config.active_model = ModelSlotConfig(
provider_id=body.provider_id,
model=body.model,
)
save_agent_config(workspace.agent_id, agent_config)
except Exception as e:
# Log warning but don't fail the request
logger.warning(
f"Failed to save active model to agent config: {e}",
)

return ActiveModelsInfo(active_llm=manager.get_active_model())
Comment on lines +195 to +197
# Get default workspace path for subsequent operations
default_workspace = Path("~/.copaw/workspaces/default").expanduser()

Comment on lines 308 to +313
self._media_dir = (
Path(media_dir).expanduser() if media_dir else _DEFAULT_MEDIA_DIR
)
self._workspace_dir = (
Path(workspace_dir).expanduser() if workspace_dir else None
)
Comment on lines +233 to 235
**Multi-Agent Support:** All commands support the `--agent-id` parameter (defaults to `default`).

```bash
Comment on lines 68 to +113
@router.patch("/{tool_name}/toggle", response_model=ToolInfo)
async def toggle_tool(tool_name: str = Path(...)) -> ToolInfo:
"""Toggle tool enabled status.
async def toggle_tool(
tool_name: str = Path(...),
request: Request = None,
) -> ToolInfo:
"""Toggle tool enabled status for active agent.

Args:
tool_name: Tool function name
request: FastAPI request

Returns:
Updated tool information

Raises:
HTTPException: If tool not found
"""
config = load_config()
from ..agent_context import get_agent_for_request
from ...config.config import load_agent_config, save_agent_config

workspace = await get_agent_for_request(request)
agent_config = load_agent_config(workspace.agent_id)

if tool_name not in config.tools.builtin_tools:
if (
not agent_config.tools
or tool_name not in agent_config.tools.builtin_tools
):
raise HTTPException(
status_code=404,
detail=f"Tool '{tool_name}' not found",
)

# Toggle enabled status
tool_config = config.tools.builtin_tools[tool_name]
tool_config = agent_config.tools.builtin_tools[tool_name]
tool_config.enabled = not tool_config.enabled

# Save config
save_config(config)
# Save agent config
save_agent_config(workspace.agent_id, agent_config)

# Hot reload config (async, non-blocking)
import asyncio

async def reload_in_background():
try:
manager = request.app.state.multi_agent_manager
await manager.reload_agent(workspace.agent_id)
Comment on lines 75 to 80
try:
# Use local file URL instead of base64
absolute_path = os.path.abspath(file_path)

if not _is_allowed_media_path(absolute_path):
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error: Media file outside allowed directory: {os.path.basename(file_path)}",
),
],
)

file_url = f"file://{absolute_path}"
source = {"type": "url", "url": file_url}

Comment on lines +71 to +83
try:
workspace = await manager.get_agent(target_agent_id)
if not workspace:
raise HTTPException(
status_code=404,
detail=f"Agent '{target_agent_id}' not found",
)
return workspace
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get agent: {str(e)}",
) from e
Comment on lines 64 to 70
for name in zf.namelist():
resolved = (WORKING_DIR / name).resolve()
if not str(resolved).startswith(str(WORKING_DIR)):
resolved = (workspace_dir / name).resolve()
if not str(resolved).startswith(str(workspace_dir)):
raise HTTPException(
status_code=400,
detail=f"Zip contains unsafe path: {name}",
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

5 participants