Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2dbeafc
lsp integration draft
terion-name Apr 3, 2026
0fb38f1
address review comments
terion-name Apr 13, 2026
115e5f7
Add post-edit LSP diagnostics for mutating tools
terion-name Apr 13, 2026
c11119b
Fix post-edit diagnostics freshness and patch file tracking
terion-name Apr 13, 2026
4f4ee43
Fail closed on task_apply_git_patch diff discovery errors
terion-name Apr 13, 2026
21781c6
🤖 feat: add workspace LSP diagnostics snapshots
terion-name Apr 13, 2026
7248bd6
🤖 fix: harden LSP diagnostics recovery
terion-name Apr 13, 2026
7d52175
fix lsp disposal diagnostics races
terion-name Apr 13, 2026
b43cd80
🤖 feat: add stats diagnostics panel
terion-name Apr 13, 2026
d9dccb3
fix diagnostics stats follow-ups
terion-name Apr 13, 2026
ad65e35
tests: fix remaining lint issues in LSP/UI tests
terion-name Apr 13, 2026
f0b9d6c
🤖 refactor: split LSP launch resolution from client transport
terion-name Apr 15, 2026
62bbc24
fix lsp launch-plan probing and caching
terion-name Apr 15, 2026
59f8750
fix stale LSP launch plan cache on restart
terion-name Apr 15, 2026
c1f8a06
Add LSP provisioning policies and config
terion-name Apr 15, 2026
dda72e5
fix LSP trust gating and provisioning reuse
terion-name Apr 15, 2026
e38f10e
Add LSP provisioning setting to GeneralSection
terion-name Apr 15, 2026
c14cb96
fix: tighten LSP settings persistence
terion-name Apr 15, 2026
fea8e10
🤖 tests: fix LSP manager trust-gating expectation
terion-name Apr 15, 2026
7d70cce
fix lsp CLI test overrides
terion-name Apr 16, 2026
06cba0c
Fix mux run trust inheritance for worktree paths
terion-name Apr 16, 2026
8c7745c
🤖 fix: poll tracked LSP diagnostics
terion-name Apr 16, 2026
a9e1429
fix lsp diagnostics refresh edge cases
terion-name Apr 16, 2026
b380e56
🤖 fix: reset polled LSP workspace refresh state
terion-name Apr 16, 2026
151c555
🤖 fix: resolve nested TypeScript LSP provisioning
terion-name Apr 16, 2026
9fa1dbb
🤖 fix: harden untrusted LSP pathCommand envs
terion-name Apr 16, 2026
396f978
🤖 fix: keep LSP provisioning overrides out of temp and saved config
terion-name Apr 16, 2026
528c91b
Only poll LSP diagnostics with active listeners
terion-name Apr 16, 2026
261e9ba
Show LSP diagnostics retry state in stats
terion-name Apr 16, 2026
9bf3d4f
🤖 fix: preserve inherited env for untrusted LSP path launches
terion-name Apr 16, 2026
5072cef
🤖 fix: keep sanitized PATH for untrusted LSP launches
terion-name Apr 16, 2026
f9df0fe
Make LSP path env sanitization runtime-aware
terion-name Apr 16, 2026
af6b7d8
🤖 fix: harden pathCommand PATH sanitization
terion-name Apr 16, 2026
4aa6dc7
🤖 fix: infer LSP workspace symbols from directories
terion-name Apr 16, 2026
d1f2bbb
🤖 fix: prefer deepest workspace_symbols root
terion-name Apr 16, 2026
f13a143
Merge origin/main into lsp-integration
terion-name Apr 17, 2026
64aed6b
🤖 fix: aggregate directory workspace symbols across roots
terion-name Apr 17, 2026
0893d37
🤖 fix: stabilize workspace_symbols directory queries
terion-name Apr 17, 2026
9e961fb
Fix TS workspace symbol warm-up root selection
terion-name Apr 17, 2026
1968129
fix TypeScript workspace_symbols warm-up
terion-name Apr 17, 2026
d9f5948
🤖 fix: prefer exact TS workspace_symbols warm-up matches
terion-name Apr 17, 2026
521f622
🤖 refactor: split workspace symbol roots by directory
terion-name Apr 18, 2026
4845c58
Fix LSP manager test lint assertion
terion-name Apr 18, 2026
e522fe3
🤖 feat: enrich workspace_symbols directory results
terion-name Apr 18, 2026
0b36f9f
🤖 fix: exact-match Go workspace_symbols
terion-name Apr 18, 2026
b2d32e0
🤖 fix: suppress fuzzy Go workspace_symbols after cross-root exact hits
terion-name Apr 18, 2026
e026ba9
Generalize TypeScript LSP project-root selection
terion-name Apr 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/common/constants/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const EXPERIMENT_IDS = {
PROGRAMMATIC_TOOL_CALLING_EXCLUSIVE: "programmatic-tool-calling-exclusive",
CONFIGURABLE_BIND_URL: "configurable-bind-url",
SYSTEM_1: "system-1",
LSP_QUERY: "lsp-query",
EXEC_SUBAGENT_HARD_RESTART: "exec-subagent-hard-restart",
MUX_GOVERNOR: "mux-governor",
MULTI_PROJECT_WORKSPACES: "multi-project-workspaces",
Expand Down Expand Up @@ -81,6 +82,15 @@ export const EXPERIMENTS: Record<ExperimentId, ExperimentDefinition> = {
userOverridable: true,
showInSettings: true,
},
[EXPERIMENT_IDS.LSP_QUERY]: {
id: EXPERIMENT_IDS.LSP_QUERY,
name: "LSP Query Tool",
description:
"Enable the built-in lsp_query tool for definitions, references, hover, and symbol lookup",
enabledByDefault: false,
userOverridable: true,
showInSettings: true,
},
[EXPERIMENT_IDS.EXEC_SUBAGENT_HARD_RESTART]: {
id: EXPERIMENT_IDS.EXEC_SUBAGENT_HARD_RESTART,
name: "Exec sub-agent hard restart",
Expand Down
4 changes: 4 additions & 0 deletions src/common/types/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
MuxAgentsReadToolResultSchema,
MuxAgentsWriteToolResultSchema,
FileReadToolResultSchema,
LspQueryToolResultSchema,
AttachFileToolResultSchema,
TaskToolResultSchema,
TaskAwaitToolResultSchema,
Expand Down Expand Up @@ -127,6 +128,9 @@ export interface ToolOutputUiOnlyFields {
// FileReadToolResult derived from Zod schema (single source of truth)
export type FileReadToolResult = z.infer<typeof FileReadToolResultSchema>;

export type LspQueryToolArgs = z.infer<typeof TOOL_DEFINITIONS.lsp_query.schema>;
export type LspQueryToolResult = z.infer<typeof LspQueryToolResultSchema>;

// AttachFileToolResult derived from Zod schema (single source of truth)
export type AttachFileToolResult = z.infer<typeof AttachFileToolResultSchema>;

Expand Down
2 changes: 2 additions & 0 deletions src/common/utils/tools/toolAvailability.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface ToolAvailabilityContext {
workspaceId: string;
parentWorkspaceId?: string | null;
enableLspQuery?: boolean;
}

/**
Expand All @@ -10,6 +11,7 @@ export interface ToolAvailabilityContext {
export function getToolAvailabilityOptions(context: ToolAvailabilityContext) {
return {
enableAgentReport: Boolean(context.parentWorkspaceId),
enableLspQuery: context.enableLspQuery === true,
// skills_catalog_* tools are always available; agent tool policy controls access.
} as const;
}
113 changes: 113 additions & 0 deletions src/common/utils/tools/toolDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,51 @@ export const TOOL_DEFINITIONS = {
})
),
},
lsp_query: {
description:
"Query the built-in language server for code intelligence. " +
"Use this for hover, definitions, references, implementations, and symbol lookup. " +
"Provide line and column using 1-based positions for hover/definition/reference/implementation. " +
"For workspace_symbols, provide a representative file path to select the correct language server plus a non-empty query.",
schema: z.preprocess(
normalizeFilePath,
z
.object({
operation: z.enum([
"hover",
"definition",
"references",
"implementation",
"document_symbols",
"workspace_symbols",
]),
path: FILE_TOOL_PATH.describe(
"Path to the file being queried (absolute or relative to the current workspace)"
),
line: z
.number()
.int()
.positive()
.nullish()
.describe("1-based line number for position-based queries"),
column: z
.number()
.int()
.positive()
.nullish()
.describe("1-based column number for position-based queries"),
query: z
.string()
.nullish()
.describe("Required for workspace_symbols; ignored by other operations"),
includeDeclaration: z
.boolean()
.nullish()
.describe("For references only: whether declarations should be included"),
})
.strict()
),
},
attach_file: {
description:
"Attach a supported file from the filesystem so later model steps receive it as a real attachment instead of a huge base64 JSON blob. " +
Expand Down Expand Up @@ -1840,6 +1885,69 @@ export const FileReadToolResultSchema = z.union([
}),
]);

const LspQueryResultRangeSchema = z
.object({
start: z.object({
line: z.number().int().positive(),
character: z.number().int().positive(),
}),
end: z.object({
line: z.number().int().positive(),
character: z.number().int().positive(),
}),
})
.strict();

const LspQueryLocationSchema = z
.object({
path: z.string(),
uri: z.string(),
range: LspQueryResultRangeSchema,
preview: z.string().optional(),
})
.strict();

const LspQuerySymbolSchema = z
.object({
name: z.string(),
kind: z.number().int(),
detail: z.string().optional(),
containerName: z.string().optional(),
path: z.string(),
range: LspQueryResultRangeSchema,
preview: z.string().optional(),
})
.strict();

export const LspQueryToolResultSchema = z.union([
z
.object({
success: z.literal(true),
operation: z.enum([
"hover",
"definition",
"references",
"implementation",
"document_symbols",
"workspace_symbols",
]),
serverId: z.string(),
rootUri: z.string(),
hover: z.string().optional(),
locations: z.array(LspQueryLocationSchema).optional(),
symbols: z.array(LspQuerySymbolSchema).optional(),
warning: z.string().optional(),
})
.strict(),
z
.object({
success: z.literal(false),
error: z.string(),
warning: z.string().optional(),
})
.strict(),
]);

const AttachFileToolTextPartSchema = z
.object({
type: z.literal("text"),
Expand Down Expand Up @@ -1963,6 +2071,7 @@ export type BridgeableToolName =
| "bash_background_list"
| "bash_background_terminate"
| "file_read"
| "lsp_query"
| "attach_file"
| "agent_skill_read"
| "agent_skill_read_file"
Expand Down Expand Up @@ -1990,6 +2099,7 @@ export const RESULT_SCHEMAS: Record<BridgeableToolName, z.ZodType> = {
bash_background_list: BashBackgroundListResultSchema,
bash_background_terminate: BashBackgroundTerminateResultSchema,
file_read: FileReadToolResultSchema,
lsp_query: LspQueryToolResultSchema,
attach_file: AttachFileToolResultSchema,
agent_skill_read: AgentSkillReadToolResultSchema,
agent_skill_read_file: AgentSkillReadFileToolResultSchema,
Expand Down Expand Up @@ -2033,13 +2143,15 @@ export function getAvailableTools(
options?: {
enableAgentReport?: boolean;
enableAnalyticsQuery?: boolean;
enableLspQuery?: boolean;
/** @deprecated Mux global tools are always included. */
enableMuxGlobalAgentsTools?: boolean;
}
): string[] {
const [provider] = modelString.split(":");
const enableAgentReport = options?.enableAgentReport ?? true;
const enableAnalyticsQuery = options?.enableAnalyticsQuery ?? true;
const enableLspQuery = options?.enableLspQuery ?? false;

// Base tools available for all models
// Note: Tool availability is controlled by agent tool policy (allowlist), not mode checks here.
Expand All @@ -2066,6 +2178,7 @@ export function getAvailableTools(
"agent_skill_read",
"agent_skill_read_file",
"file_edit_replace_string",
...(enableLspQuery ? ["lsp_query"] : []),
// "file_edit_replace_lines", // DISABLED: causes models to break repo state
"file_edit_insert",
"ask_user_question",
Expand Down
46 changes: 46 additions & 0 deletions src/common/utils/tools/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, mock, test } from "bun:test";
import type { InitStateManager } from "@/node/services/initStateManager";
import type { DesktopSessionManager } from "@/node/services/desktop/DesktopSessionManager";
import { LocalRuntime } from "@/node/runtime/LocalRuntime";
import { LspManager } from "@/node/services/lsp/lspManager";
import { getToolsForModel } from "./tools";

const DESKTOP_TOOL_NAMES = [
Expand Down Expand Up @@ -156,4 +157,49 @@ describe("getToolsForModel", () => {

expect(Object.keys(tools).filter((toolName) => toolName.startsWith("desktop_"))).toEqual([]);
});

test("only includes lsp_query when the experiment is enabled and a manager is available", async () => {
const runtime = new LocalRuntime(process.cwd());
const initStateManager = createInitStateManager();
const lspManager = new LspManager({ registry: [] });
lspManager.query = mock(() =>
Promise.resolve({
operation: "hover" as const,
serverId: "typescript",
rootUri: "file:///tmp/workspace",
hover: "",
})
);

try {
const toolsWithoutLsp = await getToolsForModel(
"noop:model",
{
cwd: process.cwd(),
runtime,
runtimeTempDir: "/tmp",
lspQueryEnabled: false,
},
"ws-1",
initStateManager
);
expect(toolsWithoutLsp.lsp_query).toBeUndefined();

const toolsWithLsp = await getToolsForModel(
"noop:model",
{
cwd: process.cwd(),
runtime,
runtimeTempDir: "/tmp",
lspManager,
lspQueryEnabled: true,
},
"ws-1",
initStateManager
);
expect(toolsWithLsp.lsp_query).toBeDefined();
} finally {
await lspManager.dispose();
}
});
});
12 changes: 12 additions & 0 deletions src/common/utils/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { createTodoWriteTool, createTodoReadTool } from "@/node/services/tools/t
import { createNotifyTool } from "@/node/services/tools/notify";
import { createAnalyticsQueryTool } from "@/node/services/tools/analyticsQuery";
import { createDesktopTools } from "@/node/services/tools/desktopTools";
import { createLspQueryTool } from "@/node/services/tools/lsp_query";
import type { MuxToolScope } from "@/common/types/toolScope";
import { createTaskTool } from "@/node/services/tools/task";
import { createTaskApplyGitPatchTool } from "@/node/services/tools/task_apply_git_patch";
Expand Down Expand Up @@ -54,6 +55,7 @@ import type { FileState } from "@/node/services/agentSession";
import type { AgentDefinitionDescriptor } from "@/common/types/agentDefinition";
import type { AgentSkillDescriptor } from "@/common/types/agentSkill";
import type { ProjectRef } from "@/common/types/workspace";
import type { LspManager } from "@/node/services/lsp/lspManager";

/**
* Configuration for tools that need runtime context
Expand Down Expand Up @@ -120,6 +122,10 @@ export interface ToolConfiguration {
};
/** Desktop session manager for desktop automation tools */
desktopSessionManager?: DesktopSessionManager;
/** Shared workspace-scoped LSP manager for built-in query tooling */
lspManager?: LspManager;
/** Whether the experiment-gated lsp_query tool should be exposed for this request */
lspQueryEnabled?: boolean;
}

/**
Expand Down Expand Up @@ -346,6 +352,11 @@ export async function getToolsForModel(
agent_skill_read_file: wrap(createAgentSkillReadFileTool(config)),
file_edit_replace_string: wrap(createFileEditReplaceStringTool(config)),
file_edit_insert: wrap(createFileEditInsertTool(config)),
...(config.lspManager && config.lspQueryEnabled
? {
lsp_query: wrap(createLspQueryTool(config)),
}
: {}),
// DISABLED: file_edit_replace_lines - causes models (particularly GPT-5-Codex)
// to leave repository in broken state due to issues with concurrent file modifications
// and line number miscalculations. Use file_edit_replace_string instead.
Expand Down Expand Up @@ -492,6 +503,7 @@ export async function getToolsForModel(
getAvailableTools(modelString, {
enableAgentReport: config.enableAgentReport,
enableAnalyticsQuery: Boolean(config.analyticsService),
enableLspQuery: config.lspManager != null && config.lspQueryEnabled === true,
// Mux global tools are always created; tool policy (agent frontmatter)
// controls which agents can actually use them.
enableMuxGlobalAgentsTools: true,
Expand Down
7 changes: 7 additions & 0 deletions src/constants/lsp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const LSP_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
export const LSP_IDLE_CHECK_INTERVAL_MS = 60 * 1000;
export const LSP_REQUEST_TIMEOUT_MS = 10 * 1000;
export const LSP_START_TIMEOUT_MS = 5 * 1000;
export const LSP_MAX_LOCATIONS = 25;
export const LSP_MAX_SYMBOLS = 100;
export const LSP_PREVIEW_CONTEXT_LINES = 1;
15 changes: 14 additions & 1 deletion src/node/services/aiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import {
import { applyToolPolicyAndExperiments, captureMcpToolTelemetry } from "./toolAssembly";
import { getErrorMessage } from "@/common/utils/errors";
import { isProjectTrusted } from "@/node/utils/projectTrust";
import type { LspManager } from "@/node/services/lsp/lspManager";

const STREAM_STARTUP_DIAGNOSTIC_THRESHOLD_MS = 1_000;

Expand Down Expand Up @@ -211,6 +212,7 @@ export class AIService extends EventEmitter {
private readonly config: Config;
private readonly workspaceMcpOverridesService: WorkspaceMcpOverridesService;
private mcpServerManager?: MCPServerManager;
private lspManager?: LspManager;
private readonly policyService?: PolicyService;
private readonly telemetryService?: TelemetryService;
private readonly opResolver?: ExternalSecretResolver;
Expand Down Expand Up @@ -312,6 +314,10 @@ export class AIService extends EventEmitter {
this.streamManager.setMCPServerManager(manager);
}

setLspManager(manager: LspManager): void {
this.lspManager = manager;
}

setTaskService(taskService: TaskService): void {
this.taskService = taskService;
}
Expand Down Expand Up @@ -1106,6 +1112,8 @@ export class AIService extends EventEmitter {
};

const desktopSessionManager = this.desktopSessionManager;
const lspQueryEnabled =
this.experimentsService?.isExperimentEnabled(EXPERIMENT_IDS.LSP_QUERY) ?? false;
let desktopCapabilityPromise: ReturnType<DesktopSessionManager["getCapability"]> | undefined;
const loadDesktopCapability =
desktopSessionManager == null
Expand Down Expand Up @@ -1204,7 +1212,10 @@ export class AIService extends EventEmitter {
runtime,
workspacePath,
modelString,
agentSystemPrompt
agentSystemPrompt,
{
enableLspQuery: lspQueryEnabled,
}
);
recordStartupPhaseTiming("readToolInstructionsMs", readToolInstructionsStartedAt);

Expand Down Expand Up @@ -1273,6 +1284,8 @@ export class AIService extends EventEmitter {
taskService: this.taskService,
analyticsService: this.analyticsService,
desktopSessionManager: this.desktopSessionManager,
lspManager: this.lspManager,
lspQueryEnabled,
// PTC experiments for inheritance to subagents
experiments,
// Dynamic context for tool descriptions (moved from system prompt for better model attention)
Expand Down
Loading