@yeaft/webchat-agent 1.0.529 → 1.0.531
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.
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +5 -5
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/engine.js +1 -0
- package/yeaft/sub-agent/runner.js +4 -1
- package/yeaft/tools/activation.js +5 -0
- package/yeaft/tools/git-read.js +121 -24
|
Binary file
|
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -2356,6 +2356,7 @@ export class Engine {
|
|
|
2356
2356
|
const resolveCurrentActiveToolNames = () => this.#toolRegistry
|
|
2357
2357
|
? resolveActiveToolNames({
|
|
2358
2358
|
toolNames: registeredToolNames,
|
|
2359
|
+
gitReadAlwaysVisible: this.#config?._gitReadAlwaysVisible === true,
|
|
2359
2360
|
prompt,
|
|
2360
2361
|
messages,
|
|
2361
2362
|
collabToolPolicy: effectiveCollabToolPolicy,
|
|
@@ -143,7 +143,10 @@ export function startSubAgent(agent, deps = {}) {
|
|
|
143
143
|
subEngine = new Engine({
|
|
144
144
|
adapter: deps.adapter,
|
|
145
145
|
trace: deps.trace,
|
|
146
|
-
config: {
|
|
146
|
+
config: {
|
|
147
|
+
...deps.config, _readOnly: true,
|
|
148
|
+
_gitReadAlwaysVisible: (agent.personaData || getPersona(agent.persona))?.id === 'reviewer',
|
|
149
|
+
},
|
|
147
150
|
conversationStore: null,
|
|
148
151
|
memoryIndex: deps.memoryIndex || null,
|
|
149
152
|
memoryStore: deps.memoryStore || null,
|
|
@@ -38,6 +38,7 @@ export const SUB_AGENT_MANAGEMENT_TOOL_NAMES = Object.freeze([
|
|
|
38
38
|
]);
|
|
39
39
|
|
|
40
40
|
export const CONDITIONAL_BUILTIN_TOOL_NAMES = new Set([
|
|
41
|
+
'GitRead',
|
|
41
42
|
'HistorySearch',
|
|
42
43
|
'DiskUsage',
|
|
43
44
|
'ApplyPatch',
|
|
@@ -57,6 +58,7 @@ export const CONDITIONAL_BUILTIN_TOOL_NAMES = new Set([
|
|
|
57
58
|
'ImageGeneration',
|
|
58
59
|
]);
|
|
59
60
|
|
|
61
|
+
const GIT_INTENT_RE = /(?:\bgit(?:read)?\b|\bdiff\b|\breview\b|\bcommit(?:s)?\b|\bbranch(?:es)?\b|\bworktree\b|\bpull request\b|\bPR\b|代码审查|审查|评审|提交|分支|工作树|工作区(?:状态|改动)|合并|变更差异)/iu;
|
|
60
62
|
const HISTORY_INTENT_RE = /(?:\bhistory\b|\b(?:prior|previous) (?:chat|conversation|discussion)\b|\bprevious(?:ly)? discussed\b|\bwhat did we (?:decide|discuss|say|agree)\b|\b(?:our|the) (?:earlier|last) decision\b|历史|之前(?:的)?(?:对话|讨论|会话|决定)|过去(?:的)?会话|我们(?:之前|上次)(?:决定|讨论|说)了什么)/iu;
|
|
61
63
|
const DISK_INTENT_RE = /(?:\bdisk (?:usage|space|full)\b|\bstorage (?:usage|space|full)\b|\blargest director|\benospc\b|\bno space left on device\b|磁盘(?:占用|空间|已满)|存储空间|目录占用|空间不足)/iu;
|
|
62
64
|
const PATCH_INTENT_RE = /(?:\b(?:implement|refactor|fix|edit)\b|修复|重构|修改|实现|\bapply (?:a )?patch\b|\bunified diff\b|\bpatch file\b|应用补丁|统一 diff|补丁文件)/iu;
|
|
@@ -120,6 +122,7 @@ function matchedMcpTools(intentText, toolNames) {
|
|
|
120
122
|
* activeTasks?: object[],
|
|
121
123
|
* subAgentToolsActivated?: boolean,
|
|
122
124
|
* imageGenerationConfigured?: boolean,
|
|
125
|
+
* gitReadAlwaysVisible?: boolean,
|
|
123
126
|
* }} opts
|
|
124
127
|
* @returns {Set<string>}
|
|
125
128
|
*/
|
|
@@ -131,6 +134,7 @@ export function resolveActiveToolNames({
|
|
|
131
134
|
activeTasks = [],
|
|
132
135
|
subAgentToolsActivated = false,
|
|
133
136
|
imageGenerationConfigured = false,
|
|
137
|
+
gitReadAlwaysVisible = false,
|
|
134
138
|
} = {}) {
|
|
135
139
|
const registered = new Set(Array.isArray(toolNames) ? toolNames : []);
|
|
136
140
|
const active = new Set(ALWAYS_VISIBLE_TOOL_NAMES.filter(name => registered.has(name)));
|
|
@@ -139,6 +143,7 @@ export function resolveActiveToolNames({
|
|
|
139
143
|
const hasActiveTasks = tasks.length > 0;
|
|
140
144
|
const hasSubAgentTask = tasks.some(task => task?.kind === 'sub_agent');
|
|
141
145
|
|
|
146
|
+
if (gitReadAlwaysVisible || GIT_INTENT_RE.test(intentText)) active.add('GitRead');
|
|
142
147
|
if (HISTORY_INTENT_RE.test(intentText)) active.add('HistorySearch');
|
|
143
148
|
if (DISK_INTENT_RE.test(intentText)) active.add('DiskUsage');
|
|
144
149
|
if (PATCH_INTENT_RE.test(intentText)) active.add('ApplyPatch');
|
package/yeaft/tools/git-read.js
CHANGED
|
@@ -27,13 +27,13 @@ const COMMON_ARGS = Object.freeze([
|
|
|
27
27
|
// Worktree status/diff may invoke clean/process filters even with textconv and
|
|
28
28
|
// external diff disabled. Discover their keys, never values, and override them
|
|
29
29
|
// for this process only. Incomplete discovery fails closed.
|
|
30
|
-
async function filterOverrides(run
|
|
30
|
+
async function filterOverrides(run) {
|
|
31
31
|
const result = await run('git', [
|
|
32
32
|
...COMMON_ARGS, 'config', '--null', '--name-only', '--get-regexp',
|
|
33
33
|
'^filter\\..*\\.(clean|smudge|process|required)$',
|
|
34
|
-
]
|
|
35
|
-
if (result.truncated || result.timedOut || (result.code !== 0 && result.code !== 1)) {
|
|
36
|
-
throw new Error('Cannot safely inspect Git content filters');
|
|
34
|
+
]);
|
|
35
|
+
if (result.truncated || result.timedOut || result.terminationError || (result.code !== 0 && result.code !== 1)) {
|
|
36
|
+
throw Object.assign(new Error('Cannot safely inspect Git content filters'), { result });
|
|
37
37
|
}
|
|
38
38
|
if (result.code === 1) return [];
|
|
39
39
|
const keys = [...new Set(result.stdout.split('\0').filter(Boolean))];
|
|
@@ -47,8 +47,8 @@ async function filterOverrides(run, options) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function errorOutput(message, operation) {
|
|
50
|
-
return
|
|
51
|
-
error: message, errorEffect: 'none', code: 'invalid_arguments',
|
|
50
|
+
return boundedFailure({
|
|
51
|
+
error: takeUtf8(message, 1024), errorEffect: 'none', code: 'invalid_arguments',
|
|
52
52
|
hint: `Use only fields for the chosen operation. Minimal example: ${JSON.stringify({ operation: ['status', 'diff', 'show', 'log'].includes(operation) ? operation : 'status' })}`,
|
|
53
53
|
});
|
|
54
54
|
}
|
|
@@ -170,21 +170,19 @@ function takeUtf8(text, maxBytes) {
|
|
|
170
170
|
return buffer.subarray(0, end).toString('utf8');
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
const timedOut = Boolean(result.timedOut);
|
|
175
|
-
const runnerTruncated = Boolean(result.truncated);
|
|
173
|
+
function formatSuccess(operation, result) {
|
|
176
174
|
const sections = [];
|
|
177
175
|
if (result.stdout) sections.push(`STDOUT:\n${result.stdout}`);
|
|
178
176
|
if (result.stderr) sections.push(`STDERR:\n${result.stderr}`);
|
|
179
177
|
const body = sections.join('\n');
|
|
180
178
|
const baseHeader = truncated => [
|
|
181
179
|
`operation: ${operation}`,
|
|
182
|
-
`exitCode: ${
|
|
183
|
-
|
|
180
|
+
`exitCode: ${result.code}`,
|
|
181
|
+
'timedOut: false',
|
|
184
182
|
`truncated: ${truncated}`,
|
|
185
183
|
].join('\n');
|
|
186
|
-
const initial = `${baseHeader(
|
|
187
|
-
if (
|
|
184
|
+
const initial = `${baseHeader(false)}\n\n${body || '(no output)'}`;
|
|
185
|
+
if (Buffer.byteLength(initial, 'utf8') <= MAX_RESULT_BYTES) return initial;
|
|
188
186
|
|
|
189
187
|
const marker = '\n\n[Output truncated by GitRead; narrow the revision or paths.]';
|
|
190
188
|
const header = `${baseHeader(true)}\n\n`;
|
|
@@ -195,6 +193,62 @@ export function formatGitReadResult(operation, result) {
|
|
|
195
193
|
return header + takeUtf8(body || '(no output)', bodyBudget) + marker;
|
|
196
194
|
}
|
|
197
195
|
|
|
196
|
+
// Keep returned errors parseable even when JSON escaping expands raw output.
|
|
197
|
+
// Engine uses this envelope (not text exit codes) for tool_end.isError.
|
|
198
|
+
function boundedFailure(fields, output = '') {
|
|
199
|
+
const envelope = { ...fields, ...(output ? { output: String(output) } : {}) };
|
|
200
|
+
// Metadata is not necessarily small: even an invalid cwd reaches spawn.
|
|
201
|
+
// Bound each string after allowing for JSON's worst-case 6x escaping;
|
|
202
|
+
// the fixed envelope fields then leave ample room for diagnostics.
|
|
203
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
204
|
+
if (typeof value === 'string' && Buffer.byteLength(value, 'utf8') > 512) {
|
|
205
|
+
envelope[key] = takeUtf8(value, 512) + '[truncated]';
|
|
206
|
+
envelope.truncated = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
let serialized = JSON.stringify(envelope);
|
|
210
|
+
const marker = '\n[GitRead diagnostic truncated; narrow the revision or paths.]';
|
|
211
|
+
if (Buffer.byteLength(serialized, 'utf8') > MAX_RESULT_BYTES) {
|
|
212
|
+
envelope.truncated = true;
|
|
213
|
+
let low = 0;
|
|
214
|
+
let high = Math.min(Buffer.byteLength(output, 'utf8'), MAX_RESULT_BYTES);
|
|
215
|
+
while (low < high) {
|
|
216
|
+
const mid = Math.ceil((low + high) / 2);
|
|
217
|
+
envelope.output = takeUtf8(output, mid) + marker;
|
|
218
|
+
if (Buffer.byteLength(JSON.stringify(envelope), 'utf8') <= MAX_RESULT_BYTES) low = mid;
|
|
219
|
+
else high = mid - 1;
|
|
220
|
+
}
|
|
221
|
+
envelope.output = takeUtf8(output, low) + marker;
|
|
222
|
+
serialized = JSON.stringify(envelope);
|
|
223
|
+
}
|
|
224
|
+
return serialized;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function formatGitReadResult(operation, result, { resolvedCwd, stage = operation } = {}) {
|
|
228
|
+
if (result.code === 0 && !result.timedOut && !result.truncated && !result.terminationError) {
|
|
229
|
+
return formatSuccess(operation, result);
|
|
230
|
+
}
|
|
231
|
+
const code = result.terminationError ? 'git_exit_unconfirmed'
|
|
232
|
+
: result.timedOut ? 'git_timeout'
|
|
233
|
+
: result.truncated ? 'git_output_limit'
|
|
234
|
+
: !Number.isInteger(result.code) ? 'git_exit_unconfirmed' : 'git_failed';
|
|
235
|
+
const error = {
|
|
236
|
+
git_exit_unconfirmed: 'Git process exit was not confirmed',
|
|
237
|
+
git_timeout: 'Git read timed out',
|
|
238
|
+
git_output_limit: 'Git read stopped at the capture limit; output is incomplete',
|
|
239
|
+
git_failed: `Git exited with code ${result.code}`,
|
|
240
|
+
}[code];
|
|
241
|
+
// Put stderr first so a long partial diff cannot hide Git's explanation.
|
|
242
|
+
const output = [result.terminationError, result.stderr && `STDERR:\n${result.stderr}`,
|
|
243
|
+
result.stdout && `STDOUT:\n${result.stdout}`].filter(Boolean).join('\n');
|
|
244
|
+
return boundedFailure({
|
|
245
|
+
error, errorEffect: 'none', code, operation, stage, resolvedCwd,
|
|
246
|
+
exitCode: result.truncated || result.terminationError ? null : (result.code ?? null),
|
|
247
|
+
timedOut: Boolean(result.timedOut), truncated: Boolean(result.truncated),
|
|
248
|
+
...(result.terminationError || code === 'git_exit_unconfirmed' ? { terminationConfirmed: false } : {}),
|
|
249
|
+
}, output);
|
|
250
|
+
}
|
|
251
|
+
|
|
198
252
|
const gitReadTool = defineTool({
|
|
199
253
|
name: 'GitRead',
|
|
200
254
|
description: {
|
|
@@ -203,19 +257,19 @@ const gitReadTool = defineTool({
|
|
|
203
257
|
Supported operations are intentionally limited:
|
|
204
258
|
- status: compact branch and working-tree status.
|
|
205
259
|
- diff: tracked changes against HEAD by default, or an explicit base...head range; optional paths narrow the result.
|
|
206
|
-
- show: one commit (HEAD by default), optionally narrowed by paths.
|
|
260
|
+
- show: exactly one commit (HEAD by default; tags are peeled to commits), optionally narrowed by paths. Ranges and non-commit objects are rejected.
|
|
207
261
|
- log: a compact bounded commit list (20 entries by default, maximum 50).
|
|
208
262
|
|
|
209
|
-
GitRead never fetches, writes Git state, or creates worktrees. It disables pagers, external diff, textconv, content filters, optional locks, fsmonitor, and submodule traversal. Filter-normalized files (such as LFS) show raw worktree bytes; submodule status needs separate inspection. Revisions and paths beginning with "-" are rejected. Output reports
|
|
263
|
+
GitRead never fetches, writes Git state, or creates worktrees. It disables pagers, external diff, textconv, content filters, optional locks, fsmonitor, and submodule traversal. Filter-normalized files (such as LFS) show raw worktree bytes; submodule status needs separate inspection. Revisions and paths beginning with "-" are rejected. Output reports truncation. Git failures, timeouts and capture-limit stops return an error with bounded diagnostics.`,
|
|
210
264
|
zh: `有界读取本地 Git 证据,不使用 shell,也不访问网络。
|
|
211
265
|
|
|
212
266
|
操作范围刻意限制为:
|
|
213
267
|
- status:紧凑显示分支和工作区状态。
|
|
214
268
|
- diff:默认显示相对 HEAD 的已跟踪改动,也可指定 base...head;可用 paths 缩小范围。
|
|
215
|
-
- show
|
|
269
|
+
- show:显示唯一提交(默认 HEAD;tag 解析到 commit),可用 paths 缩小范围。拒绝范围及非 commit 对象。
|
|
216
270
|
- log:紧凑且有界的提交列表(默认 20 条,最多 50 条)。
|
|
217
271
|
|
|
218
|
-
GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、external diff、textconv、内容 filter、optional locks、fsmonitor 和子模块遍历。LFS 等 filter 文件显示原始工作区字节,子模块状态需单独检查。拒绝以 "-" 开头的 revision
|
|
272
|
+
GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、external diff、textconv、内容 filter、optional locks、fsmonitor 和子模块遍历。LFS 等 filter 文件显示原始工作区字节,子模块状态需单独检查。拒绝以 "-" 开头的 revision 与路径;结果明确标识是否截断;Git 失败、超时及捕获上限终止返回含有界诊断的错误。`,
|
|
219
273
|
},
|
|
220
274
|
parameters: {
|
|
221
275
|
type: 'object',
|
|
@@ -224,7 +278,7 @@ GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、e
|
|
|
224
278
|
operation: { type: 'string', enum: ['status', 'diff', 'show', 'log'] },
|
|
225
279
|
base: { type: 'string', maxLength: MAX_VALUE_LENGTH, description: 'diff only; empty/omitted means working-tree changes against HEAD' },
|
|
226
280
|
head: { type: 'string', maxLength: MAX_VALUE_LENGTH, description: 'diff only; requires base, empty/omitted defaults to HEAD' },
|
|
227
|
-
revision: { type: 'string', maxLength: MAX_VALUE_LENGTH, description: 'show/log only; empty/omitted defaults to HEAD' },
|
|
281
|
+
revision: { type: 'string', maxLength: MAX_VALUE_LENGTH, description: 'show/log only; empty/omitted defaults to HEAD. show requires a single commit, not a range/tree/blob' },
|
|
228
282
|
paths: {
|
|
229
283
|
type: 'array',
|
|
230
284
|
maxItems: MAX_PATHS,
|
|
@@ -241,7 +295,9 @@ GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、e
|
|
|
241
295
|
async execute(input, ctx) {
|
|
242
296
|
const built = buildGitReadArgs(input);
|
|
243
297
|
if (built.error) return errorOutput(built.error, input?.operation);
|
|
298
|
+
input = normalizeInput(input);
|
|
244
299
|
const cwd = resolve(ctx?.cwd || process.cwd());
|
|
300
|
+
let stage = input.operation;
|
|
245
301
|
try {
|
|
246
302
|
const run = ctx?.[RUN_PROCESS_OVERRIDE] || runProcess;
|
|
247
303
|
const startedAt = Date.now();
|
|
@@ -250,6 +306,7 @@ GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、e
|
|
|
250
306
|
signal: ctx?.signal,
|
|
251
307
|
timeoutMs: TIMEOUT_MS,
|
|
252
308
|
maxBytes: MAX_CAPTURE_BYTES,
|
|
309
|
+
requireExitConfirmation: true,
|
|
253
310
|
env: {
|
|
254
311
|
...process.env,
|
|
255
312
|
GIT_PAGER: 'cat',
|
|
@@ -261,14 +318,54 @@ GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、e
|
|
|
261
318
|
NO_COLOR: '1',
|
|
262
319
|
},
|
|
263
320
|
};
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
321
|
+
const read = (command, args) => {
|
|
322
|
+
const remaining = TIMEOUT_MS - (Date.now() - startedAt);
|
|
323
|
+
if (remaining <= 0) {
|
|
324
|
+
throw Object.assign(new Error('Git read timed out'), { code: 'git_timeout', timedOut: true });
|
|
325
|
+
}
|
|
326
|
+
return run(command, args, { ...options, timeoutMs: remaining });
|
|
327
|
+
};
|
|
328
|
+
// Only these operations inspect worktree bytes. Object-only reads must
|
|
329
|
+
// not pay for filter discovery or fail on an unusable worktree filter.
|
|
330
|
+
const readsWorktree = input.operation === 'status'
|
|
331
|
+
|| (input.operation === 'diff' && input.base === undefined);
|
|
332
|
+
let overrides = [];
|
|
333
|
+
if (readsWorktree) {
|
|
334
|
+
stage = 'filter_inspection';
|
|
335
|
+
overrides = await filterOverrides(read);
|
|
336
|
+
}
|
|
337
|
+
let args = built.args;
|
|
338
|
+
if (input.operation === 'show') {
|
|
339
|
+
stage = 'resolve_commit';
|
|
340
|
+
const resolved = await read('git', [
|
|
341
|
+
...COMMON_ARGS, 'rev-parse', '--verify', '--end-of-options', `${input.revision || 'HEAD'}^{commit}`,
|
|
342
|
+
]);
|
|
343
|
+
if (resolved.code !== 0 || resolved.truncated || resolved.timedOut || resolved.terminationError) {
|
|
344
|
+
return formatGitReadResult(input.operation, resolved, { resolvedCwd: cwd, stage });
|
|
345
|
+
}
|
|
346
|
+
const commit = resolved.stdout.trim();
|
|
347
|
+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(commit)) {
|
|
348
|
+
throw new Error('Expected exactly one resolved commit object ID');
|
|
349
|
+
}
|
|
350
|
+
args = buildGitReadArgs({ ...input, revision: commit }).args;
|
|
351
|
+
}
|
|
352
|
+
stage = input.operation;
|
|
353
|
+
const result = await read('git', [...overrides, ...args]);
|
|
354
|
+
return formatGitReadResult(input.operation, result, { resolvedCwd: cwd, stage });
|
|
269
355
|
} catch (error) {
|
|
270
356
|
if (error?.name === 'AbortError') throw error;
|
|
271
|
-
|
|
357
|
+
if (error?.result) {
|
|
358
|
+
return formatGitReadResult(input.operation, error.result, { resolvedCwd: cwd, stage });
|
|
359
|
+
}
|
|
360
|
+
return boundedFailure({
|
|
361
|
+
error: 'GitRead failed', errorEffect: 'none',
|
|
362
|
+
code: error?.name === 'ProcessTerminationError' ? 'git_exit_unconfirmed'
|
|
363
|
+
: error?.code === 'git_timeout' ? 'git_timeout'
|
|
364
|
+
: stage === 'filter_inspection' ? 'git_filter_inspection_failed' : 'git_execution_error',
|
|
365
|
+
operation: input.operation, stage, resolvedCwd: cwd,
|
|
366
|
+
...(error?.timedOut ? { timedOut: true } : {}),
|
|
367
|
+
...(error?.name === 'ProcessTerminationError' ? { terminationConfirmed: false } : {}),
|
|
368
|
+
}, error?.message || String(error));
|
|
272
369
|
}
|
|
273
370
|
},
|
|
274
371
|
});
|