minimal-agent 0.11.0 → 0.12.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 (128) hide show
  1. package/package.json +4 -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/index.js +5 -3
  7. package/plugins/workflow-runner/src/index.ts +31 -13
  8. package/plugins/workflow-runner/src/multimodal.js +2 -1
  9. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  10. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  11. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  12. package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
  13. package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
  14. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  15. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  16. package/plugins/workflow-runner/src/types.ts +26 -7
  17. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  18. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  19. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  20. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  21. package/src/cli/print.js +19 -4
  22. package/src/cli/print.ts +17 -4
  23. package/src/cli/streamJson.js +19 -4
  24. package/src/cli/streamJson.ts +17 -4
  25. package/src/config/configDefaults.js +25 -0
  26. package/src/config/configDefaults.ts +33 -0
  27. package/src/config/configFile.js +2 -21
  28. package/src/config/configFile.ts +11 -24
  29. package/src/config/configManagement.js +1 -1
  30. package/src/config/configManagement.ts +2 -2
  31. package/src/context/compact.js +4 -0
  32. package/src/context/compact.ts +4 -0
  33. package/src/context/contextBundle.js +38 -13
  34. package/src/context/contextBundle.ts +47 -13
  35. package/src/harness/stallGuard.js +19 -4
  36. package/src/harness/stallGuard.ts +30 -7
  37. package/src/loop.js +95 -11
  38. package/src/loop.ts +125 -16
  39. package/src/main.js +8 -1
  40. package/src/main.tsx +7 -1
  41. package/src/memory/constants.js +10 -0
  42. package/src/memory/constants.ts +13 -0
  43. package/src/memory/episodicMemory.js +274 -2
  44. package/src/memory/episodicMemory.ts +306 -1
  45. package/src/memory/fileStore.js +24 -4
  46. package/src/memory/fileStore.ts +22 -2
  47. package/src/memory/memoryService.js +79 -12
  48. package/src/memory/memoryService.ts +108 -9
  49. package/src/memory/repository.js +415 -85
  50. package/src/memory/repository.ts +437 -86
  51. package/src/memory/turnJournal.js +95 -0
  52. package/src/memory/turnJournal.ts +120 -0
  53. package/src/memory/types.ts +16 -0
  54. package/src/plugins/pluginApi.ts +30 -6
  55. package/src/plugins/pluginRunner.js +6 -2
  56. package/src/plugins/pluginRunner.ts +12 -7
  57. package/src/prompts/policyRegistry.js +27 -0
  58. package/src/prompts/policyRegistry.ts +27 -0
  59. package/src/prompts/promptAssembly.js +39 -7
  60. package/src/prompts/promptAssembly.ts +145 -59
  61. package/src/session/runAgentTurn.js +43 -21
  62. package/src/session/runAgentTurn.ts +68 -26
  63. package/src/session/runTurn.js +11 -46
  64. package/src/session/runTurn.ts +12 -46
  65. package/src/session/runtimeStore.js +149 -0
  66. package/src/session/runtimeStore.ts +197 -0
  67. package/src/session/sessionState.js +5 -0
  68. package/src/session/sessionState.ts +14 -0
  69. package/src/tools/agent/agent.js +143 -14
  70. package/src/tools/agent/agent.ts +149 -14
  71. package/src/tools/agent/agentTaskService.js +60 -0
  72. package/src/tools/agent/agentTaskService.ts +113 -0
  73. package/src/tools/agent/agentTypes.js +5 -0
  74. package/src/tools/agent/agentTypes.ts +5 -0
  75. package/src/tools/bash/bash.js +31 -11
  76. package/src/tools/bash/bash.ts +29 -10
  77. package/src/tools/capabilities.js +5 -0
  78. package/src/tools/capabilities.ts +11 -1
  79. package/src/tools/edit/edit.js +7 -4
  80. package/src/tools/edit/edit.ts +7 -4
  81. package/src/tools/edit/multi-edit.js +4 -2
  82. package/src/tools/edit/multi-edit.ts +4 -2
  83. package/src/tools/glob/glob.js +156 -40
  84. package/src/tools/glob/glob.ts +185 -46
  85. package/src/tools/index.js +35 -17
  86. package/src/tools/index.ts +81 -20
  87. package/src/tools/lsp/lsp.js +165 -22
  88. package/src/tools/lsp/lsp.ts +206 -36
  89. package/src/tools/powershell/powershell.js +116 -26
  90. package/src/tools/powershell/powershell.ts +155 -30
  91. package/src/tools/presentresult/presentresult.js +10 -0
  92. package/src/tools/presentresult/presentresult.ts +10 -0
  93. package/src/tools/read/read.js +244 -29
  94. package/src/tools/read/read.ts +391 -59
  95. package/src/tools/shared/diffSummary.js +44 -0
  96. package/src/tools/shared/diffSummary.ts +63 -0
  97. package/src/tools/shared/fileActivity.js +2 -1
  98. package/src/tools/shared/fileActivity.ts +3 -0
  99. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  100. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  101. package/src/tools/shared/toolOutputStore.js +97 -0
  102. package/src/tools/shared/toolOutputStore.ts +110 -0
  103. package/src/tools/skill/skill.js +86 -0
  104. package/src/tools/skill/skill.ts +94 -0
  105. package/src/tools/snip/snip.js +67 -0
  106. package/src/tools/snip/snip.ts +80 -0
  107. package/src/tools/tasks/backgroundTaskService.js +79 -28
  108. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  109. package/src/tools/tasks/taskManagement.js +163 -0
  110. package/src/tools/tasks/taskManagement.ts +180 -0
  111. package/src/tools/tasks/taskOutput.js +64 -14
  112. package/src/tools/tasks/taskOutput.ts +102 -23
  113. package/src/tools/tasks/taskStop.js +48 -8
  114. package/src/tools/tasks/taskStop.ts +69 -9
  115. package/src/tools/tasks/taskStore.js +46 -0
  116. package/src/tools/tasks/taskStore.ts +77 -0
  117. package/src/tools/terminal/terminalCapture.js +167 -0
  118. package/src/tools/terminal/terminalCapture.ts +179 -0
  119. package/src/tools/trash/trash.js +233 -0
  120. package/src/tools/trash/trash.ts +283 -0
  121. package/src/tools/worktree/sessionWorktree.js +128 -0
  122. package/src/tools/worktree/sessionWorktree.ts +158 -0
  123. package/src/tools/write/write.js +6 -2
  124. package/src/tools/write/write.ts +6 -2
  125. package/src/types.ts +12 -0
  126. package/src/ui/hooks/useChat.js +31 -11
  127. package/src/ui/hooks/useChat.ts +31 -13
  128. package/src/web/protocol.ts +78 -3
@@ -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
  };
@@ -11,6 +11,8 @@ import { killTree } from '../shared/killTree.js';
11
11
  import { startBackgroundTask } from '../tasks/backgroundTaskService.js';
12
12
  import { processExecutionMetadata } from '../tasks/processEvidence.js';
13
13
  import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
14
+ import { permanentDeleteReason } from '../shared/permanentDeleteGuard.js';
15
+ import { ToolOutputStore } from '../shared/toolOutputStore.js';
14
16
  const MAX_TIMEOUT_MS = 600_000;
15
17
  function findPowerShell() {
16
18
  if (process.platform !== 'win32')
@@ -35,23 +37,42 @@ const inputSchema = z.object({
35
37
  run_in_background: z.boolean().optional(),
36
38
  });
37
39
  function invocation(command) {
38
- return ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', `$OutputEncoding=[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); ${command}`];
40
+ return [
41
+ '-NoLogo',
42
+ '-NoProfile',
43
+ '-NonInteractive',
44
+ '-Command',
45
+ `$OutputEncoding=[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); ${command}`,
46
+ ];
39
47
  }
40
48
  async function approved(command, signal) {
49
+ const permanentDelete = permanentDeleteReason(command, 'powershell');
50
+ if (permanentDelete)
51
+ return {
52
+ ok: false,
53
+ error: `永久删除检查拒绝执行:${permanentDelete}\n命令:${command}`,
54
+ };
41
55
  const forbidden = checkForbidden(command);
42
56
  if (forbidden)
43
- return { ok: false, error: `安全检查拒绝执行:${forbidden}\n命令:${command}` };
57
+ return {
58
+ ok: false,
59
+ error: `安全检查拒绝执行:${forbidden}\n命令:${command}`,
60
+ };
44
61
  const irreversible = classifyCommand(command);
45
62
  if (!irreversible)
46
63
  return undefined;
47
- const [answer] = await requestUserChoice([{
48
- question: `这条命令不可逆:${irreversible.why}。\n\n命令:${command}\n\n确认执行吗?`, header: '不可逆操作',
64
+ const [answer] = await requestUserChoice([
65
+ {
66
+ question: `这条命令不可逆:${irreversible.why}。\n\n命令:${command}\n\n确认执行吗?`,
67
+ header: '不可逆操作',
49
68
  options: [
50
69
  { label: '取消,不执行 (推荐)', description: '放弃并改用可回滚做法' },
51
70
  { label: '我已确认后果,执行', description: '继续执行不可逆操作' },
52
71
  ],
53
- }], signal);
54
- return answer?.selected?.[0]?.includes('执行') && !answer.selected[0]?.includes('取消')
72
+ },
73
+ ], signal);
74
+ return answer?.selected?.[0]?.includes('执行') &&
75
+ !answer.selected[0]?.includes('取消')
55
76
  ? undefined
56
77
  : { ok: false, error: `已拒绝执行不可逆 PowerShell 命令:${command}` };
57
78
  }
@@ -66,39 +87,105 @@ async function call(input, signal, context) {
66
87
  const cwd = context?.cwd ?? getWorkingDir();
67
88
  if (input.run_in_background) {
68
89
  try {
69
- const task = await startBackgroundTask({ command: input.command, executable, args, cwd, env: sanitizedChildEnv(), sessionId: context?.sessionId, shellKind: 'powershell' }, context?.sessionState?.backgroundTasks);
90
+ const task = await startBackgroundTask({
91
+ command: input.command,
92
+ executable,
93
+ args,
94
+ cwd,
95
+ env: sanitizedChildEnv(),
96
+ sessionId: context?.sessionId,
97
+ workspaceId: context?.workspaceId,
98
+ shellKind: 'powershell',
99
+ }, context?.sessionState?.backgroundTasks);
70
100
  return {
71
101
  ok: true,
72
- content: `PowerShell 后台任务已启动。task_id=${task.id}\nlog=${task.logPath}`,
73
- metadata: processExecutionMetadata({ shell: 'powershell', mode: 'background', command: input.command, status: 'started', taskId: task.id }),
102
+ content: `PowerShell 后台任务已启动。task_id=${task.id.replace(/^proc_/, '')}\nprocess_id=${task.id}\nlog=${task.logPath}`,
103
+ metadata: processExecutionMetadata({
104
+ shell: 'powershell',
105
+ mode: 'background',
106
+ command: input.command,
107
+ status: 'started',
108
+ taskId: task.id,
109
+ }),
74
110
  };
75
111
  }
76
112
  catch (error) {
77
- return { ok: false, error: `PowerShell 后台任务启动失败:${error.message}` };
113
+ return {
114
+ ok: false,
115
+ error: `PowerShell 后台任务启动失败:${error.message}`,
116
+ };
78
117
  }
79
118
  }
80
119
  return await new Promise((resolvePromise) => {
81
- const child = spawn(executable, args, { cwd, env: sanitizedChildEnv(), windowsHide: true, detached: false });
82
- let stdout = '';
83
- let stderr = '';
120
+ const child = spawn(executable, args, {
121
+ cwd,
122
+ env: sanitizedChildEnv(),
123
+ windowsHide: true,
124
+ detached: false,
125
+ });
84
126
  let finished = false;
85
- const finish = (result) => { if (finished)
86
- return; finished = true; clearTimeout(timer); signal?.removeEventListener('abort', abort); resolvePromise(result); };
87
- const abort = () => { killTree(child); finish({ ok: false, error: `PowerShell 命令被中断\n${stdout}\n${stderr}`.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS), metadata: processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: 'stopped', exitCode: null }) }); };
127
+ const store = new ToolOutputStore({
128
+ workspaceId: context?.workspaceId,
129
+ sessionId: context?.sessionId,
130
+ });
131
+ const finish = async (status, code, prefix = '') => {
132
+ if (finished)
133
+ return;
134
+ finished = true;
135
+ clearTimeout(timer);
136
+ signal?.removeEventListener('abort', abort);
137
+ const output = await store.finish();
138
+ const reference = output.path
139
+ ? `\n[完整输出: ${output.path}; bytes=${output.bytes}; sha256=${output.sha256}; complete=${output.complete}]`
140
+ : '';
141
+ let text = `${prefix}${output.stdout ? `<stdout>\n${output.stdout.trimEnd()}\n</stdout>\n` : ''}${output.stderr ? `<stderr>\n${output.stderr.trimEnd()}\n</stderr>\n` : ''}[exit code: ${code ?? 'killed'}]`;
142
+ if (text.length + reference.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
143
+ const suffix = `\n... (输出预览已截断)${reference}`;
144
+ text =
145
+ text.slice(0, Math.max(0, DEFAULT_MAX_RESULT_SIZE_CHARS - suffix.length)) + suffix;
146
+ }
147
+ else
148
+ text += reference;
149
+ const metadata = processExecutionMetadata({
150
+ shell: 'powershell',
151
+ mode: 'foreground',
152
+ command: input.command,
153
+ status,
154
+ exitCode: code,
155
+ });
156
+ resolvePromise(status === 'completed'
157
+ ? { ok: true, content: text, metadata }
158
+ : { ok: false, error: text, metadata });
159
+ };
160
+ const abort = () => {
161
+ killTree(child);
162
+ void finish('stopped', null, 'PowerShell 命令被中断\n');
163
+ };
88
164
  const timeout = input.timeout ?? 120_000;
89
- const timer = setTimeout(() => { killTree(child); finish({ ok: false, error: `PowerShell 命令超时(${timeout}ms)\n${stdout}\n${stderr}`.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS), metadata: processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: 'failed', exitCode: null }) }); }, timeout);
165
+ const timer = setTimeout(() => {
166
+ killTree(child);
167
+ void finish('failed', null, `PowerShell 命令超时(${timeout}ms)\n`);
168
+ }, timeout);
90
169
  signal?.addEventListener('abort', abort, { once: true });
91
170
  child.stdout?.setEncoding('utf8');
92
171
  child.stderr?.setEncoding('utf8');
93
- child.stdout?.on('data', (chunk) => { if (stdout.length < DEFAULT_MAX_RESULT_SIZE_CHARS * 2)
94
- stdout += chunk; });
95
- child.stderr?.on('data', (chunk) => { if (stderr.length < DEFAULT_MAX_RESULT_SIZE_CHARS * 2)
96
- stderr += chunk; });
97
- child.once('error', (error) => finish({ ok: false, error: `PowerShell 启动失败:${error.message}`, metadata: processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: 'failed', exitCode: null }) }));
172
+ child.stdout?.on('data', (chunk) => {
173
+ if (!store.append('stdout', chunk)) {
174
+ child.stdout?.pause();
175
+ store.onDrain(() => child.stdout?.resume());
176
+ }
177
+ });
178
+ child.stderr?.on('data', (chunk) => {
179
+ if (!store.append('stderr', chunk)) {
180
+ child.stderr?.pause();
181
+ store.onDrain(() => child.stderr?.resume());
182
+ }
183
+ });
184
+ child.once('error', (error) => {
185
+ void finish('failed', null, `PowerShell 启动失败:${error.message}\n`);
186
+ });
98
187
  child.once('close', (code) => {
99
- const text = `${stdout ? `<stdout>\n${stdout.trimEnd()}\n</stdout>\n` : ''}${stderr ? `<stderr>\n${stderr.trimEnd()}\n</stderr>\n` : ''}[exit code: ${code}]`.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS);
100
- const metadata = processExecutionMetadata({ shell: 'powershell', mode: 'foreground', command: input.command, status: code === 0 ? 'completed' : 'failed', exitCode: code });
101
- finish(code === 0 ? { ok: true, content: text, metadata } : { ok: false, error: text, metadata });
188
+ void finish(code === 0 ? 'completed' : 'failed', code);
102
189
  });
103
190
  });
104
191
  }
@@ -113,7 +200,10 @@ export const powerShellTool = {
113
200
  interruptBehavior: () => 'block',
114
201
  resolveAvailability: () => {
115
202
  const executable = findPowerShell();
116
- return { available: Boolean(executable), signature: executable ?? 'unavailable' };
203
+ return {
204
+ available: Boolean(executable),
205
+ signature: executable ?? 'unavailable',
206
+ };
117
207
  },
118
208
  call: (input, signal) => call(input, signal),
119
209
  callWithContext: (input, signal, context) => call(input, signal, context),