minimal-agent 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/package.json +5 -1
  2. package/plugins/workflow-runner/plugin.js +27 -15
  3. package/plugins/workflow-runner/plugin.ts +34 -20
  4. package/plugins/workflow-runner/src/agentCommon.js +13 -12
  5. package/plugins/workflow-runner/src/agentCommon.ts +22 -13
  6. package/plugins/workflow-runner/src/expressions.js +29 -8
  7. package/plugins/workflow-runner/src/expressions.ts +28 -10
  8. package/plugins/workflow-runner/src/index.js +50 -20
  9. package/plugins/workflow-runner/src/index.ts +75 -31
  10. package/plugins/workflow-runner/src/multimodal.js +2 -1
  11. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  12. package/plugins/workflow-runner/src/recovery.js +9 -2
  13. package/plugins/workflow-runner/src/recovery.ts +9 -2
  14. package/plugins/workflow-runner/src/runner.js +31 -5
  15. package/plugins/workflow-runner/src/runner.ts +33 -5
  16. package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
  17. package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
  18. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  19. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  20. package/plugins/workflow-runner/src/stepExecutors/tool.js +91 -61
  21. package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
  22. package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
  23. package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
  24. package/plugins/workflow-runner/src/taskIdentity.js +197 -21
  25. package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
  26. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  27. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  28. package/plugins/workflow-runner/src/types.js +113 -7
  29. package/plugins/workflow-runner/src/types.ts +137 -14
  30. package/plugins/workflow-runner/src/workflowState.js +455 -54
  31. package/plugins/workflow-runner/src/workflowState.ts +508 -63
  32. package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
  33. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  34. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
  35. package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
  36. package/plugins/workflow-runner/test/expressions.test.ts +42 -0
  37. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  38. package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
  39. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  40. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
  41. package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
  42. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
  43. package/plugins/workflow-runner/test/research.load.test.ts +328 -0
  44. package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
  45. package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
  46. package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
  47. package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
  48. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  49. package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
  50. package/plugins/workflow-runner/test/word.load.test.ts +9 -9
  51. package/skills/skill-creator/scripts/run_eval.py +3 -2
  52. package/src/cli/print.js +36 -4
  53. package/src/cli/print.ts +42 -4
  54. package/src/cli/streamJson.js +19 -4
  55. package/src/cli/streamJson.ts +17 -4
  56. package/src/config/configDefaults.js +25 -0
  57. package/src/config/configDefaults.ts +33 -0
  58. package/src/config/configFile.js +2 -21
  59. package/src/config/configFile.ts +11 -24
  60. package/src/config/configManagement.js +1 -1
  61. package/src/config/configManagement.ts +2 -2
  62. package/src/config/configService.js +24 -0
  63. package/src/config/configService.ts +23 -0
  64. package/src/context/compact.js +4 -0
  65. package/src/context/compact.ts +4 -0
  66. package/src/context/contextBundle.js +38 -13
  67. package/src/context/contextBundle.ts +47 -13
  68. package/src/harness/stallGuard.js +19 -4
  69. package/src/harness/stallGuard.ts +30 -7
  70. package/src/infra/verification.js +25 -18
  71. package/src/infra/verification.ts +50 -18
  72. package/src/loop.js +95 -11
  73. package/src/loop.ts +125 -16
  74. package/src/main.js +8 -1
  75. package/src/main.tsx +7 -1
  76. package/src/memory/constants.js +10 -0
  77. package/src/memory/constants.ts +13 -0
  78. package/src/memory/episodicMemory.js +274 -2
  79. package/src/memory/episodicMemory.ts +306 -1
  80. package/src/memory/fileStore.js +24 -4
  81. package/src/memory/fileStore.ts +22 -2
  82. package/src/memory/memoryService.js +79 -12
  83. package/src/memory/memoryService.ts +108 -9
  84. package/src/memory/repository.js +415 -85
  85. package/src/memory/repository.ts +437 -86
  86. package/src/memory/turnJournal.js +95 -0
  87. package/src/memory/turnJournal.ts +120 -0
  88. package/src/memory/types.ts +16 -0
  89. package/src/plugins/pluginApi.ts +30 -6
  90. package/src/plugins/pluginRunner.js +23 -8
  91. package/src/plugins/pluginRunner.ts +32 -13
  92. package/src/prompts/policyRegistry.js +55 -1
  93. package/src/prompts/policyRegistry.ts +55 -1
  94. package/src/prompts/promptAssembly.js +39 -7
  95. package/src/prompts/promptAssembly.ts +145 -59
  96. package/src/session/runAgentTurn.js +43 -21
  97. package/src/session/runAgentTurn.ts +68 -26
  98. package/src/session/runTurn.js +11 -46
  99. package/src/session/runTurn.ts +12 -46
  100. package/src/session/runtimeStore.js +149 -0
  101. package/src/session/runtimeStore.ts +197 -0
  102. package/src/session/sessionState.js +5 -0
  103. package/src/session/sessionState.ts +14 -0
  104. package/src/tools/advisor/advisor.js +76 -9
  105. package/src/tools/advisor/advisor.ts +79 -9
  106. package/src/tools/agent/agent.js +143 -14
  107. package/src/tools/agent/agent.ts +149 -14
  108. package/src/tools/agent/agentTaskService.js +60 -0
  109. package/src/tools/agent/agentTaskService.ts +113 -0
  110. package/src/tools/agent/agentTypes.js +5 -0
  111. package/src/tools/agent/agentTypes.ts +5 -0
  112. package/src/tools/bash/bash.js +80 -15
  113. package/src/tools/bash/bash.ts +83 -15
  114. package/src/tools/capabilities.js +5 -0
  115. package/src/tools/capabilities.ts +11 -1
  116. package/src/tools/edit/edit.js +7 -4
  117. package/src/tools/edit/edit.ts +7 -4
  118. package/src/tools/edit/multi-edit.js +4 -2
  119. package/src/tools/edit/multi-edit.ts +4 -2
  120. package/src/tools/glob/glob.js +156 -40
  121. package/src/tools/glob/glob.ts +185 -46
  122. package/src/tools/index.js +35 -17
  123. package/src/tools/index.ts +81 -20
  124. package/src/tools/lsp/lsp.js +165 -22
  125. package/src/tools/lsp/lsp.ts +206 -36
  126. package/src/tools/powershell/powershell.js +134 -27
  127. package/src/tools/powershell/powershell.ts +171 -30
  128. package/src/tools/presentresult/presentresult.js +10 -0
  129. package/src/tools/presentresult/presentresult.ts +10 -0
  130. package/src/tools/read/read.js +249 -29
  131. package/src/tools/read/read.ts +396 -59
  132. package/src/tools/shared/diffSummary.js +44 -0
  133. package/src/tools/shared/diffSummary.ts +63 -0
  134. package/src/tools/shared/fileActivity.js +2 -1
  135. package/src/tools/shared/fileActivity.ts +3 -0
  136. package/src/tools/shared/permanentDeleteGuard.js +57 -0
  137. package/src/tools/shared/permanentDeleteGuard.ts +71 -0
  138. package/src/tools/shared/toolOutputStore.js +97 -0
  139. package/src/tools/shared/toolOutputStore.ts +110 -0
  140. package/src/tools/skill/skill.js +86 -0
  141. package/src/tools/skill/skill.ts +94 -0
  142. package/src/tools/snip/snip.js +67 -0
  143. package/src/tools/snip/snip.ts +80 -0
  144. package/src/tools/tasks/backgroundTaskService.js +79 -28
  145. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  146. package/src/tools/tasks/taskManagement.js +163 -0
  147. package/src/tools/tasks/taskManagement.ts +180 -0
  148. package/src/tools/tasks/taskOutput.js +64 -14
  149. package/src/tools/tasks/taskOutput.ts +102 -23
  150. package/src/tools/tasks/taskStop.js +48 -8
  151. package/src/tools/tasks/taskStop.ts +69 -9
  152. package/src/tools/tasks/taskStore.js +46 -0
  153. package/src/tools/tasks/taskStore.ts +77 -0
  154. package/src/tools/terminal/terminalCapture.js +167 -0
  155. package/src/tools/terminal/terminalCapture.ts +179 -0
  156. package/src/tools/trash/trash.js +233 -0
  157. package/src/tools/trash/trash.ts +283 -0
  158. package/src/tools/webbrowser/browserControl.js +5 -1
  159. package/src/tools/webbrowser/browserControl.ts +7 -1
  160. package/src/tools/webbrowser/webbrowser.js +8 -4
  161. package/src/tools/webbrowser/webbrowser.ts +8 -4
  162. package/src/tools/worktree/sessionWorktree.js +128 -0
  163. package/src/tools/worktree/sessionWorktree.ts +158 -0
  164. package/src/tools/write/write.js +6 -2
  165. package/src/tools/write/write.ts +6 -2
  166. package/src/types.ts +22 -4
  167. package/src/ui/hooks/useChat.js +31 -11
  168. package/src/ui/hooks/useChat.ts +31 -13
  169. package/src/web/protocol.ts +78 -3
  170. package/workflows/adaptive-research.yaml +5 -5
  171. package/workflows/book-review-short.yaml +5 -5
  172. package/workflows/e2e-write-greet.yaml +2 -2
  173. package/workflows/illustrate.yaml +12 -12
  174. package/workflows/plan.yaml +18 -18
  175. package/workflows/ppt.yaml +201 -201
  176. package/workflows/ralph-loop.yaml +3 -1
  177. package/workflows/research.md +449 -0
  178. package/workflows/research.yaml +873 -0
  179. package/workflows/webppt.yaml +47 -47
  180. package/workflows/word.yaml +80 -80
  181. package/workflows/youtube-shorts.yaml +25 -25
@@ -1,29 +1,71 @@
1
1
  import { stat } from 'node:fs/promises';
2
2
  import { isAbsolute, relative, resolve, sep } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import { z } from 'zod';
5
6
  import { getWorkingDir } from '../../bootstrap/workingDir.js';
6
7
  import { toToolParameters } from '../../utils/zodToJson.js';
7
8
  import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
8
9
  import { discoverLspServers, lspAvailabilitySignature } from './config.js';
9
10
  import { getLspManager } from './service.js';
10
- const positionOperations = new Set(['goToDefinition', 'findReferences', 'hover', 'goToImplementation', 'incomingCalls', 'outgoingCalls']);
11
- const inputSchema = z.object({
11
+ const positionOperations = new Set([
12
+ 'goToDefinition',
13
+ 'findReferences',
14
+ 'hover',
15
+ 'goToImplementation',
16
+ 'incomingCalls',
17
+ 'outgoingCalls',
18
+ ]);
19
+ const inputSchema = z
20
+ .object({
12
21
  operation: z.enum([
13
- 'goToDefinition', 'findReferences', 'hover', 'documentSymbols', 'workspaceSymbols',
14
- 'goToImplementation', 'incomingCalls', 'outgoingCalls', 'diagnostics',
22
+ 'goToDefinition',
23
+ 'findReferences',
24
+ 'hover',
25
+ 'documentSymbols',
26
+ 'workspaceSymbols',
27
+ 'goToImplementation',
28
+ 'incomingCalls',
29
+ 'outgoingCalls',
30
+ 'diagnostics',
15
31
  ]),
16
32
  file_path: z.string().min(1),
17
- line: z.number().int().positive().optional().describe('1-based source line for position operations'),
18
- character: z.number().int().positive().optional().describe('1-based UTF-16 character for position operations'),
33
+ line: z
34
+ .number()
35
+ .int()
36
+ .positive()
37
+ .optional()
38
+ .describe('1-based source line for position operations'),
39
+ character: z
40
+ .number()
41
+ .int()
42
+ .positive()
43
+ .optional()
44
+ .describe('1-based UTF-16 character for position operations'),
19
45
  query: z.string().optional().describe('Symbol query for workspaceSymbols'),
20
- limit: z.number().int().min(1).max(500).optional().describe('Maximum formatted results, default 200'),
21
- }).superRefine((input, context) => {
22
- if (positionOperations.has(input.operation) && (!input.line || !input.character)) {
23
- context.addIssue({ code: z.ZodIssueCode.custom, message: `${input.operation} requires line and character` });
46
+ limit: z
47
+ .number()
48
+ .int()
49
+ .min(1)
50
+ .max(500)
51
+ .optional()
52
+ .describe('Maximum formatted results, default 200'),
53
+ output_format: z.enum(['compact', 'json']).default('compact').optional(),
54
+ })
55
+ .superRefine((input, context) => {
56
+ if (positionOperations.has(input.operation) &&
57
+ (!input.line || !input.character)) {
58
+ context.addIssue({
59
+ code: z.ZodIssueCode.custom,
60
+ message: `${input.operation} requires line and character`,
61
+ });
24
62
  }
25
63
  if (input.operation === 'workspaceSymbols' && !input.query?.trim()) {
26
- context.addIssue({ code: z.ZodIssueCode.custom, path: ['query'], message: 'workspaceSymbols requires a non-empty query' });
64
+ context.addIssue({
65
+ code: z.ZodIssueCode.custom,
66
+ path: ['query'],
67
+ message: 'workspaceSymbols requires a non-empty query',
68
+ });
27
69
  }
28
70
  });
29
71
  function compact(value, limit) {
@@ -35,6 +77,74 @@ function compact(value, limit) {
35
77
  ? `${text.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS)}\n... (LSP result truncated)`
36
78
  : text;
37
79
  }
80
+ function compactSemantic(value, cwd, limit) {
81
+ if (value == null)
82
+ return 'No results.';
83
+ const rows = [];
84
+ const walk = (item, label) => {
85
+ if (item == null)
86
+ return;
87
+ if (Array.isArray(item)) {
88
+ item.forEach((v) => walk(v, label));
89
+ return;
90
+ }
91
+ if (typeof item !== 'object') {
92
+ rows.push(String(item));
93
+ return;
94
+ }
95
+ const location = item.location ?? item.fromRanges?.[0] ?? item;
96
+ const uri = location.uri ?? location.targetUri ?? item.uri;
97
+ const range = location.range ??
98
+ location.targetSelectionRange ??
99
+ item.range ??
100
+ item.selectionRange;
101
+ let where = '';
102
+ if (typeof uri === 'string') {
103
+ let path = uri;
104
+ try {
105
+ path = uri.startsWith('file:') ? fileURLToPath(uri) : uri;
106
+ }
107
+ catch { }
108
+ const rel = relative(resolve(cwd), path);
109
+ where = (rel && !rel.startsWith('..') ? rel : path).split(sep).join('/');
110
+ }
111
+ if (range?.start)
112
+ where += `:${Number(range.start.line) + 1}:${Number(range.start.character) + 1}`;
113
+ const message = item.message ?? item.name ?? item.detail ?? item.kind ?? label;
114
+ const severity = item.severity
115
+ ? `[${['', 'error', 'warning', 'info', 'hint'][item.severity] ?? item.severity}] `
116
+ : '';
117
+ const code = item.code !== undefined ? `(${String(item.code)}) ` : '';
118
+ if (where || message)
119
+ rows.push(`${severity}${where || '<symbol>'} — ${code}${typeof message === 'string' ? message : JSON.stringify(message)}`);
120
+ if (item.contents) {
121
+ const contents = Array.isArray(item.contents)
122
+ ? item.contents
123
+ : [item.contents];
124
+ for (const c of contents)
125
+ rows.push(typeof c === 'string'
126
+ ? c
127
+ : typeof c?.value === 'string'
128
+ ? c.value
129
+ : JSON.stringify(c));
130
+ }
131
+ if (item.from)
132
+ walk(item.from, 'caller');
133
+ if (item.to)
134
+ walk(item.to, 'callee');
135
+ if (Array.isArray(item.children))
136
+ item.children.forEach((v) => walk(v));
137
+ };
138
+ walk(value);
139
+ const unique = [...new Set(rows)].sort();
140
+ const selected = unique.slice(0, limit);
141
+ let text = selected.join('\n') || 'No results.';
142
+ if (unique.length > selected.length)
143
+ text += `\n... (${unique.length - selected.length} more; total=${unique.length})`;
144
+ return text.length > DEFAULT_MAX_RESULT_SIZE_CHARS
145
+ ? `${text.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS)}\n... (LSP result truncated)`
146
+ : text;
147
+ }
38
148
  async function call(input, cwd = getWorkingDir()) {
39
149
  const filePath = resolve(cwd, input.file_path);
40
150
  const rel = relative(resolve(cwd), filePath);
@@ -52,32 +162,54 @@ async function call(input, cwd = getWorkingDir()) {
52
162
  try {
53
163
  await server.openFile(filePath);
54
164
  const uri = pathToFileURL(filePath).href;
55
- const position = { line: (input.line ?? 1) - 1, character: (input.character ?? 1) - 1 };
165
+ const position = {
166
+ line: (input.line ?? 1) - 1,
167
+ character: (input.character ?? 1) - 1,
168
+ };
56
169
  let result;
57
170
  switch (input.operation) {
58
171
  case 'goToDefinition':
59
- result = await server.request('textDocument/definition', { textDocument: { uri }, position });
172
+ result = await server.request('textDocument/definition', {
173
+ textDocument: { uri },
174
+ position,
175
+ });
60
176
  break;
61
177
  case 'findReferences':
62
- result = await server.request('textDocument/references', { textDocument: { uri }, position, context: { includeDeclaration: true } });
178
+ result = await server.request('textDocument/references', {
179
+ textDocument: { uri },
180
+ position,
181
+ context: { includeDeclaration: true },
182
+ });
63
183
  break;
64
184
  case 'hover':
65
- result = await server.request('textDocument/hover', { textDocument: { uri }, position });
185
+ result = await server.request('textDocument/hover', {
186
+ textDocument: { uri },
187
+ position,
188
+ });
66
189
  break;
67
190
  case 'documentSymbols':
68
- result = await server.request('textDocument/documentSymbol', { textDocument: { uri } });
191
+ result = await server.request('textDocument/documentSymbol', {
192
+ textDocument: { uri },
193
+ });
69
194
  break;
70
195
  case 'workspaceSymbols':
71
- result = await server.request('workspace/symbol', { query: input.query });
196
+ result = await server.request('workspace/symbol', {
197
+ query: input.query,
198
+ });
72
199
  break;
73
200
  case 'goToImplementation':
74
- result = await server.request('textDocument/implementation', { textDocument: { uri }, position });
201
+ result = await server.request('textDocument/implementation', {
202
+ textDocument: { uri },
203
+ position,
204
+ });
75
205
  break;
76
206
  case 'incomingCalls':
77
207
  case 'outgoingCalls': {
78
208
  const prepared = await server.request('textDocument/prepareCallHierarchy', { textDocument: { uri }, position });
79
209
  const item = prepared?.[0];
80
- result = item ? await server.request(`callHierarchy/${input.operation}`, { item }) : [];
210
+ result = item
211
+ ? await server.request(`callHierarchy/${input.operation}`, { item })
212
+ : [];
81
213
  break;
82
214
  }
83
215
  case 'diagnostics': {
@@ -87,10 +219,18 @@ async function call(input, cwd = getWorkingDir()) {
87
219
  break;
88
220
  }
89
221
  }
90
- return { ok: true, content: compact(result, input.limit ?? 200) };
222
+ return {
223
+ ok: true,
224
+ content: input.output_format === 'json'
225
+ ? compact(result, input.limit ?? 200)
226
+ : compactSemantic(result, cwd, input.limit ?? 200),
227
+ };
91
228
  }
92
229
  catch (error) {
93
- return { ok: false, error: `LSP ${input.operation} failed: ${error.message}` };
230
+ return {
231
+ ok: false,
232
+ error: `LSP ${input.operation} failed: ${error.message}`,
233
+ };
94
234
  }
95
235
  }
96
236
  export const lspTool = {
@@ -106,7 +246,10 @@ export const lspTool = {
106
246
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
107
247
  resolveAvailability(runtime) {
108
248
  const servers = discoverLspServers(runtime.cwd);
109
- return { available: servers.length > 0, signature: lspAvailabilitySignature(runtime.cwd, servers) };
249
+ return {
250
+ available: servers.length > 0,
251
+ signature: lspAvailabilitySignature(runtime.cwd, servers),
252
+ };
110
253
  },
111
254
  call: (input) => call(input),
112
255
  callWithContext: (input, _signal, context) => call(input, context.cwd),
@@ -1,6 +1,7 @@
1
1
  import { stat } from 'node:fs/promises';
2
2
  import { isAbsolute, relative, resolve, sep } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import { z } from 'zod';
5
6
 
6
7
  import { getWorkingDir } from '../../bootstrap/workingDir.js';
@@ -11,25 +12,68 @@ import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
11
12
  import { discoverLspServers, lspAvailabilitySignature } from './config.js';
12
13
  import { getLspManager } from './service.js';
13
14
 
14
- const positionOperations = new Set(['goToDefinition', 'findReferences', 'hover', 'goToImplementation', 'incomingCalls', 'outgoingCalls']);
15
- const inputSchema = z.object({
16
- operation: z.enum([
17
- 'goToDefinition', 'findReferences', 'hover', 'documentSymbols', 'workspaceSymbols',
18
- 'goToImplementation', 'incomingCalls', 'outgoingCalls', 'diagnostics',
19
- ]),
20
- file_path: z.string().min(1),
21
- line: z.number().int().positive().optional().describe('1-based source line for position operations'),
22
- character: z.number().int().positive().optional().describe('1-based UTF-16 character for position operations'),
23
- query: z.string().optional().describe('Symbol query for workspaceSymbols'),
24
- limit: z.number().int().min(1).max(500).optional().describe('Maximum formatted results, default 200'),
25
- }).superRefine((input, context) => {
26
- if (positionOperations.has(input.operation) && (!input.line || !input.character)) {
27
- context.addIssue({ code: z.ZodIssueCode.custom, message: `${input.operation} requires line and character` });
28
- }
29
- if (input.operation === 'workspaceSymbols' && !input.query?.trim()) {
30
- context.addIssue({ code: z.ZodIssueCode.custom, path: ['query'], message: 'workspaceSymbols requires a non-empty query' });
31
- }
32
- });
15
+ const positionOperations = new Set([
16
+ 'goToDefinition',
17
+ 'findReferences',
18
+ 'hover',
19
+ 'goToImplementation',
20
+ 'incomingCalls',
21
+ 'outgoingCalls',
22
+ ]);
23
+ const inputSchema = z
24
+ .object({
25
+ operation: z.enum([
26
+ 'goToDefinition',
27
+ 'findReferences',
28
+ 'hover',
29
+ 'documentSymbols',
30
+ 'workspaceSymbols',
31
+ 'goToImplementation',
32
+ 'incomingCalls',
33
+ 'outgoingCalls',
34
+ 'diagnostics',
35
+ ]),
36
+ file_path: z.string().min(1),
37
+ line: z
38
+ .number()
39
+ .int()
40
+ .positive()
41
+ .optional()
42
+ .describe('1-based source line for position operations'),
43
+ character: z
44
+ .number()
45
+ .int()
46
+ .positive()
47
+ .optional()
48
+ .describe('1-based UTF-16 character for position operations'),
49
+ query: z.string().optional().describe('Symbol query for workspaceSymbols'),
50
+ limit: z
51
+ .number()
52
+ .int()
53
+ .min(1)
54
+ .max(500)
55
+ .optional()
56
+ .describe('Maximum formatted results, default 200'),
57
+ output_format: z.enum(['compact', 'json']).default('compact').optional(),
58
+ })
59
+ .superRefine((input, context) => {
60
+ if (
61
+ positionOperations.has(input.operation) &&
62
+ (!input.line || !input.character)
63
+ ) {
64
+ context.addIssue({
65
+ code: z.ZodIssueCode.custom,
66
+ message: `${input.operation} requires line and character`,
67
+ });
68
+ }
69
+ if (input.operation === 'workspaceSymbols' && !input.query?.trim()) {
70
+ context.addIssue({
71
+ code: z.ZodIssueCode.custom,
72
+ path: ['query'],
73
+ message: 'workspaceSymbols requires a non-empty query',
74
+ });
75
+ }
76
+ });
33
77
  type LspInput = z.infer<typeof inputSchema>;
34
78
 
35
79
  function compact(value: unknown, limit: number): string {
@@ -41,34 +85,147 @@ function compact(value: unknown, limit: number): string {
41
85
  : text;
42
86
  }
43
87
 
44
- async function call(input: LspInput, cwd = getWorkingDir()): Promise<ToolResult> {
88
+ function compactSemantic(value: unknown, cwd: string, limit: number): string {
89
+ if (value == null) return 'No results.';
90
+ const rows: string[] = [];
91
+ const walk = (item: any, label?: string) => {
92
+ if (item == null) return;
93
+ if (Array.isArray(item)) {
94
+ item.forEach((v) => walk(v, label));
95
+ return;
96
+ }
97
+ if (typeof item !== 'object') {
98
+ rows.push(String(item));
99
+ return;
100
+ }
101
+ const location = item.location ?? item.fromRanges?.[0] ?? item;
102
+ const uri = location.uri ?? location.targetUri ?? item.uri;
103
+ const range =
104
+ location.range ??
105
+ location.targetSelectionRange ??
106
+ item.range ??
107
+ item.selectionRange;
108
+ let where = '';
109
+ if (typeof uri === 'string') {
110
+ let path = uri;
111
+ try {
112
+ path = uri.startsWith('file:') ? fileURLToPath(uri) : uri;
113
+ } catch {}
114
+ const rel = relative(resolve(cwd), path);
115
+ where = (rel && !rel.startsWith('..') ? rel : path).split(sep).join('/');
116
+ }
117
+ if (range?.start)
118
+ where += `:${Number(range.start.line) + 1}:${Number(range.start.character) + 1}`;
119
+ const message =
120
+ item.message ?? item.name ?? item.detail ?? item.kind ?? label;
121
+ const severity = item.severity
122
+ ? `[${['', 'error', 'warning', 'info', 'hint'][item.severity] ?? item.severity}] `
123
+ : '';
124
+ const code = item.code !== undefined ? `(${String(item.code)}) ` : '';
125
+ if (where || message)
126
+ rows.push(
127
+ `${severity}${where || '<symbol>'} — ${code}${typeof message === 'string' ? message : JSON.stringify(message)}`,
128
+ );
129
+ if (item.contents) {
130
+ const contents = Array.isArray(item.contents)
131
+ ? item.contents
132
+ : [item.contents];
133
+ for (const c of contents)
134
+ rows.push(
135
+ typeof c === 'string'
136
+ ? c
137
+ : typeof c?.value === 'string'
138
+ ? c.value
139
+ : JSON.stringify(c),
140
+ );
141
+ }
142
+ if (item.from) walk(item.from, 'caller');
143
+ if (item.to) walk(item.to, 'callee');
144
+ if (Array.isArray(item.children))
145
+ item.children.forEach((v: any) => walk(v));
146
+ };
147
+ walk(value);
148
+ const unique = [...new Set(rows)].sort();
149
+ const selected = unique.slice(0, limit);
150
+ let text = selected.join('\n') || 'No results.';
151
+ if (unique.length > selected.length)
152
+ text += `\n... (${unique.length - selected.length} more; total=${unique.length})`;
153
+ return text.length > DEFAULT_MAX_RESULT_SIZE_CHARS
154
+ ? `${text.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS)}\n... (LSP result truncated)`
155
+ : text;
156
+ }
157
+
158
+ async function call(
159
+ input: LspInput,
160
+ cwd = getWorkingDir(),
161
+ ): Promise<ToolResult> {
45
162
  const filePath = resolve(cwd, input.file_path);
46
163
  const rel = relative(resolve(cwd), filePath);
47
164
  if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
48
165
  return { ok: false, error: `LSP 只处理当前工作区内的文件:${filePath}` };
49
166
  }
50
167
  const info = await stat(filePath).catch(() => null);
51
- if (!info?.isFile()) return { ok: false, error: `LSP file does not exist: ${filePath}` };
52
- if (info.size > 10_000_000) return { ok: false, error: 'LSP refuses files larger than 10 MB' };
168
+ if (!info?.isFile())
169
+ return { ok: false, error: `LSP file does not exist: ${filePath}` };
170
+ if (info.size > 10_000_000)
171
+ return { ok: false, error: 'LSP refuses files larger than 10 MB' };
53
172
  const server = getLspManager(cwd).serverFor(filePath);
54
- if (!server) return { ok: false, error: `No configured LSP server handles ${filePath}` };
173
+ if (!server)
174
+ return { ok: false, error: `No configured LSP server handles ${filePath}` };
55
175
  try {
56
176
  await server.openFile(filePath);
57
177
  const uri = pathToFileURL(filePath).href;
58
- const position = { line: (input.line ?? 1) - 1, character: (input.character ?? 1) - 1 };
178
+ const position = {
179
+ line: (input.line ?? 1) - 1,
180
+ character: (input.character ?? 1) - 1,
181
+ };
59
182
  let result: unknown;
60
183
  switch (input.operation) {
61
- case 'goToDefinition': result = await server.request('textDocument/definition', { textDocument: { uri }, position }); break;
62
- case 'findReferences': result = await server.request('textDocument/references', { textDocument: { uri }, position, context: { includeDeclaration: true } }); break;
63
- case 'hover': result = await server.request('textDocument/hover', { textDocument: { uri }, position }); break;
64
- case 'documentSymbols': result = await server.request('textDocument/documentSymbol', { textDocument: { uri } }); break;
65
- case 'workspaceSymbols': result = await server.request('workspace/symbol', { query: input.query }); break;
66
- case 'goToImplementation': result = await server.request('textDocument/implementation', { textDocument: { uri }, position }); break;
184
+ case 'goToDefinition':
185
+ result = await server.request('textDocument/definition', {
186
+ textDocument: { uri },
187
+ position,
188
+ });
189
+ break;
190
+ case 'findReferences':
191
+ result = await server.request('textDocument/references', {
192
+ textDocument: { uri },
193
+ position,
194
+ context: { includeDeclaration: true },
195
+ });
196
+ break;
197
+ case 'hover':
198
+ result = await server.request('textDocument/hover', {
199
+ textDocument: { uri },
200
+ position,
201
+ });
202
+ break;
203
+ case 'documentSymbols':
204
+ result = await server.request('textDocument/documentSymbol', {
205
+ textDocument: { uri },
206
+ });
207
+ break;
208
+ case 'workspaceSymbols':
209
+ result = await server.request('workspace/symbol', {
210
+ query: input.query,
211
+ });
212
+ break;
213
+ case 'goToImplementation':
214
+ result = await server.request('textDocument/implementation', {
215
+ textDocument: { uri },
216
+ position,
217
+ });
218
+ break;
67
219
  case 'incomingCalls':
68
220
  case 'outgoingCalls': {
69
- const prepared = await server.request<unknown[]>('textDocument/prepareCallHierarchy', { textDocument: { uri }, position });
221
+ const prepared = await server.request<unknown[]>(
222
+ 'textDocument/prepareCallHierarchy',
223
+ { textDocument: { uri }, position },
224
+ );
70
225
  const item = prepared?.[0];
71
- result = item ? await server.request(`callHierarchy/${input.operation}`, { item }) : [];
226
+ result = item
227
+ ? await server.request(`callHierarchy/${input.operation}`, { item })
228
+ : [];
72
229
  break;
73
230
  }
74
231
  case 'diagnostics': {
@@ -78,9 +235,18 @@ async function call(input: LspInput, cwd = getWorkingDir()): Promise<ToolResult>
78
235
  break;
79
236
  }
80
237
  }
81
- return { ok: true, content: compact(result, input.limit ?? 200) };
238
+ return {
239
+ ok: true,
240
+ content:
241
+ input.output_format === 'json'
242
+ ? compact(result, input.limit ?? 200)
243
+ : compactSemantic(result, cwd, input.limit ?? 200),
244
+ };
82
245
  } catch (error) {
83
- return { ok: false, error: `LSP ${input.operation} failed: ${(error as Error).message}` };
246
+ return {
247
+ ok: false,
248
+ error: `LSP ${input.operation} failed: ${(error as Error).message}`,
249
+ };
84
250
  }
85
251
  }
86
252
 
@@ -97,8 +263,12 @@ export const lspTool: Tool<LspInput> = {
97
263
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
98
264
  resolveAvailability(runtime: AgentRuntimeContext) {
99
265
  const servers = discoverLspServers(runtime.cwd);
100
- return { available: servers.length > 0, signature: lspAvailabilitySignature(runtime.cwd, servers) };
266
+ return {
267
+ available: servers.length > 0,
268
+ signature: lspAvailabilitySignature(runtime.cwd, servers),
269
+ };
101
270
  },
102
271
  call: (input) => call(input),
103
- callWithContext: (input, _signal, context: ToolCallContext) => call(input, context.cwd),
272
+ callWithContext: (input, _signal, context: ToolCallContext) =>
273
+ call(input, context.cwd),
104
274
  };