@nowcrew/daemon 0.5.14 → 0.5.16
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/README.md +57 -3
- package/dist/bound-im-decision.js +22 -0
- package/dist/computer-cli.js +214 -0
- package/dist/computer-profile.js +195 -0
- package/dist/computer-service.js +358 -0
- package/dist/config.js +2 -1
- package/dist/console.js +10 -0
- package/dist/execution-backend.js +11 -0
- package/dist/execution-protocol.js +11 -0
- package/dist/execution-runner.js +38 -11
- package/dist/execution-supervisor.js +48 -5
- package/dist/execution-telemetry-journal.js +71 -0
- package/dist/i18n.js +25 -0
- package/dist/local-executor.js +5 -2
- package/dist/machine-info.js +25 -4
- package/dist/main.js +15 -0
- package/dist/normalize.js +11 -0
- package/dist/origin-decision.js +1 -1
- package/dist/prompt.js +40 -21
- package/dist/runner.js +25 -1
- package/dist/runtime-capabilities.js +5 -0
- package/dist/runtimes/kimi-acp-runner.js +264 -0
- package/dist/runtimes/kimi.js +10 -0
- package/dist/scheduled-report.js +4 -0
- package/dist/scheduled-run-report.js +1 -0
- package/dist/serve.js +55 -10
- package/package.json +3 -2
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { fork } from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { executionBackendCapability } from "./execution-backend.js";
|
|
3
4
|
const DEFAULT_ABORT_TIMEOUT_MS = 5_000;
|
|
4
5
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
5
6
|
const DEFAULT_TASKKILL_TIMEOUT_MS = 5_000;
|
|
7
|
+
const PROCESS_GROUP_POLL_MS = 10;
|
|
6
8
|
function messageError(error) {
|
|
7
9
|
return error instanceof Error ? error : new Error(String(error));
|
|
8
10
|
}
|
|
@@ -21,6 +23,27 @@ async function waitForExit(exit, timeoutMs, pid) {
|
|
|
21
23
|
clearTimeout(timer);
|
|
22
24
|
}
|
|
23
25
|
}
|
|
26
|
+
async function waitForProcessGroupExit(pid, timeoutMs) {
|
|
27
|
+
const deadline = Date.now() + timeoutMs;
|
|
28
|
+
while (true) {
|
|
29
|
+
try {
|
|
30
|
+
process.kill(-pid, 0);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const code = error.code;
|
|
34
|
+
if (code === "ESRCH")
|
|
35
|
+
return;
|
|
36
|
+
// POSIX defines EPERM here as "the process group exists, but is not signalable".
|
|
37
|
+
// It is therefore an alive observation, not a completed cleanup or an API failure.
|
|
38
|
+
if (code !== "EPERM")
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
if (Date.now() >= deadline) {
|
|
42
|
+
throw new Error(`Supervisor process group ${pid} did not exit within ${timeoutMs}ms`);
|
|
43
|
+
}
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, PROCESS_GROUP_POLL_MS));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
24
47
|
async function withTimeout(promise, timeoutMs, phase) {
|
|
25
48
|
let timer;
|
|
26
49
|
try {
|
|
@@ -63,9 +86,9 @@ export async function signalSupervisorTree(pid, signal, platform = process.platf
|
|
|
63
86
|
}
|
|
64
87
|
export async function startDormantSupervisor(launch, options = {}) {
|
|
65
88
|
const platform = options.platform ?? process.platform;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
const backend = executionBackendCapability(platform);
|
|
90
|
+
if (!backend.supported)
|
|
91
|
+
throw new Error(backend.reason);
|
|
69
92
|
const childEntry = options.childEntry
|
|
70
93
|
?? fileURLToPath(new URL("./execution-supervisor-child.js", import.meta.url));
|
|
71
94
|
const abortTimeoutMs = options.abortTimeoutMs ?? DEFAULT_ABORT_TIMEOUT_MS;
|
|
@@ -87,7 +110,7 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
87
110
|
}
|
|
88
111
|
let runtimeResult;
|
|
89
112
|
let supervisorSpawnError;
|
|
90
|
-
const
|
|
113
|
+
const supervisorExit = new Promise((resolve) => {
|
|
91
114
|
child.once("error", (error) => { supervisorSpawnError = error.message; });
|
|
92
115
|
child.once("close", (code, signal) => resolve(runtimeResult ?? {
|
|
93
116
|
exitCode: supervisorSpawnError === undefined ? (code ?? 128) : -1,
|
|
@@ -95,6 +118,11 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
95
118
|
...(supervisorSpawnError === undefined && signal !== null ? { terminationSignal: signal } : {}),
|
|
96
119
|
}));
|
|
97
120
|
});
|
|
121
|
+
const exit = supervisorExit.then(async (result) => {
|
|
122
|
+
if (platform !== "win32")
|
|
123
|
+
await waitForProcessGroupExit(pid, abortTimeoutMs);
|
|
124
|
+
return result;
|
|
125
|
+
});
|
|
98
126
|
const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
99
127
|
let readyResolve;
|
|
100
128
|
let readyReject;
|
|
@@ -158,6 +186,21 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
158
186
|
await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
|
|
159
187
|
}
|
|
160
188
|
};
|
|
189
|
+
const cancel = async () => {
|
|
190
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
191
|
+
await supervisorClosed;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
await new Promise((resolve, reject) => {
|
|
196
|
+
child.send({ type: "abort" }, (error) => error === null ? resolve() : reject(error));
|
|
197
|
+
});
|
|
198
|
+
await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
await abort();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
161
204
|
try {
|
|
162
205
|
await new Promise((resolve, reject) => {
|
|
163
206
|
child.send({ type: "launch", launch: { ...launch, args: [...launch.args], env } }, (error) => {
|
|
@@ -204,6 +247,6 @@ export async function startDormantSupervisor(launch, options = {}) {
|
|
|
204
247
|
}
|
|
205
248
|
},
|
|
206
249
|
abort,
|
|
207
|
-
cancel
|
|
250
|
+
cancel,
|
|
208
251
|
};
|
|
209
252
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { access, open, mkdir, readdir, readFile, rename, unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { DaemonToServerExecutionFrameSchema, } from "./execution-protocol.js";
|
|
5
|
+
const MAX_PENDING_FILES = 4_096;
|
|
6
|
+
const fileName = (frame) => `${frame.executionId}.${frame.type === "execution:activity" ? "activity" : "console"}.${frame.seq}.json`;
|
|
7
|
+
export class ExecutionTelemetryJournal {
|
|
8
|
+
directory;
|
|
9
|
+
constructor(agentsRoot) {
|
|
10
|
+
this.directory = join(agentsRoot, ".execution-telemetry");
|
|
11
|
+
}
|
|
12
|
+
async append(frame) {
|
|
13
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
14
|
+
const path = join(this.directory, fileName(frame));
|
|
15
|
+
if (await access(path).then(() => true, () => false))
|
|
16
|
+
return;
|
|
17
|
+
const pending = await readdir(this.directory);
|
|
18
|
+
if (pending.length >= MAX_PENDING_FILES) {
|
|
19
|
+
throw new Error(`execution telemetry journal is full (${MAX_PENDING_FILES} frames)`);
|
|
20
|
+
}
|
|
21
|
+
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
22
|
+
let handle;
|
|
23
|
+
try {
|
|
24
|
+
handle = await open(tempPath, "wx", 0o600);
|
|
25
|
+
await handle.writeFile(JSON.stringify(frame), "utf8");
|
|
26
|
+
await handle.sync();
|
|
27
|
+
await handle.close();
|
|
28
|
+
handle = undefined;
|
|
29
|
+
await rename(tempPath, path);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
await unlink(tempPath).catch(() => { });
|
|
33
|
+
if (error.code !== "EEXIST")
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await handle?.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async acknowledge(executionId, kind, seq) {
|
|
41
|
+
await unlink(join(this.directory, `${executionId}.${kind}.${seq}.json`)).catch((error) => {
|
|
42
|
+
if (error.code !== "ENOENT")
|
|
43
|
+
throw error;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async replay() {
|
|
47
|
+
const names = await readdir(this.directory).catch((error) => {
|
|
48
|
+
if (error.code === "ENOENT")
|
|
49
|
+
return [];
|
|
50
|
+
throw error;
|
|
51
|
+
});
|
|
52
|
+
const frames = [];
|
|
53
|
+
for (const name of names.filter((entry) => entry.endsWith(".json")).sort()) {
|
|
54
|
+
let raw;
|
|
55
|
+
try {
|
|
56
|
+
raw = JSON.parse(await readFile(join(this.directory, name), "utf8"));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new Error(`invalid execution telemetry journal entry: ${name}`, { cause: error });
|
|
60
|
+
}
|
|
61
|
+
const parsed = DaemonToServerExecutionFrameSchema.safeParse(raw);
|
|
62
|
+
if (!parsed.success || (parsed.data.type !== "execution:activity" && parsed.data.type !== "execution:console")) {
|
|
63
|
+
throw new Error(`invalid execution telemetry journal entry: ${name}`);
|
|
64
|
+
}
|
|
65
|
+
frames.push(parsed.data);
|
|
66
|
+
}
|
|
67
|
+
return frames.sort((left, right) => left.at.localeCompare(right.at)
|
|
68
|
+
|| left.type.localeCompare(right.type) || left.seq - right.seq);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export const createExecutionTelemetryJournal = (agentsRoot) => new ExecutionTelemetryJournal(agentsRoot);
|
package/dist/i18n.js
CHANGED
|
@@ -29,9 +29,34 @@ const zh = {
|
|
|
29
29
|
"for channel": "处理频道",
|
|
30
30
|
"agent exited": "agent 退出",
|
|
31
31
|
"activities": "活动数",
|
|
32
|
+
"Computer lifecycle:": "电脑生命周期:",
|
|
33
|
+
"profile save reads the machine token from CREW_MACHINE_TOKEN, or stdin with --token-stdin.": "profile save 从 CREW_MACHINE_TOKEN 或 --token-stdin 的标准输入读取机器令牌。",
|
|
34
|
+
"Missing {{name}}": "缺少 {{name}}",
|
|
35
|
+
"Service '{{id}}' is still installed; uninstall it before removing the profile": "服务 '{{id}}' 仍已安装;请先卸载服务再删除配置",
|
|
36
|
+
"Removed profile '{{name}}'.": "已删除配置 '{{name}}'。",
|
|
37
|
+
"Expected profile save, list, show, or remove": "profile 子命令应为 save、list、show 或 remove",
|
|
38
|
+
"Choose one token source: CREW_MACHINE_TOKEN or --token-stdin": "CREW_MACHINE_TOKEN 与 --token-stdin 只能选择一种令牌来源",
|
|
39
|
+
"Missing machine token; use CREW_MACHINE_TOKEN or --token-stdin": "缺少机器令牌;请使用 CREW_MACHINE_TOKEN 或 --token-stdin",
|
|
40
|
+
"Saved profile '{{name}}' with private credentials.": "已保存配置 '{{name}}',凭证仅私有可读。",
|
|
41
|
+
"Service '{{id}}' is not installed": "服务 '{{id}}' 尚未安装",
|
|
42
|
+
"Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.": "daemon 已升级,并已请求重启 '{{name}}';请用 status 确认。",
|
|
43
|
+
"Upgraded daemon. Installed services were not restarted; pass --profile to restart one.": "daemon 已升级;已安装服务尚未重启,可传入 --profile 重启指定服务。",
|
|
44
|
+
"Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry": "服务生命周期必须使用已构建的 daemon 入口(.js),不能使用 TypeScript 开发入口",
|
|
45
|
+
"Installed '{{id}}'. Use status to confirm runtime state.": "已安装 '{{id}}';请用 status 确认运行状态。",
|
|
46
|
+
"Uninstalled '{{id}}'.": "已卸载 '{{id}}'。",
|
|
47
|
+
"{{action}} request accepted for '{{id}}'. Verify with status.": "已接受对 '{{id}}' 的 {{action}} 请求;请用 status 确认。",
|
|
48
|
+
"--token-stdin requires a token on standard input": "--token-stdin 需要从标准输入读取令牌",
|
|
49
|
+
"Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@latest": "服务生命周期需要全局安装 @nowcrew/daemon;请运行 npm install --global @nowcrew/daemon@latest",
|
|
32
50
|
};
|
|
33
51
|
export function translateDaemon(lang, message) {
|
|
34
52
|
if (lang === "zh")
|
|
35
53
|
return zh[message] ?? message;
|
|
36
54
|
return message;
|
|
37
55
|
}
|
|
56
|
+
export function formatDaemonText(lang, message, values = {}) {
|
|
57
|
+
let rendered = translateDaemon(lang, message);
|
|
58
|
+
for (const [key, value] of Object.entries(values)) {
|
|
59
|
+
rendered = rendered.replaceAll(`{{${key}}}`, String(value));
|
|
60
|
+
}
|
|
61
|
+
return rendered;
|
|
62
|
+
}
|
package/dist/local-executor.js
CHANGED
|
@@ -261,8 +261,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
261
261
|
callbacks.onActivity?.(activity);
|
|
262
262
|
}
|
|
263
263
|
const extracted = extractFinalText(event);
|
|
264
|
-
if (extracted)
|
|
265
|
-
|
|
264
|
+
if (extracted) {
|
|
265
|
+
const incremental = typeof event === "object" && event !== null
|
|
266
|
+
&& "type" in event && event.type === "kimi.acp.text_delta";
|
|
267
|
+
finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
|
|
268
|
+
}
|
|
266
269
|
for (const chunk of toConsoleLines(event))
|
|
267
270
|
callbacks.onConsole?.(chunk);
|
|
268
271
|
});
|
package/dist/machine-info.js
CHANGED
|
@@ -11,11 +11,16 @@ import { readdir } from "node:fs/promises";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import { dirname, resolve } from "node:path";
|
|
14
|
+
import { executableRuntimes } from "./runtime-capabilities.js";
|
|
15
|
+
import { executionBackendCapability } from "./execution-backend.js";
|
|
16
|
+
import { probeKimiAcp } from "./runtimes/kimi-acp-runner.js";
|
|
17
|
+
export { executableRuntimes } from "./runtime-capabilities.js";
|
|
14
18
|
const execFileP = promisify(execFile);
|
|
15
19
|
export const DAEMON_CAPABILITIES = [
|
|
16
20
|
"scheduled_job_v1",
|
|
17
21
|
"reply_origin_v1",
|
|
18
22
|
"origin_decision_v1",
|
|
23
|
+
"execution_telemetry_ack_v1",
|
|
19
24
|
];
|
|
20
25
|
export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
|
|
21
26
|
/** 候选 runtime CLI:展示名 → 可执行文件名。 */
|
|
@@ -43,6 +48,17 @@ export async function detectRuntimes() {
|
|
|
43
48
|
const checks = await Promise.all(RUNTIME_BINS.map(async ([name, bin]) => ((await isInstalled(bin)) ? name : null)));
|
|
44
49
|
return checks.filter((x) => x !== null);
|
|
45
50
|
}
|
|
51
|
+
async function supportsKimiAcp() {
|
|
52
|
+
return probeKimiAcp({ bin: "kimi" });
|
|
53
|
+
}
|
|
54
|
+
/** Runtime adapters that can satisfy the durable protocol-v1 process contract. */
|
|
55
|
+
export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp) {
|
|
56
|
+
const present = await installed;
|
|
57
|
+
const supported = executableRuntimes(present).filter((runtime) => runtime !== "kimi");
|
|
58
|
+
if (present.includes("kimi") && await kimiAcpProbe())
|
|
59
|
+
supported.push("kimi");
|
|
60
|
+
return supported;
|
|
61
|
+
}
|
|
46
62
|
export function daemonVersion() {
|
|
47
63
|
try {
|
|
48
64
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -77,22 +93,27 @@ async function listAgentHandles(agentsRoot) {
|
|
|
77
93
|
return [];
|
|
78
94
|
}
|
|
79
95
|
}
|
|
80
|
-
export async function collectMachineHello(agentsRoot, executionLimits, runtimePlatform = process.platform) {
|
|
96
|
+
export async function collectMachineHello(agentsRoot, executionLimits, runtimePlatform = process.platform, dependencies = {}) {
|
|
81
97
|
const [runtimes, agentHandles] = await Promise.all([
|
|
82
|
-
detectRuntimes(),
|
|
98
|
+
(dependencies.detectInstalled ?? detectRuntimes)(),
|
|
83
99
|
listAgentHandles(agentsRoot),
|
|
84
100
|
]);
|
|
101
|
+
const backend = executionBackendCapability(runtimePlatform);
|
|
102
|
+
const executionRuntimes = backend.supported
|
|
103
|
+
? await (dependencies.detectExecutable ?? detectExecutionRuntimes)(runtimes)
|
|
104
|
+
: [];
|
|
85
105
|
return {
|
|
86
106
|
type: "machine:hello",
|
|
87
107
|
hostname: hostname(),
|
|
88
108
|
os: `${osPlatform()} ${arch()}`,
|
|
89
109
|
daemonVersion: daemonVersion(),
|
|
90
110
|
runtimes,
|
|
111
|
+
executionRuntimes,
|
|
91
112
|
capabilities: DAEMON_CAPABILITIES,
|
|
92
|
-
...(
|
|
113
|
+
...(backend.supported ? {
|
|
93
114
|
executionProtocol: EXECUTION_PROTOCOL,
|
|
94
115
|
executionLimits: Object.freeze({ ...executionLimits }),
|
|
95
|
-
}),
|
|
116
|
+
} : {}),
|
|
96
117
|
agentHandles,
|
|
97
118
|
};
|
|
98
119
|
}
|
package/dist/main.js
CHANGED
|
@@ -13,7 +13,14 @@ import { runAgent } from "./runner.js";
|
|
|
13
13
|
import { serve } from "./serve.js";
|
|
14
14
|
import { initSlog, flushSlog } from "./slog.js";
|
|
15
15
|
import { formatDaemonLogLine } from "./log-format.js";
|
|
16
|
+
import { loadProfile, applyProfileToEnv } from "./computer-profile.js";
|
|
17
|
+
import { runComputerCommand } from "./computer-cli.js";
|
|
16
18
|
async function main() {
|
|
19
|
+
const computerResult = await runComputerCommand(process.argv.slice(2));
|
|
20
|
+
if (computerResult !== null) {
|
|
21
|
+
process.exitCode = computerResult;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
17
24
|
const lang = detectDaemonLang();
|
|
18
25
|
const td = (message) => translateDaemon(lang, message);
|
|
19
26
|
const { values, positionals } = parseArgs({
|
|
@@ -28,6 +35,8 @@ async function main() {
|
|
|
28
35
|
"server-url": { type: "string" },
|
|
29
36
|
"api-key": { type: "string" },
|
|
30
37
|
token: { type: "string" }, // --api-key 的别名
|
|
38
|
+
profile: { type: "string" },
|
|
39
|
+
"daemon-home": { type: "string" },
|
|
31
40
|
},
|
|
32
41
|
});
|
|
33
42
|
// 无子命令时默认 serve(对齐 `npx @nowcrew/daemon@latest --server-url ... --api-key ...`)
|
|
@@ -38,6 +47,12 @@ async function main() {
|
|
|
38
47
|
" crew-daemon run --agent <h> --channel <id> [--wake ...] # " + td("run once manually") + "\n");
|
|
39
48
|
process.exit(2);
|
|
40
49
|
}
|
|
50
|
+
// profile 先加载,显式命令行参数仍具有最高优先级。服务描述只保留 profile 名,
|
|
51
|
+
// 不把 machine token 放进 argv / plist / systemd unit / scheduled task。
|
|
52
|
+
if (values["daemon-home"])
|
|
53
|
+
process.env.CREW_DAEMON_HOME = values["daemon-home"];
|
|
54
|
+
if (values.profile)
|
|
55
|
+
applyProfileToEnv(await loadProfile(values.profile), process.env);
|
|
41
56
|
// 命令行参数优先于环境变量,填回 env 供 loadConfig 读取
|
|
42
57
|
if (values["server-url"])
|
|
43
58
|
process.env.CREW_SERVER_URL = values["server-url"];
|
package/dist/normalize.js
CHANGED
|
@@ -34,6 +34,8 @@ export function classifyCommand(command) {
|
|
|
34
34
|
/** 从各 runtime 的最终事件提取可交付文本。调用方按事件顺序保留最后一个非空值。 */
|
|
35
35
|
export function extractFinalText(event) {
|
|
36
36
|
const e = (event ?? {});
|
|
37
|
+
if (e.type === "kimi.acp.text_delta" && e.text)
|
|
38
|
+
return e.text;
|
|
37
39
|
if (e.type === "result" && !e.is_error && e.result?.trim())
|
|
38
40
|
return e.result.trim();
|
|
39
41
|
if (e.type === "item.completed" && e.item?.type === "agent_message" && e.item.text?.trim()) {
|
|
@@ -59,6 +61,15 @@ function parseKimiBashCommand(args) {
|
|
|
59
61
|
/** 把一个 stream-json 事件归一化为 0..N 个活动。 */
|
|
60
62
|
export function normalizeEvent(event) {
|
|
61
63
|
const e = (event ?? {});
|
|
64
|
+
if (e.type === "kimi.acp.text_delta" && e.text?.trim()) {
|
|
65
|
+
return [{ kind: "text", label: "思考/说明", detail: e.text }];
|
|
66
|
+
}
|
|
67
|
+
if (e.type === "kimi.acp.tool_call") {
|
|
68
|
+
return [{ kind: "tool", label: e.title ? `工具:${e.title}` : "工具调用" }];
|
|
69
|
+
}
|
|
70
|
+
if (e.type === "kimi.acp.tool_result") {
|
|
71
|
+
return [{ kind: "tool_result", label: "工具返回" }];
|
|
72
|
+
}
|
|
62
73
|
if (e.type === "system" && e.subtype === "init") {
|
|
63
74
|
return [{ kind: "init", label: "agent 启动" }];
|
|
64
75
|
}
|
package/dist/origin-decision.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function readOriginDecisionFile(path) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
export function shouldRetryOriginDecision(wakeOrigin, decision, attempt) {
|
|
30
|
-
return wakeOrigin === "wecom" && decision
|
|
30
|
+
return wakeOrigin === "wecom" && decision?.decision !== "reply" && attempt === 0;
|
|
31
31
|
}
|
|
32
32
|
export async function runWithOriginDecisionGuard(wakeOrigin, runAttempt) {
|
|
33
33
|
const first = await runAttempt(0);
|
package/dist/prompt.js
CHANGED
|
@@ -29,8 +29,25 @@ export function buildSystemPrompt(ctx) {
|
|
|
29
29
|
?? (ctx.scheduled ? "silent_unless_report" : null);
|
|
30
30
|
const scheduled = scheduledPolicy !== null;
|
|
31
31
|
const alwaysReport = scheduledPolicy === "always_report";
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const messageBodyInput = ctx.platform === "win32"
|
|
33
|
+
? `PowerShell 5.1 向 native 进程传管道时默认可能使用 US-ASCII。发送中文等非 ASCII 正文时,**必须在同一条 shell 命令内**先设置无 BOM UTF-8:
|
|
34
|
+
\`\`\`powershell
|
|
35
|
+
$utf8 = New-Object System.Text.UTF8Encoding($false)
|
|
36
|
+
$OutputEncoding = $utf8
|
|
37
|
+
[Console]::OutputEncoding = $utf8
|
|
38
|
+
@'
|
|
39
|
+
你的消息正文
|
|
40
|
+
'@ | crew message send --channel <id> --thread <完整线程根消息 id>
|
|
41
|
+
\`\`\`
|
|
42
|
+
不得在未设置 \`$OutputEncoding\` 时把非 ASCII 正文通过 PowerShell 管道传给 \`crew\`。`
|
|
43
|
+
: `正文从 stdin 读,用 heredoc 避免 shell 解释引号/反引号/代码块:
|
|
44
|
+
\`\`\`bash
|
|
45
|
+
crew message send --channel <id> <<'CREWMSG'
|
|
46
|
+
你的消息正文,可含 "引号"、\\\`反引号\\\`、代码块。
|
|
47
|
+
CREWMSG
|
|
48
|
+
\`\`\``;
|
|
49
|
+
// 启动序列:普通交互对可直答来信只发一条完整回复,多步工作仍可发有信息量的进度;
|
|
50
|
+
// scheduled 换成静默版,不出现任何"先确认/先回复"的措辞。
|
|
34
51
|
const startupSequence = alwaysReport
|
|
35
52
|
? `## 启动序列(每轮汇报定时任务)
|
|
36
53
|
1. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
|
|
@@ -47,7 +64,7 @@ export function buildSystemPrompt(ctx) {
|
|
|
47
64
|
4. 只有存在需要后续跟进的具体事项时,才用 \`crew task create\` 创建任务。
|
|
48
65
|
5. 无异常时直接结束,不需要任何输出,不必读频道历史(除非指令要求)。`
|
|
49
66
|
: `## 启动序列
|
|
50
|
-
1.
|
|
67
|
+
1. 若本轮来信可以直接回答,不要先发确认消息;处理完后只发一条完整回复。只有需要澄清、调用工具、协调他人或较长时间处理时,才先发有信息量的确认/进度消息。
|
|
51
68
|
2. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
|
|
52
69
|
3. 若本轮只有"有未读"的 inbox notice、没有正文:notice 表示存在你尚未看到的消息(正文被暂时省略以免刷屏,不是没有内容)。是否读、何时读由你判断,可用 \`crew message check\` / \`crew message read\` 拉取。**绝不能仅凭一条 content-free notice 就断定"没有工作"**;若选择暂不读,要诚实当作 defer。
|
|
53
70
|
4. 收到消息就处理,并用 \`crew message send\` 回复。
|
|
@@ -74,14 +91,15 @@ export function buildSystemPrompt(ctx) {
|
|
|
74
91
|
## 定时任务创建与管理
|
|
75
92
|
平台不会用关键词预先区分创建或管理意图,由你根据完整来信和上下文判断用户是要创建定时任务,还是查看、修改、暂停、恢复、取消或立即执行已有任务。
|
|
76
93
|
1. 定时任务创建与管理无需 \`crew task claim\`;不要仅为创建或管理定时任务而创建 task。
|
|
77
|
-
2. 创建时使用 \`crew schedule create --agent ${ctx.handle} --channel ${ctx.channelId} --prompt '<text>' (--cron '<expr>' | --at '<ISO>')\`,不得编造或替换当前 agent 身份与频道。
|
|
94
|
+
2. 创建时使用 \`crew schedule create --agent ${ctx.handle} --channel ${ctx.channelId} --prompt '<text>' (--cron '<expr>' | --at '<ISO>') [--external-notification <disabled|agent-decides>]\`,不得编造或替换当前 agent 身份与频道。
|
|
78
95
|
3. \`crew schedule create\` 和 \`crew schedule update\` 都必须遵守这条规则:\`prompt\`、\`title\`、\`cron\`、\`at\`、\`timezone\` 的值都必须分别作为单个 shell 参数传入,使用单引号 shell 引用;值内若含单引号,必须按标准方式关闭单引号、写入转义后的单引号、再重新开启单引号。不得在创建或更新持久化前展开或执行用户内容中的 \`$\`、反引号或 \`$()\`。
|
|
79
96
|
4. 创建时默认使用 \`--output-policy always-report\`。创建时只有用户明确要求静默、不发送正常结果或仅在异常时汇报时,才使用 \`--output-policy on-exception\`。
|
|
80
97
|
5. 更新时若用户未明确要求改变汇报行为,必须省略 \`--output-policy\` 并保留已有策略。只有用户明确要求改变汇报行为时,才按同样规则映射输出策略:正常汇报用 \`always-report\`,静默或仅异常汇报用 \`on-exception\`。创建或更新汇报行为时,把用户原始的汇报条件保留在 \`--prompt\` 中,不要改写或省略。汇报要求确实有歧义时,先向用户确认。
|
|
81
|
-
6.
|
|
82
|
-
7.
|
|
83
|
-
8.
|
|
84
|
-
9.
|
|
98
|
+
6. 绑定会话通知创建时默认使用 \`disabled\`。只有用户明确要求将合适结果通知绑定会话时,才使用 \`--external-notification agent-decides\`;用户未明确要求修改该授权时,更新时必须省略 \`--external-notification\` 并保留已有策略。
|
|
99
|
+
7. 执行时间、时区或执行指令信息不足时,先向用户确认,不得猜测。
|
|
100
|
+
8. 查询现状或执行 create/update/pause/resume/cancel/run-now 任何操作前,先运行 \`crew schedule list --channel ${ctx.channelId} --agent ${ctx.handle} --json\` 查询本频道绑定到你自己的候选;仅查看时也使用这条 JSON 查询,以获取完整的 cron/at/timezone/prompt/output policy/external notification policy。
|
|
101
|
+
9. 只有一个明确匹配时,才运行 \`crew schedule update <jobId> ...\` 或对应 pause/resume/cancel/run-now 命令。有多个合理候选时,列出候选并先让用户选择;用户选定前不得修改任何任务。
|
|
102
|
+
10. 修改请求不能用 \`crew schedule create\` 代替。完成后回复 job ID、最终 schedule、timezone、output policy 和 external notification policy;更新还要列出变更字段的前后值。`;
|
|
85
103
|
const voiceRule = alwaysReport
|
|
86
104
|
? "- **本轮报告只写在 runtime 最终回复中**:不要调用 `crew message send`;daemon 会把最终回复投递到频道。"
|
|
87
105
|
: "- **始终只通过 crew CLI 发声。在 crew 命令之外产生的任何文字都不会送达任何人。**";
|
|
@@ -98,7 +116,7 @@ export function buildSystemPrompt(ctx) {
|
|
|
98
116
|
8. **\`crew task update <taskId> --status <in_progress|in_review|done>\`** —— 推进任务状态。
|
|
99
117
|
9. **\`crew task unclaim <taskId>\`** —— 释放认领,把任务让给别人。
|
|
100
118
|
10. **\`crew task assign <taskId> --to <handle>\`** —— 把任务指派/交接给另一个 agent(用于交接,见下)。
|
|
101
|
-
11. **\`crew schedule create --agent <handle> --channel <id> --prompt <text> [--title <t>] (--cron <expr> | --at <ISO>) [--timezone <iana>] [--output-policy <always-report|on-exception>]\`** —— 创建定时任务。
|
|
119
|
+
11. **\`crew schedule create --agent <handle> --channel <id> --prompt <text> [--title <t>] (--cron <expr> | --at <ISO>) [--timezone <iana>] [--output-policy <always-report|on-exception>] [--external-notification <disabled|agent-decides>]\`** —— 创建定时任务。
|
|
102
120
|
12. **\`crew schedule list --channel <id> --agent <handle> [--json]\`** —— 查询本频道指定 agent 的定时任务,\`--json\` 返回完整字段。
|
|
103
121
|
13. **\`crew schedule update <jobId> ...\`** —— 修改定时任务标题、指令、时间或输出策略。
|
|
104
122
|
14. **\`crew schedule pause|resume|cancel|run-now <jobId>\`** —— 控制定时任务。`;
|
|
@@ -114,12 +132,7 @@ ${taskAndScheduleCommands}`
|
|
|
114
132
|
1. **\`crew whoami\`** —— 查看你自己的身份。
|
|
115
133
|
2. **\`crew message read --channel <id>\`** —— 读频道历史(读取即自动推进你的已读/新鲜度游标)。支持 \`--after <seq>\` / \`--limit <n>\`。
|
|
116
134
|
3. **\`crew message check --channel <id>\`** —— 非阻塞查看未读数。工作中可在自然断点随时用。
|
|
117
|
-
4. **\`crew message send --channel <id>\`** ——
|
|
118
|
-
\`\`\`bash
|
|
119
|
-
crew message send --channel <id> <<'CREWMSG'
|
|
120
|
-
你的消息正文,可含 "引号"、\\\`反引号\\\`、代码块。
|
|
121
|
-
CREWMSG
|
|
122
|
-
\`\`\`
|
|
135
|
+
4. **\`crew message send --channel <id>\`** —— 发消息。${messageBodyInput}
|
|
123
136
|
也可用 \`--content "<短正文>"\`。线程内回复:加 \`--thread <完整线程根消息 id>\`。被唤醒时优先使用环境变量 \`$CREW_WAKE_MESSAGE_ID\` 或唤醒提示里的完整 id,不要手动截短。
|
|
124
137
|
${taskAndScheduleCommands}`;
|
|
125
138
|
const freshnessRule = alwaysReport
|
|
@@ -127,10 +140,13 @@ ${taskAndScheduleCommands}`;
|
|
|
127
140
|
: `
|
|
128
141
|
- **freshness/draft**:发送若被保存为 draft(kind=held),要么重读后用普通 send 改写,要么用 \`crew message send --send-draft\` 原样发出(不要在改内容时用 --send-draft)。`;
|
|
129
142
|
const externalReplyRule = scheduled
|
|
130
|
-
? ""
|
|
143
|
+
? ctx.scheduledExternalNotificationPolicy === "agent_decides"
|
|
144
|
+
? `
|
|
145
|
+
- **绑定会话通知由你选择**:只有本轮完整结果确实值得打扰绑定会话时,运行 \`crew message notify-bound-im --channel ${ctx.channelId}\` 一次,然后仍只返回一个完整最终报告。该命令只记录本轮决策,不会自行发消息,也不能指定收件人。`
|
|
146
|
+
: ""
|
|
131
147
|
: ctx.wakeOrigin === "wecom"
|
|
132
148
|
? `
|
|
133
|
-
-
|
|
149
|
+
- **本轮来自企微,结束本轮前必须给出一条完整回复**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。最终只用一次 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数)发送有实质内容的完整结果;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定,并在本轮未形成可交付内容时发送统一兜底回复。`
|
|
134
150
|
: `
|
|
135
151
|
- **外部来源回复由你决定**:普通 \`crew message send\` 只写入 NowWork。只有当前 thread 明确来自企微等外部 IM、且这条消息确实要回复外部发言人时,才给该次发送加 \`--reply-origin\`;Server 会按当前 thread 的已验证来源路由,你不能指定任意机器人或会话。确认、过程进展、内部协作消息不要使用该参数。若判断无需回外部,只发普通内部消息或保持沉默。`;
|
|
136
152
|
const interactiveTaskRules = scheduled ? "" : `
|
|
@@ -243,11 +259,14 @@ ${ctx.memory ? `\n## [注入] 你的 MEMORY.md(索引,只读参考)\n${ctx.memor
|
|
|
243
259
|
/** 静默定时任务的唤醒提示词:不注入协作礼仪/线程提示;默认零输出(设计文档 §5.4)。
|
|
244
260
|
* 汇报用 --send-draft:绕过 freshness hold(-p 单发不跑 crew read,游标落后,
|
|
245
261
|
* 普通 send 会被 202 扣成 draft——监控告警绝不能被静默扣留)。 */
|
|
246
|
-
export function buildScheduledPrompt(channelId, jobPrompt, outputPolicy = "silent_unless_report") {
|
|
247
|
-
if (outputPolicy === "always_report") {
|
|
262
|
+
export function buildScheduledPrompt(channelId, jobPrompt, outputPolicy = "silent_unless_report", externalNotificationPolicy = "disabled") {
|
|
263
|
+
if (outputPolicy === "always_report" || externalNotificationPolicy === "agent_decides") {
|
|
248
264
|
return [
|
|
249
265
|
"You are executing a scheduled job. No one is waiting for an acknowledgement.",
|
|
250
266
|
`Scheduled instruction: ${jobPrompt}`,
|
|
267
|
+
...(externalNotificationPolicy === "agent_decides" ? [
|
|
268
|
+
`If and only if this complete result warrants notifying the bound conversation, run \`crew message notify-bound-im --channel ${channelId}\` once. The command records a local decision and does not send a second message.`,
|
|
269
|
+
] : []),
|
|
251
270
|
"Return exactly one self-contained final report as your runtime final response.",
|
|
252
271
|
"Do not call crew message send; the daemon delivers the final response.",
|
|
253
272
|
"Do not send acknowledgements or progress messages.",
|
|
@@ -277,8 +296,8 @@ export function buildOriginDecisionRetryPrompt(channelId, threadId) {
|
|
|
277
296
|
const thread = threadId ? ` --thread ${threadId}` : "";
|
|
278
297
|
return [
|
|
279
298
|
"本轮尚未完成企微回复决策。不要重复执行已经完成的工作,只完成下面这个决策后结束:",
|
|
280
|
-
`-
|
|
281
|
-
|
|
299
|
+
`- 必须回复企微:用 crew message send --channel ${channelId}${thread} --reply-origin --content "完整最终回复"。`,
|
|
300
|
+
"- 只发送一条有实质内容的完整回复,不要拆成确认、进展和结论多条外部消息。",
|
|
282
301
|
"普通内部消息不算外部回复决策;--send-draft 也不是外部回复开关。",
|
|
283
302
|
].join("\n");
|
|
284
303
|
}
|
package/dist/runner.js
CHANGED
|
@@ -7,6 +7,7 @@ import { deliverScheduledReport, } from "./scheduled-report.js";
|
|
|
7
7
|
import { executeLocal } from "./local-executor.js";
|
|
8
8
|
import { ReasoningSchema } from "./execution-protocol.js";
|
|
9
9
|
import { readOriginDecisionFile, resetOriginDecisionFile } from "./origin-decision.js";
|
|
10
|
+
import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
|
|
10
11
|
export { awaitExit, exitActivity, sanitizeEnvVars } from "./local-executor.js";
|
|
11
12
|
const ICON = {
|
|
12
13
|
init: "🟢", text: "💬", reading: "📖", sending: "📨", checking: "🔎",
|
|
@@ -31,6 +32,9 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
31
32
|
const originDecisionFileName = input.wakeOrigin === "wecom"
|
|
32
33
|
? `.origin-decision-${executionId}.json`
|
|
33
34
|
: null;
|
|
35
|
+
const boundImDecisionFileName = input.scheduled?.externalNotificationPolicy === "agent_decides"
|
|
36
|
+
? `.bound-im-decision-${executionId}.json`
|
|
37
|
+
: null;
|
|
34
38
|
const local = await executeLocal({
|
|
35
39
|
executionId,
|
|
36
40
|
handle: input.handle,
|
|
@@ -43,7 +47,11 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
43
47
|
agentId: credential.agentId,
|
|
44
48
|
homeDir: workspace.dir,
|
|
45
49
|
productName: config.productName,
|
|
50
|
+
platform: process.platform,
|
|
46
51
|
...(input.scheduled ? { scheduledOutputPolicy: input.scheduled.outputPolicy } : {}),
|
|
52
|
+
...(input.scheduled?.externalNotificationPolicy
|
|
53
|
+
? { scheduledExternalNotificationPolicy: input.scheduled.externalNotificationPolicy }
|
|
54
|
+
: {}),
|
|
47
55
|
...(input.wakeOrigin ? { wakeOrigin: input.wakeOrigin } : {}),
|
|
48
56
|
memory: capMemoryForInject(workspace.memory),
|
|
49
57
|
...(resuming ? {} : { workLog: capWorkLogForInject(workspace.workLog) }),
|
|
@@ -81,6 +89,9 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
81
89
|
CREW_WAKE_ORIGIN: "wecom",
|
|
82
90
|
CREW_ORIGIN_DECISION_FILE: originDecisionFileName,
|
|
83
91
|
} : {}),
|
|
92
|
+
...(boundImDecisionFileName ? {
|
|
93
|
+
CREW_BOUND_IM_DECISION_FILE: boundImDecisionFileName,
|
|
94
|
+
} : {}),
|
|
84
95
|
},
|
|
85
96
|
},
|
|
86
97
|
session: {
|
|
@@ -116,9 +127,20 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
116
127
|
process.stdout.write(`📊 tokens: in=${usage.inputTokens} out=${usage.outputTokens} cache_read=${usage.cacheReadTokens} cache_create=${usage.cacheCreationTokens}`
|
|
117
128
|
+ `${usage.costUsd != null ? ` cost=$${usage.costUsd.toFixed(4)}` : ""} ${local.resumed ? "(resumed)" : "(fresh)"}\n`);
|
|
118
129
|
}
|
|
130
|
+
const boundImDecisionPath = boundImDecisionFileName
|
|
131
|
+
? join(local.workspaceRunDir, boundImDecisionFileName)
|
|
132
|
+
: null;
|
|
133
|
+
const selectedBoundImDecision = boundImDecisionPath
|
|
134
|
+
? await readBoundImDecisionFile(boundImDecisionPath)
|
|
135
|
+
: null;
|
|
136
|
+
if (boundImDecisionPath)
|
|
137
|
+
await resetBoundImDecisionFile(boundImDecisionPath);
|
|
138
|
+
const boundImDecision = boundImDecisionPath
|
|
139
|
+
? selectedBoundImDecision?.decision ?? "silent"
|
|
140
|
+
: undefined;
|
|
119
141
|
const report = input.scheduled
|
|
120
142
|
? await deliverScheduledReport({
|
|
121
|
-
policy: input.scheduled.outputPolicy,
|
|
143
|
+
policy: boundImDecision === "notify" ? "always_report" : input.scheduled.outputPolicy,
|
|
122
144
|
title: input.scheduled.title,
|
|
123
145
|
exitCode: local.exitCode,
|
|
124
146
|
finalText: local.finalText,
|
|
@@ -126,6 +148,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
126
148
|
send: (content) => sendAgentMessage(config.serverUrl, credential.token, input.channelId, {
|
|
127
149
|
content,
|
|
128
150
|
force: true,
|
|
151
|
+
...(boundImDecision === "notify" ? { notifyBoundIm: true } : {}),
|
|
129
152
|
}),
|
|
130
153
|
})
|
|
131
154
|
: undefined;
|
|
@@ -145,6 +168,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
145
168
|
errorMessage: local.errorMessage,
|
|
146
169
|
...(local.usage ? { usage: local.usage } : {}),
|
|
147
170
|
...(report ? { report } : {}),
|
|
171
|
+
...(boundImDecision ? { boundImDecision } : {}),
|
|
148
172
|
...(input.wakeOrigin === "wecom" ? {
|
|
149
173
|
originDecision: originDecision?.decision ?? "missing",
|
|
150
174
|
...(originDecision?.decision === "silent" && originDecision.reason
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const LOCAL_EXECUTION_RUNTIMES = ["claude", "codex", "kimi"];
|
|
2
|
+
const LOCAL_EXECUTION_RUNTIME_SET = new Set(LOCAL_EXECUTION_RUNTIMES);
|
|
3
|
+
export function executableRuntimes(installed) {
|
|
4
|
+
return [...new Set(installed)].filter((runtime) => LOCAL_EXECUTION_RUNTIME_SET.has(runtime));
|
|
5
|
+
}
|