@yeaft/webchat-agent 1.0.527 → 1.0.528
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/package.json +1 -1
- package/yeaft/engine.js +13 -1
- package/yeaft/sub-agent/execution-control.js +16 -9
- package/yeaft/sub-agent/liveness.js +20 -12
- package/yeaft/sub-agent/runner.js +12 -15
- package/yeaft/sub-agent/status.js +5 -5
- package/yeaft/tools/activation.js +1 -1
- package/yeaft/tools/agent.js +21 -40
- package/yeaft/tools/apply-patch.js +442 -134
- package/yeaft/tools/bash.js +11 -9
- package/yeaft/tools/exit-worktree.js +1 -1
- package/yeaft/tools/file-read.js +36 -4
- package/yeaft/tools/git-read.js +29 -9
- package/yeaft/tools/list-agents.js +60 -58
- package/yeaft/tools/list-tasks.js +31 -3
- package/yeaft/tools/process-runner.js +29 -1
- package/yeaft/tools/registry.js +18 -0
- package/yeaft/tools/update-agent.js +2 -1
- package/yeaft/tools/wait-agent.js +17 -17
- package/yeaft/tools/web-fetch.js +6 -1
package/yeaft/tools/bash.js
CHANGED
|
@@ -77,6 +77,7 @@ function runCommand(command, { cwd, timeout, signal, runtimePlatform, runProcess
|
|
|
77
77
|
signal,
|
|
78
78
|
timeoutMs: timeout,
|
|
79
79
|
maxBytes: MAX_OUTPUT,
|
|
80
|
+
outputLimitAction: 'head-tail',
|
|
80
81
|
requireExitConfirmation: true,
|
|
81
82
|
systemdScope: invocation.systemdControl,
|
|
82
83
|
onSettled: invocation.cleanup || null,
|
|
@@ -86,6 +87,7 @@ function runCommand(command, { cwd, timeout, signal, runtimePlatform, runProcess
|
|
|
86
87
|
stderr: result.stderr,
|
|
87
88
|
exitCode: result.code,
|
|
88
89
|
timedOut: result.timedOut,
|
|
90
|
+
truncated: result.truncated,
|
|
89
91
|
terminationError: result.terminationError || null,
|
|
90
92
|
}));
|
|
91
93
|
}
|
|
@@ -103,7 +105,7 @@ Guidelines:
|
|
|
103
105
|
- Commands run in the working directory (cwd from context)
|
|
104
106
|
- Match command syntax to the Agent OS shown in the runtime_platform prompt
|
|
105
107
|
- Timeout defaults to 2 minutes (max 10 minutes)
|
|
106
|
-
- Large outputs
|
|
108
|
+
- Large outputs retain a bounded head/tail (256KB per stream); capture limits do not stop the command. Use background tasks for persistent full logs
|
|
107
109
|
- Use absolute paths when possible
|
|
108
110
|
- Avoid interactive commands (no stdin support)
|
|
109
111
|
- Use background=true for long-running or persistent tasks that should survive across turns
|
|
@@ -116,7 +118,7 @@ Guidelines:
|
|
|
116
118
|
使用指南:
|
|
117
119
|
- 命令在工作目录中执行(上下文中的 cwd)
|
|
118
120
|
- 默认超时 2 分钟(最大 10 分钟)
|
|
119
|
-
-
|
|
121
|
+
- 大输出有界保留首尾(每个流 256KB),不因输出上限终止命令;需要持久完整日志时使用后台任务
|
|
120
122
|
- 尽量使用绝对路径
|
|
121
123
|
- 避免交互式命令(不支持 stdin)
|
|
122
124
|
- 长时间或需要跨 turn 持续存在的任务使用 background=true
|
|
@@ -136,8 +138,8 @@ Guidelines:
|
|
|
136
138
|
cwd: {
|
|
137
139
|
type: 'string',
|
|
138
140
|
description: {
|
|
139
|
-
en: 'Working directory
|
|
140
|
-
zh: '
|
|
141
|
+
en: 'Working directory; relative paths resolve against engine cwd',
|
|
142
|
+
zh: '命令工作目录;相对路径以引擎当前目录为基准',
|
|
141
143
|
},
|
|
142
144
|
},
|
|
143
145
|
timeout_ms: {
|
|
@@ -189,9 +191,7 @@ Guidelines:
|
|
|
189
191
|
if (!command) throw new Error('command is required');
|
|
190
192
|
|
|
191
193
|
// Resolve working directory
|
|
192
|
-
const cwd = inputCwd
|
|
193
|
-
? resolve(inputCwd)
|
|
194
|
-
: (ctx?.cwd || process.cwd());
|
|
194
|
+
const cwd = resolve(ctx?.cwd || process.cwd(), inputCwd || '.');
|
|
195
195
|
|
|
196
196
|
if (!existsSync(cwd)) {
|
|
197
197
|
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
@@ -217,7 +217,7 @@ Guidelines:
|
|
|
217
217
|
threadId: ctx.threadId || 'main',
|
|
218
218
|
},
|
|
219
219
|
});
|
|
220
|
-
return `Started background task ${task.id}.\nStatus: ${task.status}\nLog: ${task.log?.path || ''}\nThe task is detached from this turn. Use ListTasks, ReadTaskLog, or CancelTask to inspect or control it.`;
|
|
220
|
+
return `Started background task ${task.id}.\nWorking directory: ${cwd}\nStatus: ${task.status}\nLog: ${task.log?.path || ''}\nThe task is detached from this turn. Use ListTasks, ReadTaskLog, or CancelTask to inspect or control it.`;
|
|
221
221
|
} catch (err) {
|
|
222
222
|
throw new Error(err?.message || String(err));
|
|
223
223
|
}
|
|
@@ -236,6 +236,7 @@ Guidelines:
|
|
|
236
236
|
const parts = [];
|
|
237
237
|
if (result.stdout) parts.push(result.stdout);
|
|
238
238
|
if (result.stderr) parts.push(`STDERR:\n${result.stderr}`);
|
|
239
|
+
if (result.truncated) parts.push('[Captured output truncated; command exit/timeout status is reported separately. Use a background task for a durable log.]');
|
|
239
240
|
if (result.timedOut) parts.push(`\n(Command timed out after ${timeout}ms)`);
|
|
240
241
|
if (result.terminationError) {
|
|
241
242
|
parts.push([
|
|
@@ -258,10 +259,11 @@ Guidelines:
|
|
|
258
259
|
|
|
259
260
|
const output = parts.join('\n');
|
|
260
261
|
if (result.exitCode !== 0) {
|
|
261
|
-
return `Exit code: ${result.exitCode}\n${output}`;
|
|
262
|
+
return `Exit code: ${result.exitCode}\nWorking directory: ${cwd}\n${output}`;
|
|
262
263
|
}
|
|
263
264
|
return output || '(no output)';
|
|
264
265
|
} catch (err) {
|
|
266
|
+
err.message = `${err.message} (working directory: ${cwd})`;
|
|
265
267
|
if (err?.name === 'ProcessTerminationError') err.fatalToolTimeout = true;
|
|
266
268
|
throw err;
|
|
267
269
|
}
|
|
@@ -67,7 +67,7 @@ unless discard_changes is set to true.`,
|
|
|
67
67
|
},
|
|
68
68
|
isDestructive: (input) => input?.action === 'remove',
|
|
69
69
|
async execute(input, ctx) {
|
|
70
|
-
const worktreePath = resolve(input.path);
|
|
70
|
+
const worktreePath = resolve(ctx?.cwd || process.cwd(), input.path);
|
|
71
71
|
const mainCwd = ctx?.cwd || process.cwd();
|
|
72
72
|
|
|
73
73
|
if (!existsSync(worktreePath)) {
|
package/yeaft/tools/file-read.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Modeled after Claude Code's Read tool.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
11
12
|
import { defineTool } from './types.js';
|
|
12
13
|
import { readFile, stat } from 'fs/promises';
|
|
13
14
|
import { existsSync } from 'fs';
|
|
@@ -77,6 +78,34 @@ function formatLinesWithinBudget(allLines, startLine, endLine, startColumn = 0,
|
|
|
77
78
|
return { text: parts.join(''), nextLine, nextColumn };
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
// Query-local observations are hints only: always read the current file and
|
|
82
|
+
// never suppress ranges. Hashing observed bytes handles same-size/mtime edits.
|
|
83
|
+
function describeRead(ctx, path, content, start, end, startColumn, nextColumn) {
|
|
84
|
+
const version = createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
85
|
+
const observations = ctx?.fileReadObservations;
|
|
86
|
+
const previous = observations?.get(path);
|
|
87
|
+
const ranges = previous?.version === version ? previous.ranges : [];
|
|
88
|
+
const overlaps = ranges.filter(([from, to]) => from < end && to > start)
|
|
89
|
+
.map(([from, to]) => `${Math.max(from, start) + 1}-${Math.min(to, end)}`);
|
|
90
|
+
// Partial Unicode lines are not considered fully observed.
|
|
91
|
+
const from = start + (startColumn > 0 ? 1 : 0);
|
|
92
|
+
const to = end - (nextColumn > 0 ? 1 : 0);
|
|
93
|
+
if (observations && to > from) {
|
|
94
|
+
const merged = [...ranges, [from, to]].sort((a, b) => a[0] - b[0]).reduce((out, range) => {
|
|
95
|
+
const last = out.at(-1);
|
|
96
|
+
if (last && range[0] <= last[1]) last[1] = Math.max(last[1], range[1]);
|
|
97
|
+
else out.push([...range]);
|
|
98
|
+
return out;
|
|
99
|
+
}, []);
|
|
100
|
+
observations.delete(path);
|
|
101
|
+
observations.set(path, { version, ranges: merged.slice(-16) });
|
|
102
|
+
if (observations.size > 64) observations.delete(observations.keys().next().value);
|
|
103
|
+
}
|
|
104
|
+
const hint = overlaps.length
|
|
105
|
+
? ` Previously returned unchanged lines in this query: ${overlaps.slice(0, 4).join(', ')}. Read other ranges only if needed.` : '';
|
|
106
|
+
return `\n[File: ${path}; observed version: ${version}.${hint}]`;
|
|
107
|
+
}
|
|
108
|
+
|
|
80
109
|
export default defineTool({
|
|
81
110
|
name: 'FileRead',
|
|
82
111
|
description: {
|
|
@@ -140,7 +169,9 @@ Guidelines:
|
|
|
140
169
|
},
|
|
141
170
|
isConcurrencySafe: () => true,
|
|
142
171
|
isReadOnly: () => true,
|
|
143
|
-
|
|
172
|
+
// The workspace may change outside this Engine (editor, process, peer).
|
|
173
|
+
// Observation hints must describe a fresh read, never a cached snapshot.
|
|
174
|
+
cacheWithinQuery: false,
|
|
144
175
|
async execute(input, ctx) {
|
|
145
176
|
const { file_path, offset = 0, column_offset = 0, limit = DEFAULT_LIMIT } = input;
|
|
146
177
|
if (!file_path) return JSON.stringify({ error: 'file_path is required' });
|
|
@@ -201,6 +232,8 @@ Guidelines:
|
|
|
201
232
|
const startColumn = column_offset;
|
|
202
233
|
const { text: numbered, nextLine, nextColumn } = formatLinesWithinBudget(allLines, startLine, endLine, startColumn);
|
|
203
234
|
|
|
235
|
+
const shownEnd = nextColumn > 0 ? nextLine + 1 : nextLine;
|
|
236
|
+
const observation = describeRead(ctx, absPath, content, startLine, shownEnd, startColumn, nextColumn);
|
|
204
237
|
const hasMoreContent = nextColumn > 0 || nextLine < totalLines;
|
|
205
238
|
if (startLine > 0 || startColumn > 0 || hasMoreContent) {
|
|
206
239
|
const continuation = hasMoreContent
|
|
@@ -208,11 +241,10 @@ Guidelines:
|
|
|
208
241
|
? ` Continue with offset=${nextLine}, column_offset=${nextColumn}.`
|
|
209
242
|
: ` Continue with offset=${nextLine}.`
|
|
210
243
|
: '';
|
|
211
|
-
|
|
212
|
-
return `${numbered}\n\n[Showing lines ${startLine + 1}-${shownEnd} of ${totalLines} total.${continuation}]`;
|
|
244
|
+
return `${numbered}\n\n[Showing lines ${startLine + 1}-${shownEnd} of ${totalLines} total.${continuation}]${observation}`;
|
|
213
245
|
}
|
|
214
246
|
|
|
215
|
-
return numbered;
|
|
247
|
+
return numbered + observation;
|
|
216
248
|
} catch (err) {
|
|
217
249
|
return JSON.stringify({ error: `Failed to read file: ${err.message}` });
|
|
218
250
|
}
|
package/yeaft/tools/git-read.js
CHANGED
|
@@ -46,8 +46,27 @@ async function filterOverrides(run, options) {
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function errorOutput(message) {
|
|
50
|
-
return JSON.stringify({
|
|
49
|
+
function errorOutput(message, operation) {
|
|
50
|
+
return JSON.stringify({
|
|
51
|
+
error: message, errorEffect: 'none', code: 'invalid_arguments',
|
|
52
|
+
hint: `Use only fields for the chosen operation. Minimal example: ${JSON.stringify({ operation: ['status', 'diff', 'show', 'log'].includes(operation) ? operation : 'status' })}`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Some strict-schema providers require every property. Treat their empty
|
|
57
|
+
// placeholders as omitted, without accepting unknown keys or non-default
|
|
58
|
+
// arguments belonging to another operation.
|
|
59
|
+
function normalizeInput(input) {
|
|
60
|
+
const result = { ...input };
|
|
61
|
+
for (const key of ['base', 'head', 'revision', 'paths', 'limit']) {
|
|
62
|
+
if (result[key] === null || result[key] === undefined
|
|
63
|
+
|| (['base', 'head', 'revision'].includes(key) && result[key] === '')
|
|
64
|
+
|| (key === 'paths' && Array.isArray(result[key]) && result[key].length === 0)
|
|
65
|
+
|| (key === 'limit' && result.operation !== 'log' && result[key] === DEFAULT_LOG_LIMIT)) {
|
|
66
|
+
delete result[key];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
function validateValue(value, name) {
|
|
@@ -80,6 +99,7 @@ export function buildGitReadArgs(input) {
|
|
|
80
99
|
return { error: 'input must be an object' };
|
|
81
100
|
}
|
|
82
101
|
|
|
102
|
+
input = normalizeInput(input);
|
|
83
103
|
const { operation } = input;
|
|
84
104
|
if (!['status', 'diff', 'show', 'log'].includes(operation)) {
|
|
85
105
|
return { error: 'operation must be one of: status, diff, show, log' };
|
|
@@ -202,16 +222,16 @@ GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、e
|
|
|
202
222
|
additionalProperties: false,
|
|
203
223
|
properties: {
|
|
204
224
|
operation: { type: 'string', enum: ['status', 'diff', 'show', 'log'] },
|
|
205
|
-
base: { type: 'string', maxLength: MAX_VALUE_LENGTH, description: '
|
|
206
|
-
head: { type: 'string', maxLength: MAX_VALUE_LENGTH, description: '
|
|
207
|
-
revision: { type: 'string', maxLength: MAX_VALUE_LENGTH, description: '
|
|
225
|
+
base: { type: 'string', maxLength: MAX_VALUE_LENGTH, description: 'diff only; empty/omitted means working-tree changes against HEAD' },
|
|
226
|
+
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' },
|
|
208
228
|
paths: {
|
|
209
229
|
type: 'array',
|
|
210
230
|
maxItems: MAX_PATHS,
|
|
211
231
|
items: { type: 'string', minLength: 1, maxLength: MAX_VALUE_LENGTH },
|
|
212
|
-
description: '
|
|
232
|
+
description: 'diff/show only; empty/omitted means all paths',
|
|
213
233
|
},
|
|
214
|
-
limit: { type: 'integer', minimum: 1, maximum: MAX_LOG_LIMIT, description: '
|
|
234
|
+
limit: { type: 'integer', minimum: 1, maximum: MAX_LOG_LIMIT, description: 'log only; default 20. Empty optional fields are ignored; omit fields for other operations.' },
|
|
215
235
|
},
|
|
216
236
|
required: ['operation'],
|
|
217
237
|
},
|
|
@@ -220,7 +240,7 @@ GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、e
|
|
|
220
240
|
isReadOnly: () => true,
|
|
221
241
|
async execute(input, ctx) {
|
|
222
242
|
const built = buildGitReadArgs(input);
|
|
223
|
-
if (built.error) return errorOutput(built.error);
|
|
243
|
+
if (built.error) return errorOutput(built.error, input?.operation);
|
|
224
244
|
const cwd = resolve(ctx?.cwd || process.cwd());
|
|
225
245
|
try {
|
|
226
246
|
const run = ctx?.[RUN_PROCESS_OVERRIDE] || runProcess;
|
|
@@ -248,7 +268,7 @@ GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、e
|
|
|
248
268
|
return formatGitReadResult(input.operation, result);
|
|
249
269
|
} catch (error) {
|
|
250
270
|
if (error?.name === 'AbortError') throw error;
|
|
251
|
-
return
|
|
271
|
+
return JSON.stringify({ error: `GitRead failed: ${error?.message || String(error)}`, resolvedCwd: cwd });
|
|
252
272
|
}
|
|
253
273
|
},
|
|
254
274
|
});
|
|
@@ -1,40 +1,36 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* list-agents.js — List all active (and optionally terminal) sub-agents.
|
|
3
|
-
*
|
|
4
|
-
* Returns: { agents: [{ id, name, status, task, outputFile, liveness,
|
|
5
|
-
* lastEventAt, msSinceLastEvent, error, hasResult, createdAt }, …] }.
|
|
6
|
-
*
|
|
7
|
-
* The default filter drops `closed` agents to stay tidy; pass
|
|
8
|
-
* include_closed=true (or include_terminal=true) to see them all. The
|
|
9
|
-
* include_closed alias is kept for backward-compat with the old shape.
|
|
10
|
-
*/
|
|
1
|
+
/** Compact, caller-scoped status projection for sub-agent orchestration. */
|
|
11
2
|
|
|
12
3
|
import { defineTool } from './types.js';
|
|
13
4
|
import { agentBelongsToCaller, getAgentRegistry } from './agent.js';
|
|
14
|
-
import { isTerminalAgentStatus } from '../sub-agent/status.js';
|
|
5
|
+
import { isTerminalAgentStatus, STATUS } from '../sub-agent/status.js';
|
|
15
6
|
import { diagnoseAgentLiveness } from '../sub-agent/liveness.js';
|
|
16
7
|
|
|
8
|
+
function nextStepFor(agent, liveness) {
|
|
9
|
+
if (isTerminalAgentStatus(agent.status)) {
|
|
10
|
+
return 'Use WaitAgent to collect the final result, or read outputFile for the full timeline.';
|
|
11
|
+
}
|
|
12
|
+
if (agent.status === STATUS.IDLE) {
|
|
13
|
+
return 'Use WaitAgent to collect the reply; PromptAgent only if follow-up guidance is needed.';
|
|
14
|
+
}
|
|
15
|
+
if (liveness.stale) {
|
|
16
|
+
return 'Diagnostic only: inspect outputFile before deciding whether to CloseAgent; do not assume the work is dead.';
|
|
17
|
+
}
|
|
18
|
+
return 'Continue parent work; completion arrives by notification. Read outputFile only when detailed progress is needed.';
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
export default defineTool({
|
|
18
22
|
name: 'ListAgents',
|
|
19
23
|
description: {
|
|
20
|
-
en: `List
|
|
24
|
+
en: `List caller-owned sub-agents as compact status references.
|
|
21
25
|
|
|
22
|
-
Returns
|
|
23
|
-
liveness counters (toolUseCount, tokenCount, msSinceLastEvent, recentTools),
|
|
24
|
-
stale/stalled diagnostics, result tail, and message count for each agent. Use
|
|
25
|
-
this as the primary non-blocking monitor for async sub-agent work, and Read
|
|
26
|
-
\`outputFile\` for any single agent if you need its full timeline.
|
|
26
|
+
Returns identity, status, bounded mission summary, durable outputFile, actual tool/LLM/token usage, recent activity, diagnostic staleness, and an actionable next step. It does not copy result text or the full log; use WaitAgent for a reply and Read outputFile for the timeline.
|
|
27
27
|
|
|
28
|
-
By default
|
|
29
|
-
|
|
30
|
-
zh: `列出所有子 Agent 及其当前状态。
|
|
28
|
+
By default terminal agents are omitted. Pass include_closed=true to include them.`,
|
|
29
|
+
zh: `以紧凑状态引用列出调用方拥有的子 Agent。
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
liveness 计数器(toolUseCount、tokenCount、msSinceLastEvent、recentTools)、
|
|
34
|
-
stale/stalled 诊断、result 尾部和消息数量。将此作为异步子 Agent 工作的主要非阻塞监控工具;
|
|
35
|
-
如需查看某个 Agent 的完整时间线,可 Read 其 outputFile。
|
|
31
|
+
返回身份、状态、有界任务摘要、持久化 outputFile、真实工具/LLM/token 用量、最近活动、诊断性 stale 状态和有效下一步。不复制结果文本或完整日志;回复用 WaitAgent 获取,时间线用 Read outputFile 查看。
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
默认省略终止 Agent;传 include_closed=true 可包含。`,
|
|
38
34
|
},
|
|
39
35
|
parameters: {
|
|
40
36
|
type: 'object',
|
|
@@ -42,15 +38,15 @@ stale/stalled 诊断、result 尾部和消息数量。将此作为异步子 Agen
|
|
|
42
38
|
include_closed: {
|
|
43
39
|
type: 'boolean',
|
|
44
40
|
description: {
|
|
45
|
-
en: 'Include
|
|
46
|
-
zh: '
|
|
41
|
+
en: 'Include terminal agents (default: false)',
|
|
42
|
+
zh: '包含终止 Agent(默认 false)',
|
|
47
43
|
},
|
|
48
44
|
},
|
|
49
45
|
include_terminal: {
|
|
50
46
|
type: 'boolean',
|
|
51
47
|
description: {
|
|
52
|
-
en: '
|
|
53
|
-
zh: 'include_closed
|
|
48
|
+
en: 'Backward-compatible alias for include_closed',
|
|
49
|
+
zh: 'include_closed 的兼容别名',
|
|
54
50
|
},
|
|
55
51
|
},
|
|
56
52
|
},
|
|
@@ -61,51 +57,57 @@ stale/stalled 诊断、result 尾部和消息数量。将此作为异步子 Agen
|
|
|
61
57
|
duplicateCallPolicy: () => 'allow',
|
|
62
58
|
async execute(input, ctx) {
|
|
63
59
|
const includeTerminal = Boolean(input?.include_closed || input?.include_terminal);
|
|
64
|
-
const agents = getAgentRegistry();
|
|
65
60
|
const now = Date.now();
|
|
61
|
+
const agents = [];
|
|
66
62
|
|
|
67
|
-
const
|
|
68
|
-
for (const [id, agent] of agents) {
|
|
63
|
+
for (const [id, agent] of getAgentRegistry()) {
|
|
69
64
|
if (!agentBelongsToCaller(agent, ctx)) continue;
|
|
70
65
|
if (!includeTerminal && isTerminalAgentStatus(agent.status)) continue;
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
: (agent.lastResult || '');
|
|
75
|
-
agentList.push({
|
|
66
|
+
const live = diagnoseAgentLiveness(agent, { now });
|
|
67
|
+
const execution = live.execution;
|
|
68
|
+
agents.push({
|
|
76
69
|
id,
|
|
77
70
|
name: agent.name,
|
|
78
71
|
status: agent.status,
|
|
79
72
|
task: typeof agent.task === 'string' ? agent.task.slice(0, 200) : null,
|
|
80
73
|
outputFile: agent.outputFile || null,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
74
|
+
activity: {
|
|
75
|
+
lastEventAt: live.lastEventAt,
|
|
76
|
+
msSinceLastEvent: live.msSinceLastEvent,
|
|
77
|
+
lastEventType: live.lastEventType,
|
|
78
|
+
recentTools: live.recentTools,
|
|
79
|
+
outputChars: live.outputChars,
|
|
80
|
+
},
|
|
81
|
+
usage: {
|
|
82
|
+
toolExecutions: execution?.toolCalls || 0,
|
|
83
|
+
llmRequests: execution?.llmCalls || agent.usage?.llmCalls || 0,
|
|
84
|
+
providerTokens: live.usageTokens,
|
|
85
|
+
turns: agent.usage?.turns || 0,
|
|
86
|
+
},
|
|
87
|
+
control: {
|
|
88
|
+
limits: execution?.limits || { ...agent.budget },
|
|
89
|
+
remainingToolCalls: execution?.remainingToolCalls ?? null,
|
|
90
|
+
remainingLlmCalls: execution?.remainingLlmCalls ?? null,
|
|
91
|
+
remainingWallTimeMs: execution?.remainingWallTimeMs ?? null,
|
|
92
|
+
reportingLlmCalls: execution?.reportingLlmCalls || 0,
|
|
93
|
+
allowTools: execution?.allowTools || [...(agent.allowTools || [])],
|
|
94
|
+
controlRevision: execution?.controlRevision || 0,
|
|
95
|
+
},
|
|
96
|
+
stale: live.stale,
|
|
97
|
+
diagnostic: live.diagnostic,
|
|
88
98
|
error: agent.error || null,
|
|
89
99
|
hasResult: Boolean(agent.result || agent.lastResult),
|
|
90
|
-
|
|
91
|
-
messages: Array.isArray(agent.messages) ? agent.messages.length : 0,
|
|
92
|
-
turns: agent.usage?.turns || 0,
|
|
93
|
-
createdAt: agent.createdAt,
|
|
100
|
+
next_step: nextStepFor(agent, live),
|
|
94
101
|
});
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
return JSON.stringify({
|
|
105
|
+
agents,
|
|
106
|
+
...(agents.length === 0 ? {
|
|
100
107
|
message: includeTerminal
|
|
101
108
|
? 'No sub-agents in the registry'
|
|
102
109
|
: 'No active sub-agents (pass include_closed=true to see terminal ones)',
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return JSON.stringify({
|
|
107
|
-
agents: agentList,
|
|
108
|
-
totalCount: agentList.length,
|
|
109
|
-
}, null, 2);
|
|
110
|
+
} : {}),
|
|
111
|
+
});
|
|
110
112
|
},
|
|
111
113
|
});
|
|
@@ -4,11 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
import { defineTool } from './types.js';
|
|
6
6
|
|
|
7
|
+
/** Model-facing projection only; TaskManager snapshots remain the UI/API source. */
|
|
8
|
+
export function compactTaskSnapshot(task) {
|
|
9
|
+
if (!task || typeof task !== 'object') return null;
|
|
10
|
+
const runtime = task.runtime && typeof task.runtime === 'object' ? task.runtime : {};
|
|
11
|
+
const log = task.log && typeof task.log === 'object' ? task.log : {};
|
|
12
|
+
return {
|
|
13
|
+
id: task.id,
|
|
14
|
+
kind: task.kind,
|
|
15
|
+
title: typeof task.title === 'string' ? task.title.slice(0, 200) : task.title,
|
|
16
|
+
status: task.status,
|
|
17
|
+
resultDelivery: task.resultDelivery,
|
|
18
|
+
updatedAt: task.updatedAt,
|
|
19
|
+
...(runtime.subAgentId ? { agentId: runtime.subAgentId } : {}),
|
|
20
|
+
...(log.path ? { logPath: log.path } : {}),
|
|
21
|
+
...(runtime.cancelRequestedAt ? { cancelPending: true } : {}),
|
|
22
|
+
...(runtime.cancelEscalatedAt ? { cancelEscalated: true } : {}),
|
|
23
|
+
...(runtime.cancelEscalationFailed ? { cancelEscalationFailed: true } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
export default defineTool({
|
|
8
28
|
name: 'ListTasks',
|
|
9
29
|
description: {
|
|
10
|
-
en: 'List currently running Session background tasks.',
|
|
11
|
-
zh: '
|
|
30
|
+
en: 'List currently running Session background tasks as compact status references. Use ReadTaskLog for task output.',
|
|
31
|
+
zh: '以紧凑状态引用列出当前运行的 Session 后台任务;任务输出请用 ReadTaskLog。',
|
|
12
32
|
},
|
|
13
33
|
parameters: {
|
|
14
34
|
type: 'object',
|
|
@@ -25,6 +45,14 @@ export default defineTool({
|
|
|
25
45
|
async execute(input = {}, ctx = {}) {
|
|
26
46
|
if (!ctx.taskManager) return JSON.stringify({ error: 'task manager unavailable' });
|
|
27
47
|
const sessionId = input.sessionId || ctx.sessionId || null;
|
|
28
|
-
|
|
48
|
+
const tasks = ctx.taskManager.listActiveTasks(sessionId)
|
|
49
|
+
.map(compactTaskSnapshot)
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
return JSON.stringify({
|
|
52
|
+
tasks,
|
|
53
|
+
next_steps: tasks.length > 0
|
|
54
|
+
? 'ReadTaskLog reads output by task id. For sub_agent tasks use WaitAgent/CloseAgent with agentId to collect/cancel; for shell tasks use CancelTask only when cancellation is intended. cancelPending is not proof the process has stopped.'
|
|
55
|
+
: 'No active tasks require follow-up.',
|
|
56
|
+
});
|
|
29
57
|
},
|
|
30
58
|
});
|
|
@@ -119,7 +119,7 @@ function processGroupIsInactive(pid) {
|
|
|
119
119
|
*
|
|
120
120
|
* @param {string} command
|
|
121
121
|
* @param {string[]} args
|
|
122
|
-
* @param {{ cwd?: string, signal?: AbortSignal, timeoutMs?: number, maxBytes?: number, env?: NodeJS.ProcessEnv, preserveCarriageReturns?: boolean, killGraceMs?: number, gracefulTerminationDeadline?: number, terminationDeadline?: number, forceSettleMs?: number, treeKillTimeoutMs?: number, requireExitConfirmation?: boolean, requireProcessGroupExit?: boolean, systemdScope?: { unit: string, systemctlPath: string, env?: NodeJS.ProcessEnv } | null, onSettled?: (() => void) | null, platform?: NodeJS.Platform, spawnProcess?: typeof spawn, spawnProcessSync?: typeof spawnSync }} [options]
|
|
122
|
+
* @param {{ cwd?: string, signal?: AbortSignal, timeoutMs?: number, maxBytes?: number, outputLimitAction?: 'stop'|'head-tail', env?: NodeJS.ProcessEnv, preserveCarriageReturns?: boolean, killGraceMs?: number, gracefulTerminationDeadline?: number, terminationDeadline?: number, forceSettleMs?: number, treeKillTimeoutMs?: number, requireExitConfirmation?: boolean, requireProcessGroupExit?: boolean, systemdScope?: { unit: string, systemctlPath: string, env?: NodeJS.ProcessEnv } | null, onSettled?: (() => void) | null, platform?: NodeJS.Platform, spawnProcess?: typeof spawn, spawnProcessSync?: typeof spawnSync }} [options]
|
|
123
123
|
* @returns {Promise<{ code: number, stdout: string, stderr: string, truncated: boolean, timedOut: boolean, terminationError?: string }>}
|
|
124
124
|
*/
|
|
125
125
|
export function runProcess(command, args, options = {}) {
|
|
@@ -135,6 +135,7 @@ export function runProcess(command, args, options = {}) {
|
|
|
135
135
|
const maxBytes = Number.isFinite(options.maxBytes)
|
|
136
136
|
? Math.max(0, options.maxBytes)
|
|
137
137
|
: DEFAULT_MAX_BYTES;
|
|
138
|
+
const keepOutputTail = options.outputLimitAction === 'head-tail';
|
|
138
139
|
const killGraceMs = Number.isFinite(options.killGraceMs)
|
|
139
140
|
? Math.max(0, options.killGraceMs)
|
|
140
141
|
: DEFAULT_KILL_GRACE_MS;
|
|
@@ -212,6 +213,18 @@ export function runProcess(command, args, options = {}) {
|
|
|
212
213
|
};
|
|
213
214
|
const decode = (chunks, wasTruncated, preserveCarriageReturns = false) => {
|
|
214
215
|
const decoder = new StringDecoder('utf8');
|
|
216
|
+
if (wasTruncated && keepOutputTail) {
|
|
217
|
+
const buffer = Buffer.concat(chunks);
|
|
218
|
+
const marker = '\n[Process output middle omitted]\n';
|
|
219
|
+
const budget = Math.max(0, maxBytes - Buffer.byteLength(marker));
|
|
220
|
+
const headEnd = Math.floor(budget / 2);
|
|
221
|
+
let tailStart = buffer.length - (budget - headEnd);
|
|
222
|
+
while (tailStart < buffer.length && (buffer[tailStart] & 0xc0) === 0x80) tailStart += 1;
|
|
223
|
+
const head = decoder.write(buffer.subarray(0, headEnd));
|
|
224
|
+
const tail = new StringDecoder('utf8').write(buffer.subarray(tailStart));
|
|
225
|
+
const value = head + marker.slice(0, maxBytes) + tail;
|
|
226
|
+
return preserveCarriageReturns ? value : value.replace(/\r/g, '');
|
|
227
|
+
}
|
|
215
228
|
let value = decoder.write(Buffer.concat(chunks));
|
|
216
229
|
if (!wasTruncated) value += decoder.end();
|
|
217
230
|
return preserveCarriageReturns ? value : value.replace(/\r/g, '');
|
|
@@ -357,6 +370,21 @@ export function runProcess(command, args, options = {}) {
|
|
|
357
370
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
358
371
|
const current = isStdout ? stdoutBytes : stderrBytes;
|
|
359
372
|
const remaining = maxBytes - current;
|
|
373
|
+
if (keepOutputTail && (buffer.length > remaining || (isStdout ? stdoutTruncated : stderrTruncated))) {
|
|
374
|
+
const prior = Buffer.concat(target);
|
|
375
|
+
const headSize = Math.floor(maxBytes / 2);
|
|
376
|
+
const tailSize = maxBytes - headSize;
|
|
377
|
+
const head = prior.length >= headSize ? prior.subarray(0, headSize)
|
|
378
|
+
: Buffer.concat([prior, buffer.subarray(0, headSize - prior.length)]);
|
|
379
|
+
const tail = buffer.length >= tailSize ? buffer.subarray(buffer.length - tailSize)
|
|
380
|
+
: Buffer.concat([prior.subarray(Math.max(0, prior.length - (tailSize - buffer.length))), buffer]);
|
|
381
|
+
// Copy slices so a tiny retained tail cannot retain an unbounded chunk.
|
|
382
|
+
target.splice(0, target.length, Buffer.from(head), Buffer.from(tail));
|
|
383
|
+
truncated = true;
|
|
384
|
+
if (isStdout) { stdoutTruncated = true; stdoutBytes = maxBytes; }
|
|
385
|
+
else { stderrTruncated = true; stderrBytes = maxBytes; }
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
360
388
|
if (remaining <= 0) {
|
|
361
389
|
truncated = true;
|
|
362
390
|
if (isStdout) stdoutTruncated = true;
|
package/yeaft/tools/registry.js
CHANGED
|
@@ -216,6 +216,14 @@ export function isToolErrorOutput(output) {
|
|
|
216
216
|
return parseToolErrorOutput(output) !== null;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
// Only an explicit, side-effect-free validation envelope qualifies. Runtime
|
|
220
|
+
// failures (network, tests, IO) must never be guessed to be invalid arguments.
|
|
221
|
+
export function toolValidationError(output) {
|
|
222
|
+
const parsed = parseToolErrorOutput(output);
|
|
223
|
+
return parsed?.code === 'invalid_arguments' && parsed.errorEffect === 'none'
|
|
224
|
+
? parsed.error : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
219
227
|
export function toolErrorEffect(output) {
|
|
220
228
|
return parseToolErrorOutput(output)?.errorEffect === 'none' ? 'none' : 'unknown';
|
|
221
229
|
}
|
|
@@ -245,6 +253,16 @@ export function truncateToolResultIfNeeded(output, { toolName, language } = {})
|
|
|
245
253
|
}
|
|
246
254
|
marker = truncateUtf8(marker, TOOL_RESULT_MAX_BYTES);
|
|
247
255
|
const contentBudget = Math.max(0, TOOL_RESULT_MAX_BYTES - Buffer.byteLength(marker, 'utf8'));
|
|
256
|
+
if (toolName === 'Bash' && contentBudget > 256) {
|
|
257
|
+
const omission = normalizeLanguage(language) === 'zh'
|
|
258
|
+
? '\n[中间输出省略;以下为末尾]\n' : '\n[Middle omitted; output tail follows]\n';
|
|
259
|
+
const budget = contentBudget - Buffer.byteLength(omission, 'utf8');
|
|
260
|
+
const headBudget = Math.floor(budget / 2);
|
|
261
|
+
const buffer = Buffer.from(text, 'utf8');
|
|
262
|
+
let start = buffer.length - (budget - headBudget);
|
|
263
|
+
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) start += 1;
|
|
264
|
+
return truncateUtf8(text, headBudget) + omission + buffer.subarray(start).toString('utf8') + marker;
|
|
265
|
+
}
|
|
248
266
|
return truncateUtf8(text, contentBudget) + marker;
|
|
249
267
|
}
|
|
250
268
|
|
|
@@ -59,7 +59,8 @@ export default defineTool({
|
|
|
59
59
|
agent.budget = { ...agent.budget, ...input.budget };
|
|
60
60
|
if (grants) agent.allowTools = grants.tools;
|
|
61
61
|
agent.controlRevision = (agent.controlRevision || 0) + 1;
|
|
62
|
-
if (agent.execution && agent.
|
|
62
|
+
if (agent.execution && (agent.budget.max_tool_calls === undefined
|
|
63
|
+
|| agent.execution.toolCalls < agent.budget.max_tool_calls * 0.75)) agent.execution.warning = null;
|
|
63
64
|
if (!agent.budgetReportStarted) {
|
|
64
65
|
agent.toolBudgetReason = null;
|
|
65
66
|
agent.executionBudgetReason = null;
|