refactor(x): workspace context becomes an always-activated capability

The agent-notes and work-directory prompt blocks move out of the
composer into WORKSPACE_CONTEXT_CAPABILITY (activation: 'always'),
whose fragment is pure over the resolved inputs — the resolver still
loads notes/work-dir only for agents with the workspaceContext trait
and passes null otherwise, so trait gating is unchanged. The composer
now iterates one capability list (workspace + modes) in fixed order;
golden snapshots pass unchanged, byte-identical output. The composer's
own body is down to: base instructions + hidden-user-context + the
capability loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-09 22:40:27 +05:30
parent e194e32a23
commit f2a874a214
3 changed files with 51 additions and 24 deletions

View file

@ -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}`;

View file

@ -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;

View file

@ -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.`;