diff --git a/apps/x/packages/core/src/agents/compose-instructions.ts b/apps/x/packages/core/src/agents/compose-instructions.ts index 2cda8819..025f9196 100644 --- a/apps/x/packages/core/src/agents/compose-instructions.ts +++ b/apps/x/packages/core/src/agents/compose-instructions.ts @@ -1,4 +1,5 @@ import { MODE_CAPABILITIES } from "../application/assistant/capabilities/modes.js"; +import { WORKSPACE_CONTEXT_CAPABILITY } from "../application/assistant/capabilities/workspace.js"; import type { CapabilityContext } from "../application/assistant/capabilities/types.js"; // System-prompt composition for agent assembly: the base instructions plus @@ -52,33 +53,12 @@ export function composeSystemInstructions({ coachMode, }: ComposeSystemInstructionsInput): string { let instructionsWithDateTime = `${instructions}\n\n${USER_CONTEXT_SYSTEM_INSTRUCTIONS}`; - if (agentNotesContext) { - instructionsWithDateTime += `\n\n${agentNotesContext}`; - } - if (userWorkDir) { - instructionsWithDateTime += `\n\n# User Work Directory -The user has chosen the following directory as their current **work directory**: - -\`${userWorkDir}\` - -Treat this as the **default location** for file operations whenever the user refers to files generically: -- "list the files", "show me what's in here", "what's the latest report" — list or look in the work directory. -- "save this", "export it", "write that to a file" — write the output into the work directory unless the user names another location. -- "open the file I was just working on", "the doc from earlier" — assume the work directory first. - -Use absolute paths rooted at this directory with the \`file-*\` tools. For example, list with \`file-list({ path: "${userWorkDir}" })\`, read text with \`file-readText\`, and write text with \`file-writeText\`. For PDFs, Office docs, images, scanned docs, and other non-text files, use \`parseFile\` or \`LLMParse\` with the absolute path; you do NOT need to copy the file into the workspace first. - -**Exceptions — these ALWAYS take precedence over the work directory default:** -1. **Knowledge base questions.** If the user asks about anything in the knowledge graph (notes, people, organizations, projects, topics) or paths starting with \`knowledge/\`, use file tools against \`knowledge/\` as documented above. Do NOT redirect those into the work directory. -2. **Explicit paths.** If the user names a different directory or gives an absolute/relative path (e.g. "in ~/Downloads", "from /tmp/foo", "the Desktop"), honor that path exactly and ignore the work-directory default for that request. -3. **Workspace-specific operations.** Anything that obviously belongs in the Rowboat workspace (config files, MCP servers, agent schedules, etc.) stays in the workspace, not the work directory. - -Do not announce the work directory unless it's relevant. Just use it.`; - } // App-activated mode capabilities compose here, in MODE_CAPABILITIES // order — a fixed total order, so identical inputs yield identical // bytes. The fragment text lives with the capability records. const ctx: CapabilityContext = { + agentNotesContext, + userWorkDir, voiceInput, voiceOutput, searchEnabled, @@ -87,7 +67,7 @@ Do not announce the work directory unless it's relevant. Just use it.`; videoMode: videoMode ?? false, coachMode: coachMode ?? false, }; - for (const capability of MODE_CAPABILITIES) { + for (const capability of [WORKSPACE_CONTEXT_CAPABILITY, ...MODE_CAPABILITIES]) { const fragment = capability.promptFragment?.(ctx); if (fragment) { instructionsWithDateTime += `\n\n${fragment}`; diff --git a/apps/x/packages/core/src/application/assistant/capabilities/types.ts b/apps/x/packages/core/src/application/assistant/capabilities/types.ts index f0fee3cd..6a5c7129 100644 --- a/apps/x/packages/core/src/application/assistant/capabilities/types.ts +++ b/apps/x/packages/core/src/application/assistant/capabilities/types.ts @@ -26,6 +26,10 @@ export type CapabilityActivation = "model" | "app" | "always"; // stay byte-identical for identical inputs (provider prefix caching and // agent-snapshot inheritance both depend on it). export interface CapabilityContext { + // Workspace context (workspaceContext trait agents only; the resolver + // loads these and leaves them null for everyone else). + agentNotesContext: string | null; + userWorkDir: string | null; voiceInput: boolean; voiceOutput: "summary" | "full" | null; searchEnabled: boolean; diff --git a/apps/x/packages/core/src/application/assistant/capabilities/workspace.ts b/apps/x/packages/core/src/application/assistant/capabilities/workspace.ts new file mode 100644 index 00000000..77fb7511 --- /dev/null +++ b/apps/x/packages/core/src/application/assistant/capabilities/workspace.ts @@ -0,0 +1,43 @@ +import type { CapabilityContext, CapabilityDefinition } from "./types.js"; + +// The always-activated workspace-context capability: agent notes and the +// user work directory, composed for agents with the workspaceContext trait +// (the resolver loads both inputs and leaves them null for everyone else). +// Fragment text extracted verbatim from the historical composer; the golden +// snapshots in agents/compose-instructions.test.ts pin the bytes. + +export const WORKSPACE_CONTEXT_CAPABILITY: CapabilityDefinition = { + id: "workspace-context", + title: "Workspace Context", + summary: "Agent notes and the user's chosen work directory.", + activation: "always", + promptFragment: (ctx: CapabilityContext) => { + const parts: string[] = []; + if (ctx.agentNotesContext) { + parts.push(ctx.agentNotesContext); + } + if (ctx.userWorkDir) { + parts.push(WORK_DIR_TEMPLATE(ctx.userWorkDir)); + } + return parts.length > 0 ? parts.join("\n\n") : null; + }, +}; + +const WORK_DIR_TEMPLATE = (userWorkDir: string): string => `# User Work Directory +The user has chosen the following directory as their current **work directory**: + +\`${userWorkDir}\` + +Treat this as the **default location** for file operations whenever the user refers to files generically: +- "list the files", "show me what's in here", "what's the latest report" — list or look in the work directory. +- "save this", "export it", "write that to a file" — write the output into the work directory unless the user names another location. +- "open the file I was just working on", "the doc from earlier" — assume the work directory first. + +Use absolute paths rooted at this directory with the \`file-*\` tools. For example, list with \`file-list({ path: "${userWorkDir}" })\`, read text with \`file-readText\`, and write text with \`file-writeText\`. For PDFs, Office docs, images, scanned docs, and other non-text files, use \`parseFile\` or \`LLMParse\` with the absolute path; you do NOT need to copy the file into the workspace first. + +**Exceptions — these ALWAYS take precedence over the work directory default:** +1. **Knowledge base questions.** If the user asks about anything in the knowledge graph (notes, people, organizations, projects, topics) or paths starting with \`knowledge/\`, use file tools against \`knowledge/\` as documented above. Do NOT redirect those into the work directory. +2. **Explicit paths.** If the user names a different directory or gives an absolute/relative path (e.g. "in ~/Downloads", "from /tmp/foo", "the Desktop"), honor that path exactly and ignore the work-directory default for that request. +3. **Workspace-specific operations.** Anything that obviously belongs in the Rowboat workspace (config files, MCP servers, agent schedules, etc.) stays in the workspace, not the work directory. + +Do not announce the work directory unless it's relevant. Just use it.`;