@joekytc/dsh-swarm 0.3.12 → 0.5.1
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/client/ConfigSection.tsx +127 -18
- package/client/ConfigSelect.tsx +1 -0
- package/client/config-store.ts +6 -3
- package/client/config.css +89 -1
- package/client/index.ts +8 -4
- package/client/session-bridge.ts +26 -2
- package/cordis.patch.yml +10 -52
- package/lib/client.js +215 -37
- package/lib/config.d.ts +7 -0
- package/lib/config.js +7 -1
- package/lib/dispatcher/agent-runner.d.ts +4 -0
- package/lib/dispatcher/agent-runner.js +28 -17
- package/lib/dispatcher/chain-auditor.d.ts +3 -2
- package/lib/dispatcher/chain-auditor.js +2 -2
- package/lib/dispatcher/model-candidates.d.ts +9 -3
- package/lib/dispatcher/model-candidates.js +27 -6
- package/lib/dispatcher/model-chain.d.ts +19 -0
- package/lib/dispatcher/model-chain.js +110 -0
- package/lib/dispatcher/session-events.d.ts +9 -0
- package/lib/dispatcher/session-events.js +28 -0
- package/lib/dispatcher/v-orchestrator.d.ts +2 -0
- package/lib/dispatcher/v-orchestrator.js +14 -9
- package/lib/domain/config-override.d.ts +20 -0
- package/lib/domain/config-override.js +46 -2
- package/lib/index.js +13 -3
- package/lib/roles/preset-installer.d.ts +50 -5
- package/lib/roles/preset-installer.js +154 -20
- package/lib/routes/kanban-http.js +24 -3
- package/lib/services/config-provider.js +5 -0
- package/package.json +23 -17
- package/personas/kanban-d/agent.cordis.yml +3 -1
- package/personas/kanban-dt/agent.cordis.yml +2 -1
- package/personas/kanban-p/agent.cordis.yml +1 -1
- package/personas/kanban-pt/agent.cordis.yml +1 -1
- package/personas/kanban-v/agent.cordis.yml +1 -1
- package/personas/kanban-w/agent.cordis.yml +1 -1
- package/personas/swarm/agent.cordis.yml +1 -1
|
@@ -1,30 +1,51 @@
|
|
|
1
|
+
/** 单角色降级候选上限(与 config schema .max(2)、config-override 校验同口径)。 */
|
|
2
|
+
const MAX_FALLBACKS = 2;
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
+
* 模型候选链(四层):
|
|
5
|
+
* ① 角色 primary(config.roles.models[role])→ ② 降级候选(fallbacks,顺序=优先序,≤2)
|
|
6
|
+
* → ③ 全局官方兜底(config.roles.chainFallback,恒垫链尾)→ ④ 宿主默认模型(defaultModel)。
|
|
7
|
+
* 角色链全空 → 官方兜底单候选直用;官方兜底也未配 → 宿主默认模型;都无 → 空链(调用方不传 agentOptions)。
|
|
4
8
|
* reasoningEffort 未指定一律 'high'。
|
|
5
9
|
*
|
|
6
10
|
* @returns 有序候选链(不含不可用者;可能为空 = 无任何配置)
|
|
7
11
|
*/
|
|
8
12
|
export function buildModelCandidates(config, role, defaultModel) {
|
|
9
13
|
const m = config.roles?.models?.[role];
|
|
14
|
+
const cf = config.roles?.chainFallback;
|
|
15
|
+
const officialTail = [];
|
|
16
|
+
if (cf?.provider && cf?.model) {
|
|
17
|
+
officialTail.push({ provider: cf.provider, model: cf.model, reasoningEffort: cf.reasoningEffort ?? 'high' });
|
|
18
|
+
}
|
|
10
19
|
const chain = [];
|
|
11
20
|
if (m?.provider && m?.model) {
|
|
12
21
|
chain.push({ provider: m.provider, model: m.model, reasoningEffort: m.reasoningEffort ?? 'high' });
|
|
13
|
-
for (const f of m.fallbacks ?? []) {
|
|
22
|
+
for (const f of (m.fallbacks ?? []).slice(0, MAX_FALLBACKS)) {
|
|
14
23
|
if (f?.provider && f?.model)
|
|
15
24
|
chain.push({ provider: f.provider, model: f.model, reasoningEffort: f.reasoningEffort ?? 'high' });
|
|
16
25
|
}
|
|
26
|
+
chain.push(...officialTail);
|
|
17
27
|
}
|
|
18
|
-
else
|
|
19
|
-
|
|
28
|
+
else {
|
|
29
|
+
// 角色链全空:官方兜底直用;未配官方兜底 → 宿主默认模型(第四层安全网)。
|
|
30
|
+
if (officialTail.length)
|
|
31
|
+
chain.push(...officialTail);
|
|
32
|
+
else if (defaultModel?.provider && defaultModel?.model) {
|
|
33
|
+
chain.push({ provider: defaultModel.provider, model: defaultModel.model, reasoningEffort: defaultModel.reasoningEffort ?? 'high' });
|
|
34
|
+
}
|
|
20
35
|
}
|
|
21
36
|
return chain;
|
|
22
37
|
}
|
|
23
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* 判定 create/resume 错误是否属于候选不可用(可静默切换下一候选)。
|
|
40
|
+
* 覆盖三类:provider/model 不可用(无适配器/不存在)、凭据缺失(官方账号未登录
|
|
41
|
+
* MISSING_CREDENTIAL / 登录提示——不切换会把登录态问题误判成任务失败)。
|
|
42
|
+
*/
|
|
24
43
|
export function isModelUnavailableError(err) {
|
|
25
44
|
const msg = String(err instanceof Error ? err.message : err).toLowerCase();
|
|
26
45
|
if (msg.includes('adapter'))
|
|
27
46
|
return true; // no adapter registered for provider …
|
|
47
|
+
if (msg.includes('missing_credential') || msg.includes('credentials') || msg.includes('login') || msg.includes('登录'))
|
|
48
|
+
return true;
|
|
28
49
|
if (msg.includes('provider') && (msg.includes('unavailable') || msg.includes('not') || msg.includes('fail')))
|
|
29
50
|
return true;
|
|
30
51
|
return msg.includes('model') && (msg.includes('unavailable') || msg.includes('not found') || msg.includes('no adapter'));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { KanbanConfig } from '../config.js';
|
|
2
|
+
import type { AgentModelOptions } from './dispatcher.js';
|
|
3
|
+
export interface ModelChainState {
|
|
4
|
+
candidates: AgentModelOptions[];
|
|
5
|
+
index: number;
|
|
6
|
+
taskId?: string;
|
|
7
|
+
comment?: (body: string) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function registerModelChain(agent: object, state: ModelChainState): void;
|
|
10
|
+
export declare function isSwitchableModelFailure(failure: {
|
|
11
|
+
code?: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
status?: number;
|
|
14
|
+
}): boolean;
|
|
15
|
+
export interface ModelChainHookDeps {
|
|
16
|
+
getConfig: () => KanbanConfig;
|
|
17
|
+
log?: (line: string) => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function installModelChainHooks(scope: unknown, deps: ModelChainHookDeps): () => void;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { buildModelCandidates } from './model-candidates.js';
|
|
2
|
+
import { agentPresetSelected } from './session-events.js';
|
|
3
|
+
const chainStates = new WeakMap();
|
|
4
|
+
export function registerModelChain(agent, state) {
|
|
5
|
+
chainStates.set(agent, state);
|
|
6
|
+
const session = agent.session;
|
|
7
|
+
if (session && typeof session === 'object')
|
|
8
|
+
chainStates.set(session, state);
|
|
9
|
+
}
|
|
10
|
+
export function isSwitchableModelFailure(failure) {
|
|
11
|
+
const code = String(failure.code ?? '').toLowerCase();
|
|
12
|
+
const msg = String(failure.message ?? '').toLowerCase();
|
|
13
|
+
if (code.includes('quota'))
|
|
14
|
+
return true;
|
|
15
|
+
if (failure.status === 402 || failure.status === 429)
|
|
16
|
+
return true;
|
|
17
|
+
if (/insufficient balance|quota|rate.?limit|\b429\b/.test(msg))
|
|
18
|
+
return true;
|
|
19
|
+
if (code === 'unknown_model' || code === 'unsupported_reasoning_effort')
|
|
20
|
+
return true;
|
|
21
|
+
if (code.includes('credential') || code.includes('login'))
|
|
22
|
+
return true;
|
|
23
|
+
if (msg.includes('adapter'))
|
|
24
|
+
return true;
|
|
25
|
+
if (msg.includes('missing_credential') || msg.includes('credentials') || msg.includes('login') || msg.includes('登录'))
|
|
26
|
+
return true;
|
|
27
|
+
if (msg.includes('provider') && (msg.includes('unavailable') || msg.includes('not') || msg.includes('fail')))
|
|
28
|
+
return true;
|
|
29
|
+
return msg.includes('model') && (msg.includes('unavailable') || msg.includes('not found') || msg.includes('no adapter'));
|
|
30
|
+
}
|
|
31
|
+
const ROLE_IDS = new Set(['v', 'p', 'w', 'd', 'pt', 'dt']);
|
|
32
|
+
function sessionEventsOf(session) {
|
|
33
|
+
const s = session;
|
|
34
|
+
if (typeof s?.snapshotEvents === 'function')
|
|
35
|
+
return s.snapshotEvents();
|
|
36
|
+
return s?.events ?? [];
|
|
37
|
+
}
|
|
38
|
+
function detectManualChain(agent, config) {
|
|
39
|
+
const session = agent.session;
|
|
40
|
+
const preset = agentPresetSelected(sessionEventsOf(session))
|
|
41
|
+
?? (session?.header?.agentPreset ?? null);
|
|
42
|
+
const role = typeof preset === 'string' && preset.startsWith('kanban-') ? preset.slice('kanban-'.length) : '';
|
|
43
|
+
let state = null;
|
|
44
|
+
if (ROLE_IDS.has(role)) {
|
|
45
|
+
const m = config.roles?.models?.[role];
|
|
46
|
+
if (m?.provider && m?.model) {
|
|
47
|
+
const candidates = buildModelCandidates(config, role);
|
|
48
|
+
if (candidates.length > 0)
|
|
49
|
+
state = { candidates, index: 0 };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
chainStates.set(agent, state);
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
function resolveState(agent, config) {
|
|
56
|
+
const session = agent.session;
|
|
57
|
+
for (const key of [agent, session]) {
|
|
58
|
+
if (key && typeof key === 'object' && chainStates.has(key))
|
|
59
|
+
return chainStates.get(key) ?? null;
|
|
60
|
+
}
|
|
61
|
+
return detectManualChain(agent, config);
|
|
62
|
+
}
|
|
63
|
+
export function installModelChainHooks(scope, deps) {
|
|
64
|
+
const scoped = scope;
|
|
65
|
+
const disposeRequest = scoped.on('agent/request', async (payload, next) => {
|
|
66
|
+
const resolved = (await next());
|
|
67
|
+
const agent = payload.agent;
|
|
68
|
+
if (!agent || typeof agent !== 'object')
|
|
69
|
+
return resolved;
|
|
70
|
+
const state = resolveState(agent, deps.getConfig());
|
|
71
|
+
const cur = state?.candidates[state.index];
|
|
72
|
+
if (!cur)
|
|
73
|
+
return resolved;
|
|
74
|
+
return { ...resolved, provider: cur.provider, model: cur.model, reasoningEffort: cur.reasoningEffort ?? 'high' };
|
|
75
|
+
});
|
|
76
|
+
const disposeError = scoped.on('agent/request-error', async (payload, next) => {
|
|
77
|
+
const p = payload;
|
|
78
|
+
const agent = p.agent;
|
|
79
|
+
if (!agent || typeof agent !== 'object')
|
|
80
|
+
return next();
|
|
81
|
+
const state = resolveState(agent, deps.getConfig());
|
|
82
|
+
if (!state)
|
|
83
|
+
return next();
|
|
84
|
+
if (!isSwitchableModelFailure(p.failure ?? {}))
|
|
85
|
+
return next();
|
|
86
|
+
const nextIndex = state.index + 1;
|
|
87
|
+
if (nextIndex >= state.candidates.length)
|
|
88
|
+
return next();
|
|
89
|
+
const prev = state.candidates[state.index];
|
|
90
|
+
state.index = nextIndex;
|
|
91
|
+
const cur = state.candidates[nextIndex];
|
|
92
|
+
const detail = String(p.failure?.code ?? '') + ' ' + String(p.failure?.message ?? '');
|
|
93
|
+
const line = '[model-fallback] ' + prev.provider + '/' + prev.model + ' → ' + cur.provider + '/' + cur.model
|
|
94
|
+
+ '(reasoningEffort=' + (cur.reasoningEffort ?? 'high') + '): ' + detail;
|
|
95
|
+
console.error('[dsh-swarm][debug] ' + line);
|
|
96
|
+
deps.log?.(line);
|
|
97
|
+
if (state.taskId && state.comment) {
|
|
98
|
+
try {
|
|
99
|
+
await state.comment('[model-fallback] 主模型不可用,静默切换 ' + cur.provider + '/' + cur.model
|
|
100
|
+
+ '(reasoningEffort=' + (cur.reasoningEffort ?? 'high') + ')');
|
|
101
|
+
}
|
|
102
|
+
catch { /* 证据记录失败不影响切换 */ }
|
|
103
|
+
}
|
|
104
|
+
return { kind: 'retry' };
|
|
105
|
+
});
|
|
106
|
+
return () => {
|
|
107
|
+
disposeRequest();
|
|
108
|
+
disposeError();
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -18,6 +18,13 @@ export declare function eventType(e: {
|
|
|
18
18
|
export declare function toolName(e: unknown): string | undefined;
|
|
19
19
|
/** 取工具调用参数对象(兼容 JSON 字符串 / 对象两种落盘形态)。 */
|
|
20
20
|
export declare function toolArgs(e: unknown): Record<string, unknown>;
|
|
21
|
+
/** run_code 派发子调用事件名(start 与完成两种)新旧宿主双名集合:
|
|
22
|
+
* 旧宿主会话落盘为 tool/code-dispatch-start / tool/code-dispatch;
|
|
23
|
+
* 宿主 V3+ 会话格式改名为 tool/ptc-dispatch-start / tool/ptc-dispatch。
|
|
24
|
+
* 消费方(链审计器按派发子调用判定 run_code 实际写行为)双名等价命中,勿散落硬编码。 */
|
|
25
|
+
export declare const RUN_CODE_DISPATCH_EVENT_TYPES: ReadonlySet<string>;
|
|
26
|
+
/** 该事件是否为 run_code 派发子调用事件(start 或完成,新旧事件名皆认)。 */
|
|
27
|
+
export declare function isRunCodeDispatchEvent(e: unknown): boolean;
|
|
21
28
|
/** 取 assistant 消息的网关缓存回放标记(replayState.response.responseModel,如 'from-cache')。
|
|
22
29
|
* 非 assistant/message 或无标记返回 null。2026-09-04:远程网关回复缓存整包回放时由 SSE
|
|
23
30
|
* id/model 字段透传(dsh 纯透传),插件侧据此识别缓存劫持零产出。兼容落盘(data.message)
|
|
@@ -41,6 +48,8 @@ export interface TurnEndInfo {
|
|
|
41
48
|
message: string | null;
|
|
42
49
|
}
|
|
43
50
|
export declare function turnEndOf(e: unknown): TurnEndInfo | null;
|
|
51
|
+
/** 会话内最后一次 agent-preset/selected 的 preset id(如 'kanban-w');无该事件返回 null。 */
|
|
52
|
+
export declare function agentPresetSelected(events: ReadonlyArray<unknown>): string | null;
|
|
44
53
|
/** 本轮增量(seq > fromSeq)内最后一轮 turn/end——拒答重试会产生第二轮,取 seq 最大者。
|
|
45
54
|
* 无 turn/end(测试桩/宿主变体)→ null,调用方按「无轮信息」回退原判据,不改变既有行为。 */
|
|
46
55
|
export declare function lastTurnEnd(events: ReadonlyArray<unknown>, fromSeq: number): TurnEndInfo | null;
|
|
@@ -35,6 +35,21 @@ export function toolArgs(e) {
|
|
|
35
35
|
}
|
|
36
36
|
return {};
|
|
37
37
|
}
|
|
38
|
+
/** run_code 派发子调用事件名(start 与完成两种)新旧宿主双名集合:
|
|
39
|
+
* 旧宿主会话落盘为 tool/code-dispatch-start / tool/code-dispatch;
|
|
40
|
+
* 宿主 V3+ 会话格式改名为 tool/ptc-dispatch-start / tool/ptc-dispatch。
|
|
41
|
+
* 消费方(链审计器按派发子调用判定 run_code 实际写行为)双名等价命中,勿散落硬编码。 */
|
|
42
|
+
export const RUN_CODE_DISPATCH_EVENT_TYPES = new Set([
|
|
43
|
+
'tool/code-dispatch-start',
|
|
44
|
+
'tool/code-dispatch',
|
|
45
|
+
'tool/ptc-dispatch-start',
|
|
46
|
+
'tool/ptc-dispatch',
|
|
47
|
+
]);
|
|
48
|
+
/** 该事件是否为 run_code 派发子调用事件(start 或完成,新旧事件名皆认)。 */
|
|
49
|
+
export function isRunCodeDispatchEvent(e) {
|
|
50
|
+
const t = eventType(e);
|
|
51
|
+
return t !== undefined && RUN_CODE_DISPATCH_EVENT_TYPES.has(t);
|
|
52
|
+
}
|
|
38
53
|
/** 取 assistant 消息的网关缓存回放标记(replayState.response.responseModel,如 'from-cache')。
|
|
39
54
|
* 非 assistant/message 或无标记返回 null。2026-09-04:远程网关回复缓存整包回放时由 SSE
|
|
40
55
|
* id/model 字段透传(dsh 纯透传),插件侧据此识别缓存劫持零产出。兼容落盘(data.message)
|
|
@@ -71,6 +86,19 @@ export function turnEndOf(e) {
|
|
|
71
86
|
message: typeof reason?.error?.message === 'string' ? reason.error.message : null,
|
|
72
87
|
};
|
|
73
88
|
}
|
|
89
|
+
/** 会话内最后一次 agent-preset/selected 的 preset id(如 'kanban-w');无该事件返回 null。 */
|
|
90
|
+
export function agentPresetSelected(events) {
|
|
91
|
+
let found = null;
|
|
92
|
+
for (const e of events) {
|
|
93
|
+
if (eventType(e) !== 'agent-preset/selected')
|
|
94
|
+
continue;
|
|
95
|
+
const rec = e;
|
|
96
|
+
const p = rec?.agentPreset ?? rec?.data?.agentPreset;
|
|
97
|
+
if (typeof p === 'string')
|
|
98
|
+
found = p;
|
|
99
|
+
}
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
74
102
|
/** 本轮增量(seq > fromSeq)内最后一轮 turn/end——拒答重试会产生第二轮,取 seq 最大者。
|
|
75
103
|
* 无 turn/end(测试桩/宿主变体)→ null,调用方按「无轮信息」回退原判据,不改变既有行为。 */
|
|
76
104
|
export function lastTurnEnd(events, fromSeq) {
|
|
@@ -34,7 +34,9 @@ export declare function buildPhaseInstruction(phase: VPhase, ctx: {
|
|
|
34
34
|
taskId?: string;
|
|
35
35
|
}, kbMode: 'remote' | 'local'): string;
|
|
36
36
|
interface AgentLike {
|
|
37
|
+
/** 宿主 UserMessage 要求 identified(id 必填):缺 id 的消息会被 v4 会话校验拒读。 */
|
|
37
38
|
followup(msg: {
|
|
39
|
+
id: string;
|
|
38
40
|
content: {
|
|
39
41
|
type: string;
|
|
40
42
|
text: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { installRoleTools } from '../roles/toolsets.js';
|
|
2
3
|
import { mountOrRecompose } from '../roles/preset-installer.js';
|
|
3
4
|
import { resolveTaskParents } from '../domain/task-parents.js';
|
|
@@ -5,6 +6,7 @@ import { missingParentDelivery } from '../domain/delivery-contract.js';
|
|
|
5
6
|
import { buildRepoSlug } from '../domain/memory.js';
|
|
6
7
|
import { toolArgs, toolName, replayModel } from './session-events.js';
|
|
7
8
|
import { buildModelCandidates, isModelUnavailableError } from './model-candidates.js';
|
|
9
|
+
import { registerModelChain } from './model-chain.js';
|
|
8
10
|
import { attachSessionToWorkspace, resolveOrCreateWorkspace } from './workspace-attach.js';
|
|
9
11
|
export const R20_PHASE_ORDER = ['p', 'pt', 'w2', 'd', 'dt', 'w3', 'summary'];
|
|
10
12
|
/** 每 phase 的期望建卡(pt 由 P 交付 pt_decision.needed=true 触发;dt 固定)。 */
|
|
@@ -266,7 +268,7 @@ export class VOrchestrator {
|
|
|
266
268
|
'gave_up 任务说明链路已停止,建议查看对应 [blocked-final] 证据链(block 时间线 + 复核/评论时间线 + 最终原因),给出终态解释。',
|
|
267
269
|
'规则:只评论、不建卡、不改任务状态;已有 [blocked-review] 评论的任务不要重复评论。',
|
|
268
270
|
].join('\n\n');
|
|
269
|
-
agent.followup({ content: [{ type: 'text', text: context }], source: { kind: 'user' } });
|
|
271
|
+
agent.followup({ id: randomUUID(), content: [{ type: 'text', text: context }], source: { kind: 'user' } });
|
|
270
272
|
await agent.whenIdle();
|
|
271
273
|
return; // 本轮 V 唯一动作是阻塞复核,不再推进阶段
|
|
272
274
|
}
|
|
@@ -411,7 +413,7 @@ export class VOrchestrator {
|
|
|
411
413
|
let turnError = null;
|
|
412
414
|
try {
|
|
413
415
|
agent = await this.getVAgent(orch);
|
|
414
|
-
agent.followup({ content: [{ type: 'text', text: context }], source: { kind: 'user' } });
|
|
416
|
+
agent.followup({ id: randomUUID(), content: [{ type: 'text', text: context }], source: { kind: 'user' } });
|
|
415
417
|
await agent.whenIdle();
|
|
416
418
|
}
|
|
417
419
|
catch (err) {
|
|
@@ -619,7 +621,7 @@ export class VOrchestrator {
|
|
|
619
621
|
// 与 agent-runner.ts 的 installRoleTools 用法一致。
|
|
620
622
|
// V 后台编排会话与 P/W/D 一致,显式设置 approval=never + sandbox=workspace-write,
|
|
621
623
|
// 避免在无 preset 装配时因默认审批策略在后台无应答者而挂起(卡死调度器首轮 tick)。
|
|
622
|
-
const setup = async (agentCtx) => {
|
|
624
|
+
const setup = async (agentCtx, agent) => {
|
|
623
625
|
// 思考等级强制(waterfall):与 agent-runner 同缺陷——宿主 selection 无 create-options 覆盖层,
|
|
624
626
|
// agentOptions.reasoningEffort 不被消费,V 编排会话思考等级会落回宿主默认。走 DSH agent/request
|
|
625
627
|
// waterfall 逐请求强制 'high'(宿主 installModelSelection 同机制),作用域仅本 V 会话。
|
|
@@ -629,7 +631,8 @@ export class VOrchestrator {
|
|
|
629
631
|
const resolved = await next();
|
|
630
632
|
return { ...resolved, reasoningEffort: 'high' };
|
|
631
633
|
});
|
|
632
|
-
|
|
634
|
+
// approval/sandbox 种子事件经 AgentSetup 第二参的 agent 显式直写(0.2.0 起 ctx.agent 挂载已移除)。
|
|
635
|
+
const session = agent?.session;
|
|
633
636
|
session?.append?.('approval/policy', { policy: 'never', source: 'delegation' });
|
|
634
637
|
session?.append?.('sandbox/mode', { mode: 'workspace-write', source: 'delegation' });
|
|
635
638
|
// 对齐(2026-08-17):V=butler·orchestrator 零执行能力——先挂 kanban-v 裁剪 preset
|
|
@@ -653,9 +656,9 @@ export class VOrchestrator {
|
|
|
653
656
|
}
|
|
654
657
|
await installRoleTools(agentCtx, 'v', { kanban: this.kanban, wiki: this.wiki });
|
|
655
658
|
// setup 完整成功(mount + 工具面)后写身份标记——live 复用校验依据(同 agent-runner 组合标记机制)。
|
|
656
|
-
|
|
657
|
-
if (
|
|
658
|
-
vSessionCompositions.set(
|
|
659
|
+
// 键 = AgentSetup 第二参的 Agent 实例(与发布后 agents.get(id) 返回同一实例)。
|
|
660
|
+
if (agent)
|
|
661
|
+
vSessionCompositions.set(agent, V_SESSION_PRESET_ID);
|
|
659
662
|
};
|
|
660
663
|
// 模型候选链:V 会话 create/resume 按 primary→fallbacks 静默切换;
|
|
661
664
|
// V 无任务卡可 block——全候选不可用抛最后错误(wakeV 调用方按既有错误路径处理)。
|
|
@@ -695,9 +698,11 @@ export class VOrchestrator {
|
|
|
695
698
|
if (candidates.length === 0)
|
|
696
699
|
return spawnWith({});
|
|
697
700
|
let lastErr = null;
|
|
698
|
-
for (const candidate of candidates) {
|
|
701
|
+
for (const [candidateIndex, candidate] of candidates.entries()) {
|
|
699
702
|
try {
|
|
700
|
-
|
|
703
|
+
const spawned = await spawnWith({ agentOptions: candidate });
|
|
704
|
+
registerModelChain(spawned, { candidates, index: candidateIndex });
|
|
705
|
+
return spawned;
|
|
701
706
|
}
|
|
702
707
|
catch (err) {
|
|
703
708
|
lastErr = err;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { KanbanConfig } from '../config.js';
|
|
2
2
|
import type { Role } from './types.js';
|
|
3
3
|
export declare const ROLES: readonly Role[];
|
|
4
|
+
export interface ModelFallbackInput {
|
|
5
|
+
provider?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
reasoningEffort?: string;
|
|
8
|
+
}
|
|
4
9
|
export interface EditableModelInput {
|
|
5
10
|
provider?: string;
|
|
6
11
|
model?: string;
|
|
7
12
|
reasoningEffort?: string;
|
|
13
|
+
fallbacks?: ModelFallbackInput[];
|
|
8
14
|
}
|
|
9
15
|
export interface EditableOverride {
|
|
10
16
|
wikiVault?: {
|
|
@@ -25,10 +31,16 @@ export interface EditableOverride {
|
|
|
25
31
|
fallbackBotId?: string;
|
|
26
32
|
};
|
|
27
33
|
}
|
|
34
|
+
export interface ModelFallbackSnapshot {
|
|
35
|
+
provider: string;
|
|
36
|
+
model: string;
|
|
37
|
+
reasoningEffort: string;
|
|
38
|
+
}
|
|
28
39
|
export interface EditableModelSnapshot {
|
|
29
40
|
provider: string;
|
|
30
41
|
model: string;
|
|
31
42
|
reasoningEffort: string;
|
|
43
|
+
fallbacks: ModelFallbackSnapshot[];
|
|
32
44
|
}
|
|
33
45
|
export interface EditableSnapshot {
|
|
34
46
|
wikiVault: {
|
|
@@ -38,6 +50,12 @@ export interface EditableSnapshot {
|
|
|
38
50
|
roles: {
|
|
39
51
|
models: Partial<Record<Role, EditableModelSnapshot>>;
|
|
40
52
|
};
|
|
53
|
+
/** 官方兜底(全局)——只读投影:编辑入口在部署配置(bundle patch),面板仅展示链尾。 */
|
|
54
|
+
chainFallback: {
|
|
55
|
+
provider: string;
|
|
56
|
+
model: string;
|
|
57
|
+
reasoningEffort: string;
|
|
58
|
+
};
|
|
41
59
|
reviewEngine: {
|
|
42
60
|
mode: 'delegate' | 'managed';
|
|
43
61
|
managed: {
|
|
@@ -51,6 +69,8 @@ export interface EditableSnapshot {
|
|
|
51
69
|
}
|
|
52
70
|
export type ConfigSource = 'override' | 'inherited';
|
|
53
71
|
export type SourceMap = Record<string, ConfigSource>;
|
|
72
|
+
/** 单角色降级候选上限(与 config schema .max(2)、GUI 上限同口径)。 */
|
|
73
|
+
export declare const MAX_FALLBACKS = 2;
|
|
54
74
|
export declare function mergeConfig(baseline: KanbanConfig, override: EditableOverride | undefined): KanbanConfig;
|
|
55
75
|
export declare function computeSources(override: EditableOverride | undefined): SourceMap;
|
|
56
76
|
export declare function projectEditable(effective: KanbanConfig): EditableSnapshot;
|
|
@@ -2,6 +2,18 @@ export const ROLES = ['v', 'p', 'w', 'd', 'pt', 'dt'];
|
|
|
2
2
|
/** botId 形态(与 dsh-im 的 id 校验同口径):非空时只允许 id 安全字符。 */
|
|
3
3
|
const BOT_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
4
4
|
const MODEL_FIELDS = ['provider', 'model', 'reasoningEffort'];
|
|
5
|
+
/** 单角色降级候选上限(与 config schema .max(2)、GUI 上限同口径)。 */
|
|
6
|
+
export const MAX_FALLBACKS = 2;
|
|
7
|
+
/** 降级候选归一化:全空行丢弃(GUI 占位行)、effort 空白补 'high'、部分填写行保留(交校验报错)。 */
|
|
8
|
+
function normalizeFallbacks(list) {
|
|
9
|
+
return (list ?? [])
|
|
10
|
+
.filter((f) => (f?.provider ?? '').trim() || (f?.model ?? '').trim())
|
|
11
|
+
.map((f) => ({
|
|
12
|
+
provider: (f.provider ?? '').trim(),
|
|
13
|
+
model: (f.model ?? '').trim(),
|
|
14
|
+
reasoningEffort: f.reasoningEffort?.trim() ? f.reasoningEffort : 'high',
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
5
17
|
export function mergeConfig(baseline, override) {
|
|
6
18
|
const wiki = { ...baseline.wikiVault };
|
|
7
19
|
if (override?.wikiVault?.baseUrl !== undefined)
|
|
@@ -32,12 +44,15 @@ export function mergeConfig(baseline, override) {
|
|
|
32
44
|
// 防御:空字符串 reasoningEffort 视为"未设置",永不覆盖 baseline/默认(读点回退 'high')。
|
|
33
45
|
if (over.reasoningEffort !== undefined && over.reasoningEffort.trim() !== '')
|
|
34
46
|
cur.reasoningEffort = over.reasoningEffort;
|
|
47
|
+
if (over.fallbacks !== undefined)
|
|
48
|
+
cur.fallbacks = normalizeFallbacks(over.fallbacks);
|
|
35
49
|
if (cur.provider && cur.model)
|
|
36
50
|
models[role] = cur;
|
|
37
51
|
}
|
|
38
52
|
const imDelivery = { ...baseline.imDelivery };
|
|
39
53
|
if (override?.imDelivery?.fallbackBotId !== undefined)
|
|
40
54
|
imDelivery.fallbackBotId = override.imDelivery.fallbackBotId;
|
|
55
|
+
// chainFallback 只属 baseline(部署配置),override 不触碰。
|
|
41
56
|
return { ...baseline, wikiVault: wiki, roles: { ...baseline.roles, models }, reviewEngine, imDelivery };
|
|
42
57
|
}
|
|
43
58
|
export function computeSources(override) {
|
|
@@ -51,6 +66,7 @@ export function computeSources(override) {
|
|
|
51
66
|
const hit = override?.roles?.models?.[role]?.[f] !== undefined;
|
|
52
67
|
src[`roles.models.${role}.${f}`] = hit ? 'override' : 'inherited';
|
|
53
68
|
}
|
|
69
|
+
src[`roles.models.${role}.fallbacks`] = override?.roles?.models?.[role]?.fallbacks !== undefined ? 'override' : 'inherited';
|
|
54
70
|
}
|
|
55
71
|
src['reviewEngine.mode'] = override?.reviewEngine?.mode !== undefined ? 'override' : 'inherited';
|
|
56
72
|
src['reviewEngine.managed.provider'] = override?.reviewEngine?.managed?.provider !== undefined ? 'override' : 'inherited';
|
|
@@ -62,12 +78,21 @@ export function projectEditable(effective) {
|
|
|
62
78
|
const models = {};
|
|
63
79
|
for (const role of ROLES) {
|
|
64
80
|
const m = effective.roles.models?.[role];
|
|
65
|
-
if (m?.provider && m?.model)
|
|
66
|
-
models[role] = {
|
|
81
|
+
if (m?.provider && m?.model) {
|
|
82
|
+
models[role] = {
|
|
83
|
+
provider: m.provider, model: m.model, reasoningEffort: m.reasoningEffort ?? 'high',
|
|
84
|
+
fallbacks: normalizeFallbacks(m.fallbacks),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
67
87
|
}
|
|
88
|
+
const cf = effective.roles.chainFallback;
|
|
68
89
|
return {
|
|
69
90
|
wikiVault: { baseUrl: effective.wikiVault.baseUrl, pagePrefix: effective.wikiVault.pagePrefix },
|
|
70
91
|
roles: { models },
|
|
92
|
+
chainFallback: {
|
|
93
|
+
provider: cf?.provider ?? '', model: cf?.model ?? '',
|
|
94
|
+
reasoningEffort: cf?.reasoningEffort?.trim() ? cf.reasoningEffort : 'high',
|
|
95
|
+
},
|
|
71
96
|
reviewEngine: {
|
|
72
97
|
mode: effective.reviewEngine?.mode ?? 'delegate',
|
|
73
98
|
managed: { provider: effective.reviewEngine?.managed?.provider ?? '', model: effective.reviewEngine?.managed?.model ?? '' },
|
|
@@ -95,6 +120,18 @@ export function validateConfig(snapshot) {
|
|
|
95
120
|
errs.push(`roles.models.${role}.provider`);
|
|
96
121
|
if (!m.model?.trim())
|
|
97
122
|
errs.push(`roles.models.${role}.model`);
|
|
123
|
+
const fbs = m.fallbacks ?? [];
|
|
124
|
+
if (fbs.length > MAX_FALLBACKS)
|
|
125
|
+
errs.push(`roles.models.${role}.fallbacks`);
|
|
126
|
+
fbs.forEach((f, i) => {
|
|
127
|
+
const p = (f?.provider ?? '').trim();
|
|
128
|
+
const mm = (f?.model ?? '').trim();
|
|
129
|
+
// 全空行 = GUI 占位(合法);部分填写 = 未完成编辑,报错阻断。
|
|
130
|
+
if (!p && !mm)
|
|
131
|
+
return;
|
|
132
|
+
if (!p || !mm)
|
|
133
|
+
errs.push(`roles.models.${role}.fallbacks.${i}`);
|
|
134
|
+
});
|
|
98
135
|
}
|
|
99
136
|
// 默认机器人:留空=未设默认(合法,语义为「未命中就交互」);非空必须形如 dsh-im 的 botId。
|
|
100
137
|
const fb = (snapshot.imDelivery?.fallbackBotId ?? '').trim();
|
|
@@ -124,6 +161,13 @@ export function diffOverride(baseline, snapshot) {
|
|
|
124
161
|
const snapEffort = snap?.reasoningEffort?.trim() ? snap.reasoningEffort : undefined;
|
|
125
162
|
if (snapEffort !== undefined && snapEffort !== (base?.reasoningEffort ?? 'high'))
|
|
126
163
|
cur.reasoningEffort = snapEffort;
|
|
164
|
+
// 降级链按归一化后整体比较:变化才整组写入。缺字段=显式空链(清空语义);
|
|
165
|
+
// 旧客户端兼容由 HTTP 保存口回填 effective 链,不在此特判。
|
|
166
|
+
if (snap) {
|
|
167
|
+
const snapFbs = normalizeFallbacks(snap.fallbacks);
|
|
168
|
+
if (JSON.stringify(snapFbs) !== JSON.stringify(normalizeFallbacks(base?.fallbacks)))
|
|
169
|
+
cur.fallbacks = snapFbs;
|
|
170
|
+
}
|
|
127
171
|
if (Object.keys(cur).length)
|
|
128
172
|
models[role] = cur;
|
|
129
173
|
}
|
package/lib/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { installRolePresets } from './roles/preset-installer.js';
|
|
|
6
6
|
import { installLlWikiSkill } from './roles/skill-installer.js';
|
|
7
7
|
import { registerKanbanHttp } from './routes/kanban-http.js';
|
|
8
8
|
import { startDispatcher } from './dispatcher/dispatcher.js';
|
|
9
|
+
import { installModelChainHooks } from './dispatcher/model-chain.js';
|
|
10
|
+
import { logToDispatcherLog } from './dispatcher/agent-runner.js';
|
|
9
11
|
import { ConfigProvider } from './services/config-provider.js';
|
|
10
12
|
import { wireImDelivery } from './services/im-delivery.js';
|
|
11
13
|
export const name = 'dsh-swarm';
|
|
@@ -36,14 +38,22 @@ export function apply(ctx, config) {
|
|
|
36
38
|
const configProvider = new ConfigProvider(ctx, config, storageDir);
|
|
37
39
|
// KanbanProvider 持 ConfigProvider 引用,kb_url base 经 getter 热读取(配置面板改后无需重建)。
|
|
38
40
|
const provider = new KanbanProvider(ctx, config, configProvider);
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
+
// 双路安装角色裁剪 preset:目录写 $DSH_HOME/.agent-presets/(0.1.7-rc.x 宿主发现根)+
|
|
42
|
+
// 运行时注册 agentPresets.register(0.2.0 宿主唯一生效路径)。注册等服务就绪后完成,
|
|
43
|
+
// 由 installer 如实记录「目录写 / 运行时注册」各自清单,不假阳性。
|
|
44
|
+
void installRolePresets(ctx).catch((err) => {
|
|
45
|
+
console.warn('[dsh-swarm] role preset install error: ' + String(err));
|
|
46
|
+
});
|
|
41
47
|
installLlWikiSkill(); // 尽力而为:失败仅告警不阻断
|
|
42
|
-
console.info('[dsh-swarm] role presets installed: ' + (installed.length ? installed.join(',') : 'none'));
|
|
43
48
|
// IM 投递(企微,2026-09-07 评审决议):W3 收尾/链阻塞时经 dsh-im 投群。
|
|
44
49
|
// imDelivery.enabled=false(默认)时事件处理器零开销早退;dshIm 服务缺失显式降级留痕。
|
|
45
50
|
const disposeImDelivery = wireImDelivery(ctx, provider.service, configProvider);
|
|
46
51
|
ctx.on('dispose', disposeImDelivery);
|
|
52
|
+
const disposeModelChain = installModelChainHooks(ctx, {
|
|
53
|
+
getConfig: () => configProvider.getEffective(),
|
|
54
|
+
log: (line) => logToDispatcherLog(configProvider, line),
|
|
55
|
+
});
|
|
56
|
+
ctx.on('dispose', disposeModelChain);
|
|
47
57
|
// LLM 运行时延迟取用:registerKanbanHttp 内部消费(llm-catalog 枚举),接线时 llm 服务已就绪。
|
|
48
58
|
const llm = () => ctx.get('llm');
|
|
49
59
|
// 可选服务接线均延迟到服务可用后:
|
|
@@ -1,11 +1,56 @@
|
|
|
1
1
|
/** 包内组合目录(随包分发)。src/roles/ 与 lib/roles/ 深度一致,均经 ../../ 回到包根。 */
|
|
2
2
|
export declare function packagePresetsDir(): string;
|
|
3
|
-
/** $DSH_HOME/.agent-presets(
|
|
3
|
+
/** $DSH_HOME/.agent-presets(0.1.7-rc.x 宿主用户预设根;DSH_HOME 缺省 ~/.dsh)。 */
|
|
4
4
|
export declare function userPresetsRoot(): string;
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
/** 单条 preset 组合行(官方 PresetDefinition.plugins 行的结构面:cordis entry list)。 */
|
|
6
|
+
export interface PresetPluginRow {
|
|
7
|
+
readonly id?: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly config?: unknown;
|
|
10
|
+
readonly disabled?: unknown;
|
|
11
|
+
}
|
|
12
|
+
/** 注册给宿主 registry 的 preset 定义(官方 PresetDefinition 的结构面)。 */
|
|
13
|
+
export interface PresetDefinitionLike {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly name?: string;
|
|
16
|
+
readonly description?: string;
|
|
17
|
+
readonly order?: number;
|
|
18
|
+
readonly plugins: readonly PresetPluginRow[];
|
|
19
|
+
}
|
|
20
|
+
/** 宿主 agentPresets registry 的结构面(本插件只消费 register)。 */
|
|
21
|
+
export interface PresetRegistryLike {
|
|
22
|
+
register(definition: PresetDefinitionLike): Promise<() => Promise<void>>;
|
|
23
|
+
}
|
|
24
|
+
/** 安装期 ctx 的最小面(只需可选 get)。 */
|
|
25
|
+
export interface PresetCtxLike {
|
|
26
|
+
get?(name: string): unknown;
|
|
27
|
+
}
|
|
28
|
+
/** 解析包内单个 preset 的组合与元数据为宿主 registry 定义;组合缺失返回 undefined。 */
|
|
29
|
+
export declare function readPresetDefinition(pkgDir: string, id: string): PresetDefinitionLike | undefined;
|
|
30
|
+
/** 路径二:运行时注册全部包内 preset;返回注册成功/失败 id 列表与成功项的撤销句柄(组合缺失的 id 不计)。 */
|
|
31
|
+
export declare function registerRolePresets(registry: PresetRegistryLike, ids: readonly string[], pkgDir?: string): Promise<{
|
|
32
|
+
registered: string[];
|
|
33
|
+
failed: string[];
|
|
34
|
+
disposers: Array<() => Promise<void>>;
|
|
35
|
+
}>;
|
|
36
|
+
/** 安装结果(如实区分两条路径,不假阳性)。 */
|
|
37
|
+
export interface RolePresetInstallReport {
|
|
38
|
+
/** 目录写成功的 preset id(路径一)。 */
|
|
39
|
+
dirWritten: string[];
|
|
40
|
+
/** 运行时注册成功的 preset id(路径二;服务不可用时为空)。 */
|
|
41
|
+
runtimeRegistered: string[];
|
|
42
|
+
/** 运行时注册失败的 preset id。 */
|
|
43
|
+
runtimeFailed: string[];
|
|
44
|
+
/** 路径二实际形态:runtime=已注册;unavailable=宿主无 agentPresets 服务(仅目录写生效)。 */
|
|
45
|
+
runtimeMode: 'runtime' | 'unavailable';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 双路安装入口。目录写同步完成并即时留痕;运行时注册等服务就绪后完成并留痕。
|
|
49
|
+
* 返回前目录写已落盘;注册部分异步收敛(fire-and-forget 调用方须 catch)。
|
|
50
|
+
*/
|
|
51
|
+
export declare function installRolePresets(ctx: PresetCtxLike, opts?: {
|
|
52
|
+
registerWaitMs?: number;
|
|
53
|
+
}): Promise<RolePresetInstallReport>;
|
|
9
54
|
/** 角色 preset 挂载的容器面(dsh-agent-presets roster 的最小结构类型)。 */
|
|
10
55
|
export interface PresetMountLike {
|
|
11
56
|
mount(ctx: unknown, id: string): Promise<unknown>;
|