Skip to content

Commit ab086ed

Browse files
committed
Load authorized workspace repository instructions into agent context
1 parent 873330b commit ab086ed

25 files changed

Lines changed: 400 additions & 1 deletion

File tree

api/server/services/ToolService.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const {
4949
isFatalAgentInitializationError,
5050
codeExecutionAuthHeaders,
5151
createAttachedWorkspaceBashTool,
52+
createRepositoryInstructionLoader,
5253
resolveAttachedWorkspaceCommandTimeoutMax,
5354
createGitIdentityProgrammaticBashTool,
5455
resolveCodeExecutionContext,
@@ -105,6 +106,7 @@ const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/p
105106
const { primeFiles: primeSearchFiles } = require('~/app/clients/tools/util/fileSearch');
106107
const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process');
107108
const { manifestToolMap, toolkits } = require('~/app/clients/tools/manifest');
109+
const loadRepositoryInstructions = createRepositoryInstructionLoader();
108110
const { createOnSearchResults } = require('~/server/services/Tools/search');
109111
const { reinitMCPServer } = require('~/server/services/Tools/mcp');
110112
const {
@@ -1757,6 +1759,24 @@ async function loadAgentTools({
17571759
getAppConfig,
17581760
});
17591761

1762+
const repositoryInstructionBlock = await loadRepositoryInstructions({
1763+
assertContent: (content) =>
1764+
assertModelBoundContent({
1765+
filters: appConfig.filters,
1766+
agents: [{ instructions: content }],
1767+
onTraversalFailure: reportLocatorTraversalFailure,
1768+
}),
1769+
enabled: codeExecutionEnabled,
1770+
context: codeExecutionContext,
1771+
mode: agent.repositoryInstructions,
1772+
principalId: JSON.stringify([getTenantId(), req.user.id]),
1773+
signal,
1774+
authHeaders: () =>
1775+
codeExecutionAuthHeaders(
1776+
(bridgeWorkerId) => getCodeApiAuthHeaders(req, bridgeWorkerId),
1777+
codeExecutionContext,
1778+
),
1779+
});
17601780
const { loadedTools, toolContextMap, dynamicToolContextMap, primedCodeFiles } = await loadTools({
17611781
agent,
17621782
signal,
@@ -1859,6 +1879,7 @@ async function loadAgentTools({
18591879

18601880
if (preparedActionSnapshot == null) {
18611881
return {
1882+
repositoryInstructionBlock,
18621883
toolRegistry,
18631884
requestScopedConnections: getMCPRequestContext(req, res),
18641885
userMCPAuthMap,
@@ -1880,6 +1901,7 @@ async function loadAgentTools({
18801901
logger.warn(`No tools found for ${_agentTools.length} specified tool call(s)`);
18811902
}
18821903
return {
1904+
repositoryInstructionBlock,
18831905
toolRegistry,
18841906
requestScopedConnections: getMCPRequestContext(req, res),
18851907
userMCPAuthMap,
@@ -2012,6 +2034,7 @@ async function loadAgentTools({
20122034
toolRegistry,
20132035
requestScopedConnections: getMCPRequestContext(req, res),
20142036
toolContextMap,
2037+
repositoryInstructionBlock,
20152038
dynamicToolContextMap,
20162039
userMCPAuthMap,
20172040
toolDefinitions,

client/src/common/agents-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export type AgentForm = {
5353
/** Operator-configured managed or attached execution environment. */
5454
code_environment_id?: string | null;
5555
code_workspace_id?: string;
56+
repositoryInstructions?: 'prefer' | 'defer' | 'off';
5657
/** Git authorship applied to sandboxed commands for this agent. */
5758
git_identity?: Agent['git_identity'];
5859
provider?: AgentProvider | OptionWithIcon;

client/src/components/Chat/Input/CodeWorkspaceMenu.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ function EnvironmentWorkspaces({
9595
isSelected: (workspaceId: string) => boolean;
9696
onSelect: (selection: CodeWorkspaceSelection) => void;
9797
}) {
98+
const localize = useLocalize();
9899
return (
99100
<div>
100101
<Ariakit.MenuHeading render={<div />} className={headingClasses}>
@@ -123,6 +124,18 @@ function EnvironmentWorkspaces({
123124
{descriptor.name && (
124125
<p className="truncate text-xs text-text-secondary">{descriptor.id}</p>
125126
)}
127+
{descriptor.instructions !== undefined && (
128+
<p className="truncate text-xs text-text-secondary">
129+
{descriptor.instructions.length === 0
130+
? localize('com_ui_repository_instructions_none')
131+
: descriptor.instructions
132+
.map(
133+
(file) =>
134+
`${file.path} · ${(file.bytes / 1024).toFixed(1)} KB${file.truncated ? ` · ${localize('com_ui_repository_instructions_truncated')}` : ''}`,
135+
)
136+
.join(', ')}
137+
</p>
138+
)}
126139
{(descriptor.environment?.repo || descriptor.environment?.ref) && (
127140
<p className="truncate text-xs text-text-secondary">
128141
{[descriptor.environment.repo, descriptor.environment.ref]

client/src/components/Chat/Input/__tests__/CodeWorkspaceMenu.spec.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,19 @@ function renderMenu(ui: React.ReactElement) {
8888
}
8989

9090
describe('CodeWorkspaceMenu', () => {
91+
test('shows the instruction file and truncation reported by the worker', async () => {
92+
const state = workspace();
93+
state.environments[0].workspaces[0].instructions = [
94+
{ path: 'AGENTS.md', bytes: 32768, sha256: 'a'.repeat(64), truncated: true },
95+
];
96+
renderMenu(
97+
<CodeWorkspaceMenu setConversation={jest.fn()} workspace={state} disabled={false} />,
98+
);
99+
await userEvent.click(screen.getByTestId('code-workspace'));
100+
expect(
101+
await screen.findByText('AGENTS.md · 32.0 KB · com_ui_repository_instructions_truncated'),
102+
).toBeInTheDocument();
103+
});
91104
test.each([
92105
['example/app', 'example/app · dev'],
93106
[undefined, 'dev'],

client/src/components/SidePanel/Agents/AgentPanel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export function composeAgentUpdatePayload(
9191
stateful_code_sessions,
9292
stateful_code_environment,
9393
code_environment_id,
94+
repositoryInstructions,
9495
code_workspace_id,
9596
git_identity,
9697
recursion_limit,
@@ -162,6 +163,7 @@ export function composeAgentUpdatePayload(
162163
stateful_code_sessions: normalizedStatefulCodeSessions,
163164
stateful_code_environment: normalizedStatefulCodeEnvironment,
164165
code_environment_id: agent_id ? code_environment_id : (code_environment_id ?? undefined),
166+
repositoryInstructions,
165167
code_workspace_id,
166168
git_identity: normalizedGitIdentity,
167169
recursion_limit,
@@ -667,6 +669,7 @@ export default function AgentPanel() {
667669
create.mutate({
668670
...basePayload,
669671
git_identity: basePayload.git_identity ?? undefined,
672+
repositoryInstructions: basePayload.repositoryInstructions,
670673
model,
671674
tools,
672675
provider,

client/src/components/SidePanel/Agents/AgentSelect.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ function AgentSelect({
101101
avatar_action: null,
102102
stateful_code_environment: fullAgent.stateful_code_environment ?? 'user',
103103
code_environment_id: fullAgent.code_environment_id,
104+
repositoryInstructions: fullAgent.repositoryInstructions,
104105
code_workspace_id: fullAgent.code_workspace_id,
105106
git_identity: fullAgent.git_identity,
106107
};

client/src/components/SidePanel/Agents/Code/Settings.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,37 @@ export default function CodeSettings() {
308308
)}
309309
{showGitIdentity && (
310310
<div className="space-y-2 border-t border-border-light pt-3">
311+
<label
312+
htmlFor="repository-instructions"
313+
className="text-xs font-medium text-text-secondary"
314+
>
315+
{localize('com_ui_repository_instructions')}
316+
</label>
317+
<Select
318+
value={watch('repositoryInstructions') ?? 'prefer'}
319+
onValueChange={(value) => {
320+
if (value === 'prefer' || value === 'defer' || value === 'off')
321+
setValue('repositoryInstructions', value, { shouldDirty: true });
322+
}}
323+
>
324+
<SelectTrigger id="repository-instructions">
325+
<SelectValue />
326+
</SelectTrigger>
327+
<SelectContent>
328+
<SelectItem value="prefer">
329+
{localize('com_ui_repository_instructions_prefer')}
330+
</SelectItem>
331+
<SelectItem value="defer">
332+
{localize('com_ui_repository_instructions_defer')}
333+
</SelectItem>
334+
<SelectItem value="off">
335+
{localize('com_ui_repository_instructions_off')}
336+
</SelectItem>
337+
</SelectContent>
338+
</Select>
339+
<p className="text-xs text-text-tertiary">
340+
{localize('com_ui_repository_instructions_description')}
341+
</p>
311342
<div className="text-xs font-medium text-text-secondary">
312343
{localize('com_ui_agent_git_identity')}
313344
</div>

client/src/locales/en/translation.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
{
2+
"com_ui_repository_instructions": "Repository instructions",
3+
"com_ui_repository_instructions_prefer": "Prefer repository conventions",
4+
"com_ui_repository_instructions_defer": "Prefer agent conventions",
5+
"com_ui_repository_instructions_off": "Off",
6+
"com_ui_repository_instructions_description": "Load AGENTS.md, or CLAUDE.md when absent, from the selected workspace when its owner enables discovery. Tool permissions remain unchanged.",
7+
"com_ui_repository_instructions_none": "No repository instructions",
8+
"com_ui_repository_instructions_truncated": "Truncated",
29
"com_ui_code_workspace_default": "Default workspace for new chats",
310
"com_ui_code_workspace_last_used": "Last used workspace",
411
"com_ui_code_workspace_default_description": "New chats use this workspace. Without a default, reuse your last available choice for this agent and machine in this browser. Existing chats keep their workspace.",

packages/api/src/agents/__tests__/initialize.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,30 @@ describe('initializeAgent — execution context', () => {
287287
jest.clearAllMocks();
288288
});
289289

290+
it('places loaded repository instructions after agent instructions in the stable block', async () => {
291+
const { agent, req, res, loadTools, db } = createMocks();
292+
agent.instructions = 'Agent conventions';
293+
loadTools.mockResolvedValue({
294+
tools: [],
295+
toolDefinitions: [],
296+
repositoryInstructionBlock: 'Repository conventions',
297+
});
298+
const result = await initializeAgent(
299+
{
300+
req,
301+
res,
302+
agent,
303+
loadTools,
304+
endpointOption: { endpoint: EModelEndpoint.agents },
305+
allowedProviders: new Set([agent.provider]),
306+
isInitialAgent: true,
307+
},
308+
db,
309+
);
310+
expect(result.instructions).toBe('Agent conventions\n\nRepository conventions');
311+
expect(result.additional_instructions ?? '').not.toContain('Repository conventions');
312+
});
313+
290314
it('carries request-resolved Azure identity to the run without changing persisted agent fields', async () => {
291315
const { agent, req, res, loadTools, db } = createMocks({
292316
provider: EModelEndpoint.azureOpenAI,

packages/api/src/agents/execution.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface CodeExecutionContext {
4141
/** Live, server-validated directory selection. Never derive session reuse from this field. */
4242
codeWorkspace?: CodeWorkspaceSelection & {
4343
operations: CodeWorkspaceOperation[];
44+
instructions?: CodeWorkspaceDescriptor['instructions'];
4445
environment?: CodeWorkspaceDescriptor['environment'];
4546
};
4647
}

0 commit comments

Comments
 (0)