@nowcrew/daemon 0.5.11 → 0.5.13
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 +77 -0
- package/dist/config.js +38 -0
- package/dist/execution-event-limit.js +59 -0
- package/dist/execution-journal-lock.js +262 -0
- package/dist/execution-journal.js +678 -0
- package/dist/execution-protocol.js +310 -0
- package/dist/execution-runner.js +637 -0
- package/dist/execution-supervisor-child.js +185 -0
- package/dist/execution-supervisor.js +209 -0
- package/dist/local-executor.js +326 -0
- package/dist/machine-info.js +13 -4
- package/dist/origin-decision.js +42 -0
- package/dist/prompt.js +23 -1
- package/dist/runner.js +146 -340
- package/dist/runtimes/claude.js +9 -1
- package/dist/runtimes/codex.js +21 -5
- package/dist/runtimes/kimi.js +3 -0
- package/dist/scheduled-run-report.js +15 -7
- package/dist/serve.js +412 -57
- package/dist/token.js +5 -2
- package/dist/workspace.js +39 -11
- package/package.json +3 -2
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
import { prepareWorkspace, rotateAgentSession, } from "./workspace.js";
|
|
5
|
+
import { spawnClaude } from "./runtimes/claude.js";
|
|
6
|
+
import { spawnCodex } from "./runtimes/codex.js";
|
|
7
|
+
import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
8
|
+
import { applyProviderEnv, providerFingerprint } from "./provider-env.js";
|
|
9
|
+
import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
|
|
10
|
+
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
11
|
+
import { toConsoleLines } from "./console.js";
|
|
12
|
+
import { capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
13
|
+
function truncateUtf8(value, maxBytes) {
|
|
14
|
+
if (maxBytes <= 0)
|
|
15
|
+
return "";
|
|
16
|
+
let bytes = 0;
|
|
17
|
+
let end = 0;
|
|
18
|
+
for (const character of value) {
|
|
19
|
+
const size = Buffer.byteLength(character, "utf8");
|
|
20
|
+
if (bytes + size > maxBytes)
|
|
21
|
+
break;
|
|
22
|
+
bytes += size;
|
|
23
|
+
end += character.length;
|
|
24
|
+
}
|
|
25
|
+
return value.slice(0, end);
|
|
26
|
+
}
|
|
27
|
+
/** v1 server instructions opt in to daemon-owned paths and bounded local workspace context. */
|
|
28
|
+
export function withLocalExecutionFacts(serverPrompt, maxBytes) {
|
|
29
|
+
return ({ workspace, resuming }) => {
|
|
30
|
+
const memory = capMemoryForInject(workspace.memory);
|
|
31
|
+
const workLog = resuming ? "" : capWorkLogForInject(workspace.workLog);
|
|
32
|
+
const localFacts = `\n\n## Local execution facts
|
|
33
|
+
- $CREW_HOME: ${workspace.dir}
|
|
34
|
+
- $CREW_TASK_LOG: ${workspace.workLogPath}${memory
|
|
35
|
+
? `\n\n## Injected MEMORY.md (bounded local context)\n${memory}`
|
|
36
|
+
: ""}${workLog
|
|
37
|
+
? `\n\n## Injected work-log (bounded local context)\n${workLog}`
|
|
38
|
+
: ""}`;
|
|
39
|
+
const remaining = maxBytes - Buffer.byteLength(serverPrompt, "utf8");
|
|
40
|
+
return `${serverPrompt}${truncateUtf8(localFacts, remaining)}`;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const STDERR_TAIL_CAP = 2_000;
|
|
44
|
+
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
45
|
+
const RESERVED_ENV = new Set([
|
|
46
|
+
"PATH",
|
|
47
|
+
"XDG_CONFIG_HOME",
|
|
48
|
+
"XDG_DATA_HOME",
|
|
49
|
+
"XDG_CACHE_HOME",
|
|
50
|
+
"GH_CONFIG_DIR",
|
|
51
|
+
"CLOUDSDK_CONFIG",
|
|
52
|
+
]);
|
|
53
|
+
export function sanitizeEnvVars(raw) {
|
|
54
|
+
if (!raw)
|
|
55
|
+
return {};
|
|
56
|
+
return Object.fromEntries(Object.entries(raw).filter(([key, value]) => typeof value === "string"
|
|
57
|
+
&& ENV_KEY_RE.test(key)
|
|
58
|
+
&& !key.toUpperCase().startsWith("CREW_")
|
|
59
|
+
&& !RESERVED_ENV.has(key.toUpperCase())));
|
|
60
|
+
}
|
|
61
|
+
export function awaitExit(child) {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
let settled = false;
|
|
64
|
+
const settle = (result) => {
|
|
65
|
+
if (settled)
|
|
66
|
+
return;
|
|
67
|
+
settled = true;
|
|
68
|
+
resolve(result);
|
|
69
|
+
};
|
|
70
|
+
child.on("error", (error) => settle({ exitCode: -1, spawnError: error.message }));
|
|
71
|
+
child.on("close", (code, signal) => settle(code === null
|
|
72
|
+
? { exitCode: 128, terminationSignal: signal ?? "unknown" }
|
|
73
|
+
: { exitCode: code }));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function exitActivity(runtime, exitCode, stderrTail) {
|
|
77
|
+
if ((runtime === "codex" || runtime === "kimi" || exitCode === -1) && exitCode !== 0) {
|
|
78
|
+
return {
|
|
79
|
+
kind: "error",
|
|
80
|
+
label: "运行出错",
|
|
81
|
+
detail: `${runtime} exited with code ${exitCode}${stderrTail ? `: ${stderrTail}` : ""}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (runtime === "kimi" && exitCode === 0)
|
|
85
|
+
return { kind: "done", label: "本轮结束" };
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function wrapChild(child) {
|
|
89
|
+
return {
|
|
90
|
+
...(child.pid === undefined ? {} : { pid: child.pid }),
|
|
91
|
+
stdout: child.stdout,
|
|
92
|
+
stderr: child.stderr,
|
|
93
|
+
exit: awaitExit(child),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function launchLegacyRuntime(request) {
|
|
97
|
+
const common = {
|
|
98
|
+
bin: request.bin,
|
|
99
|
+
cwd: request.cwd,
|
|
100
|
+
env: request.env,
|
|
101
|
+
dangerous: request.effectivePermission === "full_access",
|
|
102
|
+
effectivePermission: request.effectivePermission,
|
|
103
|
+
...(request.model === undefined ? {} : { model: request.model }),
|
|
104
|
+
...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
|
|
105
|
+
};
|
|
106
|
+
if (request.runtime === "claude") {
|
|
107
|
+
return wrapChild(spawnClaude({
|
|
108
|
+
...common,
|
|
109
|
+
systemPromptPath: request.systemPromptPath,
|
|
110
|
+
wakePrompt: request.wakePrompt,
|
|
111
|
+
...(request.sessionId === undefined ? {} : {
|
|
112
|
+
sessionId: request.sessionId,
|
|
113
|
+
resume: request.resume,
|
|
114
|
+
}),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
if (request.runtime === "codex") {
|
|
118
|
+
return wrapChild(spawnCodex({
|
|
119
|
+
...common,
|
|
120
|
+
wakePrompt: `${request.systemPrompt}\n\n${request.wakePrompt}`,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
return wrapChild(spawnKimi({
|
|
124
|
+
bin: request.bin,
|
|
125
|
+
cwd: request.cwd,
|
|
126
|
+
env: request.env,
|
|
127
|
+
effectivePermission: request.effectivePermission,
|
|
128
|
+
wakePrompt: `${request.systemPrompt}\n\n${request.wakePrompt}`,
|
|
129
|
+
...(request.model === undefined ? {} : { model: request.model }),
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
function resolvePrompt(prompt, context) {
|
|
133
|
+
return typeof prompt === "string" ? prompt : prompt(context);
|
|
134
|
+
}
|
|
135
|
+
const nativeSessionLeaseTails = new Map();
|
|
136
|
+
async function withKeyedLease(key, operation) {
|
|
137
|
+
const predecessor = nativeSessionLeaseTails.get(key) ?? Promise.resolve();
|
|
138
|
+
let release;
|
|
139
|
+
const current = new Promise((resolve) => { release = resolve; });
|
|
140
|
+
const tail = predecessor.then(() => current);
|
|
141
|
+
nativeSessionLeaseTails.set(key, tail);
|
|
142
|
+
await predecessor;
|
|
143
|
+
try {
|
|
144
|
+
return await operation();
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
release();
|
|
148
|
+
if (nativeSessionLeaseTails.get(key) === tail)
|
|
149
|
+
nativeSessionLeaseTails.delete(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
153
|
+
if (input.runtime.name !== "claude" || !input.session.enabled) {
|
|
154
|
+
return executeLocalUnlocked(input, callbacks, dependencies);
|
|
155
|
+
}
|
|
156
|
+
const leaseKey = JSON.stringify([
|
|
157
|
+
input.launch.agentsRoot,
|
|
158
|
+
input.handle,
|
|
159
|
+
input.keyMode ?? "legacy",
|
|
160
|
+
input.resumeKey ?? input.taskKey ?? "",
|
|
161
|
+
]);
|
|
162
|
+
return withKeyedLease(leaseKey, () => executeLocalUnlocked(input, callbacks, dependencies));
|
|
163
|
+
}
|
|
164
|
+
async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
165
|
+
const providerConfig = input.launch.providerConfig ?? {};
|
|
166
|
+
const { runtime } = input;
|
|
167
|
+
const currentModel = runtime.model ?? null;
|
|
168
|
+
const providerFp = providerFingerprint(runtime.name, providerConfig);
|
|
169
|
+
const workspace = await prepareWorkspace({
|
|
170
|
+
agentsRoot: input.launch.agentsRoot,
|
|
171
|
+
handle: input.handle,
|
|
172
|
+
cliPath: input.launch.cliPath,
|
|
173
|
+
executionId: input.executionId,
|
|
174
|
+
...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
|
|
175
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
176
|
+
...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
|
|
177
|
+
...(input.launch.description ? { description: input.launch.description } : {}),
|
|
178
|
+
});
|
|
179
|
+
try {
|
|
180
|
+
const supportsNativeResume = runtime.name === "claude";
|
|
181
|
+
const prior = input.session.enabled && supportsNativeResume
|
|
182
|
+
? await readSession(workspace.sessionDir)
|
|
183
|
+
: null;
|
|
184
|
+
const resuming = supportsNativeResume && workspace.sessionResume
|
|
185
|
+
&& pickResumeId(prior, Date.now(), input.session.warmMs, currentModel, input.session.budgetTokens, input.session.maxTurns, providerFp) !== null;
|
|
186
|
+
const rotatedForBudget = !resuming && supportsNativeResume && workspace.sessionResume
|
|
187
|
+
&& pickResumeId(prior, Date.now(), input.session.warmMs, currentModel, 0, 0, providerFp) !== null;
|
|
188
|
+
const nearBudget = resuming && isNearBudget(prior, input.session.softTokens);
|
|
189
|
+
const rotated = Boolean(workspace.agentSessionId && workspace.sessionResume && !resuming);
|
|
190
|
+
const launchSessionId = rotated
|
|
191
|
+
? await rotateAgentSession(workspace.sessionDir)
|
|
192
|
+
: workspace.agentSessionId;
|
|
193
|
+
const promptContext = {
|
|
194
|
+
workspace,
|
|
195
|
+
resuming,
|
|
196
|
+
rotatedForBudget,
|
|
197
|
+
nearBudget,
|
|
198
|
+
};
|
|
199
|
+
const systemPrompt = resolvePrompt(input.systemPrompt, promptContext);
|
|
200
|
+
const wakePrompt = resolvePrompt(input.wakePrompt, promptContext);
|
|
201
|
+
await writeFile(workspace.systemPromptPath, systemPrompt, "utf8");
|
|
202
|
+
const baseEnv = {
|
|
203
|
+
...process.env,
|
|
204
|
+
...sanitizeEnvVars(providerConfig.envVars),
|
|
205
|
+
...input.launch.systemEnv,
|
|
206
|
+
PATH: `${workspace.crewDir}${delimiter}${process.env.PATH ?? ""}`,
|
|
207
|
+
CREW_SERVER_URL: input.launch.serverUrl,
|
|
208
|
+
CREW_TOKEN: input.launch.token,
|
|
209
|
+
CREW_CHANNEL: input.channelId,
|
|
210
|
+
CREW_HOME: workspace.dir,
|
|
211
|
+
CREW_TASK_LOG: workspace.workLogPath,
|
|
212
|
+
...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
|
|
213
|
+
XDG_CONFIG_HOME: join(workspace.homeDir, ".config"),
|
|
214
|
+
XDG_DATA_HOME: join(workspace.homeDir, ".local", "share"),
|
|
215
|
+
XDG_CACHE_HOME: join(workspace.homeDir, ".cache"),
|
|
216
|
+
GH_CONFIG_DIR: join(workspace.homeDir, ".config", "gh"),
|
|
217
|
+
CLOUDSDK_CONFIG: join(workspace.homeDir, ".config", "gcloud"),
|
|
218
|
+
...(runtime.name === "kimi" && runtime.reasoning
|
|
219
|
+
&& KIMI_EFFORT_LEVELS.includes(runtime.reasoning)
|
|
220
|
+
? { KIMI_MODEL_THINKING_EFFORT: runtime.reasoning }
|
|
221
|
+
: {}),
|
|
222
|
+
};
|
|
223
|
+
const childEnv = applyProviderEnv(baseEnv, runtime.name, providerConfig, workspace.homeDir);
|
|
224
|
+
const launchRuntime = dependencies.launchRuntime ?? launchLegacyRuntime;
|
|
225
|
+
const child = await launchRuntime({
|
|
226
|
+
runtime: runtime.name,
|
|
227
|
+
bin: runtime.name,
|
|
228
|
+
cwd: workspace.runDir,
|
|
229
|
+
systemPromptPath: workspace.systemPromptPath,
|
|
230
|
+
systemPrompt,
|
|
231
|
+
wakePrompt,
|
|
232
|
+
env: childEnv,
|
|
233
|
+
effectivePermission: input.effectivePermission,
|
|
234
|
+
...(runtime.model === undefined ? {} : { model: runtime.model }),
|
|
235
|
+
...(runtime.reasoning === undefined ? {} : { reasoning: runtime.reasoning }),
|
|
236
|
+
...(launchSessionId === null ? {} : { sessionId: launchSessionId }),
|
|
237
|
+
resume: resuming,
|
|
238
|
+
});
|
|
239
|
+
const activities = [];
|
|
240
|
+
let sessionId = launchSessionId;
|
|
241
|
+
let usage;
|
|
242
|
+
let observedModel = currentModel;
|
|
243
|
+
let finalText = null;
|
|
244
|
+
let sentViaCrew = false;
|
|
245
|
+
const readline = createInterface({ input: child.stdout });
|
|
246
|
+
readline.on("line", (line) => {
|
|
247
|
+
const event = parseLine(line);
|
|
248
|
+
if (!event)
|
|
249
|
+
return;
|
|
250
|
+
const meta = extractRunMeta(event);
|
|
251
|
+
if (meta.sessionId)
|
|
252
|
+
sessionId = meta.sessionId;
|
|
253
|
+
if (meta.usage)
|
|
254
|
+
usage = meta.usage;
|
|
255
|
+
if (meta.model)
|
|
256
|
+
observedModel = meta.model;
|
|
257
|
+
for (const activity of normalizeEvent(event)) {
|
|
258
|
+
if (activity.kind === "sending")
|
|
259
|
+
sentViaCrew = true;
|
|
260
|
+
activities.push(activity);
|
|
261
|
+
callbacks.onActivity?.(activity);
|
|
262
|
+
}
|
|
263
|
+
const extracted = extractFinalText(event);
|
|
264
|
+
if (extracted)
|
|
265
|
+
finalText = extracted;
|
|
266
|
+
for (const chunk of toConsoleLines(event))
|
|
267
|
+
callbacks.onConsole?.(chunk);
|
|
268
|
+
});
|
|
269
|
+
let stderrTail = "";
|
|
270
|
+
child.stderr.on("data", (data) => {
|
|
271
|
+
process.stderr.write(data);
|
|
272
|
+
stderrTail = (stderrTail + String(data)).slice(-STDERR_TAIL_CAP);
|
|
273
|
+
});
|
|
274
|
+
const { exitCode, spawnError, terminationSignal } = await child.exit;
|
|
275
|
+
const errorTail = [
|
|
276
|
+
stderrTail.trim(),
|
|
277
|
+
spawnError,
|
|
278
|
+
terminationSignal ? `terminated by ${terminationSignal}` : undefined,
|
|
279
|
+
].filter(Boolean).join(" ").trim();
|
|
280
|
+
const finish = exitActivity(runtime.name, exitCode, errorTail);
|
|
281
|
+
if (finish) {
|
|
282
|
+
activities.push(finish);
|
|
283
|
+
callbacks.onActivity?.(finish);
|
|
284
|
+
if (finish.kind === "error") {
|
|
285
|
+
callbacks.onConsole?.({ stream: "error", text: `✖ ${finish.detail ?? finish.label}` });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (input.session.enabled && supportsNativeResume && sessionId) {
|
|
289
|
+
const contextTokens = usage
|
|
290
|
+
? usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens
|
|
291
|
+
: (resuming ? prior?.contextTokens : undefined);
|
|
292
|
+
await writeSession(workspace.sessionDir, {
|
|
293
|
+
sessionId,
|
|
294
|
+
lastRunAt: Date.now(),
|
|
295
|
+
turns: resuming ? (prior?.turns ?? 0) + 1 : 1,
|
|
296
|
+
model: currentModel,
|
|
297
|
+
providerFingerprint: providerFp,
|
|
298
|
+
lastExitOk: exitCode === 0,
|
|
299
|
+
...(contextTokens === undefined ? {} : { contextTokens }),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
workspaceRunDir: workspace.runDir,
|
|
304
|
+
exitCode,
|
|
305
|
+
...(terminationSignal === undefined ? {} : { terminationSignal }),
|
|
306
|
+
activities,
|
|
307
|
+
...(usage === undefined ? {} : { usage }),
|
|
308
|
+
model: observedModel,
|
|
309
|
+
runtime: runtime.name,
|
|
310
|
+
resumed: resuming,
|
|
311
|
+
sessionId,
|
|
312
|
+
errorMessage: errorTail || null,
|
|
313
|
+
finalText: input.captureFinal ? finalText : null,
|
|
314
|
+
sentViaCrew,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
try {
|
|
319
|
+
await rm(workspace.systemPromptPath, { force: true });
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
323
|
+
process.stderr.write(`[execution] failed to remove prompt ${workspace.systemPromptPath}: ${detail}\n`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
package/dist/machine-info.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 采集本机信息上报给控制面 (machine:hello):hostname / os / daemon 版本 / 已装 runtimes。
|
|
3
3
|
* runtimes 探测靠 `which <bin>`(win32 用 `where`),只报真实可执行的 CLI(用于展示 Detected Runtimes)。
|
|
4
4
|
*/
|
|
5
|
-
import { hostname, arch, platform } from "node:os";
|
|
5
|
+
import { hostname, arch, platform as osPlatform } from "node:os";
|
|
6
6
|
import { lookupCmd } from "./platform.js";
|
|
7
7
|
import { execFile } from "node:child_process";
|
|
8
8
|
import { promisify } from "node:util";
|
|
@@ -12,7 +12,12 @@ import { fileURLToPath } from "node:url";
|
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import { dirname, resolve } from "node:path";
|
|
14
14
|
const execFileP = promisify(execFile);
|
|
15
|
-
export const DAEMON_CAPABILITIES = [
|
|
15
|
+
export const DAEMON_CAPABILITIES = [
|
|
16
|
+
"scheduled_job_v1",
|
|
17
|
+
"reply_origin_v1",
|
|
18
|
+
"origin_decision_v1",
|
|
19
|
+
];
|
|
20
|
+
export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
|
|
16
21
|
/** 候选 runtime CLI:展示名 → 可执行文件名。 */
|
|
17
22
|
const RUNTIME_BINS = [
|
|
18
23
|
["claude", "claude"],
|
|
@@ -72,7 +77,7 @@ async function listAgentHandles(agentsRoot) {
|
|
|
72
77
|
return [];
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
|
-
export async function collectMachineHello(agentsRoot) {
|
|
80
|
+
export async function collectMachineHello(agentsRoot, executionLimits, runtimePlatform = process.platform) {
|
|
76
81
|
const [runtimes, agentHandles] = await Promise.all([
|
|
77
82
|
detectRuntimes(),
|
|
78
83
|
listAgentHandles(agentsRoot),
|
|
@@ -80,10 +85,14 @@ export async function collectMachineHello(agentsRoot) {
|
|
|
80
85
|
return {
|
|
81
86
|
type: "machine:hello",
|
|
82
87
|
hostname: hostname(),
|
|
83
|
-
os: `${
|
|
88
|
+
os: `${osPlatform()} ${arch()}`,
|
|
84
89
|
daemonVersion: daemonVersion(),
|
|
85
90
|
runtimes,
|
|
86
91
|
capabilities: DAEMON_CAPABILITIES,
|
|
92
|
+
...(runtimePlatform === "win32" ? {} : {
|
|
93
|
+
executionProtocol: EXECUTION_PROTOCOL,
|
|
94
|
+
executionLimits: Object.freeze({ ...executionLimits }),
|
|
95
|
+
}),
|
|
87
96
|
agentHandles,
|
|
88
97
|
};
|
|
89
98
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function decisionFilePath(runDir) {
|
|
4
|
+
return join(runDir, ".origin-decision.json");
|
|
5
|
+
}
|
|
6
|
+
export async function resetOriginDecisionFile(path) {
|
|
7
|
+
await unlink(path).catch((error) => {
|
|
8
|
+
if (error.code !== "ENOENT")
|
|
9
|
+
throw error;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export async function readOriginDecisionFile(path) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
15
|
+
if (!parsed || typeof parsed !== "object")
|
|
16
|
+
return null;
|
|
17
|
+
const value = parsed;
|
|
18
|
+
if (value.decision === "reply")
|
|
19
|
+
return { decision: "reply" };
|
|
20
|
+
if (value.decision !== "silent")
|
|
21
|
+
return null;
|
|
22
|
+
const reason = typeof value.reason === "string" ? value.reason.trim().slice(0, 500) : "";
|
|
23
|
+
return { decision: "silent", ...(reason ? { reason } : {}) };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function shouldRetryOriginDecision(wakeOrigin, decision, attempt) {
|
|
30
|
+
return wakeOrigin === "wecom" && decision === null && attempt === 0;
|
|
31
|
+
}
|
|
32
|
+
export async function runWithOriginDecisionGuard(wakeOrigin, runAttempt) {
|
|
33
|
+
const first = await runAttempt(0);
|
|
34
|
+
const firstDecision = first.originDecision === "reply"
|
|
35
|
+
? { decision: "reply" }
|
|
36
|
+
: first.originDecision === "silent" ? { decision: "silent" } : null;
|
|
37
|
+
if (!shouldRetryOriginDecision(wakeOrigin, firstDecision, 0)) {
|
|
38
|
+
return { results: [first], final: first };
|
|
39
|
+
}
|
|
40
|
+
const retry = await runAttempt(1);
|
|
41
|
+
return { results: [first, retry], final: retry };
|
|
42
|
+
}
|
package/dist/prompt.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
/** work-log 注入上限(字符):防冷启动注入随文件膨胀。超限保头(目标/结论)+尾(最近进展),
|
|
9
9
|
* 截中段并提示读全文——全文仍在盘上,只限"默认推送量"不限"可获取量"。纯函数。 */
|
|
10
10
|
export const WORKLOG_INJECT_CAP = 8000;
|
|
11
|
+
export const MEMORY_INJECT_CAP = 6000;
|
|
11
12
|
const WORKLOG_HEAD = 4800;
|
|
12
13
|
const WORKLOG_TAIL = 3000;
|
|
13
14
|
export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
|
|
@@ -17,6 +18,11 @@ export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
|
|
|
17
18
|
const tail = workLog.slice(-WORKLOG_TAIL);
|
|
18
19
|
return `${head}\n…(work-log 过长已截去中段,全文用 Read 读 $CREW_TASK_LOG)\n${tail}`;
|
|
19
20
|
}
|
|
21
|
+
export function capMemoryForInject(memory, cap = MEMORY_INJECT_CAP) {
|
|
22
|
+
return memory.length > cap
|
|
23
|
+
? `${memory.slice(0, cap)}\n…(MEMORY.md 过长已截断,详情用 Read 读 $CREW_HOME/MEMORY.md 或 notes/)`
|
|
24
|
+
: memory;
|
|
25
|
+
}
|
|
20
26
|
export function buildSystemPrompt(ctx) {
|
|
21
27
|
const product = ctx.productName ?? "nowwork";
|
|
22
28
|
const scheduledPolicy = ctx.scheduledOutputPolicy
|
|
@@ -120,6 +126,13 @@ ${taskAndScheduleCommands}`;
|
|
|
120
126
|
? ""
|
|
121
127
|
: `
|
|
122
128
|
- **freshness/draft**:发送若被保存为 draft(kind=held),要么重读后用普通 send 改写,要么用 \`crew message send --send-draft\` 原样发出(不要在改内容时用 --send-draft)。`;
|
|
129
|
+
const externalReplyRule = scheduled
|
|
130
|
+
? ""
|
|
131
|
+
: ctx.wakeOrigin === "wecom"
|
|
132
|
+
? `
|
|
133
|
+
- **本轮来自企微,结束本轮前必须明确选择外部回复决策**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。完整结果确实要回复企微时,用 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数);判断无需回复企微时,用 \`crew message skip-origin --reason "简短原因"\`。两者必须选择一个;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定。`
|
|
134
|
+
: `
|
|
135
|
+
- **外部来源回复由你决定**:普通 \`crew message send\` 只写入 NowWork。只有当前 thread 明确来自企微等外部 IM、且这条消息确实要回复外部发言人时,才给该次发送加 \`--reply-origin\`;Server 会按当前 thread 的已验证来源路由,你不能指定任意机器人或会话。确认、过程进展、内部协作消息不要使用该参数。若判断无需回外部,只发普通内部消息或保持沉默。`;
|
|
123
136
|
const interactiveTaskRules = scheduled ? "" : `
|
|
124
137
|
- **毫不相关的新任务才另起线程**:只有要处理的事**和当前线程毫不相关**(或用户明确要求新建)时,才用 \`crew task create --new-thread --title "…"\`——系统另起一个子线程(parent=当前线程)绑新 task;之后这件事的回复要发到**这个新子线程**里。能不拆就不拆。
|
|
125
138
|
- 任务状态流:\`todo → in_progress → in_review → done\`。claim 后用 \`crew task update\` 推进:开工→in_progress、完成待验收→in_review、人类确认后→done。只有 assignee 能改自己任务的状态。
|
|
@@ -214,7 +227,7 @@ ${startupSequence}${scheduleIntentRule}
|
|
|
214
227
|
- 你读到的消息形如 \`#<seq> [<type>] <sender>: <正文>\`,\`type\` 为 \`human\` / \`agent\` / \`system\`。
|
|
215
228
|
- **\`system\` 消息**通报频道状态变化(如新建任务),除非明确要求你行动(如刚给你指派了任务),否则不要回复。
|
|
216
229
|
${actionRule}
|
|
217
|
-
${interactiveTaskRules}${freshnessRule}${collaborationEtiquette}${humanLanguageSection}${communicationStyle}${skillIntentRule}
|
|
230
|
+
${interactiveTaskRules}${freshnessRule}${externalReplyRule}${collaborationEtiquette}${humanLanguageSection}${communicationStyle}${skillIntentRule}
|
|
218
231
|
|
|
219
232
|
## Workspace 与分层记忆(CRITICAL — 索引+按需,配合上面的并行规则)
|
|
220
233
|
你的持久记忆在 \`$CREW_HOME\`(跨你所有任务共享),分三层:
|
|
@@ -260,3 +273,12 @@ export function buildWakePrompt(channelId) {
|
|
|
260
273
|
- 有:转入主动处理。相关任务先 \`crew task claim <taskId>\` 认领再动手,完成后用 \`crew message send --channel ${channelId}\` 回复。
|
|
261
274
|
- 没有:本轮什么都不要发,读完即停。你存活期间有新消息会自动送来,无需轮询。`;
|
|
262
275
|
}
|
|
276
|
+
export function buildOriginDecisionRetryPrompt(channelId, threadId) {
|
|
277
|
+
const thread = threadId ? ` --thread ${threadId}` : "";
|
|
278
|
+
return [
|
|
279
|
+
"本轮尚未完成企微回复决策。不要重复执行已经完成的工作,只完成下面这个决策后结束:",
|
|
280
|
+
`- 需要回复企微:用 crew message send --channel ${channelId}${thread} --reply-origin --content "完整最终回复"。`,
|
|
281
|
+
`- 不需要回复企微:用 crew message skip-origin --channel ${channelId} --reason "简短原因"。`,
|
|
282
|
+
"普通内部消息不算外部回复决策;--send-draft 也不是外部回复开关。",
|
|
283
|
+
].join("\n");
|
|
284
|
+
}
|