diff --git a/packages/core/src/agent/profile/types.ts b/packages/core/src/agent/profile/types.ts index 228a0788..631eb4b1 100644 --- a/packages/core/src/agent/profile/types.ts +++ b/packages/core/src/agent/profile/types.ts @@ -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 */ diff --git a/packages/core/src/agent/runner.ts b/packages/core/src/agent/runner.ts index 105eee43..74d8edb5 100644 --- a/packages/core/src/agent/runner.ts +++ b/packages/core/src/agent/runner.ts @@ -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, diff --git a/packages/core/src/agent/system-prompt/builder.ts b/packages/core/src/agent/system-prompt/builder.ts index 78829ebf..82d01860 100644 --- a/packages/core/src/agent/system-prompt/builder.ts +++ b/packages/core/src/agent/system-prompt/builder.ts @@ -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) }, diff --git a/packages/core/src/agent/system-prompt/sections.ts b/packages/core/src/agent/system-prompt/sections.ts index bc0bc6ed..6826aa7e 100644 --- a/packages/core/src/agent/system-prompt/sections.ts +++ b/packages/core/src/agent/system-prompt/sections.ts @@ -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( diff --git a/packages/core/src/agent/system-prompt/types.ts b/packages/core/src/agent/system-prompt/types.ts index 9f75788e..cf1cc1ba 100644 --- a/packages/core/src/agent/system-prompt/types.ts +++ b/packages/core/src/agent/system-prompt/types.ts @@ -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) */ diff --git a/packages/core/src/agent/workspace.ts b/packages/core/src/agent/workspace.ts new file mode 100644 index 00000000..c30eb271 --- /dev/null +++ b/packages/core/src/agent/workspace.ts @@ -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. +`; diff --git a/packages/utils/src/paths.ts b/packages/utils/src/paths.ts index 3c047312..da5ebece 100644 --- a/packages/utils/src/paths.ts +++ b/packages/utils/src/paths.ts @@ -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");