@yeaft/webchat-agent 1.0.567 → 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/local-runtime/web/app.bundle.js +229 -120
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- 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/work-center/attachments.js +28 -0
- package/yeaft/work-center/bridge.js +3 -2
- package/yeaft/work-center/controller.js +3 -3
- package/yeaft/work-center/durable-model.js +18 -10
- package/yeaft/work-center/projection.js +27 -3
- package/yeaft/work-center/recurrence.js +103 -0
- package/yeaft/work-center/resource-control.js +2 -4
- package/yeaft/work-center/service.js +30 -3
- package/yeaft/work-center/store.js +137 -24
- package/yeaft/work-center/transaction.js +21 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Constants TOOL_LOOP_REFLECTION_INTERVAL, TURN_SUMMARY_THRESHOLD,
|
|
6
6
|
* DUP_TOOL_THRESHOLD
|
|
7
7
|
* - Reflector helpers (T1 sync, T2 async, fallback stub)
|
|
8
|
-
* - Helpers for collapsing message ranges into a single
|
|
8
|
+
* - Helpers for collapsing message ranges into a single synthetic user
|
|
9
9
|
* reflection message
|
|
10
10
|
* - Duplicate-reminder text formatter
|
|
11
11
|
*
|
|
@@ -42,13 +42,13 @@ export { buildFallbackStub } from './fallback-stub.js';
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Collapse messages[startIdx..endIdx] (inclusive) into a single
|
|
45
|
-
* `{ role: 'user', content }` reflection message. Returns
|
|
46
|
-
* not mutate the input.
|
|
45
|
+
* `{ role: 'user', content }` reflection message. Returns an explicit
|
|
46
|
+
* replacement record; does not mutate the input.
|
|
47
47
|
*
|
|
48
48
|
* The original assistant+tool sequence (the action arc) is replaced by ONE
|
|
49
49
|
* synthetic user message carrying the reflection summary. User messages
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* inside the range are preserved before the summary, including appended
|
|
51
|
+
* prompts, async completion notifications and duplicate-call reminders.
|
|
52
52
|
*
|
|
53
53
|
* Why role='user' (not 'assistant'):
|
|
54
54
|
* The Anthropic Messages API requires the messages array to end with a
|
|
@@ -68,11 +68,13 @@ export { buildFallbackStub } from './fallback-stub.js';
|
|
|
68
68
|
* @param {number} startIdx
|
|
69
69
|
* @param {number} endIdx
|
|
70
70
|
* @param {string} reflectionContent
|
|
71
|
-
* @returns {Array}
|
|
71
|
+
* @returns {{messages: Array, reflection: object|null, foldedMessages: Array}}
|
|
72
72
|
*/
|
|
73
73
|
export function collapseRangeToReflection(messages, startIdx, endIdx, reflectionContent) {
|
|
74
|
-
if (!Array.isArray(messages))
|
|
75
|
-
|
|
74
|
+
if (!Array.isArray(messages) || !Number.isInteger(startIdx) || !Number.isInteger(endIdx)
|
|
75
|
+
|| startIdx < 0 || endIdx < startIdx || endIdx >= messages.length) {
|
|
76
|
+
return { messages, reflection: null, foldedMessages: [] };
|
|
77
|
+
}
|
|
76
78
|
const before = messages.slice(0, startIdx);
|
|
77
79
|
const collapsed = messages.slice(startIdx, endIdx + 1);
|
|
78
80
|
const after = messages.slice(endIdx + 1);
|
|
@@ -101,7 +103,13 @@ export function collapseRangeToReflection(messages, startIdx, endIdx, reflection
|
|
|
101
103
|
content: wrappedContent,
|
|
102
104
|
_reflection: true,
|
|
103
105
|
};
|
|
104
|
-
return
|
|
106
|
+
return {
|
|
107
|
+
messages: [...before, ...preservedUsers, reflectionMsg, ...after],
|
|
108
|
+
reflection: reflectionMsg,
|
|
109
|
+
// Only replaced rows may be tombstoned. Preserved users include real
|
|
110
|
+
// appended prompts and internal completion/reminder messages.
|
|
111
|
+
foldedMessages: collapsed.filter(m => m && m.role !== 'user'),
|
|
112
|
+
};
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
/**
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { buildReflectionPrompt } from './reflection-prompt.js';
|
|
19
|
+
import { normalizeTokenUsage } from '../llm/usage-accounting.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* @param {{
|
|
@@ -26,22 +27,42 @@ import { buildReflectionPrompt } from './reflection-prompt.js';
|
|
|
26
27
|
* assistantText?: string,
|
|
27
28
|
* language?: string,
|
|
28
29
|
* signal?: AbortSignal,
|
|
30
|
+
* onComplete?: (diagnostic: object) => void,
|
|
29
31
|
* }} p
|
|
30
|
-
* @returns {Promise<{ content: string, durationMs: number }>}
|
|
32
|
+
* @returns {Promise<{ content: string, durationMs: number, usage: object }>}
|
|
31
33
|
*/
|
|
32
|
-
export async function runT1Reflection({ adapter, model, originalUserMsg, toolPairs, assistantText, language, signal }) {
|
|
34
|
+
export async function runT1Reflection({ adapter, model, originalUserMsg, toolPairs, assistantText, language, signal, onComplete }) {
|
|
33
35
|
const t0 = Date.now();
|
|
34
36
|
const prompt = buildReflectionPrompt({ originalUserMsg, toolPairs, assistantText, language });
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
let result;
|
|
38
|
+
let failure;
|
|
39
|
+
try {
|
|
40
|
+
result = await adapter.call({
|
|
41
|
+
model,
|
|
42
|
+
system: prompt,
|
|
43
|
+
messages: [{ role: 'user', content: 'Produce the reflection now.' }],
|
|
44
|
+
maxTokens: 2048,
|
|
45
|
+
signal,
|
|
46
|
+
});
|
|
47
|
+
const content = (result && typeof result.text === 'string') ? result.text.trim() : '';
|
|
48
|
+
if (!content) {
|
|
49
|
+
throw new Error('T1 reflection returned empty content');
|
|
50
|
+
}
|
|
51
|
+
return { content, durationMs: Date.now() - t0, usage: normalizeTokenUsage(result?.usage) };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
failure = error;
|
|
54
|
+
throw error;
|
|
55
|
+
} finally {
|
|
56
|
+
// Diagnostic only; the adapter owns billing/usage accounting. Never
|
|
57
|
+
// double-charge it or fail a completed reflection on a logging error.
|
|
58
|
+
try {
|
|
59
|
+
onComplete?.({
|
|
60
|
+
status: failure ? 'error' : 'ready',
|
|
61
|
+
durationMs: Date.now() - t0,
|
|
62
|
+
usage: normalizeTokenUsage(result?.usage),
|
|
63
|
+
usageReported: !!result?.usage,
|
|
64
|
+
...(failure ? { error: String(failure.message || failure).slice(0, 512) } : {}),
|
|
65
|
+
});
|
|
66
|
+
} catch { /* best-effort diagnostics */ }
|
|
45
67
|
}
|
|
46
|
-
return { content, durationMs: Date.now() - t0 };
|
|
47
68
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { buildReflectionPrompt } from './reflection-prompt.js';
|
|
12
|
+
import { normalizeTokenUsage } from '../llm/usage-accounting.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @param {{
|
|
@@ -19,22 +20,42 @@ import { buildReflectionPrompt } from './reflection-prompt.js';
|
|
|
19
20
|
* assistantText?: string,
|
|
20
21
|
* language?: string,
|
|
21
22
|
* signal?: AbortSignal,
|
|
23
|
+
* onComplete?: (diagnostic: object) => void,
|
|
22
24
|
* }} p
|
|
23
|
-
* @returns {Promise<{ content: string, durationMs: number }>}
|
|
25
|
+
* @returns {Promise<{ content: string, durationMs: number, usage: object }>}
|
|
24
26
|
*/
|
|
25
|
-
export async function runT2Reflection({ adapter, model, originalUserMsg, toolPairs, assistantText, language, signal }) {
|
|
27
|
+
export async function runT2Reflection({ adapter, model, originalUserMsg, toolPairs, assistantText, language, signal, onComplete }) {
|
|
26
28
|
const t0 = Date.now();
|
|
27
29
|
const prompt = buildReflectionPrompt({ originalUserMsg, toolPairs, assistantText, language });
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
let result;
|
|
31
|
+
let failure;
|
|
32
|
+
try {
|
|
33
|
+
result = await adapter.call({
|
|
34
|
+
model,
|
|
35
|
+
system: prompt,
|
|
36
|
+
messages: [{ role: 'user', content: 'Produce the reflection now.' }],
|
|
37
|
+
maxTokens: 2048,
|
|
38
|
+
signal,
|
|
39
|
+
});
|
|
40
|
+
const content = (result && typeof result.text === 'string') ? result.text.trim() : '';
|
|
41
|
+
if (!content) {
|
|
42
|
+
throw new Error('T2 reflection returned empty content');
|
|
43
|
+
}
|
|
44
|
+
return { content, durationMs: Date.now() - t0, usage: normalizeTokenUsage(result?.usage) };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
failure = error;
|
|
47
|
+
throw error;
|
|
48
|
+
} finally {
|
|
49
|
+
// Diagnostic only; the adapter owns billing/usage accounting. Never
|
|
50
|
+
// double-charge it or fail a completed reflection on a logging error.
|
|
51
|
+
try {
|
|
52
|
+
onComplete?.({
|
|
53
|
+
status: failure ? 'error' : 'ready',
|
|
54
|
+
durationMs: Date.now() - t0,
|
|
55
|
+
usage: normalizeTokenUsage(result?.usage),
|
|
56
|
+
usageReported: !!result?.usage,
|
|
57
|
+
...(failure ? { error: String(failure.message || failure).slice(0, 512) } : {}),
|
|
58
|
+
});
|
|
59
|
+
} catch { /* best-effort diagnostics */ }
|
|
38
60
|
}
|
|
39
|
-
return { content, durationMs: Date.now() - t0 };
|
|
40
61
|
}
|
|
@@ -26,6 +26,7 @@ export const ALWAYS_VISIBLE_TOOL_NAMES = Object.freeze([
|
|
|
26
26
|
export const BACKGROUND_TASK_TOOL_NAMES = Object.freeze([
|
|
27
27
|
'ListTasks',
|
|
28
28
|
'ReadTaskLog',
|
|
29
|
+
'WaitTask',
|
|
29
30
|
'CancelTask',
|
|
30
31
|
]);
|
|
31
32
|
|
|
@@ -44,6 +45,7 @@ export const CONDITIONAL_BUILTIN_TOOL_NAMES = new Set([
|
|
|
44
45
|
'ApplyPatch',
|
|
45
46
|
'ListTasks',
|
|
46
47
|
'ReadTaskLog',
|
|
48
|
+
'WaitTask',
|
|
47
49
|
'CancelTask',
|
|
48
50
|
'SpawnAgent',
|
|
49
51
|
'UpdateAgent',
|
package/yeaft/tools/agent.js
CHANGED
|
@@ -159,6 +159,8 @@ export function checkBudget(agent, now = Date.now()) {
|
|
|
159
159
|
export function budgetExceededResult(agent, reason) {
|
|
160
160
|
return {
|
|
161
161
|
status: 'budget_exceeded',
|
|
162
|
+
outcome: 'incomplete',
|
|
163
|
+
complete: false,
|
|
162
164
|
partial_output: agent.partial_output || agent.result || '',
|
|
163
165
|
reason,
|
|
164
166
|
usage: { ...(agent.usage || {}) },
|
|
@@ -168,7 +170,8 @@ export function budgetExceededResult(agent, reason) {
|
|
|
168
170
|
/**
|
|
169
171
|
* Apply an incremental delta to an agent's usage, then check budget.
|
|
170
172
|
* If exceeded: abort the agent's signal, set result to the budget envelope,
|
|
171
|
-
*
|
|
173
|
+
* end the lifecycle as 'completed' with an explicitly incomplete outcome, and
|
|
174
|
+
* return the envelope. Otherwise returns null.
|
|
172
175
|
*
|
|
173
176
|
* Call this at each turn boundary inside the sub-agent's execution loop.
|
|
174
177
|
*
|
package/yeaft/tools/bash.js
CHANGED
|
@@ -166,7 +166,7 @@ Guidelines:
|
|
|
166
166
|
},
|
|
167
167
|
required: ['command'],
|
|
168
168
|
},
|
|
169
|
-
errorOutput:
|
|
169
|
+
errorOutput: 'json-error-envelope',
|
|
170
170
|
// Foreground Bash owns a bounded timeout and process-tree cleanup state
|
|
171
171
|
// machine. A second ToolRegistry timer can preempt that cleanup and turn an
|
|
172
172
|
// owned exit 124 into a fatal orphan, so it must stay disabled for this tool.
|
|
@@ -217,7 +217,7 @@ Guidelines:
|
|
|
217
217
|
threadId: ctx.threadId || 'main',
|
|
218
218
|
},
|
|
219
219
|
});
|
|
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
|
|
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 WaitTask for bounded waiting, ReadTaskLog for output, ListTasks for active status, or CancelTask when cancellation is intended.`;
|
|
221
221
|
} catch (err) {
|
|
222
222
|
throw new Error(err?.message || String(err));
|
|
223
223
|
}
|
|
@@ -258,10 +258,35 @@ Guidelines:
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
const output = parts.join('\n');
|
|
261
|
-
if (result.exitCode !== 0) {
|
|
262
|
-
|
|
261
|
+
if (result.exitCode !== 0 || result.timedOut || result.terminationError) {
|
|
262
|
+
const failureType = result.timedOut
|
|
263
|
+
? (result.terminationError ? 'timeout_unconfirmed' : 'timeout_confirmed')
|
|
264
|
+
: result.terminationError || result.exitCode === null ? 'exit_unconfirmed' : 'exit_nonzero';
|
|
265
|
+
return JSON.stringify({
|
|
266
|
+
error: result.timedOut
|
|
267
|
+
? `Command timed out after ${timeout}ms${result.terminationError ? '; process-tree termination was not confirmed' : ''}.`
|
|
268
|
+
: `Command exited with code ${result.exitCode}.`,
|
|
269
|
+
code: `bash_${failureType}`,
|
|
270
|
+
errorEffect: 'unknown',
|
|
271
|
+
failureType,
|
|
272
|
+
exitCode: Number.isInteger(result.exitCode) ? result.exitCode : null,
|
|
273
|
+
status: `Exit code: ${result.exitCode}`,
|
|
274
|
+
workingDirectory: `Working directory: ${cwd}`,
|
|
275
|
+
timedOut: result.timedOut === true,
|
|
276
|
+
terminationConfirmed: result.timedOut ? !result.terminationError : null,
|
|
277
|
+
terminationError: result.terminationError || null,
|
|
278
|
+
cwd,
|
|
279
|
+
output: output || '(no output)',
|
|
280
|
+
replaySafe: false,
|
|
281
|
+
});
|
|
263
282
|
}
|
|
264
|
-
|
|
283
|
+
// A successful command may print application JSON containing `error`.
|
|
284
|
+
// Do not let it masquerade as a tool-level failure envelope.
|
|
285
|
+
let successfulOutput = output || '(no output)';
|
|
286
|
+
try {
|
|
287
|
+
if (JSON.parse(successfulOutput)?.error) successfulOutput = `Exit code: 0\n${successfulOutput}`;
|
|
288
|
+
} catch { /* ordinary command output */ }
|
|
289
|
+
return successfulOutput;
|
|
265
290
|
} catch (err) {
|
|
266
291
|
err.message = `${err.message} (working directory: ${cwd})`;
|
|
267
292
|
if (err?.name === 'ProcessTerminationError') err.fatalToolTimeout = true;
|
|
@@ -22,9 +22,12 @@ export default defineTool({
|
|
|
22
22
|
isReadOnly: () => false,
|
|
23
23
|
async execute(input = {}, ctx = {}) {
|
|
24
24
|
if (!ctx.taskManager) return JSON.stringify({ error: 'task manager unavailable' });
|
|
25
|
+
if (ctx.sessionId && input.sessionId && input.sessionId !== ctx.sessionId) {
|
|
26
|
+
return JSON.stringify({ error: 'Task access is limited to the current Session', errorEffect: 'none' });
|
|
27
|
+
}
|
|
25
28
|
const taskId = input.taskId;
|
|
26
29
|
if (!taskId) return JSON.stringify({ error: 'taskId is required' });
|
|
27
30
|
const sessionId = input.sessionId || ctx.sessionId || 'default';
|
|
28
|
-
return JSON.stringify(ctx.taskManager.cancelTask(sessionId, taskId), null, 2);
|
|
31
|
+
return JSON.stringify(ctx.taskManager.cancelTask(sessionId, taskId, ctx.currentVpId || null), null, 2);
|
|
29
32
|
},
|
|
30
33
|
});
|
|
@@ -12,6 +12,7 @@ import { agentBelongsToCaller, getAgentRegistry } from './agent.js';
|
|
|
12
12
|
import { isTerminalAgentStatus, STATUS } from '../sub-agent/status.js';
|
|
13
13
|
import { consumeNotificationForAgent, enqueueTerminalNotification } from '../sub-agent/notifications.js';
|
|
14
14
|
import { snapshotLiveness } from '../sub-agent/liveness.js';
|
|
15
|
+
import { describeAgentLifecycle, describeAgentOutcome } from '../sub-agent/outcome.js';
|
|
15
16
|
|
|
16
17
|
export default defineTool({
|
|
17
18
|
name: 'CloseAgent',
|
|
@@ -73,7 +74,7 @@ Do NOT end your turn silently right after CloseAgent.`,
|
|
|
73
74
|
return JSON.stringify({ next_steps: ERROR_NEXT_STEPS, error: `Agent not found: ${agent_id}` });
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
if (result) {
|
|
77
|
+
if (result && !isTerminalAgentStatus(agent.status)) {
|
|
77
78
|
agent.result = result;
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -86,7 +87,7 @@ Do NOT end your turn silently right after CloseAgent.`,
|
|
|
86
87
|
|
|
87
88
|
const finalResult = (typeof agent.result === 'string' && agent.result)
|
|
88
89
|
? agent.result
|
|
89
|
-
: (agent.lastResult || '');
|
|
90
|
+
: (agent.result?.partial_output || agent.lastResult || '');
|
|
90
91
|
|
|
91
92
|
// If the agent had already gone terminal (e.g. failed) before we got
|
|
92
93
|
// here, preserve that status; otherwise mark closed. Either way drain
|
|
@@ -139,6 +140,10 @@ Do NOT end your turn silently right after CloseAgent.`,
|
|
|
139
140
|
agentId: agent_id,
|
|
140
141
|
name: agent.name,
|
|
141
142
|
status: agent.status,
|
|
143
|
+
lifecycle: describeAgentLifecycle(agent),
|
|
144
|
+
outcome: describeAgentOutcome(agent),
|
|
145
|
+
incomplete: !describeAgentOutcome(agent).complete,
|
|
146
|
+
final_report: agent.result?.final_report || agent.finalReport || null,
|
|
142
147
|
result: finalResult,
|
|
143
148
|
outputFile: agent.outputFile || null,
|
|
144
149
|
liveness: snapshotLiveness(agent.liveness),
|
|
@@ -32,7 +32,8 @@ development without file conflicts. Useful for:
|
|
|
32
32
|
- Parallel feature development
|
|
33
33
|
|
|
34
34
|
The worktree is created in .yeaft/worktrees/ with a new branch based on HEAD.
|
|
35
|
-
Returns the worktree path and branch name
|
|
35
|
+
Returns the worktree path and branch name. Does NOT switch the execution cwd.
|
|
36
|
+
Use the returned path explicitly for Bash/GitRead cwd, file paths, and child-agent cwd.`,
|
|
36
37
|
zh: `创建一个独立的 git worktree 用于开发。
|
|
37
38
|
|
|
38
39
|
创建带独立分支的新 git worktree,允许并行开发无文件冲突。适用于:
|
|
@@ -40,7 +41,7 @@ Returns the worktree path and branch name.`,
|
|
|
40
41
|
- 合并前隔离测试改动
|
|
41
42
|
- 并行功能开发
|
|
42
43
|
|
|
43
|
-
Worktree 创建在 .yeaft/worktrees/ 中,基于 HEAD 创建新分支。返回 worktree
|
|
44
|
+
Worktree 创建在 .yeaft/worktrees/ 中,基于 HEAD 创建新分支。返回 worktree 路径和分支名,不切换执行 cwd;后续 Bash/GitRead、文件路径及子 Agent cwd 必须显式使用返回路径。`
|
|
44
45
|
},
|
|
45
46
|
parameters: {
|
|
46
47
|
type: 'object',
|
|
@@ -107,6 +108,9 @@ Worktree 创建在 .yeaft/worktrees/ 中,基于 HEAD 创建新分支。返回
|
|
|
107
108
|
branch: branchName,
|
|
108
109
|
baseRef,
|
|
109
110
|
name,
|
|
111
|
+
executionCwd: resolve(cwd),
|
|
112
|
+
cwdChanged: false,
|
|
113
|
+
nextStep: 'Use path explicitly as Bash/GitRead/SpawnAgent cwd or as the base for absolute file paths.',
|
|
110
114
|
message: `Created worktree "${name}" at ${worktreeDir} on branch ${branchName}`,
|
|
111
115
|
});
|
|
112
116
|
} catch (err) {
|
package/yeaft/tools/git-read.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
2
3
|
import { defineTool } from './types.js';
|
|
3
4
|
import { runProcess } from './process-runner.js';
|
|
4
5
|
|
|
@@ -58,9 +59,9 @@ function errorOutput(message, operation) {
|
|
|
58
59
|
// arguments belonging to another operation.
|
|
59
60
|
function normalizeInput(input) {
|
|
60
61
|
const result = { ...input };
|
|
61
|
-
for (const key of ['base', 'head', 'revision', 'paths', 'limit']) {
|
|
62
|
+
for (const key of ['cwd', 'base', 'head', 'revision', 'paths', 'limit']) {
|
|
62
63
|
if (result[key] === null || result[key] === undefined
|
|
63
|
-
|| (['base', 'head', 'revision'].includes(key) && result[key] === '')
|
|
64
|
+
|| (['cwd', 'base', 'head', 'revision'].includes(key) && result[key] === '')
|
|
64
65
|
|| (key === 'paths' && Array.isArray(result[key]) && result[key].length === 0)
|
|
65
66
|
|| (key === 'limit' && result.operation !== 'log' && result[key] === DEFAULT_LOG_LIMIT)) {
|
|
66
67
|
delete result[key];
|
|
@@ -101,18 +102,22 @@ export function buildGitReadArgs(input) {
|
|
|
101
102
|
|
|
102
103
|
input = normalizeInput(input);
|
|
103
104
|
const { operation } = input;
|
|
105
|
+
if (input.cwd !== undefined && (typeof input.cwd !== 'string'
|
|
106
|
+
|| input.cwd.length > MAX_VALUE_LENGTH || /[\0\r\n]/u.test(input.cwd))) {
|
|
107
|
+
return { error: 'cwd must be a path of at most 4096 characters without NUL or newlines' };
|
|
108
|
+
}
|
|
104
109
|
if (!['status', 'diff', 'show', 'log'].includes(operation)) {
|
|
105
110
|
return { error: 'operation must be one of: status, diff, show, log' };
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
if (operation === 'status') {
|
|
109
|
-
const error = unexpectedInput(input, new Set(['operation']));
|
|
114
|
+
const error = unexpectedInput(input, new Set(['operation', 'cwd']));
|
|
110
115
|
if (error) return { error };
|
|
111
116
|
return { args: [...COMMON_ARGS, 'status', '--short', '--branch', '--untracked-files=normal', '--ignore-submodules=all'] };
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
if (operation === 'diff') {
|
|
115
|
-
const error = unexpectedInput(input, new Set(['operation', 'base', 'head', 'paths']))
|
|
120
|
+
const error = unexpectedInput(input, new Set(['operation', 'cwd', 'base', 'head', 'paths']))
|
|
116
121
|
|| validatePaths(input.paths)
|
|
117
122
|
|| (input.base !== undefined ? validateValue(input.base, 'base') : null)
|
|
118
123
|
|| (input.head !== undefined ? validateValue(input.head, 'head') : null);
|
|
@@ -133,7 +138,7 @@ export function buildGitReadArgs(input) {
|
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
if (operation === 'show') {
|
|
136
|
-
const error = unexpectedInput(input, new Set(['operation', 'revision', 'paths']))
|
|
141
|
+
const error = unexpectedInput(input, new Set(['operation', 'cwd', 'revision', 'paths']))
|
|
137
142
|
|| validatePaths(input.paths)
|
|
138
143
|
|| (input.revision !== undefined ? validateValue(input.revision, 'revision') : null);
|
|
139
144
|
if (error) return { error };
|
|
@@ -146,7 +151,7 @@ export function buildGitReadArgs(input) {
|
|
|
146
151
|
};
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
const error = unexpectedInput(input, new Set(['operation', 'revision', 'limit']))
|
|
154
|
+
const error = unexpectedInput(input, new Set(['operation', 'cwd', 'revision', 'limit']))
|
|
150
155
|
|| (input.revision !== undefined ? validateValue(input.revision, 'revision') : null);
|
|
151
156
|
if (error) return { error };
|
|
152
157
|
const limit = input.limit === undefined ? DEFAULT_LOG_LIMIT : input.limit;
|
|
@@ -170,13 +175,15 @@ function takeUtf8(text, maxBytes) {
|
|
|
170
175
|
return buffer.subarray(0, end).toString('utf8');
|
|
171
176
|
}
|
|
172
177
|
|
|
173
|
-
function formatSuccess(operation, result) {
|
|
178
|
+
function formatSuccess(operation, result, resolvedCwd) {
|
|
174
179
|
const sections = [];
|
|
175
180
|
if (result.stdout) sections.push(`STDOUT:\n${result.stdout}`);
|
|
176
181
|
if (result.stderr) sections.push(`STDERR:\n${result.stderr}`);
|
|
177
182
|
const body = sections.join('\n');
|
|
178
183
|
const baseHeader = truncated => [
|
|
179
184
|
`operation: ${operation}`,
|
|
185
|
+
`resolvedCwd: ${JSON.stringify(takeUtf8(resolvedCwd ?? '', 1024))}`,
|
|
186
|
+
...(Buffer.byteLength(resolvedCwd || '', 'utf8') > 1024 ? ['cwdTruncated: true'] : []),
|
|
180
187
|
`exitCode: ${result.code}`,
|
|
181
188
|
'timedOut: false',
|
|
182
189
|
`truncated: ${truncated}`,
|
|
@@ -226,7 +233,7 @@ function boundedFailure(fields, output = '') {
|
|
|
226
233
|
|
|
227
234
|
export function formatGitReadResult(operation, result, { resolvedCwd, stage = operation } = {}) {
|
|
228
235
|
if (result.code === 0 && !result.timedOut && !result.truncated && !result.terminationError) {
|
|
229
|
-
return formatSuccess(operation, result);
|
|
236
|
+
return formatSuccess(operation, result, resolvedCwd);
|
|
230
237
|
}
|
|
231
238
|
const code = result.terminationError ? 'git_exit_unconfirmed'
|
|
232
239
|
: result.timedOut ? 'git_timeout'
|
|
@@ -260,6 +267,8 @@ Supported operations are intentionally limited:
|
|
|
260
267
|
- show: exactly one commit (HEAD by default; tags are peeled to commits), optionally narrowed by paths. Ranges and non-commit objects are rejected.
|
|
261
268
|
- log: a compact bounded commit list (20 entries by default, maximum 50).
|
|
262
269
|
|
|
270
|
+
cwd defaults to the execution directory; an override must resolve to the same repository (including linked worktrees). Creation does not switch cwd. Every result identifies the resolved directory.
|
|
271
|
+
|
|
263
272
|
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.`,
|
|
264
273
|
zh: `有界读取本地 Git 证据,不使用 shell,也不访问网络。
|
|
265
274
|
|
|
@@ -269,12 +278,15 @@ GitRead never fetches, writes Git state, or creates worktrees. It disables pager
|
|
|
269
278
|
- show:显示唯一提交(默认 HEAD;tag 解析到 commit),可用 paths 缩小范围。拒绝范围及非 commit 对象。
|
|
270
279
|
- log:紧凑且有界的提交列表(默认 20 条,最多 50 条)。
|
|
271
280
|
|
|
281
|
+
cwd 默认执行目录;指定目录必须属于同一仓库(可为关联 worktree)。创建 worktree 不切换 cwd,需显式指定。结果始终标明实际目录。
|
|
282
|
+
|
|
272
283
|
GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、external diff、textconv、内容 filter、optional locks、fsmonitor 和子模块遍历。LFS 等 filter 文件显示原始工作区字节,子模块状态需单独检查。拒绝以 "-" 开头的 revision 与路径;结果明确标识是否截断;Git 失败、超时及捕获上限终止返回含有界诊断的错误。`,
|
|
273
284
|
},
|
|
274
285
|
parameters: {
|
|
275
286
|
type: 'object',
|
|
276
287
|
additionalProperties: false,
|
|
277
288
|
properties: {
|
|
289
|
+
cwd: { type: 'string', maxLength: MAX_VALUE_LENGTH, description: 'Optional repository/worktree directory; relative to execution cwd. Must belong to the same repository. Empty means execution cwd.' },
|
|
278
290
|
operation: { type: 'string', enum: ['status', 'diff', 'show', 'log'] },
|
|
279
291
|
base: { type: 'string', maxLength: MAX_VALUE_LENGTH, description: 'diff only; empty/omitted means working-tree changes against HEAD' },
|
|
280
292
|
head: { type: 'string', maxLength: MAX_VALUE_LENGTH, description: 'diff only; requires base, empty/omitted defaults to HEAD' },
|
|
@@ -296,11 +308,16 @@ GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、e
|
|
|
296
308
|
const built = buildGitReadArgs(input);
|
|
297
309
|
if (built.error) return errorOutput(built.error, input?.operation);
|
|
298
310
|
input = normalizeInput(input);
|
|
299
|
-
const
|
|
311
|
+
const contextCwd = resolve(ctx?.cwd || process.cwd());
|
|
312
|
+
let cwd = resolve(contextCwd, input.cwd || '.');
|
|
300
313
|
let stage = input.operation;
|
|
301
314
|
try {
|
|
302
315
|
const run = ctx?.[RUN_PROCESS_OVERRIDE] || runProcess;
|
|
303
316
|
const startedAt = Date.now();
|
|
317
|
+
// Ambient Git location overrides must not redirect evidence elsewhere.
|
|
318
|
+
const env = { ...process.env };
|
|
319
|
+
for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE',
|
|
320
|
+
'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_NAMESPACE']) delete env[key];
|
|
304
321
|
const options = {
|
|
305
322
|
cwd,
|
|
306
323
|
signal: ctx?.signal,
|
|
@@ -308,7 +325,7 @@ GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、e
|
|
|
308
325
|
maxBytes: MAX_CAPTURE_BYTES,
|
|
309
326
|
requireExitConfirmation: true,
|
|
310
327
|
env: {
|
|
311
|
-
...
|
|
328
|
+
...env,
|
|
312
329
|
GIT_PAGER: 'cat',
|
|
313
330
|
PAGER: 'cat',
|
|
314
331
|
GIT_EXTERNAL_DIFF: '',
|
|
@@ -318,13 +335,38 @@ GitRead 不 fetch、不写 Git 状态、不创建 worktree。它禁用 pager、e
|
|
|
318
335
|
NO_COLOR: '1',
|
|
319
336
|
},
|
|
320
337
|
};
|
|
321
|
-
const read = (command, args) => {
|
|
338
|
+
const read = (command, args, readCwd = cwd) => {
|
|
322
339
|
const remaining = TIMEOUT_MS - (Date.now() - startedAt);
|
|
323
340
|
if (remaining <= 0) {
|
|
324
341
|
throw Object.assign(new Error('Git read timed out'), { code: 'git_timeout', timedOut: true });
|
|
325
342
|
}
|
|
326
|
-
return run(command, args, { ...options, timeoutMs: remaining });
|
|
343
|
+
return run(command, args, { ...options, cwd: readCwd, timeoutMs: remaining });
|
|
327
344
|
};
|
|
345
|
+
if (cwd !== contextCwd) {
|
|
346
|
+
stage = 'resolve_cwd';
|
|
347
|
+
cwd = await realpath(cwd);
|
|
348
|
+
const commonDirectory = async directory => {
|
|
349
|
+
const result = await read('git', [
|
|
350
|
+
...COMMON_ARGS, 'rev-parse', '--git-common-dir',
|
|
351
|
+
], directory);
|
|
352
|
+
if (result.code !== 0 || result.truncated || result.timedOut || result.terminationError) {
|
|
353
|
+
throw Object.assign(new Error('Cannot resolve Git repository identity'), { result });
|
|
354
|
+
}
|
|
355
|
+
const value = result.stdout.replace(/\r?\n$/, '');
|
|
356
|
+
if (!value) throw new Error('Missing Git common directory');
|
|
357
|
+
// --git-common-dir may be relative on older Git. Avoid
|
|
358
|
+
// --path-format (unsupported versions echo it as a path).
|
|
359
|
+
return realpath(resolve(directory, value));
|
|
360
|
+
};
|
|
361
|
+
const owner = await commonDirectory(contextCwd);
|
|
362
|
+
if (await commonDirectory(cwd) !== owner) {
|
|
363
|
+
return boundedFailure({
|
|
364
|
+
error: 'cwd must belong to the execution repository or one of its linked worktrees',
|
|
365
|
+
errorEffect: 'none', code: 'git_cwd_outside_repository', operation: input.operation,
|
|
366
|
+
stage, resolvedCwd: cwd,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
328
370
|
// Only these operations inspect worktree bytes. Object-only reads must
|
|
329
371
|
// not pay for filter discovery or fail on an unusable worktree filter.
|
|
330
372
|
const readsWorktree = input.operation === 'status'
|
package/yeaft/tools/index.js
CHANGED
|
@@ -43,6 +43,7 @@ import diskUsage from './disk-usage.js';
|
|
|
43
43
|
import applyPatch from './apply-patch.js';
|
|
44
44
|
import listTasks from './list-tasks.js';
|
|
45
45
|
import readTaskLog from './read-task-log.js';
|
|
46
|
+
import waitTask from './wait-task.js';
|
|
46
47
|
import cancelTask from './cancel-task.js';
|
|
47
48
|
|
|
48
49
|
// --- P1 Agent tools ---
|
|
@@ -109,6 +110,7 @@ export const allTools = [
|
|
|
109
110
|
applyPatch,
|
|
110
111
|
listTasks,
|
|
111
112
|
readTaskLog,
|
|
113
|
+
waitTask,
|
|
112
114
|
cancelTask,
|
|
113
115
|
|
|
114
116
|
// P1 Agent
|
|
@@ -4,6 +4,7 @@ import { defineTool } from './types.js';
|
|
|
4
4
|
import { agentBelongsToCaller, getAgentRegistry } from './agent.js';
|
|
5
5
|
import { isTerminalAgentStatus, STATUS } from '../sub-agent/status.js';
|
|
6
6
|
import { diagnoseAgentLiveness } from '../sub-agent/liveness.js';
|
|
7
|
+
import { describeAgentLifecycle, describeAgentOutcome } from '../sub-agent/outcome.js';
|
|
7
8
|
|
|
8
9
|
function nextStepFor(agent, liveness) {
|
|
9
10
|
if (isTerminalAgentStatus(agent.status)) {
|
|
@@ -69,6 +70,8 @@ By default terminal agents are omitted. Pass include_closed=true to include them
|
|
|
69
70
|
id,
|
|
70
71
|
name: agent.name,
|
|
71
72
|
status: agent.status,
|
|
73
|
+
lifecycle: describeAgentLifecycle(agent),
|
|
74
|
+
outcome: describeAgentOutcome(agent),
|
|
72
75
|
task: typeof agent.task === 'string' ? agent.task.slice(0, 200) : null,
|
|
73
76
|
outputFile: agent.outputFile || null,
|
|
74
77
|
activity: {
|
|
@@ -44,14 +44,21 @@ export default defineTool({
|
|
|
44
44
|
cacheWithinQuery: false,
|
|
45
45
|
async execute(input = {}, ctx = {}) {
|
|
46
46
|
if (!ctx.taskManager) return JSON.stringify({ error: 'task manager unavailable' });
|
|
47
|
+
if (ctx.sessionId && input.sessionId && input.sessionId !== ctx.sessionId) {
|
|
48
|
+
return JSON.stringify({ error: 'Task access is limited to the current Session', errorEffect: 'none' });
|
|
49
|
+
}
|
|
47
50
|
const sessionId = input.sessionId || ctx.sessionId || null;
|
|
48
|
-
const
|
|
51
|
+
const ownerVpId = ctx.currentVpId || null;
|
|
52
|
+
const activeTasks = ownerVpId
|
|
53
|
+
? ctx.taskManager.listActiveTasks(sessionId, ownerVpId)
|
|
54
|
+
: ctx.taskManager.listActiveTasks(sessionId);
|
|
55
|
+
const tasks = activeTasks
|
|
49
56
|
.map(compactTaskSnapshot)
|
|
50
57
|
.filter(Boolean);
|
|
51
58
|
return JSON.stringify({
|
|
52
59
|
tasks,
|
|
53
60
|
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.'
|
|
61
|
+
? 'WaitTask waits by task id without reading the log. 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
62
|
: 'No active tasks require follow-up.',
|
|
56
63
|
});
|
|
57
64
|
},
|
|
@@ -27,6 +27,9 @@ export default defineTool({
|
|
|
27
27
|
duplicateCallPolicy: () => 'allow',
|
|
28
28
|
async execute(input = {}, ctx = {}) {
|
|
29
29
|
if (!ctx.taskManager) return JSON.stringify({ error: 'task manager unavailable' });
|
|
30
|
+
if (ctx.sessionId && input.sessionId && input.sessionId !== ctx.sessionId) {
|
|
31
|
+
return JSON.stringify({ error: 'Task access is limited to the current Session', errorEffect: 'none' });
|
|
32
|
+
}
|
|
30
33
|
const taskId = input.taskId;
|
|
31
34
|
if (!taskId) return JSON.stringify({ error: 'taskId is required' });
|
|
32
35
|
const sessionId = input.sessionId || ctx.sessionId || 'default';
|
|
@@ -35,7 +38,7 @@ export default defineTool({
|
|
|
35
38
|
offset: input.offset,
|
|
36
39
|
maxBytes: input.maxBytes,
|
|
37
40
|
tail: typeof input.tail === 'boolean' ? input.tail : !hasOffset,
|
|
38
|
-
});
|
|
39
|
-
return JSON.stringify(result, null, 2);
|
|
41
|
+
}, ctx.currentVpId || null);
|
|
42
|
+
return JSON.stringify(result || { error: `Unknown task: ${taskId}` }, null, 2);
|
|
40
43
|
},
|
|
41
44
|
});
|