dsh-session-conductor 1.3.10 → 1.3.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 +2 -1
- package/lib/apply.js +77 -14
- package/lib/client-parts/components/processing.js +18 -11
- package/lib/client.js +18 -11
- package/lib/core/config.js +42 -22
- package/lib/core/index-store.js +28 -23
- package/lib/eio-repair.js +2 -1
- package/lib/features/continue/eligibility.js +1 -1
- package/lib/features/member-model/command.js +33 -15
- package/lib/features/member-model/create-teammate.js +208 -0
- package/lib/features/member-model/locate.js +31 -17
- package/lib/features/member-model/switch.js +242 -69
- package/lib/features/memory-recall/index.js +39 -30
- package/lib/features/rename/analysis.js +25 -16
- package/lib/features/rename/title.js +26 -14
- package/lib/features/rename/value.js +38 -26
- package/lib/features/session-messaging/logic.js +7 -14
- package/lib/features/session-ops-routes.js +178 -253
- package/lib/features/session-ops.js +32 -29
- package/lib/repair.js +5 -3
- package/lib/seq-gap-repair.js +5 -5
- package/lib/sessions/events.js +11 -5
- package/lib/sessions/interruption.js +2 -2
- package/lib/sessions/lookup.js +50 -0
- package/lib/sessions/route.js +1 -1
- package/lib/sessions/tail-io.js +155 -0
- package/lib/shared/atomic-file.js +38 -0
- package/lib/shared/json-store.js +54 -0
- package/lib/shared/timing.js +37 -0
- package/lib/zstd-frames.js +6 -11
- package/package.json +1 -1
- package/skills/dsh-session-conductor-functions.md +1 -1
- package/skills/dsh-session-conductor.md +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// 创建团队成员,并让**第一轮真实任务**就用指定模型。
|
|
2
|
+
//
|
|
3
|
+
// 【为什么需要这一层】宿主 `spawn_teammate` 的参数里没有 provider/model,服务层的
|
|
4
|
+
// SpawnTeammateRequest 也只有 provider —— 模型只能来自 agent-team 插件的
|
|
5
|
+
// freshProvider/forkProvider 配置,所以「创建时就选模型」在原生入口做不到。
|
|
6
|
+
//
|
|
7
|
+
// 【为什么不只是「创建后切一下」】宿主是「创建即开跑」:直接带真实任务创建,第一轮一定用默认模型跑掉。
|
|
8
|
+
// 所以这里拆成四步:
|
|
9
|
+
// ① 用**占位任务**创建(几乎不花钱,也不会动任何文件)
|
|
10
|
+
// ② 立刻中断,把这一轮停掉
|
|
11
|
+
// ③ 把 model/selection 写进该成员的运行记录(与 set_member_model 同一条链路)
|
|
12
|
+
// ④ 再用新模型投递真实任务
|
|
13
|
+
// 这样真实任务从第一条指令起就用指定模型,而不是「先跑错一轮再切」。
|
|
14
|
+
//
|
|
15
|
+
// 【仍然存在的边界】① 占位那一轮确实发生过(只消耗一条极短提示的输入);
|
|
16
|
+
// ② 中断与切换之间存在极短窗口;③ 要彻底「创建前定模型」只能改宿主 agent-team
|
|
17
|
+
// (把 model 加进 SpawnTeammateRequest 并在创建时应用),宿主升级会覆盖。
|
|
18
|
+
//
|
|
19
|
+
// 【两个 "provider" 别混】宿主 spawnTeammate 的 provider = **子代理 provider**(注册在 ctx.subagents,
|
|
20
|
+
// 例如 spawn),而模型用的是 **模型 provider**(agnes/wb/d…)。两者是不同命名空间,
|
|
21
|
+
// 直接拿模型 provider 去创建会报 `no subagent provider registered for "x"`。
|
|
22
|
+
//
|
|
23
|
+
// 【权限】只有 Team Lead 能创建成员(宿主会抛 TEAM_LEAD_REQUIRED),调用方 agent 必须透传。
|
|
24
|
+
|
|
25
|
+
import { log } from "../../shared/log.js";
|
|
26
|
+
import { switchAgentModel } from "./switch.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 占位任务:只为让成员被创建出来,不产生实质动作。
|
|
30
|
+
* 写得极短是为了把首轮消耗压到最低(这一轮拿不到指定模型,越短越不亏)。
|
|
31
|
+
*/
|
|
32
|
+
const PLACEHOLDER_PROMPT = "先待命:不要执行任何操作,不要调用任何工具,不要读写任何文件,等待后续消息。";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 等成员真正空闲(中断的取消信号被观测完)再投递。
|
|
36
|
+
*
|
|
37
|
+
* 【为什么必须等】只看 `interrupt()` 的返回不够:它只是「发出取消」,成员可能仍在收尾;
|
|
38
|
+
* 这时立刻投递,新一轮会撞上同一个取消信号,表现为成员「stopped/failed before it finished」。
|
|
39
|
+
* 用宿主自己的 `waitForChange` 等状态变化(事件驱动),不睡固定时间。
|
|
40
|
+
* @returns {Promise<{idle:boolean, status:string}>}
|
|
41
|
+
*/
|
|
42
|
+
async function waitUntilIdle(teams, caller, name, signal, budget = 60) {
|
|
43
|
+
if (typeof teams?.listMembers !== 'function' || typeof teams?.waitForChange !== 'function') {
|
|
44
|
+
// 老宿主没有这两个能力:退化为「不等待」,行为与之前一致
|
|
45
|
+
return { idle: false, status: 'unknown' };
|
|
46
|
+
}
|
|
47
|
+
for (let round = 0; round < budget; round += 1) {
|
|
48
|
+
let member = null;
|
|
49
|
+
try {
|
|
50
|
+
const list = teams.listMembers(caller) ?? [];
|
|
51
|
+
member = list.find((m) => m?.name === name || m?.target === name) ?? null;
|
|
52
|
+
} catch {
|
|
53
|
+
return { idle: false, status: 'unknown' };
|
|
54
|
+
}
|
|
55
|
+
if (!member || member.status !== 'running') return { idle: true, status: member?.status ?? 'absent' };
|
|
56
|
+
try {
|
|
57
|
+
// 等「下一次团队状态变化」,超时也算一次观察窗口(不是业务超时,只是等待预算)
|
|
58
|
+
await teams.waitForChange(caller, 5000, signal);
|
|
59
|
+
} catch {
|
|
60
|
+
return { idle: false, status: member.status ?? 'unknown' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { idle: false, status: 'still-running' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 创建成员 → 中断首轮 → 设置模型 → 投递真实任务。
|
|
68
|
+
* @param {object} ctx 插件上下文
|
|
69
|
+
* @param {object} exec 工具执行上下文(含 agent / signal)
|
|
70
|
+
* @param {object} args 工具参数
|
|
71
|
+
* @returns {Promise<
|
|
72
|
+
* {ok:true, member:object, interrupt:object, switchResult:object, delivery:object} |
|
|
73
|
+
* {ok:true, member:object, interrupt:object, switchResult:object, delivery:null, error:string} |
|
|
74
|
+
* {error:string}>}
|
|
75
|
+
*/
|
|
76
|
+
export async function createTeammateWithModel(ctx, exec, args) {
|
|
77
|
+
const name = String(args?.name ?? "").trim();
|
|
78
|
+
const description = String(args?.description ?? "").trim();
|
|
79
|
+
const prompt = String(args?.prompt ?? "").trim();
|
|
80
|
+
const modelProvider = String(args?.provider ?? "").trim(); // 模型 provider:agnes / wb / d …
|
|
81
|
+
const model = String(args?.model ?? "").trim(); // 模型 id
|
|
82
|
+
const context = args?.context === "fork" ? "fork" : "fresh";
|
|
83
|
+
|
|
84
|
+
if (!name || !description || !prompt) return { error: "name / description / prompt 都必填" };
|
|
85
|
+
if (!modelProvider || !model) return { error: "provider / model 都必填(先用 list_models 查确切值)" };
|
|
86
|
+
|
|
87
|
+
const teams = ctx.get("agentTeams");
|
|
88
|
+
if (typeof teams?.spawnTeammate !== "function") {
|
|
89
|
+
return { error: "agentTeams.spawnTeammate 不可用:当前 DSH 版本没有暴露创建接口" };
|
|
90
|
+
}
|
|
91
|
+
if (typeof teams?.sendMessage !== "function") {
|
|
92
|
+
return { error: "agentTeams.sendMessage 不可用:无法在创建后投递真实任务" };
|
|
93
|
+
}
|
|
94
|
+
const caller = exec?.agent ?? null;
|
|
95
|
+
if (!caller) return { error: "拿不到调用方 agent(该工具只能由 Team Lead 在自身的运行上下文里调用)" };
|
|
96
|
+
|
|
97
|
+
// 子代理 provider 与模型 provider 是两个命名空间:前者来自 ctx.subagents 注册表
|
|
98
|
+
let subagentProvider = String(args?.subagentProvider ?? "").trim();
|
|
99
|
+
if (!subagentProvider) {
|
|
100
|
+
const subagents = ctx.get("subagents");
|
|
101
|
+
const names = typeof subagents?.list === "function" ? subagents.list() : [];
|
|
102
|
+
subagentProvider = Array.isArray(names) && names.length > 0 ? String(names[0]) : "";
|
|
103
|
+
}
|
|
104
|
+
if (!subagentProvider) {
|
|
105
|
+
return { error: "没有可用的子代理 provider(ctx.subagents.list() 为空)——请在参数里显式传 subagentProvider" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
// signal:优先用工具调用自带的(可随宿主取消),否则用一个永不自废弃的控制器
|
|
110
|
+
const fallbackController = new AbortController();
|
|
111
|
+
const signal = exec?.signal ?? fallbackController.signal;
|
|
112
|
+
|
|
113
|
+
// ① 占位任务创建:真实任务留到模型设好之后再投
|
|
114
|
+
let spawned;
|
|
115
|
+
try {
|
|
116
|
+
spawned = await teams.spawnTeammate(caller, {
|
|
117
|
+
name,
|
|
118
|
+
description,
|
|
119
|
+
prompt: [{ type: "text", text: PLACEHOLDER_PROMPT }],
|
|
120
|
+
context,
|
|
121
|
+
provider: subagentProvider,
|
|
122
|
+
signal,
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return { error: `创建成员失败:${String(error?.message ?? error)}` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const member = spawned?.member ?? null;
|
|
129
|
+
const sessionId = member?.id ? String(member.id) : "";
|
|
130
|
+
if (!sessionId) return { error: "创建成功但成员视图里没有 id,无法继续设置模型" };
|
|
131
|
+
|
|
132
|
+
// ② 立刻中断:占位那一轮不该继续跑
|
|
133
|
+
let interrupt = { previousStatus: "unknown" };
|
|
134
|
+
let interruptError = "";
|
|
135
|
+
try {
|
|
136
|
+
interrupt = await teams.interrupt(caller, name);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// 中断失败不致命:成员可能刚好已经空闲(那就取不到 previousStatus)
|
|
139
|
+
interruptError = String(error?.message ?? error);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ②·5 等它真正空闲:否则紧接着的投递会撞上同一个取消信号(实测表现为「stopped before it finished」)
|
|
143
|
+
const idleState = await waitUntilIdle(teams, caller, name, signal);
|
|
144
|
+
log(ctx, `创建成员 ${name}:中断后等待空闲 → ${JSON.stringify(idleState)}`);
|
|
145
|
+
|
|
146
|
+
// ③ 写 model/selection:与 set_member_model 完全同一条链路(默认不碰任何 @Remote)
|
|
147
|
+
const switchResult = await switchAgentModel(ctx, sessionId, modelProvider, model, null);
|
|
148
|
+
log(ctx, `创建成员 ${name}(${sessionId})| 子代理provider=${subagentProvider} | 中断=${JSON.stringify(interrupt)} | 设模型 ${modelProvider}/${model}=${switchResult.error ? `失败 ${switchResult.error}` : "已写入"}`);
|
|
149
|
+
|
|
150
|
+
if (switchResult.error) {
|
|
151
|
+
// 模型没设上就不投递:否则真实任务会用默认模型跑掉,等于白跑
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
member,
|
|
155
|
+
interrupt,
|
|
156
|
+
switchResult,
|
|
157
|
+
delivery: null,
|
|
158
|
+
error: `模型未设置成功(${switchResult.error}),已放弃投递真实任务;修好后可用 set_member_model + send_to_session 补`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ④ 投递真实任务:成员此时空闲,这条消息会唤醒它并用新模型执行
|
|
163
|
+
let delivery = null;
|
|
164
|
+
try {
|
|
165
|
+
delivery = await teams.sendMessage(caller, {
|
|
166
|
+
target: name,
|
|
167
|
+
content: [{ type: "text", text: prompt }],
|
|
168
|
+
signal,
|
|
169
|
+
});
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
member,
|
|
174
|
+
interrupt,
|
|
175
|
+
switchResult,
|
|
176
|
+
delivery: null,
|
|
177
|
+
error: `模型已设置,但投递真实任务失败:${String(error?.message ?? error)}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { ok: true, member, interrupt, idleState, switchResult, delivery, subagentProvider, ...(interruptError ? { interruptError } : {}) };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 把 createTeammateWithModel 的结果渲染成给模型看的文本。
|
|
186
|
+
* @param {{name:string, provider:string, model:string}} args
|
|
187
|
+
* @param {object} result
|
|
188
|
+
*/
|
|
189
|
+
export function renderCreateTeammateResult(args, result) {
|
|
190
|
+
if (!result?.ok) return `创建成员失败:${result?.error ?? "未知原因"}`;
|
|
191
|
+
const member = result.member ?? {};
|
|
192
|
+
const sessionId = String(member.id ?? "");
|
|
193
|
+
const interrupt = result.interrupt ?? {};
|
|
194
|
+
const switchResult = result.switchResult ?? {};
|
|
195
|
+
const delivery = result.delivery ?? null;
|
|
196
|
+
const lines = [
|
|
197
|
+
`已创建成员 ${member.name ?? args?.name ?? "?"}(${sessionId})`,
|
|
198
|
+
`流程:占位任务创建(子代理 provider=${result.subagentProvider ?? "?"})→ 中断(首轮状态 ${interrupt.previousStatus ?? "unknown"})→ 等空闲(${result.idleState?.status ?? "unknown"})→ 设模型 → 投递真实任务`,
|
|
199
|
+
`模型:${args?.provider ?? "?"}/${args?.model ?? "?"}(${switchResult.via ?? "已写入 model/selection"})`,
|
|
200
|
+
];
|
|
201
|
+
if (delivery) {
|
|
202
|
+
lines.push(`真实任务已投递:messageId=${delivery.messageId ?? "(无)"} —— 该成员会用上面的模型开始执行`);
|
|
203
|
+
}
|
|
204
|
+
if (result.interruptError) lines.push(`注:中断调用报错(${result.interruptError}),已按成员可能已空闲继续处理`);
|
|
205
|
+
if (result.error) lines.push(`⚠️ ${result.error}`);
|
|
206
|
+
lines.push("边界:占位那一轮确实发生过(只消耗一条极短提示);要「创建前定模型」只能改宿主 agent-team。");
|
|
207
|
+
return lines.join("\n");
|
|
208
|
+
}
|
|
@@ -3,37 +3,42 @@
|
|
|
3
3
|
// 【职责】把「切模型」的目标解析出来:成员名/sessionId/标题 → 会话,
|
|
4
4
|
// 并在切换前做状态门禁与 provider/model 组合校验。
|
|
5
5
|
|
|
6
|
+
import { cfg } from "../../core/config.js";
|
|
7
|
+
import { log } from "../../shared/log.js";
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* 取一个可用于 Team 名册查询的 caller agent(lead 优先)。
|
|
8
11
|
* agentTeams.listMembers(caller) 需要一个 Team 成员作为 caller 来解析名册。
|
|
9
12
|
* @returns {object|null} 可用 agent,取不到返回 null
|
|
10
13
|
*/
|
|
11
14
|
export function pickTeamCaller(ctx, preferred = null) {
|
|
12
|
-
const agents = ctx.get("agents");
|
|
13
|
-
if (!agents) return null;
|
|
14
15
|
// 0) 调用方显式给的发起方最可靠:命令/工具都能直接拿到 invocation.agent / exec.agent。
|
|
15
16
|
// currentInitiator() 依赖「当前在某个 agent 的异步链里」,命令处理器里经常拿不到 ——
|
|
16
17
|
// 这正是「工具能切、聊天指令说找不到成员」的根因。
|
|
17
18
|
if (preferred) return preferred;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const roots = agents.roots?.() ?? [];
|
|
26
|
-
if (roots.length > 0) return roots[0];
|
|
27
|
-
} catch {
|
|
28
|
-
// roots 不可用时忽略
|
|
19
|
+
const agents = ctx.get("agents");
|
|
20
|
+
if (!agents) return null;
|
|
21
|
+
for (const probe of CALLER_PROBES) {
|
|
22
|
+
const hit = safeProbe(probe, agents);
|
|
23
|
+
if (hit) return hit;
|
|
29
24
|
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 依次尝试的发起方来源(按可靠性从高到低)。 */
|
|
29
|
+
const CALLER_PROBES = [
|
|
30
|
+
(agents) => agents.currentInitiator?.(),
|
|
31
|
+
(agents) => (agents.roots?.() ?? [])[0] ?? null,
|
|
32
|
+
(agents) => (agents.list?.() ?? [])[0] ?? null,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** 任一来源不可用/抛错都当作「没有」,不影响下一个来源。 */
|
|
36
|
+
function safeProbe(probe, agents) {
|
|
30
37
|
try {
|
|
31
|
-
|
|
32
|
-
if (list.length > 0) return list[0];
|
|
38
|
+
return probe(agents) ?? null;
|
|
33
39
|
} catch {
|
|
34
|
-
|
|
40
|
+
return null;
|
|
35
41
|
}
|
|
36
|
-
return null;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/**
|
|
@@ -68,12 +73,21 @@ export function memberStatusError(member) {
|
|
|
68
73
|
* @returns {Promise<{ok: true, note?: string} | {error: string}>}
|
|
69
74
|
*/
|
|
70
75
|
export async function validateModelPair(ctx, provider, model) {
|
|
76
|
+
// 组合校验只是「提前拦住无效组合」,不该成为切换的必经阻塞点:
|
|
77
|
+
// · 默认就是关闭(modelSwitchValidateCatalog 缺省 false)——@Remote 取目录可能久不返回;
|
|
78
|
+
// · 无论成败都把耗时打出来(sessionController.modelCatalog 是 @Remote,某些部署上很久才返回)。
|
|
79
|
+
if (cfg.modelSwitchValidateCatalog === false) {
|
|
80
|
+
return { ok: true, note: "已按配置跳过组合校验(modelSwitchValidateCatalog=false)" };
|
|
81
|
+
}
|
|
71
82
|
let catalog = null;
|
|
83
|
+
const startedAt = Date.now();
|
|
72
84
|
try {
|
|
73
85
|
const sc = ctx.get("sessionController");
|
|
74
86
|
catalog = sc?.modelCatalog ? await sc.modelCatalog() : null;
|
|
75
87
|
} catch {
|
|
76
88
|
catalog = null;
|
|
89
|
+
} finally {
|
|
90
|
+
log(ctx, `模型组合校验取目录耗时 ${Date.now() - startedAt}ms(分组数 ${(catalog?.groups ?? []).length})`);
|
|
77
91
|
}
|
|
78
92
|
const groups = catalog?.groups ?? [];
|
|
79
93
|
if (groups.length === 0) return { ok: true, note: "modelCatalog 不可用,已跳过组合校验" };
|