@nowcrew/daemon 0.5.21 → 0.5.22
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/dist/console-formatter.js +60 -0
- package/dist/console-payload.js +73 -0
- package/dist/console.js +11 -5
- package/dist/execution-event-limit.js +5 -0
- package/dist/execution-protocol.js +1 -0
- package/dist/execution-runner.js +1 -0
- package/dist/local-executor.js +4 -2
- package/dist/unified-diff.js +84 -0
- package/package.json +2 -2
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 单次 runtime execution 的 console 格式化器。
|
|
3
|
+
* 用 tool_use id 关联 Read 结果,避免将整文件作为普通 tool_result 倾泻到终端。
|
|
4
|
+
*/
|
|
5
|
+
import { buildFilePreview } from "./console-payload.js";
|
|
6
|
+
import { toConsoleLines, TOOL_RESULT_CAP } from "./console.js";
|
|
7
|
+
import { parseUnifiedDiff } from "./unified-diff.js";
|
|
8
|
+
const extract = (content) => {
|
|
9
|
+
if (typeof content === "string")
|
|
10
|
+
return content;
|
|
11
|
+
if (!Array.isArray(content))
|
|
12
|
+
return "";
|
|
13
|
+
return content.map((item) => item && typeof item === "object" && typeof item.text === "string"
|
|
14
|
+
? item.text : "").filter(Boolean).join("\n");
|
|
15
|
+
};
|
|
16
|
+
const clip = (text) => text.length > TOOL_RESULT_CAP
|
|
17
|
+
? `${text.slice(0, TOOL_RESULT_CAP)}… (+${text.length - TOOL_RESULT_CAP})`
|
|
18
|
+
: text;
|
|
19
|
+
export function createConsoleFormatter() {
|
|
20
|
+
const pending = new Map();
|
|
21
|
+
return {
|
|
22
|
+
format(event) {
|
|
23
|
+
const rec = event && typeof event === "object" ? event : {};
|
|
24
|
+
const message = rec.message && typeof rec.message === "object" ? rec.message : undefined;
|
|
25
|
+
if (rec.type === "assistant" && Array.isArray(message?.content)) {
|
|
26
|
+
for (const block of message.content) {
|
|
27
|
+
if (block.type === "tool_use" && block.id && block.name) {
|
|
28
|
+
pending.set(block.id, { name: block.name, ...(block.input ? { input: block.input } : {}) });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (rec.type === "user" && Array.isArray(message?.content)) {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const block of message.content) {
|
|
35
|
+
if (block.type !== "tool_result")
|
|
36
|
+
continue;
|
|
37
|
+
const text = extract(block.content);
|
|
38
|
+
if (!text)
|
|
39
|
+
continue;
|
|
40
|
+
const tool = block.tool_use_id ? pending.get(block.tool_use_id) : undefined;
|
|
41
|
+
if (block.tool_use_id)
|
|
42
|
+
pending.delete(block.tool_use_id);
|
|
43
|
+
if (!block.is_error && tool?.name === "Read" && typeof tool.input?.file_path === "string") {
|
|
44
|
+
const start = typeof tool.input.offset === "number" && Number.isSafeInteger(tool.input.offset)
|
|
45
|
+
? Math.max(1, tool.input.offset) : 1;
|
|
46
|
+
const preview = buildFilePreview("read", tool.input.file_path, text, start);
|
|
47
|
+
out.push({ stream: "tool_result", text: `Read ${preview.totalLines} line(s) from ${preview.path}`, payload: preview });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const patch = block.is_error ? null : parseUnifiedDiff(text);
|
|
51
|
+
out.push(patch
|
|
52
|
+
? { stream: "tool_result", text: `Changed ${patch.files.length} file(s): +${patch.additions} -${patch.deletions}`, payload: patch }
|
|
53
|
+
: { stream: "tool_result", text: clip(text) });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
return toConsoleLines(event);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentConsole 的结构化展示协议与有界构造器。
|
|
3
|
+
* 所有代码/文件预览在 daemon 边界裁剪,避免 server/web 接收整文件。
|
|
4
|
+
*/
|
|
5
|
+
export const CONSOLE_PREVIEW_ROWS = 12;
|
|
6
|
+
export const CONSOLE_PREVIEW_HEAD_ROWS = 8;
|
|
7
|
+
export const CONSOLE_DIFF_ROWS = 40;
|
|
8
|
+
export const CONSOLE_ROW_CHARS = 240;
|
|
9
|
+
const clipRow = (text) => text.length > CONSOLE_ROW_CHARS
|
|
10
|
+
? `${text.slice(0, CONSOLE_ROW_CHARS)}…`
|
|
11
|
+
: text;
|
|
12
|
+
/** 头 8 + 尾 4,中间显式 omitted;小文件全部显示。 */
|
|
13
|
+
export function buildFilePreview(operation, path, content, startLine = 1) {
|
|
14
|
+
const lines = content ? content.split("\n") : [];
|
|
15
|
+
const visible = lines.length <= CONSOLE_PREVIEW_ROWS
|
|
16
|
+
? lines.map((text, index) => ({ type: "line", line: startLine + index, text: clipRow(text) }))
|
|
17
|
+
: [
|
|
18
|
+
...lines.slice(0, CONSOLE_PREVIEW_HEAD_ROWS).map((text, index) => ({
|
|
19
|
+
type: "line", line: startLine + index, text: clipRow(text),
|
|
20
|
+
})),
|
|
21
|
+
{ type: "omitted", count: lines.length - CONSOLE_PREVIEW_ROWS },
|
|
22
|
+
...lines.slice(-4).map((text, index) => ({
|
|
23
|
+
type: "line", line: startLine + lines.length - 4 + index, text: clipRow(text),
|
|
24
|
+
})),
|
|
25
|
+
];
|
|
26
|
+
return {
|
|
27
|
+
kind: "file_preview",
|
|
28
|
+
operation,
|
|
29
|
+
path,
|
|
30
|
+
totalLines: lines.length,
|
|
31
|
+
totalBytes: Buffer.byteLength(content, "utf8"),
|
|
32
|
+
rows: visible,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** old/new 片段转紧凑带双行号 diff;变更中段超过预算时裁剪。 */
|
|
36
|
+
export function buildSnippetDiff(path, oldText, newText) {
|
|
37
|
+
const oldLines = oldText ? oldText.split("\n") : [];
|
|
38
|
+
const newLines = newText ? newText.split("\n") : [];
|
|
39
|
+
let prefix = 0;
|
|
40
|
+
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix])
|
|
41
|
+
prefix++;
|
|
42
|
+
let oldEnd = oldLines.length;
|
|
43
|
+
let newEnd = newLines.length;
|
|
44
|
+
while (oldEnd > prefix && newEnd > prefix && oldLines[oldEnd - 1] === newLines[newEnd - 1]) {
|
|
45
|
+
oldEnd--;
|
|
46
|
+
newEnd--;
|
|
47
|
+
}
|
|
48
|
+
const rows = [];
|
|
49
|
+
const contextStart = Math.max(0, prefix - 2);
|
|
50
|
+
for (let index = contextStart; index < prefix; index++) {
|
|
51
|
+
rows.push({ type: "context", oldLine: index + 1, newLine: index + 1, text: clipRow(oldLines[index]) });
|
|
52
|
+
}
|
|
53
|
+
for (let index = prefix; index < oldEnd; index++) {
|
|
54
|
+
rows.push({ type: "delete", oldLine: index + 1, newLine: null, text: clipRow(oldLines[index]) });
|
|
55
|
+
}
|
|
56
|
+
for (let index = prefix; index < newEnd; index++) {
|
|
57
|
+
rows.push({ type: "add", oldLine: null, newLine: index + 1, text: clipRow(newLines[index]) });
|
|
58
|
+
}
|
|
59
|
+
for (let offset = 0; offset < Math.min(2, oldLines.length - oldEnd); offset++) {
|
|
60
|
+
rows.push({ type: "context", oldLine: oldEnd + offset + 1, newLine: newEnd + offset + 1, text: clipRow(oldLines[oldEnd + offset]) });
|
|
61
|
+
}
|
|
62
|
+
const additions = Math.max(0, newEnd - prefix);
|
|
63
|
+
const deletions = Math.max(0, oldEnd - prefix);
|
|
64
|
+
if (rows.length <= CONSOLE_DIFF_ROWS)
|
|
65
|
+
return { kind: "diff_rows", files: [{ path, rows }], additions, deletions };
|
|
66
|
+
const head = rows.slice(0, 28);
|
|
67
|
+
const tail = rows.slice(-11);
|
|
68
|
+
return {
|
|
69
|
+
kind: "diff_rows",
|
|
70
|
+
files: [{ path, rows: [...head, { type: "omitted", oldCount: rows.length - 39, newCount: rows.length - 39 }, ...tail] }],
|
|
71
|
+
additions, deletions, truncated: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/console.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* kimi(OpenAI 消息风格行)。
|
|
14
14
|
*/
|
|
15
15
|
import { detectDaemonLang, translateDaemon } from "./i18n.js";
|
|
16
|
+
import { buildFilePreview, buildSnippetDiff } from "./console-payload.js";
|
|
17
|
+
import { parseUnifiedDiff } from "./unified-diff.js";
|
|
16
18
|
/** 单条工具返回正文上限(超出截断并标注),避免单条把终端/DB 撑爆。 */
|
|
17
19
|
export const TOOL_RESULT_CAP = 4000;
|
|
18
20
|
/** 工具输入摘要上限(标题行那一段)。 */
|
|
@@ -95,10 +97,10 @@ function toolPayload(name, input) {
|
|
|
95
97
|
return { kind: "command", command: clip(input.command, PAYLOAD_TEXT_CAP) };
|
|
96
98
|
}
|
|
97
99
|
if (name === "Edit" && typeof input.file_path === "string") {
|
|
98
|
-
return
|
|
100
|
+
return buildSnippetDiff(input.file_path, typeof input.old_string === "string" ? input.old_string : "", typeof input.new_string === "string" ? input.new_string : "");
|
|
99
101
|
}
|
|
100
102
|
if (name === "Write" && typeof input.file_path === "string" && typeof input.content === "string") {
|
|
101
|
-
return
|
|
103
|
+
return buildFilePreview("write", input.file_path, input.content);
|
|
102
104
|
}
|
|
103
105
|
if (name === "TodoWrite") {
|
|
104
106
|
const todos = normalizeTodos(input.todos);
|
|
@@ -125,7 +127,7 @@ function toolUseChunks(name, input) {
|
|
|
125
127
|
chunks.push({
|
|
126
128
|
stream: "tool",
|
|
127
129
|
text: `⏺ Edit(${clip(file, TOOL_INPUT_CAP)})`,
|
|
128
|
-
payload:
|
|
130
|
+
payload: buildSnippetDiff(file, typeof rec.old_string === "string" ? rec.old_string : "", typeof rec.new_string === "string" ? rec.new_string : ""),
|
|
129
131
|
});
|
|
130
132
|
}
|
|
131
133
|
if (chunks.length)
|
|
@@ -170,8 +172,12 @@ function codexItemChunks(item, td) {
|
|
|
170
172
|
},
|
|
171
173
|
}];
|
|
172
174
|
const output = item.aggregated_output?.trim();
|
|
173
|
-
if (output)
|
|
174
|
-
|
|
175
|
+
if (output) {
|
|
176
|
+
const patch = parseUnifiedDiff(output);
|
|
177
|
+
out.push(patch
|
|
178
|
+
? { stream: "tool_result", text: `Changed ${patch.files.length} file(s): +${patch.additions} -${patch.deletions}`, payload: patch }
|
|
179
|
+
: { stream: "tool_result", text: clip(output, TOOL_RESULT_CAP) });
|
|
180
|
+
}
|
|
175
181
|
return out;
|
|
176
182
|
}
|
|
177
183
|
if (item.type === "file_change" && Array.isArray(item.changes)) {
|
|
@@ -38,6 +38,11 @@ export function boundExecutionFrame(input, maxBytes) {
|
|
|
38
38
|
}
|
|
39
39
|
else if (input.type === "execution:console" || input.type === "execution:output") {
|
|
40
40
|
frame = withBoundedString(frame, "text", maxBytes, false);
|
|
41
|
+
// 富 payload 是可选增强;事件超限时先丢 payload,保留必需 text 帧。
|
|
42
|
+
if (executionFrameBytes(frame) > maxBytes && "payload" in frame) {
|
|
43
|
+
const { payload: _payload, ...withoutPayload } = frame;
|
|
44
|
+
frame = withoutPayload;
|
|
45
|
+
}
|
|
41
46
|
}
|
|
42
47
|
else if (input.type === "execution:rejected") {
|
|
43
48
|
frame = withBoundedString(frame, "message", maxBytes, true);
|
|
@@ -191,6 +191,7 @@ export const ExecutionConsoleSchema = z.object({
|
|
|
191
191
|
executionId: ExecutionIdSchema,
|
|
192
192
|
stream: ConsoleStreamSchema,
|
|
193
193
|
text: z.string(),
|
|
194
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
194
195
|
seq: SequenceSchema,
|
|
195
196
|
at: TimestampSchema,
|
|
196
197
|
}).strict();
|
package/dist/execution-runner.js
CHANGED
|
@@ -422,6 +422,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
422
422
|
executionId: spec.executionId,
|
|
423
423
|
stream: chunk.stream,
|
|
424
424
|
text: chunk.text,
|
|
425
|
+
...(chunk.payload ? { payload: chunk.payload } : {}),
|
|
425
426
|
seq: consoleSequence++,
|
|
426
427
|
at: now().toISOString(),
|
|
427
428
|
});
|
package/dist/local-executor.js
CHANGED
|
@@ -9,7 +9,7 @@ import { applyProviderEnv, providerFingerprint } from "./provider-env.js";
|
|
|
9
9
|
import { augmentedPath } from "./runtime-path.js";
|
|
10
10
|
import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
|
|
11
11
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
12
|
-
import {
|
|
12
|
+
import { createConsoleFormatter } from "./console-formatter.js";
|
|
13
13
|
import { capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
14
14
|
import { decodeExternalOutputEvent, extractExternalAnswer, ExternalAnswerDecoder, stripExternalAnswerMarkers, } from "./external-output.js";
|
|
15
15
|
import { cleanupMaterializedAttachments as cleanupAttachments, executionAttachmentDirectory, materializeAttachments, } from "./attachments.js";
|
|
@@ -300,6 +300,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
300
300
|
let finalText = null;
|
|
301
301
|
let sentViaCrew = false;
|
|
302
302
|
const externalOutput = new ExternalAnswerDecoder();
|
|
303
|
+
// 每轮独立:tool_use/result 关联状态不能跨 execution 泄漏。
|
|
304
|
+
const consoleFormatter = createConsoleFormatter();
|
|
303
305
|
const readline = createInterface({ input: child.stdout });
|
|
304
306
|
readline.on("line", (line) => {
|
|
305
307
|
const event = parseLine(line);
|
|
@@ -327,7 +329,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
327
329
|
for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
|
|
328
330
|
callbacks.onExternalOutput?.(text);
|
|
329
331
|
}
|
|
330
|
-
for (const chunk of
|
|
332
|
+
for (const chunk of consoleFormatter.format(event))
|
|
331
333
|
callbacks.onConsole?.(chunk);
|
|
332
334
|
});
|
|
333
335
|
let stderrTail = "";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** 严格、有限的 unified diff 解析器。只有完整文件头 + hunk 才识别,避免误染普通 +/- 日志。 */
|
|
2
|
+
import { CONSOLE_DIFF_ROWS, CONSOLE_ROW_CHARS, } from "./console-payload.js";
|
|
3
|
+
const HUNK = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
4
|
+
const clip = (text) => text.length > CONSOLE_ROW_CHARS ? `${text.slice(0, CONSOLE_ROW_CHARS)}…` : text;
|
|
5
|
+
const pathFromHeader = (line) => line.slice(4).split("\t", 1)[0].replace(/^[ab]\//, "");
|
|
6
|
+
export function parseUnifiedDiff(input) {
|
|
7
|
+
const lines = input.split("\n");
|
|
8
|
+
const files = [];
|
|
9
|
+
let additions = 0;
|
|
10
|
+
let deletions = 0;
|
|
11
|
+
let cursor = 0;
|
|
12
|
+
let totalRows = 0;
|
|
13
|
+
let truncated = false;
|
|
14
|
+
while (cursor < lines.length) {
|
|
15
|
+
while (cursor < lines.length && !isFileHeaderPair(lines, cursor))
|
|
16
|
+
cursor++;
|
|
17
|
+
if (cursor >= lines.length)
|
|
18
|
+
break;
|
|
19
|
+
const oldPath = pathFromHeader(lines[cursor]);
|
|
20
|
+
const newPath = pathFromHeader(lines[cursor + 1]);
|
|
21
|
+
const path = newPath === "/dev/null" ? oldPath : newPath;
|
|
22
|
+
cursor += 2;
|
|
23
|
+
const rows = [];
|
|
24
|
+
let sawHunk = false;
|
|
25
|
+
while (cursor < lines.length && !isFileHeaderPair(lines, cursor)) {
|
|
26
|
+
const match = lines[cursor].match(HUNK);
|
|
27
|
+
if (!match) {
|
|
28
|
+
cursor++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
sawHunk = true;
|
|
32
|
+
let oldLine = Number(match[1]);
|
|
33
|
+
let oldRemaining = Number(match[2] ?? 1);
|
|
34
|
+
let newLine = Number(match[3]);
|
|
35
|
+
let newRemaining = Number(match[4] ?? 1);
|
|
36
|
+
cursor++;
|
|
37
|
+
while (cursor < lines.length && (oldRemaining > 0 || newRemaining > 0)) {
|
|
38
|
+
const line = lines[cursor];
|
|
39
|
+
cursor++;
|
|
40
|
+
if (line === "\")
|
|
41
|
+
continue;
|
|
42
|
+
let row;
|
|
43
|
+
if (line.startsWith("+")) {
|
|
44
|
+
row = { type: "add", oldLine: null, newLine, text: clip(line.slice(1)) };
|
|
45
|
+
newLine++;
|
|
46
|
+
newRemaining--;
|
|
47
|
+
additions++;
|
|
48
|
+
}
|
|
49
|
+
else if (line.startsWith("-")) {
|
|
50
|
+
row = { type: "delete", oldLine, newLine: null, text: clip(line.slice(1)) };
|
|
51
|
+
oldLine++;
|
|
52
|
+
oldRemaining--;
|
|
53
|
+
deletions++;
|
|
54
|
+
}
|
|
55
|
+
else if (line.startsWith(" ")) {
|
|
56
|
+
row = { type: "context", oldLine, newLine, text: clip(line.slice(1)) };
|
|
57
|
+
oldLine++;
|
|
58
|
+
newLine++;
|
|
59
|
+
oldRemaining--;
|
|
60
|
+
newRemaining--;
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
break;
|
|
64
|
+
if (totalRows < CONSOLE_DIFF_ROWS) {
|
|
65
|
+
rows.push(row);
|
|
66
|
+
totalRows++;
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
truncated = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (sawHunk) {
|
|
73
|
+
if (truncated && !rows.some((row) => row.type === "omitted"))
|
|
74
|
+
rows.push({ type: "omitted", oldCount: 1, newCount: 1 });
|
|
75
|
+
files.push({ path, rows });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!files.length)
|
|
79
|
+
return null;
|
|
80
|
+
return { kind: "diff_rows", files, additions, deletions, ...(truncated ? { truncated: true } : {}) };
|
|
81
|
+
}
|
|
82
|
+
function isFileHeaderPair(lines, index) {
|
|
83
|
+
return lines[index]?.startsWith("--- ") === true && lines[index + 1]?.startsWith("+++ ") === true;
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"cross-spawn": "^7.0.6",
|
|
22
22
|
"ws": "^8",
|
|
23
23
|
"zod": "^3.23.0",
|
|
24
|
-
"@nowcrew/cli": "^0.4.
|
|
24
|
+
"@nowcrew/cli": "^0.4.6"
|
|
25
25
|
},
|
|
26
26
|
"optionalDependencies": {
|
|
27
27
|
"koffi": "^2.9.0"
|