@yeaft/webchat-agent 1.0.568 → 1.0.569
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 +54 -31
- package/yeaft/sub-agent/execution-control.js +11 -0
- package/yeaft/sub-agent/notifications.js +19 -3
- package/yeaft/sub-agent/outcome.js +39 -0
- package/yeaft/sub-agent/runner.js +54 -3
- package/yeaft/sub-agent/status.js +3 -3
- package/yeaft/tasks/manager.js +80 -8
- package/yeaft/tool-folding/index.js +17 -9
- package/yeaft/tool-folding/t1-reflector.js +34 -13
- package/yeaft/tool-folding/t2-reflector.js +34 -13
- package/yeaft/tools/activation.js +2 -0
- package/yeaft/tools/agent.js +4 -1
- package/yeaft/tools/bash.js +30 -5
- package/yeaft/tools/cancel-task.js +4 -1
- package/yeaft/tools/close-agent.js +7 -2
- package/yeaft/tools/enter-worktree.js +6 -2
- package/yeaft/tools/git-read.js +54 -12
- package/yeaft/tools/index.js +2 -0
- package/yeaft/tools/list-agents.js +3 -0
- package/yeaft/tools/list-tasks.js +9 -2
- package/yeaft/tools/read-task-log.js +5 -2
- package/yeaft/tools/registry.js +29 -0
- package/yeaft/tools/update-agent.js +14 -5
- package/yeaft/tools/wait-agent.js +16 -0
- package/yeaft/tools/wait-task.js +73 -0
package/yeaft/tools/registry.js
CHANGED
|
@@ -242,6 +242,35 @@ export function truncateToolResultIfNeeded(output, { toolName, language } = {})
|
|
|
242
242
|
const originalBytes = Buffer.byteLength(text, 'utf8');
|
|
243
243
|
if (originalBytes <= TOOL_RESULT_MAX_BYTES) return text;
|
|
244
244
|
|
|
245
|
+
// Preserve the error contract in the model projection; raw tool output
|
|
246
|
+
// remains available to tracing/persistence before this budget is applied.
|
|
247
|
+
if (toolName === 'Bash') {
|
|
248
|
+
const failure = parseToolErrorOutput(text);
|
|
249
|
+
if (failure && failure.code?.startsWith('bash_')) {
|
|
250
|
+
const projected = { ...failure, truncated: true, originalBytes };
|
|
251
|
+
for (const [key, value] of Object.entries(projected)) {
|
|
252
|
+
if (key !== 'output' && typeof value === 'string') projected[key] = truncateUtf8(value, 1024);
|
|
253
|
+
}
|
|
254
|
+
const source = typeof failure.output === 'string' ? failure.output : '';
|
|
255
|
+
const buffer = Buffer.from(source, 'utf8');
|
|
256
|
+
const sample = bytes => {
|
|
257
|
+
const half = Math.floor(bytes / 2);
|
|
258
|
+
let start = Math.max(0, buffer.length - half);
|
|
259
|
+
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) start += 1;
|
|
260
|
+
return truncateUtf8(source, half) + '\n[Middle omitted by tool result budget]\n' + buffer.subarray(start).toString('utf8');
|
|
261
|
+
};
|
|
262
|
+
let low = 0, high = Math.min(buffer.length, TOOL_RESULT_MAX_BYTES);
|
|
263
|
+
while (low < high) {
|
|
264
|
+
const mid = Math.ceil((low + high) / 2);
|
|
265
|
+
projected.output = sample(mid);
|
|
266
|
+
if (Buffer.byteLength(JSON.stringify(projected), 'utf8') <= TOOL_RESULT_MAX_BYTES) low = mid;
|
|
267
|
+
else high = mid - 1;
|
|
268
|
+
}
|
|
269
|
+
projected.output = sample(low);
|
|
270
|
+
return JSON.stringify(projected);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
245
274
|
const markerFor = name => normalizeLanguage(language) === 'zh'
|
|
246
275
|
? `\n\n[已截断:${name} 返回 ${formatSize(originalBytes)},上限为 ${formatSize(TOOL_RESULT_MAX_BYTES)};原因:单个 tool result 超过 ${formatSize(TOOL_RESULT_MAX_BYTES)},模型消息历史不会看到剩余内容]`
|
|
247
276
|
: `\n\n[truncated: ${name} returned ${formatSize(originalBytes)}, capped at ${formatSize(TOOL_RESULT_MAX_BYTES)}; reason: single tool result exceeded ${formatSize(TOOL_RESULT_MAX_BYTES)}, the model message history will not see the rest]`;
|
|
@@ -9,8 +9,8 @@ import { diagnoseAgentLiveness } from '../sub-agent/liveness.js';
|
|
|
9
9
|
export default defineTool({
|
|
10
10
|
name: 'UpdateAgent',
|
|
11
11
|
description: {
|
|
12
|
-
en: 'Adjust a live child in place after inspecting its progress: absolute lifetime
|
|
13
|
-
zh: '检查进展后原地调整活跃子 Agent
|
|
12
|
+
en: 'Adjust a live child in place after inspecting its progress: absolute lifetime budgets, extra tool grants, or request a cooperative evidence-only final report. Finalization is control, not a prompt containing reason. Does not reset usage or revive a terminal/reporting child. Already dispatched work is not undone.',
|
|
13
|
+
zh: '检查进展后原地调整活跃子 Agent:累计预算、额外工具授权,或请求基于现有证据协作收尾。收尾是控制信号,不会把 reason 冒充提示词。不清零用量、不复活终止/报告中的任务,也不撤回已派发操作。',
|
|
14
14
|
},
|
|
15
15
|
parameters: {
|
|
16
16
|
type: 'object',
|
|
@@ -32,6 +32,10 @@ export default defineTool({
|
|
|
32
32
|
type: 'array', items: { type: 'string' }, maxItems: 32,
|
|
33
33
|
description: { en: 'Replace extra persona grants with these canonical parent tool names (e.g. Bash, FileEdit); [] revokes extras, omission leaves grants unchanged', zh: '替换 persona 额外授权(如 Bash、FileEdit);[] 撤销额外授权,省略则不变' },
|
|
34
34
|
},
|
|
35
|
+
request_finalize: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
description: { en: 'Request one tool-free final report from existing evidence, then end the child lifecycle normally', zh: '请求仅基于现有证据生成一次无工具最终报告,然后正常结束子任务生命周期' },
|
|
38
|
+
},
|
|
35
39
|
},
|
|
36
40
|
required: ['agent_id', 'reason'],
|
|
37
41
|
},
|
|
@@ -42,10 +46,10 @@ export default defineTool({
|
|
|
42
46
|
const fail = error => JSON.stringify({ error, next_steps: 'Inspect the current agent state and correct the request; do not respawn or repeat blindly.' });
|
|
43
47
|
const agent = getAgentRegistry().get(input.agent_id);
|
|
44
48
|
if (!agent || !agentBelongsToCaller(agent, ctx)) return fail(`Agent not found: ${input.agent_id}`);
|
|
45
|
-
if (isTerminalAgentStatus(agent.status) || agent.budgetReportStarted || agent.budgetStopReason
|
|
49
|
+
if (isTerminalAgentStatus(agent.status) || agent.budgetReportStarted || agent.finalizationRequested || agent.budgetStopReason
|
|
46
50
|
|| agent.abortController?.signal.aborted) return fail('Agent is terminal, stopping or already reporting; it cannot be extended');
|
|
47
51
|
if (typeof input.reason !== 'string' || !input.reason.trim() || input.reason.length > 2000) return fail('reason must contain 1..2000 characters of evidence and remaining work');
|
|
48
|
-
if (input.budget === undefined && input.allow_tools === undefined) return fail('budget or
|
|
52
|
+
if (input.budget === undefined && input.allow_tools === undefined && input.request_finalize !== true) return fail('budget, allow_tools, or request_finalize=true is required');
|
|
49
53
|
if (input.budget !== undefined) {
|
|
50
54
|
const error = validateBudget(input.budget);
|
|
51
55
|
if (error) return fail(error);
|
|
@@ -58,6 +62,7 @@ export default defineTool({
|
|
|
58
62
|
// All validation precedes mutation. Original usage/deadline origin are retained.
|
|
59
63
|
agent.budget = { ...agent.budget, ...input.budget };
|
|
60
64
|
if (grants) agent.allowTools = grants.tools;
|
|
65
|
+
if (input.request_finalize === true) agent.finalizationRequested = true;
|
|
61
66
|
agent.controlRevision = (agent.controlRevision || 0) + 1;
|
|
62
67
|
if (agent.execution && (agent.budget.max_tool_calls === undefined
|
|
63
68
|
|| agent.execution.toolCalls < agent.budget.max_tool_calls * 0.75)) agent.execution.warning = null;
|
|
@@ -68,6 +73,7 @@ export default defineTool({
|
|
|
68
73
|
agent.refreshToolPolicy?.();
|
|
69
74
|
agent.rearmWallTimeWatchdog?.();
|
|
70
75
|
const event = { type: 'sub_agent_control_updated', at: Date.now(), reason: input.reason.trim(),
|
|
76
|
+
requestFinalize: input.request_finalize === true,
|
|
71
77
|
previousBudget, budget: { ...agent.budget }, previousTools, allowTools: [...(agent.allowTools || [])] };
|
|
72
78
|
agent.diagnostics ||= [];
|
|
73
79
|
agent.diagnostics.push(event);
|
|
@@ -75,6 +81,9 @@ export default defineTool({
|
|
|
75
81
|
try { agent.outputLog?.write(event); } catch { /* diagnostics must not fail the applied update */ }
|
|
76
82
|
return JSON.stringify({ success: true, agentId: agent.id, status: agent.status,
|
|
77
83
|
budget: agent.budget, allow_tools: agent.allowTools || [], liveness: diagnoseAgentLiveness(agent),
|
|
78
|
-
|
|
84
|
+
finalizationRequested: agent.finalizationRequested === true,
|
|
85
|
+
next_steps: input.request_finalize === true
|
|
86
|
+
? 'Cooperative wrap-up requested without turning reason into a child prompt. Use WaitAgent to collect the evidence-only final report; already dispatched work may finish first.'
|
|
87
|
+
: 'Adjustment applied without restarting work. Continue the parent task; use PromptAgent only if new guidance is needed, then collect its reply. Revocation does not cancel already dispatched work.' });
|
|
79
88
|
},
|
|
80
89
|
});
|
|
@@ -35,6 +35,7 @@ import { agentBelongsToCaller, getAgentRegistry } from './agent.js';
|
|
|
35
35
|
import { isTerminalAgentStatus, STATUS } from '../sub-agent/status.js';
|
|
36
36
|
import { diagnoseAgentLiveness } from '../sub-agent/liveness.js';
|
|
37
37
|
import { consumeNotificationForAgent } from '../sub-agent/notifications.js';
|
|
38
|
+
import { describeAgentLifecycle, describeAgentOutcome } from '../sub-agent/outcome.js';
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Build the status-specific next-step guidance the LLM reads after a wait.
|
|
@@ -55,6 +56,9 @@ function nextStepsFor(status, opts = {}) {
|
|
|
55
56
|
'as an ordinary successful completion.'
|
|
56
57
|
);
|
|
57
58
|
}
|
|
59
|
+
if (opts.incomplete) {
|
|
60
|
+
return 'Sub-agent lifecycle ended with incomplete evidence. Inspect outcome and final_report; do not present partial verdict text as a completed review.';
|
|
61
|
+
}
|
|
58
62
|
if (opts.timedOut && opts.stale) {
|
|
59
63
|
return (
|
|
60
64
|
'No observable event arrived within the diagnostic threshold. This does ' +
|
|
@@ -157,12 +161,15 @@ function buildEnvelope(agent, { timedOut = false } = {}) {
|
|
|
157
161
|
next_steps: nextStepsFor(status, {
|
|
158
162
|
timedOut,
|
|
159
163
|
budgetExceeded: !!budgetResult,
|
|
164
|
+
incomplete: isTerminalAgentStatus(status) && !describeAgentOutcome(agent).complete,
|
|
160
165
|
stale: liveness.stale,
|
|
161
166
|
mustCollectReply: mustCollectReply && !liveness.stale,
|
|
162
167
|
}),
|
|
163
168
|
agentId: agent.id,
|
|
164
169
|
name: agent.name,
|
|
165
170
|
status,
|
|
171
|
+
lifecycle: describeAgentLifecycle(agent),
|
|
172
|
+
outcome: describeAgentOutcome(agent),
|
|
166
173
|
error: agent.error || null,
|
|
167
174
|
outputFile: agent.outputFile || null,
|
|
168
175
|
liveness,
|
|
@@ -185,8 +192,17 @@ function buildEnvelope(agent, { timedOut = false } = {}) {
|
|
|
185
192
|
env.budget_status = budgetResult.status;
|
|
186
193
|
env.budget_reason = budgetResult.reason || null;
|
|
187
194
|
env.partial_output = budgetResult.partial_output || '';
|
|
195
|
+
env.incomplete = true;
|
|
196
|
+
env.truncated = Boolean(budgetResult.truncated || budgetResult.final_report?.truncated);
|
|
197
|
+
env.final_report = budgetResult.final_report || null;
|
|
188
198
|
env.budget_usage = budgetResult.usage || null;
|
|
189
199
|
}
|
|
200
|
+
if (agent.finalizationRequested) {
|
|
201
|
+
env.incomplete = true;
|
|
202
|
+
env.final_report = agent.finalReport || null;
|
|
203
|
+
env.truncated = Boolean(agent.finalReport?.truncated);
|
|
204
|
+
env.partial_output = resultText;
|
|
205
|
+
}
|
|
190
206
|
env.result = resultText;
|
|
191
207
|
return env;
|
|
192
208
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wait-task.js — Bounded wait for one Session background task.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineTool } from './types.js';
|
|
6
|
+
import { isTerminalTaskStatus } from '../tasks/store.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
9
|
+
const MAX_TIMEOUT_MS = 600_000;
|
|
10
|
+
|
|
11
|
+
export function taskWaitResult(result) {
|
|
12
|
+
if (!result?.ok) return { error: result?.error || 'Unable to wait for task' };
|
|
13
|
+
const task = result.task;
|
|
14
|
+
if (!task) return { error: 'Task state unavailable' };
|
|
15
|
+
const log = task.log && typeof task.log === 'object' ? task.log : {};
|
|
16
|
+
const taskResult = task.result && typeof task.result === 'object' ? task.result : {};
|
|
17
|
+
return {
|
|
18
|
+
taskId: task.id,
|
|
19
|
+
status: task.status,
|
|
20
|
+
terminal: isTerminalTaskStatus(task.status),
|
|
21
|
+
timedOut: result.timedOut === true,
|
|
22
|
+
exitCode: Number.isInteger(taskResult.exitCode) ? taskResult.exitCode : null,
|
|
23
|
+
signal: taskResult.signal || null,
|
|
24
|
+
resultDelivery: task.resultDelivery,
|
|
25
|
+
resultConsumed: false,
|
|
26
|
+
...(task.runtime?.subAgentId ? { agentId: task.runtime.subAgentId } : {}),
|
|
27
|
+
...(task.runtime?.cancelRequestedAt ? { cancelPending: !isTerminalTaskStatus(task.status) } : {}),
|
|
28
|
+
next_steps: 'Status only; no output was consumed. ReadTaskLog with your last read offset (or tail) for evidence; use WaitAgent for a child result. Do not use log.endOffset as an already-read cursor.',
|
|
29
|
+
error: taskResult.error || null,
|
|
30
|
+
log: {
|
|
31
|
+
...(log.path ? { path: log.path } : {}),
|
|
32
|
+
bytes: Number.isFinite(log.bytes) ? log.bytes : 0,
|
|
33
|
+
endOffset: Number.isFinite(log.bytes) ? log.bytes : 0,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default defineTool({
|
|
39
|
+
name: 'WaitTask',
|
|
40
|
+
description: {
|
|
41
|
+
en: 'Wait for one background task by taskId, up to a bounded timeout. Returns terminal status and a log cursor/reference, never the whole log. This does not cancel or retry the task.',
|
|
42
|
+
zh: '按 taskId 有界等待一个后台任务。仅返回终态和日志游标/引用,不返回完整日志;不会取消或重试任务。',
|
|
43
|
+
},
|
|
44
|
+
parameters: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
taskId: { type: 'string', description: { en: 'Task id', zh: '任务 ID' } },
|
|
48
|
+
sessionId: { type: 'string', description: { en: 'Session id (defaults to current Session)', zh: 'Session ID(默认当前 Session)' } },
|
|
49
|
+
timeout_ms: { type: 'number', description: { en: `Wait timeout in milliseconds (default ${DEFAULT_TIMEOUT_MS}, max ${MAX_TIMEOUT_MS})`, zh: `等待超时毫秒数(默认 ${DEFAULT_TIMEOUT_MS},最大 ${MAX_TIMEOUT_MS})` } },
|
|
50
|
+
},
|
|
51
|
+
required: ['taskId'],
|
|
52
|
+
},
|
|
53
|
+
timeoutMs: 0,
|
|
54
|
+
isConcurrencySafe: () => true,
|
|
55
|
+
isReadOnly: () => true,
|
|
56
|
+
cacheWithinQuery: false,
|
|
57
|
+
duplicateCallPolicy: () => 'allow',
|
|
58
|
+
async execute(input = {}, ctx = {}) {
|
|
59
|
+
if (!ctx.taskManager) return JSON.stringify({ error: 'task manager unavailable' });
|
|
60
|
+
if (ctx.sessionId && input.sessionId && input.sessionId !== ctx.sessionId) {
|
|
61
|
+
return JSON.stringify({ error: 'Task access is limited to the current Session', errorEffect: 'none' });
|
|
62
|
+
}
|
|
63
|
+
if (!input.taskId) return JSON.stringify({ error: 'taskId is required' });
|
|
64
|
+
const sessionId = input.sessionId || ctx.sessionId || 'default';
|
|
65
|
+
const timeoutMs = Math.min(Math.max(Number.isFinite(input.timeout_ms) ? input.timeout_ms : DEFAULT_TIMEOUT_MS, 0), MAX_TIMEOUT_MS);
|
|
66
|
+
const result = await ctx.taskManager.waitForTask(sessionId, input.taskId, {
|
|
67
|
+
timeoutMs,
|
|
68
|
+
signal: ctx.signal || null,
|
|
69
|
+
ownerVpId: ctx.currentVpId || null,
|
|
70
|
+
});
|
|
71
|
+
return JSON.stringify(taskWaitResult(result), null, 2);
|
|
72
|
+
},
|
|
73
|
+
});
|