@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.
@@ -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 diffPayload(input.file_path, input.old_string, input.new_string);
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 diffPayload(input.file_path, "", input.content);
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: diffPayload(file, rec.old_string, rec.new_string),
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
- out.push({ stream: "tool_result", text: clip(output, TOOL_RESULT_CAP) });
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();
@@ -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
  });
@@ -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 { toConsoleLines } from "./console.js";
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 toConsoleLines(event))
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.21",
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.12"
24
+ "@nowcrew/cli": "^0.4.6"
25
25
  },
26
26
  "optionalDependencies": {
27
27
  "koffi": "^2.9.0"