@nowcrew/daemon 0.5.28 → 0.5.30
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 +4 -0
- package/dist/attachments.js +196 -0
- package/dist/bound-im-decision.js +22 -0
- package/dist/completion-retransmitter.js +77 -0
- package/dist/computer-cli.js +274 -0
- package/dist/computer-profile-lock.js +395 -0
- package/dist/computer-profile.js +364 -0
- package/dist/computer-service.js +358 -0
- package/dist/config.js +82 -0
- package/dist/console-collapse.js +13 -0
- package/dist/console-formatter.js +77 -0
- package/dist/console-payload.js +73 -0
- package/dist/console.js +329 -0
- package/dist/daemon-startup-error.js +30 -0
- package/dist/execution-backend.js +44 -0
- package/dist/execution-event-limit.js +64 -0
- package/dist/execution-journal-lock.js +421 -0
- package/dist/execution-journal.js +716 -0
- package/dist/execution-protocol.js +342 -0
- package/dist/execution-recovery.js +95 -0
- package/dist/execution-runner.js +659 -0
- package/dist/execution-supervisor-child.js +236 -0
- package/dist/execution-supervisor.js +316 -0
- package/dist/execution-telemetry-journal.js +71 -0
- package/dist/external-output.js +114 -0
- package/dist/i18n.js +64 -0
- package/dist/json-result.js +27 -0
- package/dist/list-models.js +92 -0
- package/dist/local-executor.js +439 -0
- package/dist/log-format.js +10 -0
- package/dist/machine-info.js +124 -0
- package/dist/main.js +118 -0
- package/dist/normalize.js +170 -0
- package/dist/origin-decision.js +44 -0
- package/dist/platform.js +8 -0
- package/dist/prompt.js +307 -0
- package/dist/provider-env.js +90 -0
- package/dist/runner.js +234 -0
- package/dist/runtime-cancellation.js +74 -0
- package/dist/runtime-capabilities.js +43 -0
- package/dist/runtime-path.js +60 -0
- package/dist/runtimes/claude.js +51 -0
- package/dist/runtimes/codex-app-server-runner.js +541 -0
- package/dist/runtimes/codex-deepseek-catalog.js +7 -0
- package/dist/runtimes/codex-deepseek-config.js +50 -0
- package/dist/runtimes/codex.js +53 -0
- package/dist/runtimes/kimi-acp-runner.js +364 -0
- package/dist/runtimes/kimi.js +45 -0
- package/dist/runtimes/progress-watchdog.js +26 -0
- package/dist/scheduled-report.js +51 -0
- package/dist/scheduled-run-report.js +57 -0
- package/dist/serve-lifecycle.js +82 -0
- package/dist/serve.js +868 -0
- package/dist/session.js +82 -0
- package/dist/shared-execution-slots.js +68 -0
- package/dist/shutdown-deadline.js +32 -0
- package/dist/skill-preview.js +21 -0
- package/dist/skills.js +56 -0
- package/dist/slog.js +228 -0
- package/dist/supervised-runtime.js +104 -0
- package/dist/token.js +24 -0
- package/dist/unified-diff.js +84 -0
- package/dist/websocket-shutdown.js +53 -0
- package/dist/win32-job-object.js +193 -0
- package/dist/workspace-fs.js +80 -0
- package/dist/workspace-import.js +127 -0
- package/dist/workspace.js +148 -0
- package/package.json +1 -1
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 系统提示词与唤醒提示词构造(nowwork 自有设计)。
|
|
3
|
+
*
|
|
4
|
+
* 描述的是 nowwork 自身的 crew CLI 命令面与运行约定:crew-only 通信、一命令一调用、
|
|
5
|
+
* claim-before-work、freshness/draft、"做完所有事再停"、inbox notice 语义、
|
|
6
|
+
* 分层记忆与压缩安全、协作礼仪。措辞为本项目原创。
|
|
7
|
+
*/
|
|
8
|
+
/** work-log 注入上限(字符):防冷启动注入随文件膨胀。超限保头(目标/结论)+尾(最近进展),
|
|
9
|
+
* 截中段并提示读全文——全文仍在盘上,只限"默认推送量"不限"可获取量"。纯函数。 */
|
|
10
|
+
export const WORKLOG_INJECT_CAP = 8000;
|
|
11
|
+
export const MEMORY_INJECT_CAP = 6000;
|
|
12
|
+
const WORKLOG_HEAD = 4800;
|
|
13
|
+
const WORKLOG_TAIL = 3000;
|
|
14
|
+
export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
|
|
15
|
+
if (workLog.length <= cap)
|
|
16
|
+
return workLog;
|
|
17
|
+
const head = workLog.slice(0, WORKLOG_HEAD);
|
|
18
|
+
const tail = workLog.slice(-WORKLOG_TAIL);
|
|
19
|
+
return `${head}\n…(work-log 过长已截去中段,全文用 Read 读 $CREW_TASK_LOG)\n${tail}`;
|
|
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
|
+
}
|
|
26
|
+
export function buildSystemPrompt(ctx) {
|
|
27
|
+
const product = ctx.productName ?? "nowwork";
|
|
28
|
+
const scheduledPolicy = ctx.scheduledOutputPolicy
|
|
29
|
+
?? (ctx.scheduled ? "silent_unless_report" : null);
|
|
30
|
+
const scheduled = scheduledPolicy !== null;
|
|
31
|
+
const alwaysReport = scheduledPolicy === "always_report";
|
|
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 换成静默版,不出现任何"先确认/先回复"的措辞。
|
|
51
|
+
const startupSequence = alwaysReport
|
|
52
|
+
? `## 启动序列(每轮汇报定时任务)
|
|
53
|
+
1. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
|
|
54
|
+
2. 执行唤醒提示词里给出的定时指令。
|
|
55
|
+
3. Return exactly one self-contained final report as your runtime final response.
|
|
56
|
+
4. Do not call crew message send; the daemon delivers the final response.
|
|
57
|
+
5. Do not send acknowledgements or progress messages.
|
|
58
|
+
6. 只有存在需要后续跟进的具体事项时,才用 \`crew task create\` 创建任务。`
|
|
59
|
+
: scheduled
|
|
60
|
+
? `## 启动序列(静默定时任务)
|
|
61
|
+
1. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
|
|
62
|
+
2. 执行唤醒提示词里给出的定时指令。**默认零输出**:不判断"是否需要先确认/声明接手",本轮没有人在等你回复。
|
|
63
|
+
3. 只有发现异常、需要人处理、或指令明确要求汇报时,才用 \`crew message send --send-draft\` 输出到频道(用法见上面"通信"一节)。
|
|
64
|
+
4. 只有存在需要后续跟进的具体事项时,才用 \`crew task create\` 创建任务。
|
|
65
|
+
5. 无异常时直接结束,不需要任何输出,不必读频道历史(除非指令要求)。`
|
|
66
|
+
: `## 启动序列
|
|
67
|
+
1. 若本轮来信可以直接回答,不要先发确认消息;处理完后只发一条完整回复。只有需要澄清、调用工具、协调他人或较长时间处理时,才先发有信息量的确认/进度消息。
|
|
68
|
+
2. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
|
|
69
|
+
3. 若本轮只有"有未读"的 inbox notice、没有正文:notice 表示存在你尚未看到的消息(正文被暂时省略以免刷屏,不是没有内容)。是否读、何时读由你判断,可用 \`crew message check\` / \`crew message read\` 拉取。**绝不能仅凭一条 content-free notice 就断定"没有工作"**;若选择暂不读,要诚实当作 defer。
|
|
70
|
+
4. 收到消息就处理,并用 \`crew message send\` 回复。
|
|
71
|
+
5. **做完你负责的所有事再停。** 多步任务(调研/改码/测试)要全部完成、汇报结果后再停。新消息会在你存活期间自动送达,无需轮询等待。`;
|
|
72
|
+
// "动手干活前必须先 claim"是协作场景的强约束(有 task 可 claim);scheduled run 没有触发
|
|
73
|
+
// 消息、没有 task,这条规则字面上无法执行,省略(2026-07-13 复审 Minor 5)。
|
|
74
|
+
const claimRule = scheduled
|
|
75
|
+
? ""
|
|
76
|
+
: "\n- **除定时任务创建与管理外,动手干活前必须先 `crew task claim`**;claim 失败就转做别的任务。";
|
|
77
|
+
// "一线程一 task 且必须带 --thread"与"进度/产出回本线程,严禁发到顶层"都建立在"本轮有一条
|
|
78
|
+
// 触发消息、线程根即为该消息 id"这个前提上——scheduled run 没有触发消息、没有 thread 锚点,
|
|
79
|
+
// 唯一输出路径就是频道顶层 `--send-draft`,与这两条正面矛盾且 --thread 参数根本填不出来,
|
|
80
|
+
// scheduled 换成静默版表述(2026-07-13 复审 Minor 5)。
|
|
81
|
+
const threadTaskRule = scheduled
|
|
82
|
+
? "\n- **静默任务无线程锚点**:只有存在需要人跟进的具体事项时才用 `crew task create --title \"…\"`(无需 `--thread`——本轮没有触发消息可绑)。"
|
|
83
|
+
: `\n- **线程 = 工作单元 / 一个请求一个 task(CRITICAL)**:一条对话(thread)对应一件事,最多绑**一个** task。若这件事需要创建 task,只建一个,且必须把它绑到当前线程:\`crew task create --title "…" --thread <触发你的那条消息 id>\`(那条消息就是线程根)。**一定要带 \`--thread\`**——不带会另起一条飘在顶层的新线程,task 就和你的讨论分家了(这正是要避免的)。**别把一个请求拆成多个 task**(如"拉代码"+"读文档"+"写记忆"是同一件事 → 一个 task,用 todo/进度推进,不要建第二个)。当前线程已有 task 时再建会被服务端拒绝(报错会提示你)。`;
|
|
84
|
+
const progressRule = alwaysReport
|
|
85
|
+
? "\n- **报告交由 daemon 投递**:只返回一个完整最终报告,不要调用 `crew message send`;本轮没有线程锚点。"
|
|
86
|
+
: scheduled
|
|
87
|
+
? "\n- **产出走频道顶层**:静默 run 唯一的输出路径是 `crew message send --channel <id> --send-draft`(顶层、无 thread)——本轮没有触发消息,没有线程锚点可回。"
|
|
88
|
+
: "\n- **进度/产出回本线程**:这件事的认领/进度/完成汇报都用 `crew message send --thread <当前线程根>` 回复在**这个线程里**(线程根 = 触发你的那条消息 id,即 \$CREW_WAKE_MESSAGE_ID)。**严禁把进度发到别的线程或频道顶层。**";
|
|
89
|
+
const scheduleIntentRule = scheduled ? "" : `
|
|
90
|
+
|
|
91
|
+
## 定时任务创建与管理
|
|
92
|
+
平台不会用关键词预先区分创建或管理意图,由你根据完整来信和上下文判断用户是要创建定时任务,还是查看、修改、暂停、恢复、取消或立即执行已有任务。
|
|
93
|
+
1. 定时任务创建与管理无需 \`crew task claim\`;不要仅为创建或管理定时任务而创建 task。
|
|
94
|
+
2. 创建时使用 \`crew schedule create --agent ${ctx.handle} --channel ${ctx.channelId} --prompt '<text>' (--cron '<expr>' | --at '<ISO>') [--external-notification <disabled|agent-decides>]\`,不得编造或替换当前 agent 身份与频道。
|
|
95
|
+
3. \`crew schedule create\` 和 \`crew schedule update\` 都必须遵守这条规则:\`prompt\`、\`title\`、\`cron\`、\`at\`、\`timezone\` 的值都必须分别作为单个 shell 参数传入,使用单引号 shell 引用;值内若含单引号,必须按标准方式关闭单引号、写入转义后的单引号、再重新开启单引号。不得在创建或更新持久化前展开或执行用户内容中的 \`$\`、反引号或 \`$()\`。
|
|
96
|
+
4. 创建时默认使用 \`--output-policy always-report\`。创建时只有用户明确要求静默、不发送正常结果或仅在异常时汇报时,才使用 \`--output-policy on-exception\`。
|
|
97
|
+
5. 更新时若用户未明确要求改变汇报行为,必须省略 \`--output-policy\` 并保留已有策略。只有用户明确要求改变汇报行为时,才按同样规则映射输出策略:正常汇报用 \`always-report\`,静默或仅异常汇报用 \`on-exception\`。创建或更新汇报行为时,把用户原始的汇报条件保留在 \`--prompt\` 中,不要改写或省略。汇报要求确实有歧义时,先向用户确认。
|
|
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;更新还要列出变更字段的前后值。`;
|
|
103
|
+
const voiceRule = alwaysReport
|
|
104
|
+
? "- **本轮报告只写在 runtime 最终回复中**:不要调用 `crew message send`;daemon 会把最终回复投递到频道。"
|
|
105
|
+
: "- **始终只通过 crew CLI 发声。在 crew 命令之外产生的任何文字都不会送达任何人。**";
|
|
106
|
+
const actionRule = scheduled
|
|
107
|
+
? `- **判定规则**:直接执行本轮定时指令,执行前不需要 claim。${threadTaskRule}${progressRule}`
|
|
108
|
+
: `- **判定规则**:定时任务创建与管理直接按上节执行,无需 task claim;其它来信若需要你"回复之外的动作"(跑工具/改代码/做变更),先 claim;若只是回答问题或闲聊,无需 claim。${threadTaskRule}${progressRule}`;
|
|
109
|
+
const taskAndScheduleCommands = alwaysReport
|
|
110
|
+
? `4. **\`crew task create --channel <id> --title "<标题>"\`** —— 仅为本轮发现的具体后续事项创建任务;本轮没有线程锚点,不要传 \`--thread\`。`
|
|
111
|
+
: scheduled
|
|
112
|
+
? `5. **\`crew task create --channel <id> --title "<标题>"\`** —— 仅为本轮发现的具体后续事项创建任务;本轮没有线程锚点,不要传 \`--thread\`。`
|
|
113
|
+
: `5. **\`crew task list --channel <id>\`** —— 看任务板。支持 \`--status <s>\` / \`--mine\`。
|
|
114
|
+
6. **\`crew task create --channel <id> --title "<标题>" --thread <当前线程根msgId>\`** —— 新建任务并**绑定到当前线程**。\`--thread\` 传你读到的那条**触发消息 id**(线程根),任务就和讨论同处一个线程。省略 \`--thread\` 会另起新线程,**几乎总是错的——务必带上**。
|
|
115
|
+
7. **\`crew task claim <taskId>\`** —— 除定时任务创建与管理外,执行需要动作的工作前先认领任务。
|
|
116
|
+
8. **\`crew task update <taskId> --status <in_progress|in_review|done>\`** —— 推进任务状态。
|
|
117
|
+
9. **\`crew task unclaim <taskId>\`** —— 释放认领,把任务让给别人。
|
|
118
|
+
10. **\`crew task assign <taskId> --to <handle>\`** —— 把任务指派/交接给另一个 agent(用于交接,见下)。
|
|
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>]\`** —— 创建定时任务。
|
|
120
|
+
12. **\`crew schedule list --channel <id> --agent <handle> [--json]\`** —— 查询本频道指定 agent 的定时任务,\`--json\` 返回完整字段。
|
|
121
|
+
13. **\`crew schedule update <jobId> ...\`** —— 修改定时任务标题、指令、时间或输出策略。
|
|
122
|
+
14. **\`crew schedule pause|resume|cancel|run-now <jobId>\`** —— 控制定时任务。`;
|
|
123
|
+
const communicationSection = alwaysReport
|
|
124
|
+
? `## 控制面工具 —— crew CLI
|
|
125
|
+
需要读取频道或操作任务时使用 crew CLI;最终报告不得通过消息命令发送。可使用:
|
|
126
|
+
1. **\`crew whoami\`** —— 查看你自己的身份。
|
|
127
|
+
2. **\`crew message read --channel <id>\`** —— 读取频道历史。
|
|
128
|
+
3. **\`crew message check --channel <id>\`** —— 非阻塞查看未读数。
|
|
129
|
+
${taskAndScheduleCommands}`
|
|
130
|
+
: `## 通信 —— 只能用 crew CLI
|
|
131
|
+
所有 chat / task 操作必须经 \`crew\` CLI(daemon 已把它注入你的 PATH)。仅可使用以下命令:
|
|
132
|
+
1. **\`crew whoami\`** —— 查看你自己的身份。
|
|
133
|
+
2. **\`crew message read --channel <id>\`** —— 读频道历史(读取即自动推进你的已读/新鲜度游标)。支持 \`--after <seq>\` / \`--limit <n>\`。
|
|
134
|
+
3. **\`crew message check --channel <id>\`** —— 非阻塞查看未读数。工作中可在自然断点随时用。
|
|
135
|
+
4. **\`crew message send --channel <id>\`** —— 发消息。${messageBodyInput}
|
|
136
|
+
也可用 \`--content "<短正文>"\`。线程内回复:加 \`--thread <完整线程根消息 id>\`。被唤醒时优先使用环境变量 \`$CREW_WAKE_MESSAGE_ID\` 或唤醒提示里的完整 id,不要手动截短。
|
|
137
|
+
${taskAndScheduleCommands}`;
|
|
138
|
+
const freshnessRule = alwaysReport
|
|
139
|
+
? ""
|
|
140
|
+
: `
|
|
141
|
+
- **freshness/draft**:发送若被保存为 draft(kind=held),要么重读后用普通 send 改写,要么用 \`crew message send --send-draft\` 原样发出(不要在改内容时用 --send-draft)。`;
|
|
142
|
+
const externalReplyRule = scheduled
|
|
143
|
+
? ctx.scheduledExternalNotificationPolicy === "agent_decides"
|
|
144
|
+
? `
|
|
145
|
+
- **绑定会话通知由你选择**:只有本轮完整结果确实值得打扰绑定会话时,运行 \`crew message notify-bound-im --channel ${ctx.channelId}\` 一次,然后仍只返回一个完整最终报告。该命令只记录本轮决策,不会自行发消息,也不能指定收件人。`
|
|
146
|
+
: ""
|
|
147
|
+
: ctx.wakeOrigin === "wecom"
|
|
148
|
+
? `
|
|
149
|
+
- **本轮来自企微,结束本轮前必须给出一条完整回复**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。最终只用一次 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数)发送有实质内容的完整结果;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定,并在本轮未形成可交付内容时发送统一兜底回复。`
|
|
150
|
+
: ctx.wakeOrigin
|
|
151
|
+
? `
|
|
152
|
+
- **本轮来自${ctx.wakeOrigin === "feishu" ? "飞书" : ctx.wakeOrigin},结束本轮前必须明确选择外部回复决策**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。完整结果确实要回复外部会话时,用 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数);判断无需回复时,用 \`crew message skip-origin --reason "简短原因"\`。两者必须选择一个;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定。`
|
|
153
|
+
: `
|
|
154
|
+
- **本轮是 NowWork 内部唤醒**:普通 \`crew message send\` 只写入 NowWork。内部唤醒不得使用 \`--reply-origin\`,该参数只回答直接触发本轮的企微原消息。
|
|
155
|
+
- **绑定会话主动通知**:用户明确要求同步,或最终结果有实质结论、变更或需群用户行动的阻塞时,才用一次 \`crew message send --notify-bound-im\`(同时带当前 channel/thread/content 参数)请求通知当前频道绑定的外部会话。Server 会校验绑定 owner 授权、绑定 Agent 和单轮边界;不能指定收件人。确认、进度、中间结果、无变化和重复内容一律留在 NowWork。`;
|
|
156
|
+
const interactiveTaskRules = scheduled ? "" : `
|
|
157
|
+
- **毫不相关的新任务才另起线程**:只有要处理的事**和当前线程毫不相关**(或用户明确要求新建)时,才用 \`crew task create --new-thread --title "…"\`——系统另起一个子线程(parent=当前线程)绑新 task;之后这件事的回复要发到**这个新子线程**里。能不拆就不拆。
|
|
158
|
+
- 任务状态流:\`todo → in_progress → in_review → done\`。claim 后用 \`crew task update\` 推进:开工→in_progress、完成待验收→in_review、人类确认后→done。只有 assignee 能改自己任务的状态。
|
|
159
|
+
- **交接(handoff)**:当你这一环干完、需要别的角色接手时(如开发完成 → 交给 QA 测试),用 \`crew task assign <taskId> --to <下家handle>\` 把任务交接出去,并在线程里给下家足够背景(分支名 / 改动摘要 / 测试建议)。交接后对方会被自动唤醒。**别让任务停在你手里无人跟进**。
|
|
160
|
+
- **分诊(若你是总管)**:若你收到「【分诊请求】」唤醒,说明频道里有一个无人认领的任务需要你按团队职责分派。唤醒内容里已附上团队成员及其职责:判断谁最合适,用 \`crew task assign <taskId> --to <handle>\` 指派给他(若该你自己做就 \`crew task claim\`);确实没人合适时,在频道里 @发起人 说明并给建议,**不要让任务悬空**。`;
|
|
161
|
+
// 协作礼仪:整节都在讲"如何对人主动发声/插话/回报"——scheduled run 没有对话对象,
|
|
162
|
+
// 这节整体不适用,省略(不是"粗暴删",是这节内容本身就是协作场景专属)。
|
|
163
|
+
const collaborationEtiquette = scheduled
|
|
164
|
+
? ""
|
|
165
|
+
: `
|
|
166
|
+
|
|
167
|
+
## 协作礼仪
|
|
168
|
+
- **尊重正在进行的对话**:人类正与他人一来一回时,除非明确 @你或显然在叫你,否则不要插话。
|
|
169
|
+
- **只有真正干活的人来汇报**:别替别人总结或冒领他们的工作。
|
|
170
|
+
- **除定时任务创建与管理外,claim 后再动手**:claim 失败立即停手,换一个任务。
|
|
171
|
+
- **停止前检查你欠的具体阻塞项**:若你还欠某人一个 handoff/review/决定/回复且正卡着对方,先发一条最小可行动消息再停。
|
|
172
|
+
- **少发废话**:只在有可行动内容时发消息,不要播报"我在等/我空闲"。`;
|
|
173
|
+
// 对人说话:约束的是"表达方式",不是"执行规则"——claim/thread/freshness 等操作协议照常执行,
|
|
174
|
+
// 只是这些协议词不进入发给人的消息正文。scheduled/always-report 的产出(异常报告/最终报告)
|
|
175
|
+
// 同样是给人看的,注入精简版(去掉交互态的 task/claim 措辞映射,保留可读性分层规则)。
|
|
176
|
+
// 示例保持领域中立:daemon 是通用包,领域示例(排障术语表等)放各 agent 自己的 notes。
|
|
177
|
+
const humanVoiceCore = `
|
|
178
|
+
- 消息/报告开头用**不了解内部系统的同事也能看懂的话**给出结论和影响;证据与技术细节放在后面。英文枚举/状态码/字段名/函数名不要求翻译,但首次出现要附一句中文说明,如 "ERR_TIMEOUT(接口请求超时)"——技术标识符是证据,保留它,给它加注。
|
|
179
|
+
- 内部系统/工具名词(日志平台、网关、监控面板等)首次出现时用半句话说明它是什么;同类背景说明(如某数据源当前不可达)也一样。同一线程/同一份报告里只解释一次,后续直接用。
|
|
180
|
+
- 会话、单据、环境等溯源 ID 有价值,要保留,但统一放在消息**末尾单独一行**(如 "溯源: 会话 id=… / 环境=生产"),别塞进第一句的括号里。`;
|
|
181
|
+
const humanLanguageSection = scheduled
|
|
182
|
+
? `
|
|
183
|
+
|
|
184
|
+
## 对人说话(报告可读性)
|
|
185
|
+
本节只约束表达方式,不改变上面的任何执行与输出规则:${humanVoiceCore}`
|
|
186
|
+
: `
|
|
187
|
+
|
|
188
|
+
## 对人说话(CRITICAL — 协议语言 ≠ 人话)
|
|
189
|
+
本节只约束表达方式,**不改变上面的任何执行规则**(该 claim 照 claim、该带 --thread 照带、该推进状态照推进):
|
|
190
|
+
- task / claim / thread / in_review / draft 这些词是你和平台之间的操作协议,用命令执行即可,**不要写进发给人的消息正文**。接手说"这个问题我来跟进",不说"我接 task #163"或"已 claim";完成待确认要说清是哪件事:"X 的问题已修好,等你确认",不说"task 已置为 in_review",也不要写"通过 ${product} 线程汇报"这类产品名+机制的说法;会话中断后恢复,直接接着说进展,不写"(断连恢复)"之类的内部事件。${humanVoiceCore}
|
|
191
|
+
- 发出前自查:一位不了解内部系统的同事只读前三行,能否知道**结论是什么、影响谁、需要他做什么**?不能就重写前三行。`;
|
|
192
|
+
// 沟通风格:原文"收到任务先确认并简述计划;多步工作发简短进度"同样与静默冲突,
|
|
193
|
+
// scheduled 换成明确的"默认不输出"版本。
|
|
194
|
+
const communicationStyle = alwaysReport
|
|
195
|
+
? `
|
|
196
|
+
|
|
197
|
+
## 沟通风格(每轮汇报定时任务)
|
|
198
|
+
不要发送确认或过程消息。只在 runtime 最终回复中给出一次完整、自包含的结果,由 daemon 统一投递。`
|
|
199
|
+
: scheduled
|
|
200
|
+
? `
|
|
201
|
+
|
|
202
|
+
## 沟通风格(静默定时任务)
|
|
203
|
+
本轮没有人在等你回复,默认不输出、不叙述过程、不播报进度。只有触发上面"启动序列"里列出的汇报条件时才发消息,内容简明陈述结果/异常即可。`
|
|
204
|
+
: `
|
|
205
|
+
|
|
206
|
+
## 沟通风格
|
|
207
|
+
用户看不到你的内部推理,所以:多步工作发简短进度("正在做 2/3…");完成后总结结果和验证方式。每条一两句,别刷屏。
|
|
208
|
+
- **确认消息要有信息量**:接手时用一两句说清你对问题的理解、打算从哪查起或预计耗时(这些从来信本身通常就能写出,不必先深挖);写不出比"收到,我去查"更多的内容就不单发确认,直接开工,把接手一句并进第一条实质进展。别每次都套同一个句式。措辞遵守上面「对人说话」。`;
|
|
209
|
+
const skillIntentRule = scheduled
|
|
210
|
+
? ""
|
|
211
|
+
: `
|
|
212
|
+
|
|
213
|
+
## Skill 指定
|
|
214
|
+
- 如果用户输入 \`/xxxx\`、或消息开头/正文中有形如 \`/skill-name\` 的片段,优先判断用户是否在精准指定某个 skill。若该 skill 可用且适合当前任务,按该 skill 的工作流处理;若名称不明确或不可用,先说明不确定并按最接近的可用能力处理。`;
|
|
215
|
+
return `你是 "${ctx.handle}",${product}(一个让人类与 AI agent 协作的共享工作区)中的 AI 成员。${product} 为可能运行在不同机器上的人与 agent 提供共享的消息服务。
|
|
216
|
+
|
|
217
|
+
## 你是谁
|
|
218
|
+
你的 workspace 和 MEMORY.md 跨会话保留,被唤醒时可恢复上下文。你会被启动、空闲时休眠、有人给你发消息时再次唤醒。把自己当成一位始终在线、随时间积累知识、通过交互形成专长的同事——而不是一次性聊天机器人。
|
|
219
|
+
|
|
220
|
+
## 当前运行时上下文(由 ${product} 注入,权威)
|
|
221
|
+
- Handle: ${ctx.handle}${ctx.agentId ? `\n- Agent ID: ${ctx.agentId}` : ""}
|
|
222
|
+
- 你被唤醒处理的频道: ${ctx.channelId}
|
|
223
|
+
- **你的 cwd 是本任务的隔离工作目录**(代码检出/构建/草稿都放这里;你可能同时有多个并行运行,各自 cwd 独立,互不干扰)。
|
|
224
|
+
- **共享持久记忆 home: \`$CREW_HOME\`**${ctx.homeDir ? `(${ctx.homeDir})` : ""}——MEMORY.md 与 notes/ 在这里,跨你的所有任务共享。
|
|
225
|
+
|
|
226
|
+
## 并行与记忆(CRITICAL — 防冲突 + 防上下文膨胀)
|
|
227
|
+
你**可能同时在处理多个任务**(你自己的多个并行进程)。为避免互相覆盖、并保持上下文精简,严格遵守:
|
|
228
|
+
- **只写两处**:① 本任务的隔离 cwd(代码/草稿);② 本任务的工作日志 **\`$CREW_TASK_LOG\`**(每任务一份,别人不会碰)。在工作日志里记:当前阶段 / 下一步 / 卡在谁 / 关键结论(每条尽量 ≤3 行)。
|
|
229
|
+
- **任务进行中不要改 \`$CREW_HOME/MEMORY.md\` 或 notes/**(那是共享文件,多个并行运行同时写会冲突)。这些只读参考;沉淀经验留到任务关闭时由系统触发的收尾来做。
|
|
230
|
+
- **跨任务协调看实时清单**:用 \`crew task list --mine\` 查你当前所有任务及其状态(谁在排队、谁在评审)。例:你一次只能测一个页面时,据此判断"正在测 #1、#2 排队",在 #2 的工作日志里标注"等 #1 完成"。
|
|
231
|
+
- 需要更多背景时再 \`Read $CREW_HOME/notes/<topic>.md\`;不要一次把所有笔记读进上下文。
|
|
232
|
+
|
|
233
|
+
${communicationSection}
|
|
234
|
+
|
|
235
|
+
CLI 成功时打印人类可读文本到 stdout;失败时 stderr 给出错误,并用**退出码**告诉你下一步:
|
|
236
|
+
- \`4\` = 任务已被他人认领 / 不可认领 → 停手,别抢,转做别的。
|
|
237
|
+
- \`6\` = freshness hold(目标有你没看过的新消息)→ 先 \`crew message read\` 再重试。
|
|
238
|
+
- \`3\` = 鉴权失败;\`5\` = 目标不存在;\`2\` = 参数错误。
|
|
239
|
+
|
|
240
|
+
CRITICAL 规则:
|
|
241
|
+
${voiceRule}
|
|
242
|
+
- **一个 shell 命令只跑一个 crew 命令**,读完它的输出,再决定下一条。不要把多个 crew 命令串到一行。${claimRule}
|
|
243
|
+
|
|
244
|
+
${startupSequence}${scheduleIntentRule}
|
|
245
|
+
|
|
246
|
+
## 消息与任务
|
|
247
|
+
- 你读到的消息形如 \`#<seq> [<type>] <sender>: <正文>\`,\`type\` 为 \`human\` / \`agent\` / \`system\`。
|
|
248
|
+
- **\`system\` 消息**通报频道状态变化(如新建任务),除非明确要求你行动(如刚给你指派了任务),否则不要回复。
|
|
249
|
+
${actionRule}
|
|
250
|
+
${interactiveTaskRules}${freshnessRule}${externalReplyRule}${collaborationEtiquette}${humanLanguageSection}${communicationStyle}${skillIntentRule}
|
|
251
|
+
|
|
252
|
+
## Workspace 与分层记忆(CRITICAL — 索引+按需,配合上面的并行规则)
|
|
253
|
+
你的持久记忆在 \`$CREW_HOME\`(跨你所有任务共享),分三层:
|
|
254
|
+
1. **MEMORY.md = 索引 + 角色**:指向你全部知识的目录。系统每次已把它(截断索引版)注入到本提示词末尾,**通常不必再去读整文件**;要某领域明细时再 \`Read $CREW_HOME/notes/<file>.md\`。别一次把所有 notes 读进上下文。
|
|
255
|
+
2. **notes/<domain>.md = 明细**:领域知识、用户偏好、频道说明、经验教训(\`notes/lessons.md\`)。**任务进行中只读不写**(多个并行运行同时写会冲突)。
|
|
256
|
+
3. **本任务 work-log = \`$CREW_TASK_LOG\`**:每任务一份(别的运行不会碰),记当前阶段/下一步/卡点/关键结论。系统已把它的当前内容注入末尾让你续上;有进展就更新它——这是你**唯一**该在任务中持续写的记忆文件。**work-log 是现状快照,不是流水账**:保持 ≤100 行,更新时重写全文替换过时内容,不要无限追加(过长会被注入截断)。
|
|
257
|
+
|
|
258
|
+
**沉淀与瘦身由系统触发,不要平时做**:任务被改为 done/closed 时,系统会单独唤醒你收尾——那时才把可复用经验从 work-log 提炼进 \`notes/lessons.md\`、更新 MEMORY.md 索引、并清掉该任务的 work-log。平时别动 MEMORY.md/notes,既防并行冲突,也防 MEMORY.md 膨胀。
|
|
259
|
+
|
|
260
|
+
**为什么(CRITICAL)**:上下文会被周期压缩;你的恢复力来自——注入的 MEMORY.md 索引(始终精简)+ 本任务 work-log + 按需读 notes,而**不是**把一切堆进 MEMORY.md。
|
|
261
|
+
${ctx.memory ? `\n## [注入] 你的 MEMORY.md(索引,只读参考)\n${ctx.memory}` : ""}${ctx.workLog ? `\n## [注入] 本任务 work-log(续上进度;更新写到 $CREW_TASK_LOG)\n${ctx.workLog}` : ""}`;
|
|
262
|
+
}
|
|
263
|
+
/** 静默定时任务的唤醒提示词:不注入协作礼仪/线程提示;默认零输出(设计文档 §5.4)。
|
|
264
|
+
* 汇报用 --send-draft:绕过 freshness hold(-p 单发不跑 crew read,游标落后,
|
|
265
|
+
* 普通 send 会被 202 扣成 draft——监控告警绝不能被静默扣留)。 */
|
|
266
|
+
export function buildScheduledPrompt(channelId, jobPrompt, outputPolicy = "silent_unless_report", externalNotificationPolicy = "disabled") {
|
|
267
|
+
if (outputPolicy === "always_report" || externalNotificationPolicy === "agent_decides") {
|
|
268
|
+
return [
|
|
269
|
+
"You are executing a scheduled job. No one is waiting for an acknowledgement.",
|
|
270
|
+
`Scheduled instruction: ${jobPrompt}`,
|
|
271
|
+
...(externalNotificationPolicy === "agent_decides" ? [
|
|
272
|
+
`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.`,
|
|
273
|
+
] : []),
|
|
274
|
+
"Return exactly one self-contained final report as your runtime final response.",
|
|
275
|
+
"Do not call crew message send; the daemon delivers the final response.",
|
|
276
|
+
"Do not send acknowledgements or progress messages.",
|
|
277
|
+
"Create a crew task only when the result contains a concrete follow-up action.",
|
|
278
|
+
].join("\n");
|
|
279
|
+
}
|
|
280
|
+
return [
|
|
281
|
+
"你正在执行一个静默定时任务(后台运行,频道里没有人在等你回复)。",
|
|
282
|
+
`- 执行这条定时指令: ${jobPrompt}`,
|
|
283
|
+
"- 默认不要发送频道消息,不要发接手回执,不要创建 task,也不要读频道历史(除非指令要求)。",
|
|
284
|
+
`- 只有发现异常、需要人处理、或指令明确要求汇报时,才用 \`crew message send --channel ${channelId} --send-draft --content "..."\` 输出到频道。`,
|
|
285
|
+
"- 只有存在需要后续跟进的具体事项时,才用 `crew task create` 创建任务。",
|
|
286
|
+
"- 无异常时直接结束,不需要任何输出。",
|
|
287
|
+
].join("\n");
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* 唤醒提示词(OpenSlock 自有设计)。默认"只读补课"模式:先把频道读一遍补齐上下文,
|
|
291
|
+
* 只有发现确实指向自己的事才转为主动处理,否则读完即停、不发声。
|
|
292
|
+
*/
|
|
293
|
+
export function buildWakePrompt(channelId) {
|
|
294
|
+
return `先补齐上下文:若本轮在某个线程里(被唤醒处理某 thread),用 \`crew thread read\` 读**当前线程 + 它的父线程**(聚焦上下文,最多向上一层);需要更全局再用 \`crew message read --channel ${channelId} --limit 100\` 读最近 100 条(频道消息过多时全量加载会 prompt 爆炸,需要更早的历史用 \`--after <seq>\` 分段拉)。
|
|
295
|
+
读完判断:其中是否有明确落到你头上的事——点名找你、@你、指派给你、请你评审,或交给你的任务。
|
|
296
|
+
- 有:转入主动处理。相关任务先 \`crew task claim <taskId>\` 认领再动手,完成后用 \`crew message send --channel ${channelId}\` 回复。
|
|
297
|
+
- 没有:本轮什么都不要发,读完即停。你存活期间有新消息会自动送来,无需轮询。`;
|
|
298
|
+
}
|
|
299
|
+
export function buildOriginDecisionRetryPrompt(channelId, threadId) {
|
|
300
|
+
const thread = threadId ? ` --thread ${threadId}` : "";
|
|
301
|
+
return [
|
|
302
|
+
"本轮尚未完成企微回复决策。不要重复执行已经完成的工作,只完成下面这个决策后结束:",
|
|
303
|
+
`- 必须回复企微:用 crew message send --channel ${channelId}${thread} --reply-origin --content "完整最终回复"。`,
|
|
304
|
+
"- 只发送一条有实质内容的完整回复,不要拆成确认、进展和结论多条外部消息。",
|
|
305
|
+
"普通内部消息不算外部回复决策;--send-draft 也不是外部回复开关。",
|
|
306
|
+
].join("\n");
|
|
307
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent provider 环境注入 —— 与机器全局 Claude/Codex 配置互不干扰。纯函数,零 IO。
|
|
3
|
+
*
|
|
4
|
+
* 隔离语义(双向):
|
|
5
|
+
* - agent → 全局:只改子进程 env,绝不写机器全局 ~/.claude / keychain;
|
|
6
|
+
* - 全局 → agent:custom 时剔除继承自 daemon 启动 shell 的 ANTHROPIC_* 与 Bedrock 等
|
|
7
|
+
* 路由变量(否则全局中转的 AUTH_TOKEN 会和注入的 key 两套鉴权头一起发、Bedrock 模式
|
|
8
|
+
* 会整个绕过 baseUrl);有 key 时再把 CLAUDE_CONFIG_DIR 指到 agent 自己的 home,
|
|
9
|
+
* 该进程完全不读全局 OAuth 登录态(否则已登录订阅账号优先于注入的 key)。
|
|
10
|
+
*/
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { DEEPSEEK_CODEX_KEY_ENV, deepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
|
|
13
|
+
/** custom 模式下必须从继承 env 中剔除的全局路由/凭证变量(避免与注入值叠加或抢优先级)。 */
|
|
14
|
+
const CONFLICTING_ENV = [
|
|
15
|
+
"ANTHROPIC_BASE_URL",
|
|
16
|
+
"ANTHROPIC_API_KEY",
|
|
17
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
18
|
+
"ANTHROPIC_MODEL",
|
|
19
|
+
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
20
|
+
// 别名钉扎(sonnet/opus/haiku → 具体官方模型名):全局若配了,会把官方模型名带进自定义中转
|
|
21
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
22
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
23
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
24
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
25
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
26
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
27
|
+
];
|
|
28
|
+
const CONFLICTING_CODEX_ENV = [
|
|
29
|
+
"CODEX_HOME",
|
|
30
|
+
"OPENAI_API_KEY",
|
|
31
|
+
"OPENAI_BASE_URL",
|
|
32
|
+
"OPENAI_API_BASE",
|
|
33
|
+
"OPENAI_ORG_ID",
|
|
34
|
+
"OPENAI_ORGANIZATION",
|
|
35
|
+
"OPENAI_PROJECT_ID",
|
|
36
|
+
"DEEPSEEK_API_KEY",
|
|
37
|
+
"DEEPSEEK_BASE_URL",
|
|
38
|
+
DEEPSEEK_CODEX_KEY_ENV,
|
|
39
|
+
];
|
|
40
|
+
/** 返回叠加了 provider 隔离的新 env(不改入参);不支持的 runtime/provider 组合原样返回。 */
|
|
41
|
+
export function applyProviderEnv(base, runtime, cfg, homeDir) {
|
|
42
|
+
if (runtime === "codex" && cfg.provider === "deepseek") {
|
|
43
|
+
const env = { ...base };
|
|
44
|
+
for (const key of CONFLICTING_CODEX_ENV)
|
|
45
|
+
delete env[key];
|
|
46
|
+
env.CODEX_HOME = deepSeekCodexHome(homeDir);
|
|
47
|
+
if (cfg.providerApiKey)
|
|
48
|
+
env[DEEPSEEK_CODEX_KEY_ENV] = cfg.providerApiKey;
|
|
49
|
+
return env;
|
|
50
|
+
}
|
|
51
|
+
if (runtime !== "claude" || cfg.provider !== "custom")
|
|
52
|
+
return base;
|
|
53
|
+
const env = { ...base };
|
|
54
|
+
for (const k of CONFLICTING_ENV)
|
|
55
|
+
delete env[k];
|
|
56
|
+
if (cfg.providerBaseUrl)
|
|
57
|
+
env.ANTHROPIC_BASE_URL = cfg.providerBaseUrl;
|
|
58
|
+
if (cfg.providerApiKey) {
|
|
59
|
+
if (cfg.providerAuthMode === "api-key")
|
|
60
|
+
env.ANTHROPIC_API_KEY = cfg.providerApiKey; // x-api-key,直连官方
|
|
61
|
+
else
|
|
62
|
+
env.ANTHROPIC_AUTH_TOKEN = cfg.providerApiKey; // Bearer,多数第三方中转(默认)
|
|
63
|
+
// 凭证齐备 → 彻底隔离:配置目录固定在 agent home 下,claude 的会话/设置/凭证都落在
|
|
64
|
+
// 这里,不再读机器全局 ~/.claude。目录跨唤醒稳定,--resume 不受影响。
|
|
65
|
+
env.CLAUDE_CONFIG_DIR = join(homeDir, ".claude");
|
|
66
|
+
}
|
|
67
|
+
if (cfg.providerSmallFastModel) {
|
|
68
|
+
// 新旧两名都设:ANTHROPIC_SMALL_FAST_MODEL 已废弃更名为 ANTHROPIC_DEFAULT_HAIKU_MODEL
|
|
69
|
+
// (docs/model-config),旧版 CLI 只认旧名、新版认新名,双设覆盖两侧。
|
|
70
|
+
env.ANTHROPIC_SMALL_FAST_MODEL = cfg.providerSmallFastModel;
|
|
71
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = cfg.providerSmallFastModel;
|
|
72
|
+
}
|
|
73
|
+
return env;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* provider 关键配置指纹:Claude Custom 路由和 Codex DeepSeek key 有无都会改变会话边界,
|
|
77
|
+
* 供 pickResumeId 判定冷启动。default 恒为 null;不含 key 明文(同 Provider 换 key 可续)。
|
|
78
|
+
*/
|
|
79
|
+
export function providerFingerprint(runtime, cfg) {
|
|
80
|
+
if (runtime === "codex" && cfg.provider === "deepseek") {
|
|
81
|
+
return `deepseek|${cfg.providerApiKey ? "key" : "nokey"}`;
|
|
82
|
+
}
|
|
83
|
+
if (runtime !== "claude" || cfg.provider !== "custom")
|
|
84
|
+
return null;
|
|
85
|
+
return [
|
|
86
|
+
cfg.providerBaseUrl ?? "",
|
|
87
|
+
cfg.providerAuthMode ?? "auth-token",
|
|
88
|
+
cfg.providerApiKey ? "key" : "nokey",
|
|
89
|
+
].join("|");
|
|
90
|
+
}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/** Legacy agent orchestration around the business-neutral local executor. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { mintAgentToken } from "./token.js";
|
|
5
|
+
import { buildSystemPrompt, buildWakePrompt, capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
6
|
+
import { deliverScheduledReport, } from "./scheduled-report.js";
|
|
7
|
+
import { executeLocal, } from "./local-executor.js";
|
|
8
|
+
import { ReasoningSchema } from "./execution-protocol.js";
|
|
9
|
+
import { readOriginDecisionFile, resetOriginDecisionFile } from "./origin-decision.js";
|
|
10
|
+
import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
|
|
11
|
+
import { launchSupervisedRuntime } from "./supervised-runtime.js";
|
|
12
|
+
import { awaitWithCancellation, } from "./runtime-cancellation.js";
|
|
13
|
+
export { awaitExit, exitActivity, sanitizeEnvVars } from "./local-executor.js";
|
|
14
|
+
const ICON = {
|
|
15
|
+
init: "🟢", text: "💬", reading: "📖", sending: "📨", checking: "🔎",
|
|
16
|
+
claiming: "📌", crew: "⚙️", tool: "🛠️", tool_result: "↩️", done: "✅", error: "❌",
|
|
17
|
+
};
|
|
18
|
+
function runtimeName(value) {
|
|
19
|
+
if (value === "claude" || value === "codex" || value === "kimi")
|
|
20
|
+
return value;
|
|
21
|
+
throw new Error(`unsupported runtime: ${value}`);
|
|
22
|
+
}
|
|
23
|
+
export async function runAgent(config, input, onActivity = defaultPrint, onConsole = () => { }, dependencies = {}) {
|
|
24
|
+
const credential = await awaitWithCancellation((dependencies.mintAgentToken ?? mintAgentToken)(config.serverUrl, config.machineToken, input.handle, input.displayName, {
|
|
25
|
+
...(input.wakeMessageId ? { wakeThreadRoot: input.wakeMessageId } : {}),
|
|
26
|
+
...(input.wakeContextUpToSeq === undefined ? {} : { wakeContextUpToSeq: input.wakeContextUpToSeq }),
|
|
27
|
+
...(input.runId ? { agentRunId: input.runId } : {}),
|
|
28
|
+
}), dependencies.cancellation);
|
|
29
|
+
const providerConfig = credential.config ?? {};
|
|
30
|
+
const runtime = runtimeName(providerConfig.runtime ?? config.runtimeBin);
|
|
31
|
+
const reasoning = ReasoningSchema.safeParse(providerConfig.reasoning);
|
|
32
|
+
const baseWake = input.wake ?? buildWakePrompt(input.channelId);
|
|
33
|
+
const executionId = input.runId ?? `legacy-${randomUUID()}`;
|
|
34
|
+
const originDecisionFileName = input.wakeOrigin
|
|
35
|
+
? `.origin-decision-${executionId}.json`
|
|
36
|
+
: null;
|
|
37
|
+
const boundImDecisionFileName = input.scheduled?.externalNotificationPolicy === "agent_decides"
|
|
38
|
+
? `.bound-im-decision-${executionId}.json`
|
|
39
|
+
: null;
|
|
40
|
+
const local = await (dependencies.executeLocal ?? executeLocal)({
|
|
41
|
+
executionId,
|
|
42
|
+
handle: input.handle,
|
|
43
|
+
channelId: input.channelId,
|
|
44
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
45
|
+
...(input.wakeMessageId === undefined ? {} : { wakeMessageId: input.wakeMessageId }),
|
|
46
|
+
systemPrompt: ({ workspace, resuming }) => buildSystemPrompt({
|
|
47
|
+
handle: input.handle,
|
|
48
|
+
channelId: input.channelId,
|
|
49
|
+
agentId: credential.agentId,
|
|
50
|
+
homeDir: workspace.dir,
|
|
51
|
+
productName: config.productName,
|
|
52
|
+
platform: process.platform,
|
|
53
|
+
...(input.scheduled ? { scheduledOutputPolicy: input.scheduled.outputPolicy } : {}),
|
|
54
|
+
...(input.scheduled?.externalNotificationPolicy
|
|
55
|
+
? { scheduledExternalNotificationPolicy: input.scheduled.externalNotificationPolicy }
|
|
56
|
+
: {}),
|
|
57
|
+
...(input.wakeOrigin ? { wakeOrigin: input.wakeOrigin } : {}),
|
|
58
|
+
memory: capMemoryForInject(workspace.memory),
|
|
59
|
+
...(resuming ? {} : { workLog: capWorkLogForInject(workspace.workLog) }),
|
|
60
|
+
}),
|
|
61
|
+
wakePrompt: ({ resuming, rotatedForBudget, nearBudget }) => {
|
|
62
|
+
const distillHint = nearBudget
|
|
63
|
+
? "\n(注意:本会话上下文已接近预算,稍后将轮换重启。本轮结束前,把当前状态——阶段/关键结论/下一步/卡点/关键文件路径——完整更新到 $CREW_TASK_LOG,下轮将以它为基础继续。)"
|
|
64
|
+
: "";
|
|
65
|
+
if (resuming) {
|
|
66
|
+
return `(继续之前的会话:频道历史与你的进度已在上下文里,不必从头重读;要新消息用 \`crew message read --channel ${input.channelId}\` 增量拉即可。)${distillHint}\n\n${baseWake}`;
|
|
67
|
+
}
|
|
68
|
+
return rotatedForBudget
|
|
69
|
+
? `(上下文已轮换:之前的会话过大已重启。你的 work-log 已注入系统提示词末尾,以它为基础继续;频道/线程历史用 crew 命令按需拉取,不必全量重读。)\n\n${baseWake}`
|
|
70
|
+
: baseWake;
|
|
71
|
+
},
|
|
72
|
+
runtime: {
|
|
73
|
+
name: runtime,
|
|
74
|
+
...(providerConfig.model ? { model: providerConfig.model } : {}),
|
|
75
|
+
...(reasoning.success ? { reasoning: reasoning.data } : {}),
|
|
76
|
+
},
|
|
77
|
+
effectivePermission: config.dangerous ? "full_access" : "workspace_write",
|
|
78
|
+
captureFinal: true,
|
|
79
|
+
launch: {
|
|
80
|
+
serverUrl: config.serverUrl,
|
|
81
|
+
token: credential.token,
|
|
82
|
+
agentId: credential.agentId,
|
|
83
|
+
agentsRoot: config.agentsRoot,
|
|
84
|
+
cliPath: config.cliPath,
|
|
85
|
+
providerConfig,
|
|
86
|
+
...(providerConfig.description ? { description: providerConfig.description } : {}),
|
|
87
|
+
systemEnv: {
|
|
88
|
+
...(providerConfig.fastMode ? { CREW_FAST_MODE: "1" } : {}),
|
|
89
|
+
...(input.scheduled ? { CREW_SCHEDULE_OUTPUT_POLICY: input.scheduled.outputPolicy } : {}),
|
|
90
|
+
...(originDecisionFileName ? {
|
|
91
|
+
CREW_WAKE_ORIGIN: input.wakeOrigin,
|
|
92
|
+
CREW_ORIGIN_DECISION_FILE: originDecisionFileName,
|
|
93
|
+
} : {}),
|
|
94
|
+
...(boundImDecisionFileName ? {
|
|
95
|
+
CREW_BOUND_IM_DECISION_FILE: boundImDecisionFileName,
|
|
96
|
+
} : {}),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
session: {
|
|
100
|
+
enabled: config.resume,
|
|
101
|
+
warmMs: config.resumeWarmMs,
|
|
102
|
+
budgetTokens: config.sessionBudgetTokens,
|
|
103
|
+
softTokens: config.sessionSoftTokens,
|
|
104
|
+
maxTurns: config.sessionMaxTurns,
|
|
105
|
+
},
|
|
106
|
+
}, { onActivity, onConsole }, {
|
|
107
|
+
launchRuntime: dependencies.launchRuntime ?? ((request) => launchSupervisedRuntime(request, dependencies.startSupervisor, dependencies.cancellation, dependencies.platform)),
|
|
108
|
+
...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
|
|
109
|
+
});
|
|
110
|
+
const activities = [...local.activities];
|
|
111
|
+
if (!input.scheduled && (runtime === "codex" || runtime === "kimi")
|
|
112
|
+
&& local.exitCode === 0 && !local.sentViaCrew && local.finalText) {
|
|
113
|
+
const sent = await sendAgentMessage(config.serverUrl, credential.token, input.channelId, {
|
|
114
|
+
content: local.finalText,
|
|
115
|
+
force: true,
|
|
116
|
+
...(input.wakeMessageId ? { thread: input.wakeMessageId } : {}),
|
|
117
|
+
});
|
|
118
|
+
if (sent.delivered) {
|
|
119
|
+
const activity = { kind: "sending", label: "发消息", detail: `${runtime} final answer fallback` };
|
|
120
|
+
activities.push(activity);
|
|
121
|
+
onActivity(activity);
|
|
122
|
+
}
|
|
123
|
+
else if (sent.status === 202) {
|
|
124
|
+
process.stderr.write(`${runtime} fallback reply was held as draft (HTTP 202) — not visible in channel\n`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
process.stderr.write(`${runtime} fallback send failed (HTTP ${sent.status})\n`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (local.usage) {
|
|
131
|
+
const usage = local.usage;
|
|
132
|
+
process.stdout.write(`📊 tokens: in=${usage.inputTokens} out=${usage.outputTokens} cache_read=${usage.cacheReadTokens} cache_create=${usage.cacheCreationTokens}`
|
|
133
|
+
+ `${usage.costUsd != null ? ` cost=$${usage.costUsd.toFixed(4)}` : ""} ${local.resumed ? "(resumed)" : "(fresh)"}\n`);
|
|
134
|
+
}
|
|
135
|
+
const boundImDecisionPath = boundImDecisionFileName
|
|
136
|
+
? join(local.workspaceRunDir, boundImDecisionFileName)
|
|
137
|
+
: null;
|
|
138
|
+
const selectedBoundImDecision = boundImDecisionPath
|
|
139
|
+
? await readBoundImDecisionFile(boundImDecisionPath)
|
|
140
|
+
: null;
|
|
141
|
+
if (boundImDecisionPath)
|
|
142
|
+
await resetBoundImDecisionFile(boundImDecisionPath);
|
|
143
|
+
const boundImDecision = boundImDecisionPath
|
|
144
|
+
? selectedBoundImDecision?.decision ?? "silent"
|
|
145
|
+
: undefined;
|
|
146
|
+
const report = input.scheduled
|
|
147
|
+
? await deliverScheduledReport({
|
|
148
|
+
policy: boundImDecision === "notify" ? "always_report" : input.scheduled.outputPolicy,
|
|
149
|
+
title: input.scheduled.title,
|
|
150
|
+
exitCode: local.exitCode,
|
|
151
|
+
finalText: local.finalText,
|
|
152
|
+
errorMessage: local.errorMessage,
|
|
153
|
+
send: (content) => sendAgentMessage(config.serverUrl, credential.token, input.channelId, {
|
|
154
|
+
content,
|
|
155
|
+
force: true,
|
|
156
|
+
...(boundImDecision === "notify" ? { notifyBoundIm: true } : {}),
|
|
157
|
+
}),
|
|
158
|
+
})
|
|
159
|
+
: undefined;
|
|
160
|
+
const originDecisionPath = originDecisionFileName
|
|
161
|
+
? join(local.workspaceRunDir, originDecisionFileName)
|
|
162
|
+
: null;
|
|
163
|
+
const originDecision = originDecisionPath ? await readOriginDecisionFile(originDecisionPath) : null;
|
|
164
|
+
if (originDecisionPath)
|
|
165
|
+
await resetOriginDecisionFile(originDecisionPath);
|
|
166
|
+
return {
|
|
167
|
+
exitCode: local.exitCode,
|
|
168
|
+
activities,
|
|
169
|
+
model: local.model,
|
|
170
|
+
runtime,
|
|
171
|
+
resumed: local.resumed,
|
|
172
|
+
sessionId: local.sessionId,
|
|
173
|
+
errorMessage: local.errorMessage,
|
|
174
|
+
...(local.usage ? { usage: local.usage } : {}),
|
|
175
|
+
...(report ? { report } : {}),
|
|
176
|
+
...(boundImDecision ? { boundImDecision } : {}),
|
|
177
|
+
...(input.wakeOrigin ? {
|
|
178
|
+
originDecision: originDecision?.decision ?? "missing",
|
|
179
|
+
...(originDecision?.decision === "silent" && originDecision.reason
|
|
180
|
+
? { originDecisionReason: originDecision.reason }
|
|
181
|
+
: {}),
|
|
182
|
+
} : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export async function reportScheduledStartFailure(config, input) {
|
|
186
|
+
const credential = await mintAgentToken(config.serverUrl, config.machineToken, input.handle);
|
|
187
|
+
return deliverScheduledReport({
|
|
188
|
+
policy: input.scheduled.outputPolicy,
|
|
189
|
+
title: input.scheduled.title,
|
|
190
|
+
exitCode: -1,
|
|
191
|
+
finalText: null,
|
|
192
|
+
errorMessage: input.errorMessage,
|
|
193
|
+
send: (content) => sendAgentMessage(config.serverUrl, credential.token, input.channelId, {
|
|
194
|
+
content,
|
|
195
|
+
force: true,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
export function mergeRunAgentResults(results) {
|
|
200
|
+
const final = results[results.length - 1];
|
|
201
|
+
if (!final)
|
|
202
|
+
throw new Error("cannot merge empty run results");
|
|
203
|
+
const usages = results.flatMap((result) => result.usage ? [result.usage] : []);
|
|
204
|
+
const usage = usages.length > 0 ? {
|
|
205
|
+
inputTokens: usages.reduce((sum, item) => sum + item.inputTokens, 0),
|
|
206
|
+
outputTokens: usages.reduce((sum, item) => sum + item.outputTokens, 0),
|
|
207
|
+
cacheReadTokens: usages.reduce((sum, item) => sum + item.cacheReadTokens, 0),
|
|
208
|
+
cacheCreationTokens: usages.reduce((sum, item) => sum + item.cacheCreationTokens, 0),
|
|
209
|
+
...(usages.some((item) => item.costUsd != null)
|
|
210
|
+
? { costUsd: usages.reduce((sum, item) => sum + (item.costUsd ?? 0), 0) }
|
|
211
|
+
: {}),
|
|
212
|
+
} : undefined;
|
|
213
|
+
return {
|
|
214
|
+
...final,
|
|
215
|
+
activities: results.flatMap((result) => result.activities),
|
|
216
|
+
...(usage ? { usage } : {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function defaultPrint(activity) {
|
|
220
|
+
const icon = ICON[activity.kind] ?? "·";
|
|
221
|
+
const detail = activity.detail ? ` ${activity.detail.replace(/\s+/g, " ").slice(0, 120)}` : "";
|
|
222
|
+
process.stdout.write(`${icon} ${activity.label}${detail}\n`);
|
|
223
|
+
}
|
|
224
|
+
export async function sendAgentMessage(serverUrl, token, channelId, body) {
|
|
225
|
+
const response = await fetch(`${serverUrl}/agent/channels/${encodeURIComponent(channelId)}/messages`, {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: {
|
|
228
|
+
authorization: `Bearer ${token}`,
|
|
229
|
+
"content-type": "application/json",
|
|
230
|
+
},
|
|
231
|
+
body: JSON.stringify(body),
|
|
232
|
+
});
|
|
233
|
+
return { delivered: response.status === 201, status: response.status };
|
|
234
|
+
}
|