@nowcrew/daemon 0.1.0 → 0.1.2
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.js +80 -0
- package/dist/prompt.js +4 -1
- package/dist/runner.js +14 -1
- package/dist/runtimes/claude.js +3 -0
- package/dist/serve.js +20 -1
- package/dist/workspace.js +23 -4
- package/package.json +1 -1
package/dist/console.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把 coding-agent runtime 的 stream-json 事件转成**终端 console 行**(完全透传)。
|
|
3
|
+
*
|
|
4
|
+
* 与 normalize.ts 平行,但目标相反:normalize 为状态角标做**语义压缩**,这里为前端终端窗口
|
|
5
|
+
* 做**原文透传**——保留思考文本、工具调用、工具返回正文,尽量还原原生 claude CLI 的观感。
|
|
6
|
+
* 纯函数,无 IO,完整单测。
|
|
7
|
+
*
|
|
8
|
+
* 注:本转换器针对 claude 的 stream-json。codex 的 stream 格式不同,后续按 runtime 分派扩展;
|
|
9
|
+
* 前端/server 消费的 ConsoleChunk 形状是 runtime 无关的。
|
|
10
|
+
*/
|
|
11
|
+
/** 单条工具返回正文上限(超出截断并标注),避免单条把终端/DB 撑爆。 */
|
|
12
|
+
export const TOOL_RESULT_CAP = 4000;
|
|
13
|
+
/** 工具输入摘要上限(标题行那一段)。 */
|
|
14
|
+
const TOOL_INPUT_CAP = 160;
|
|
15
|
+
/** 把工具输入压成一行摘要:Bash 取 command,其它取首个字符串字段或紧凑 JSON。 */
|
|
16
|
+
function summarizeToolInput(name, input) {
|
|
17
|
+
if (!input)
|
|
18
|
+
return `⏺ ${name}`;
|
|
19
|
+
if (name === "Bash" && typeof input.command === "string") {
|
|
20
|
+
return `⏺ Bash(${clip(input.command.replace(/\s+/g, " "), TOOL_INPUT_CAP)})`;
|
|
21
|
+
}
|
|
22
|
+
const entries = Object.entries(input);
|
|
23
|
+
const head = entries
|
|
24
|
+
.map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`)
|
|
25
|
+
.join(", ");
|
|
26
|
+
return `⏺ ${name}(${clip(head.replace(/\s+/g, " "), TOOL_INPUT_CAP)})`;
|
|
27
|
+
}
|
|
28
|
+
/** 把 tool_result 的 content(string | block[])抽成纯文本。 */
|
|
29
|
+
function extractToolResult(content) {
|
|
30
|
+
if (typeof content === "string")
|
|
31
|
+
return content;
|
|
32
|
+
if (Array.isArray(content)) {
|
|
33
|
+
return content
|
|
34
|
+
.map((b) => (b && typeof b === "object" && typeof b.text === "string" ? b.text : ""))
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join("\n");
|
|
37
|
+
}
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
function clip(s, cap) {
|
|
41
|
+
return s.length > cap ? s.slice(0, cap) + `… (+${s.length - cap})` : s;
|
|
42
|
+
}
|
|
43
|
+
/** 把一个 stream-json 事件转成 0..N 条 console 行(完全透传)。 */
|
|
44
|
+
export function toConsoleLines(event) {
|
|
45
|
+
const e = (event ?? {});
|
|
46
|
+
if (e.type === "system" && e.subtype === "init") {
|
|
47
|
+
return [{ stream: "system", text: "● claude 会话启动" }];
|
|
48
|
+
}
|
|
49
|
+
if (e.type === "result") {
|
|
50
|
+
const text = e.result?.trim() || (e.is_error ? "运行出错" : "本轮结束");
|
|
51
|
+
return [{ stream: e.is_error ? "error" : "result", text }];
|
|
52
|
+
}
|
|
53
|
+
if (e.type === "assistant" && Array.isArray(e.message?.content)) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const block of e.message.content) {
|
|
56
|
+
if (block.type === "thinking" && block.thinking?.trim()) {
|
|
57
|
+
out.push({ stream: "thinking", text: block.thinking.trim() });
|
|
58
|
+
}
|
|
59
|
+
else if (block.type === "text" && block.text?.trim()) {
|
|
60
|
+
out.push({ stream: "text", text: block.text.trim() });
|
|
61
|
+
}
|
|
62
|
+
else if (block.type === "tool_use" && block.name) {
|
|
63
|
+
out.push({ stream: "tool", text: summarizeToolInput(block.name, block.input) });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
if (e.type === "user" && Array.isArray(e.message?.content)) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const block of e.message.content) {
|
|
71
|
+
if (block.type === "tool_result") {
|
|
72
|
+
const text = extractToolResult(block.content).trim();
|
|
73
|
+
if (text)
|
|
74
|
+
out.push({ stream: "tool_result", text: clip(text, TOOL_RESULT_CAP) });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
package/dist/prompt.js
CHANGED
|
@@ -64,6 +64,9 @@ CRITICAL 规则:
|
|
|
64
64
|
- 你读到的消息形如 \`#<seq> [<type>] <sender>: <正文>\`,\`type\` 为 \`human\` / \`agent\` / \`system\`。
|
|
65
65
|
- **\`system\` 消息**通报频道状态变化(如新建任务),除非明确要求你行动(如刚给你指派了任务),否则不要回复。
|
|
66
66
|
- **判定规则**:若满足来信需要你"回复之外的动作"(跑工具/改代码/做变更),先 claim;若只是回答问题或闲聊,无需 claim。
|
|
67
|
+
- **线程 = 工作单元 / 一个请求一个 task(CRITICAL)**:一条对话(thread)对应一件事,最多绑**一个** task。被唤醒处理来信时,对这件事**只建一个 task**:\`crew task create --title "…"\`(不带 --new-thread)会把它绑到**当前线程**(触发你的那条消息所在线程),不另发顶层消息。**别把一个请求拆成多个 task**(如"拉代码"+"读文档"+"写记忆"是同一件事 → 一个 task,用 todo/进度推进,不要建第二个)。当前线程已有 task 时再 \`crew task create\` 会被服务端拒绝(报错会提示你)。
|
|
68
|
+
- **进度/产出回本线程**:这件事的认领/进度/完成汇报都用 \`crew message send --thread <当前线程根>\` 回复在**这个线程里**(线程根 = 触发你的那条消息 id,即 \$CREW_WAKE_MESSAGE_ID)。**严禁把进度发到别的线程或频道顶层。**
|
|
69
|
+
- **毫不相关的新任务才另起线程**:只有要处理的事**和当前线程毫不相关**(或用户明确要求新建)时,才用 \`crew task create --new-thread --title "…"\`——系统另起一个子线程(parent=当前线程)绑新 task;之后这件事的回复要发到**这个新子线程**里。能不拆就不拆。
|
|
67
70
|
- 任务状态流:\`todo → in_progress → in_review → done\`。claim 后用 \`crew task update\` 推进:开工→in_progress、完成待验收→in_review、人类确认后→done。只有 assignee 能改自己任务的状态。
|
|
68
71
|
- **交接(handoff)**:当你这一环干完、需要别的角色接手时(如开发完成 → 交给 QA 测试),用 \`crew task assign <taskId> --to <下家handle>\` 把任务交接出去,并在线程里给下家足够背景(分支名 / 改动摘要 / 测试建议)。交接后对方会被自动唤醒。**别让任务停在你手里无人跟进**。
|
|
69
72
|
- **分诊(若你是总管)**:若你收到「【分诊请求】」唤醒,说明频道里有一个无人认领的任务需要你按团队职责分派。唤醒内容里已附上团队成员及其职责:判断谁最合适,用 \`crew task assign <taskId> --to <handle>\` 指派给他(若该你自己做就 \`crew task claim\`);确实没人合适时,在频道里 @发起人 说明并给建议,**不要让任务悬空**。
|
|
@@ -95,7 +98,7 @@ ${ctx.memory ? `\n## [注入] 你的 MEMORY.md(索引,只读参考)\n${ctx.memor
|
|
|
95
98
|
* 只有发现确实指向自己的事才转为主动处理,否则读完即停、不发声。
|
|
96
99
|
*/
|
|
97
100
|
export function buildWakePrompt(channelId) {
|
|
98
|
-
return
|
|
101
|
+
return `先补齐上下文:若本轮在某个线程里(被唤醒处理某 thread),用 \`crew thread read\` 读**当前线程 + 它的父线程**(聚焦上下文,最多向上一层);需要更全局再用 \`crew message read --channel ${channelId}\` 读整个频道。
|
|
99
102
|
读完判断:其中是否有明确落到你头上的事——点名找你、@你、指派给你、请你评审,或交给你的任务。
|
|
100
103
|
- 有:转入主动处理。相关任务先 \`crew task claim <taskId>\` 认领再动手,完成后用 \`crew message send --channel ${channelId}\` 回复。
|
|
101
104
|
- 没有:本轮什么都不要发,读完即停。你存活期间有新消息会自动送来,无需轮询。`;
|
package/dist/runner.js
CHANGED
|
@@ -9,13 +9,16 @@ import { prepareWorkspace } from "./workspace.js";
|
|
|
9
9
|
import { buildSystemPrompt, buildWakePrompt } from "./prompt.js";
|
|
10
10
|
import { spawnClaude } from "./runtimes/claude.js";
|
|
11
11
|
import { normalizeEvent, parseLine } from "./normalize.js";
|
|
12
|
+
import { toConsoleLines } from "./console.js";
|
|
12
13
|
// 注入提示词的 MEMORY.md 上限:只喂索引/角色,避免把膨胀的记忆全塞进上下文。
|
|
13
14
|
const MEMORY_INJECT_CAP = 6000;
|
|
14
15
|
const ICON = {
|
|
15
16
|
init: "🟢", text: "💬", reading: "📖", sending: "📨", checking: "🔎",
|
|
16
17
|
claiming: "📌", crew: "⚙️", tool: "🛠️", tool_result: "↩️", done: "✅", error: "❌",
|
|
17
18
|
};
|
|
18
|
-
export async function runAgent(config, input, onActivity = defaultPrint
|
|
19
|
+
export async function runAgent(config, input, onActivity = defaultPrint,
|
|
20
|
+
// 终端透传:每条 stream-json 事件除归一化为状态活动外,再产出 console 行供前端终端窗口渲染。
|
|
21
|
+
onConsole = () => { }) {
|
|
19
22
|
// 1) 用机器令牌换 per-launch agent 令牌
|
|
20
23
|
const cred = await mintAgentToken(config.serverUrl, config.machineToken, input.handle, input.displayName);
|
|
21
24
|
// 2) 准备 workspace(共享 home + 本任务隔离 cwd + per-task work-log)。
|
|
@@ -25,6 +28,8 @@ export async function runAgent(config, input, onActivity = defaultPrint) {
|
|
|
25
28
|
handle: input.handle,
|
|
26
29
|
cliPath: config.cliPath,
|
|
27
30
|
...(input.taskKey ? { taskKey: input.taskKey } : {}),
|
|
31
|
+
// 仅在首次创建 MEMORY.md 时,用 agent 的 description 种子化 ## Role
|
|
32
|
+
...(cred.config?.description ? { description: cred.config.description } : {}),
|
|
28
33
|
});
|
|
29
34
|
const systemPrompt = buildSystemPrompt({
|
|
30
35
|
handle: input.handle,
|
|
@@ -49,6 +54,8 @@ export async function runAgent(config, input, onActivity = defaultPrint) {
|
|
|
49
54
|
wakePrompt: input.wake ?? buildWakePrompt(input.channelId),
|
|
50
55
|
dangerous: config.dangerous,
|
|
51
56
|
...(cfg.model ? { model: cfg.model } : {}),
|
|
57
|
+
// 一线程一会话:首轮 --session-id 固定 id,之后 --resume 续上(更原生地复用 claude 记忆)
|
|
58
|
+
...(ws.agentSessionId ? { sessionId: ws.agentSessionId, resume: ws.sessionResume } : {}),
|
|
52
59
|
env: {
|
|
53
60
|
...process.env,
|
|
54
61
|
PATH: `${ws.crewDir}${delimiter}${process.env.PATH ?? ""}`,
|
|
@@ -58,6 +65,9 @@ export async function runAgent(config, input, onActivity = defaultPrint) {
|
|
|
58
65
|
// 共享持久记忆 home(MEMORY.md/notes 在此;cwd 是本任务隔离目录)+ 本任务 work-log 路径
|
|
59
66
|
CREW_HOME: ws.dir,
|
|
60
67
|
CREW_TASK_LOG: ws.workLogPath,
|
|
68
|
+
// 唤醒锚点消息 id:有则 `crew task create` 把任务锚定到这条触发消息(讨论与任务锚点统一),
|
|
69
|
+
// 而非另发一条标题消息当锚点(那会让点开 task 的 thread 永远为空)。
|
|
70
|
+
...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
|
|
61
71
|
// per-agent 凭证隔离:XDG 指向本 agent 独立目录(gh/gcloud 等 CLI 的 token 不互相串)。
|
|
62
72
|
// 不覆盖 HOME(否则会破坏 claude 自身的 ~/.claude 鉴权);常用 CLI 也单独点名隔离。
|
|
63
73
|
XDG_CONFIG_HOME: join(ws.homeDir, ".config"),
|
|
@@ -87,6 +97,9 @@ export async function runAgent(config, input, onActivity = defaultPrint) {
|
|
|
87
97
|
activities.push(a);
|
|
88
98
|
onActivity(a);
|
|
89
99
|
}
|
|
100
|
+
// 同一事件再透传为终端 console 行(独立于状态活动,内容不压缩)。
|
|
101
|
+
for (const c of toConsoleLines(evt))
|
|
102
|
+
onConsole(c);
|
|
90
103
|
});
|
|
91
104
|
child.stderr.on("data", (d) => process.stderr.write(d));
|
|
92
105
|
const exitCode = await new Promise((resolve) => {
|
package/dist/runtimes/claude.js
CHANGED
|
@@ -13,6 +13,9 @@ export function buildClaudeArgs(input) {
|
|
|
13
13
|
];
|
|
14
14
|
if (input.model)
|
|
15
15
|
args.push("--model", input.model);
|
|
16
|
+
if (input.sessionId) {
|
|
17
|
+
args.push(input.resume ? "--resume" : "--session-id", input.sessionId);
|
|
18
|
+
}
|
|
16
19
|
if (input.dangerous)
|
|
17
20
|
args.push("--dangerously-skip-permissions");
|
|
18
21
|
args.push(input.wakePrompt);
|
package/dist/serve.js
CHANGED
|
@@ -167,6 +167,22 @@ export function serve(config, opts = {}) {
|
|
|
167
167
|
}
|
|
168
168
|
catch { /* ws 非 OPEN,忽略 */ }
|
|
169
169
|
};
|
|
170
|
+
// 终端透传:把底层 claude 的每条 console 行按线程上送给 server(独立 seq,落库+广播给 web 终端窗口)。
|
|
171
|
+
let conSeq = 0;
|
|
172
|
+
const reportConsole = (c) => {
|
|
173
|
+
try {
|
|
174
|
+
ws?.send(JSON.stringify({
|
|
175
|
+
type: "agent:console",
|
|
176
|
+
agentHandle: msg.agentHandle,
|
|
177
|
+
channelId: msg.channelId,
|
|
178
|
+
threadId: threadId ?? null,
|
|
179
|
+
stream: c.stream,
|
|
180
|
+
text: c.text,
|
|
181
|
+
seq: conSeq++,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
catch { /* ws 非 OPEN,忽略 */ }
|
|
185
|
+
};
|
|
170
186
|
try {
|
|
171
187
|
// 线程聚合:触发消息即任务线程根,你的确认+后续所有回复都要发到它的线程里,
|
|
172
188
|
// 不要发顶层——这样 task 讨论全部聚合在该 thread 下。
|
|
@@ -191,9 +207,12 @@ export function serve(config, opts = {}) {
|
|
|
191
207
|
handle: msg.agentHandle,
|
|
192
208
|
channelId: msg.channelId,
|
|
193
209
|
taskKey, // 每任务隔离 cwd + work-log(并行不冲突)
|
|
210
|
+
// 唤醒锚点是具体消息(非纯频道唤醒)时,把它透传下去,供 `crew task create` 锚定到该消息。
|
|
211
|
+
...(threadId ? { wakeMessageId: threadId } : {}),
|
|
194
212
|
...(msg.wake?.content ? { wake: `你被唤醒(${msg.reason}): ${msg.wake.content}\n用 crew message read --channel ${msg.channelId} 读频道后按需处理。${reasonHint}${ackHint}${threadHint}${attHint}` } : {}),
|
|
195
|
-
}, reportActivity);
|
|
213
|
+
}, reportActivity, reportConsole);
|
|
196
214
|
reportActivity({ kind: "done", label: "本轮结束" });
|
|
215
|
+
reportConsole({ stream: "result", text: "● 本轮结束" });
|
|
197
216
|
log(`✅ agent=${msg.agentHandle} 本轮完成`);
|
|
198
217
|
}
|
|
199
218
|
catch (e) {
|
package/dist/workspace.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* spawn runtime 时把 <ws>/.crew 置于 PATH 最前,于是 agent 的 `crew ...` 命中 wrapper。
|
|
9
9
|
*/
|
|
10
10
|
import { mkdir, writeFile, readFile, chmod, access } from "node:fs/promises";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
11
12
|
import { join } from "node:path";
|
|
12
13
|
/** 文件系统安全的 taskKey:仅留 [\w.-],其余转 _,截断,避免路径穿越/超长。 */
|
|
13
14
|
export function safeKey(key) {
|
|
@@ -29,7 +30,7 @@ export async function prepareWorkspace(input) {
|
|
|
29
30
|
// MEMORY.md:首次创建"索引 + Active Context"骨架,之后由 agent 自己维护
|
|
30
31
|
const memoryPath = join(dir, "MEMORY.md");
|
|
31
32
|
if (!(await exists(memoryPath))) {
|
|
32
|
-
await writeFile(memoryPath, memorySeed(input.handle), "utf8");
|
|
33
|
+
await writeFile(memoryPath, memorySeed(input.handle, input.description), "utf8");
|
|
33
34
|
}
|
|
34
35
|
const memory = await readFile(memoryPath, "utf8");
|
|
35
36
|
// 系统提示词 (每次覆盖,保证最新);省略时由调用方在拿到 memory 后自行写入。
|
|
@@ -55,17 +56,35 @@ export async function prepareWorkspace(input) {
|
|
|
55
56
|
workLogPath = join(runDir, "work-log.md");
|
|
56
57
|
}
|
|
57
58
|
const workLog = (await exists(workLogPath)) ? await readFile(workLogPath, "utf8") : "";
|
|
58
|
-
|
|
59
|
+
// 一线程一会话:tasks/<taskKey>/.session 存底层会话 id。已存在→resume 续上;否则新建。
|
|
60
|
+
let agentSessionId = null;
|
|
61
|
+
let sessionResume = false;
|
|
62
|
+
if (input.taskKey) {
|
|
63
|
+
const sessionPath = join(runDir, ".session");
|
|
64
|
+
if (await exists(sessionPath)) {
|
|
65
|
+
agentSessionId = (await readFile(sessionPath, "utf8")).trim() || null;
|
|
66
|
+
sessionResume = !!agentSessionId;
|
|
67
|
+
}
|
|
68
|
+
if (!agentSessionId) {
|
|
69
|
+
agentSessionId = randomUUID();
|
|
70
|
+
await writeFile(sessionPath, agentSessionId, "utf8");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { dir, crewDir, systemPromptPath, memory, homeDir, runDir, workLogPath, workLog, agentSessionId, sessionResume };
|
|
59
74
|
}
|
|
60
75
|
/**
|
|
61
76
|
* MEMORY.md 种子骨架:分层记忆的"索引 + Active Context"结构。
|
|
62
77
|
* 第一次准备工作区时写入;之后 agent 自己维护(系统提示词里有协议)。
|
|
78
|
+
* 建 agent 时填了 description,则用它种子化 ## Role(否则留占位符,由 agent 自填)。
|
|
63
79
|
*/
|
|
64
|
-
function memorySeed(handle) {
|
|
80
|
+
function memorySeed(handle, description) {
|
|
81
|
+
const role = description?.trim()
|
|
82
|
+
? description.trim()
|
|
83
|
+
: "(Who you are and what you're responsible for. Keep it to a few lines.)";
|
|
65
84
|
return `# ${handle}
|
|
66
85
|
|
|
67
86
|
## Role
|
|
68
|
-
|
|
87
|
+
${role}
|
|
69
88
|
|
|
70
89
|
## Core Goals
|
|
71
90
|
(What you optimize for.)
|