feat(agent): add default workspace directory per profile

Each agent profile now gets a dedicated workspace directory
(~/.super-multica/workspace/{profileId}) used as the default CWD
for tool operations. Supports override via MULTICA_WORKSPACE_DIR
env var or config.json workspaceDir field. The workspace path is
injected into system prompt and runtime info.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jiang Bohan 2026-02-12 18:07:59 +08:00
parent ade5e5a17c
commit ca0b4624fd
7 changed files with 83 additions and 3 deletions

View file

@ -31,6 +31,8 @@ export interface ProfileConfig {
reasoningMode?: "off" | "on" | "stream" | undefined;
/** Exec approval configuration (security level, ask mode, allowlist) */
execApproval?: ExecApprovalConfig | undefined;
/** Custom workspace directory path (overrides default) */
workspaceDir?: string | undefined;
/** Heartbeat configuration */
heartbeat?: {
/** Global heartbeat enable switch */

View file

@ -50,6 +50,7 @@ import {
sanitizeToolUseResultPairing,
} from "./session/session-transcript-repair.js";
import { isContextOverflowError } from "./errors.js";
import { resolveWorkspaceDir, ensureWorkspaceDir } from "./workspace.js";
// ============================================================
// Error classification for auth profile rotation
@ -121,6 +122,9 @@ export class Agent {
private readonly pinnedProfile: boolean;
private readonly explicitApiKey: boolean;
/** Resolved workspace directory */
readonly workspaceDir: string;
/** Current session ID */
readonly sessionId: string;
@ -318,13 +322,22 @@ export class Agent {
this.originalToolsConfig = options.tools;
}
// Resolve workspace directory
const profileConfig = this.profile?.getProfile()?.config;
this.workspaceDir = resolveWorkspaceDir({
profileId: options.profileId,
configWorkspaceDir: profileConfig?.workspaceDir,
});
ensureWorkspaceDir(this.workspaceDir);
const effectiveCwd = options.cwd ?? this.workspaceDir;
// Merge Profile tools config with options.tools (options takes precedence)
const profileToolsConfig = this.profile?.getToolsConfig();
const mergedToolsConfig = mergeToolsConfig(profileToolsConfig, options.tools);
const profileDir = this.profile?.getProfileDir();
this.toolsOptions = mergedToolsConfig
? { ...options, tools: mergedToolsConfig, profileDir, provider: this.resolvedProvider }
: { ...options, profileDir, provider: this.resolvedProvider };
? { ...options, cwd: effectiveCwd, tools: mergedToolsConfig, profileDir, provider: this.resolvedProvider }
: { ...options, cwd: effectiveCwd, profileDir, provider: this.resolvedProvider };
const tools = resolveTools(this.toolsOptions);
if (this.debug) {
@ -1155,6 +1168,7 @@ export class Agent {
agentName: this.profile?.getName(),
provider: this.resolvedProvider,
model: this.agent.state.model?.id,
cwd: this.toolsOptions.cwd,
});
return buildStructuredSystemPrompt({
@ -1168,6 +1182,7 @@ export class Agent {
config: profile.config,
},
profileDir: this.profile!.getProfileDir(),
workspaceDir: this.workspaceDir,
tools: toolNames,
skillsPrompt,
runtime,

View file

@ -46,6 +46,7 @@ export function buildSystemPromptWithReport(options: SystemPromptOptions): {
mode,
profile,
profileDir,
workspaceDir,
tools,
skillsPrompt,
runtime,
@ -58,7 +59,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, profileDir) },
{ name: "workspace", lines: buildWorkspaceSection(profile, mode, profileDir, workspaceDir) },
{ name: "memory", lines: buildMemoryFileSection(profile, mode) },
{ name: "heartbeat", lines: buildHeartbeatSection(profile, mode) },
{ name: "safety", lines: buildSafetySection(includeSafety) },

View file

@ -89,11 +89,23 @@ export function buildWorkspaceSection(
profile: ProfileContent | undefined,
mode: SystemPromptMode,
profileDir?: string,
workspaceDir?: string,
): string[] {
if (mode !== "full") return [];
const lines: string[] = [];
// Working directory info
if (workspaceDir) {
lines.push(
"## Working Directory",
"",
`Your working directory is: \`${workspaceDir}\``,
"Use this as the default location for file operations unless the user specifies a different path.",
"",
);
}
// Add profile directory context first
if (profileDir) {
lines.push(

View file

@ -75,6 +75,8 @@ export interface SystemPromptOptions {
runtime?: RuntimeInfo | undefined;
/** Subagent context (for minimal/none modes) */
subagent?: SubagentContext | undefined;
/** Workspace directory path (for agent working directory info) */
workspaceDir?: string | undefined;
/** Extra system prompt to append */
extraSystemPrompt?: string | undefined;
/** Whether to include the safety constitution (default: true) */

View file

@ -0,0 +1,45 @@
import { mkdirSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { DATA_DIR, DEFAULT_WORKSPACE_DIR } from "@multica/utils";
/**
* Resolve the workspace directory for a given profile.
* Priority: env var > config > profile-based default
*/
export function resolveWorkspaceDir(options?: {
profileId?: string;
configWorkspaceDir?: string;
}): string {
// 1. Env var override
const envDir = process.env.MULTICA_WORKSPACE_DIR?.trim();
if (envDir) return path.resolve(envDir.replace(/^~/, os.homedir()));
// 2. Config override (from profile config.json)
if (options?.configWorkspaceDir?.trim()) {
return path.resolve(options.configWorkspaceDir.replace(/^~/, os.homedir()));
}
// 3. Profile-based default: ~/.super-multica/workspace/{profileId}
const profileId = options?.profileId ?? "default";
return path.join(DEFAULT_WORKSPACE_DIR, profileId);
}
/**
* Ensure workspace directory exists. Creates README.md on first creation.
*/
export function ensureWorkspaceDir(dir: string): void {
mkdirSync(dir, { recursive: true });
const readmePath = path.join(dir, "README.md");
try {
writeFileSync(readmePath, README_CONTENT, { encoding: "utf-8", flag: "wx" });
} catch (err) {
if ((err as { code?: string }).code !== "EEXIST") throw err;
}
}
const README_CONTENT = `# Multica Workspace
This directory is the default workspace for your Multica agent.
Files created by the agent will be saved here unless you specify a different directory.
`;

View file

@ -6,3 +6,6 @@ export const DATA_DIR = join(homedir(), ".super-multica");
/** Cache directory for downloaded media files */
export const MEDIA_CACHE_DIR = join(DATA_DIR, "cache", "media");
/** Default workspace directory for the default profile */
export const DEFAULT_WORKSPACE_DIR = join(DATA_DIR, "workspace");