@steerable/agent-shell 0.6.27 → 0.6.29
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/contracts/tool-contract.json +8 -0
- package/dist/local-backend/agent-capability.d.ts +4 -5
- package/dist/local-backend/agent-capability.js +3 -4
- package/dist/local-backend/ai-suggestions.d.ts +11 -29
- package/dist/local-backend/ai-suggestions.js +73 -277
- package/dist/local-backend/mention-targets.d.ts +41 -0
- package/dist/local-backend/mention-targets.js +88 -0
- package/dist/local-backend/router.d.ts +7 -9
- package/dist/local-backend/router.js +133 -88
- package/dist/local-backend/skills/00-identity/SKILL.md +11 -0
- package/dist/local-backend/subagent-profiles.d.ts +88 -1
- package/dist/local-backend/subagent-profiles.js +180 -4
- package/dist/local-backend/turn-timeline.d.ts +12 -2
- package/dist/local-backend/turn-timeline.js +31 -3
- package/dist/sidecar/supervisor.d.ts +8 -0
- package/dist/sidecar/supervisor.js +88 -30
- package/dist/sidecar/types.d.ts +19 -1
- package/package.json +4 -4
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@提及` → 被点名的智能体 id。
|
|
3
|
+
*
|
|
4
|
+
* 渲染层从 `@` 菜单点选时会把 id 放进 `metadata.mentionedAgentIds`;手打
|
|
5
|
+
* 的 `@名字` 不带 id。只认 id 的话,手打提及在后端等于没提及:不下发子代理
|
|
6
|
+
* 画像、不注入强制派发指令,而 Web 顶栏又按正文扫 `@词` 画徽章——界面显示
|
|
7
|
+
* 点了三个人,实际一个都没派活。这里把正文解析补在后端,两侧同一口径。
|
|
8
|
+
*
|
|
9
|
+
* 中文没有词边界,所以按「最长画像名/slug 前缀」匹配,而不是按空白切词:
|
|
10
|
+
* `@日程规划,帮我…` 里的标点不会被吃进名字。
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* 请求体里显式带来的提及 id(`@` 菜单点选的结果)。
|
|
14
|
+
*
|
|
15
|
+
* @param payload 发送请求体。
|
|
16
|
+
* @returns 去重后的 id 列表,保持 `mentionedAgentId` 优先的原有顺序。
|
|
17
|
+
*/
|
|
18
|
+
export function collectExplicitMentionIds(payload) {
|
|
19
|
+
const listed = Array.isArray(payload.mentionedAgentIds)
|
|
20
|
+
? payload.mentionedAgentIds.filter((item) => typeof item === 'string' && item.length > 0)
|
|
21
|
+
: [];
|
|
22
|
+
const single = typeof payload.mentionedAgentId === 'string' && payload.mentionedAgentId
|
|
23
|
+
? [payload.mentionedAgentId]
|
|
24
|
+
: [];
|
|
25
|
+
return dedupe([...single, ...listed]);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 从消息正文里解析 `@名字` / `@slug`。
|
|
29
|
+
*
|
|
30
|
+
* @param text 用户消息正文。
|
|
31
|
+
* @param agents 本会话可用的智能体。
|
|
32
|
+
* @returns 命中的智能体 id,按正文出现顺序去重。
|
|
33
|
+
*/
|
|
34
|
+
export function mentionedAgentIdsFromText(text, agents) {
|
|
35
|
+
if (!text.includes('@'))
|
|
36
|
+
return [];
|
|
37
|
+
const labels = mentionLabels(agents);
|
|
38
|
+
if (labels.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
const ids = [];
|
|
41
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
42
|
+
if (text[i] !== '@')
|
|
43
|
+
continue;
|
|
44
|
+
// 只在行首/空白后开门:邮箱地址与 `a@b` 这类文本不是提及。
|
|
45
|
+
const prev = i === 0 ? '' : text[i - 1];
|
|
46
|
+
if (prev && !/\s/.test(prev))
|
|
47
|
+
continue;
|
|
48
|
+
const rest = text.slice(i + 1);
|
|
49
|
+
const matched = labels.find((entry) => rest.slice(0, entry.label.length).toLowerCase() === entry.label.toLowerCase());
|
|
50
|
+
if (!matched)
|
|
51
|
+
continue;
|
|
52
|
+
ids.push(matched.id);
|
|
53
|
+
i += matched.label.length;
|
|
54
|
+
}
|
|
55
|
+
return dedupe(ids);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 本轮生效的提及 id:显式 id 优先,正文解析补齐。
|
|
59
|
+
*
|
|
60
|
+
* @param payload 发送请求体。
|
|
61
|
+
* @param text 用户消息正文。
|
|
62
|
+
* @param agents 本会话可用的智能体。
|
|
63
|
+
* @returns 去重后的 id 列表。
|
|
64
|
+
*/
|
|
65
|
+
export function resolveMentionedAgentIds(payload, text, agents) {
|
|
66
|
+
const explicit = collectExplicitMentionIds(payload);
|
|
67
|
+
const known = new Set(agents.map((agent) => agent.id));
|
|
68
|
+
return dedupe([
|
|
69
|
+
...explicit,
|
|
70
|
+
...mentionedAgentIdsFromText(text, agents).filter((id) => known.has(id)),
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
/** 画像名与 slug 合成匹配表,长名在前——`@调研员组` 不该命中 `调研员`。 */
|
|
74
|
+
function mentionLabels(agents) {
|
|
75
|
+
const labels = [];
|
|
76
|
+
for (const agent of agents) {
|
|
77
|
+
const name = agent.name.trim();
|
|
78
|
+
const slug = agent.slug?.trim() ?? '';
|
|
79
|
+
if (name)
|
|
80
|
+
labels.push({ label: name, id: agent.id });
|
|
81
|
+
if (slug && slug !== name)
|
|
82
|
+
labels.push({ label: slug, id: agent.id });
|
|
83
|
+
}
|
|
84
|
+
return labels.sort((a, b) => b.label.length - a.label.length);
|
|
85
|
+
}
|
|
86
|
+
function dedupe(ids) {
|
|
87
|
+
return ids.filter((id, index, all) => all.indexOf(id) === index);
|
|
88
|
+
}
|
|
@@ -110,17 +110,17 @@ export declare class LocalBackendRouter {
|
|
|
110
110
|
/**
|
|
111
111
|
* 解析本轮生效的智能体:人设前言、技能勾选、工具策略同源于这一次解析。
|
|
112
112
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
113
|
+
* 父代理 = payload.agentId(专家下拉)> chat.agentId。`@` 提及只进
|
|
114
|
+
* delegates,不再顶替人设。点到父自己也照样进 delegates:`@` 的语义是
|
|
115
|
+
* 「起一个子代理」,点自己就是起一个自己的独立副本,父代理仍是拆分与
|
|
116
|
+
* 汇总的那一个。
|
|
117
117
|
*
|
|
118
118
|
* 必须在拼装工具列表之前调用:`toolPolicy` 决定每轮 `turnTools`,
|
|
119
119
|
* 而工具列表又反过来决定技能的触发条件。
|
|
120
120
|
*
|
|
121
121
|
* @param chatId 当前对话 id。
|
|
122
122
|
* @param payload 前端提交的流请求体。
|
|
123
|
-
* @returns
|
|
123
|
+
* @returns 父代理、委派对象、人设前言与父的能力面。
|
|
124
124
|
*/
|
|
125
125
|
private resolveTurnAgents;
|
|
126
126
|
private buildConversationMessages;
|
|
@@ -174,13 +174,11 @@ export declare class LocalBackendRouter {
|
|
|
174
174
|
*/
|
|
175
175
|
private handleCoreLoopTurn;
|
|
176
176
|
/**
|
|
177
|
-
*
|
|
178
|
-
|
|
177
|
+
* 回合结束后生成下一轮用户输入建议。只走一次 LLM 判断
|
|
178
|
+
*(来源:`[next_steps]` 或最后一段),只广播一次最终结果。
|
|
179
179
|
* 走 broadcast 而不是 SSE:`[DONE]` 之后渲染端已经不再监听这条流。
|
|
180
180
|
*/
|
|
181
181
|
private runSuggestedRepliesInBackground;
|
|
182
|
-
/** 用户/工作区技能正文,供追问建议抽出「下一步」。内置技能不参与。 */
|
|
183
|
-
private loadUserSkillSuggestionTexts;
|
|
184
182
|
private publishSuggestedReplies;
|
|
185
183
|
/**
|
|
186
184
|
* 标准"匿名事件" SSE:不带 event 行,前端 SSEParser 会落到默认 onMessage 分支,
|
|
@@ -8,8 +8,10 @@ import { llmService, getSidecarSupervisor, whenSidecarSupervisor } from '../llm/
|
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
import { buildWorldState, getActiveCoreLoopStreamId, streamCoreLoopTurn, } from './coreloop-stream.js';
|
|
10
10
|
import { driveWithAutoContinue, resolveAutoContinueMax, } from './auto-continue-helper.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { buildDelegateDispatchInstruction, buildMentionDelegateRoster, mergeTurnSubagentParam, } from './subagent-profiles.js';
|
|
12
|
+
import { resolveMentionedAgentIds } from './mention-targets.js';
|
|
13
|
+
import { readSidecarHistoryEntries, timelineFromHistoryEntries, } from './task-process.js';
|
|
14
|
+
import { appendTimelineDelta, freezeTimelineReasoning, sealLastTimelineBlock, syncTimelineTools, } from './turn-timeline.js';
|
|
13
15
|
import { turnDurationMs } from './turn-duration.js';
|
|
14
16
|
import { DEFAULT_SYSTEM_PROMPT, telemetryEnabled } from '../storage/index.js';
|
|
15
17
|
import { isSkillPinned, isToolAllowed, mergeAgentCapabilities, normalizeToolPolicy, resolveSkillExcludes, } from './agent-capability.js';
|
|
@@ -27,7 +29,7 @@ import { parseUserMessageTriggers } from './message-triggers.js';
|
|
|
27
29
|
import { findSkill, loadSkills, classifySkillOrigin, listSkillRoots } from './skill-loader.js';
|
|
28
30
|
import { installSkillFromDirectory } from './skill-install.js';
|
|
29
31
|
import { generateChatTitle } from './ai-title.js';
|
|
30
|
-
import {
|
|
32
|
+
import { generateSuggestedReplies } from './ai-suggestions.js';
|
|
31
33
|
import { detectDeferredExecution } from './deferred-detector.js';
|
|
32
34
|
import { formatHistoryForSummary, truncateMiddle, } from './context-compactor.js';
|
|
33
35
|
import { planRegenerateTruncate, resolveRegenerateContext, resolveRegenerateForkOrdinal, } from './regenerate-helper.js';
|
|
@@ -124,6 +126,11 @@ function buildLocalApiUser() {
|
|
|
124
126
|
}
|
|
125
127
|
const LOCAL_USER = LOCAL_USER_BASE;
|
|
126
128
|
const LOCAL_AGENT_ID = 'local-agent';
|
|
129
|
+
function isRoundBoundaryHook(notice) {
|
|
130
|
+
if (notice?.kind !== 'hook_action')
|
|
131
|
+
return false;
|
|
132
|
+
return notice.action === 'retry' || notice.action === 'narrate';
|
|
133
|
+
}
|
|
127
134
|
function buildLocalAgent() {
|
|
128
135
|
const now = new Date().toISOString();
|
|
129
136
|
return {
|
|
@@ -148,6 +155,24 @@ function buildLocalAgent() {
|
|
|
148
155
|
updatedAt: now,
|
|
149
156
|
};
|
|
150
157
|
}
|
|
158
|
+
function parseMessageMetadata(raw) {
|
|
159
|
+
if (!raw)
|
|
160
|
+
return null;
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(raw);
|
|
163
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
164
|
+
return parsed;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
function persistedAgentIdFromMetadata(raw) {
|
|
173
|
+
const agentId = parseMessageMetadata(raw)?.agentId;
|
|
174
|
+
return typeof agentId === 'string' && agentId ? agentId : undefined;
|
|
175
|
+
}
|
|
151
176
|
export class LocalBackendRouter {
|
|
152
177
|
toolRouter;
|
|
153
178
|
defaultStore;
|
|
@@ -508,6 +533,20 @@ export class LocalBackendRouter {
|
|
|
508
533
|
},
|
|
509
534
|
};
|
|
510
535
|
}
|
|
536
|
+
// 子代理(delegate_subagent)的推理过程:子回合写自己的 durable record
|
|
537
|
+
// (`<父 record>:child:<lineage id>`,随 child_spawned 上报),这里按
|
|
538
|
+
// record 直读重建时间线——与后台任务的过程面板同一套渲染。
|
|
539
|
+
if (method === 'GET' && pathname === '/api/v2/child-process') {
|
|
540
|
+
const recordId = url.searchParams.get('recordId')?.trim();
|
|
541
|
+
if (!recordId) {
|
|
542
|
+
return { status: 400, data: { detail: 'recordId is required' } };
|
|
543
|
+
}
|
|
544
|
+
const entries = readSidecarHistoryEntries(recordId);
|
|
545
|
+
return {
|
|
546
|
+
status: 200,
|
|
547
|
+
data: { recordId, timeline: timelineFromHistoryEntries(entries) },
|
|
548
|
+
};
|
|
549
|
+
}
|
|
511
550
|
// 4.6c Task×Worktree:合并到主仓 / 丢弃。两个操作都是幂等目标态
|
|
512
551
|
// (worktreeState 非 pending 时 TaskService 抛错 → 409)。
|
|
513
552
|
const taskActionMatch = pathname.match(/^\/api\/v2\/tasks\/([^/]+)\/(merge|discard)$/);
|
|
@@ -666,14 +705,18 @@ export class LocalBackendRouter {
|
|
|
666
705
|
const chatId = messageListMatch[1];
|
|
667
706
|
const limit = parsePositiveIntParam(url.searchParams.get('limit'), 200);
|
|
668
707
|
const records = await this.store.listMessages(chatId, limit);
|
|
669
|
-
const messages = records.map(item =>
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
708
|
+
const messages = records.map(item => {
|
|
709
|
+
const agentId = persistedAgentIdFromMetadata(item.messageMetadata);
|
|
710
|
+
return {
|
|
711
|
+
id: item.id,
|
|
712
|
+
chatId: item.chatId,
|
|
713
|
+
role: item.role === 'tool' ? 'assistant' : item.role,
|
|
714
|
+
content: item.content,
|
|
715
|
+
createdAt: item.createdAt,
|
|
716
|
+
messageMetadata: item.messageMetadata,
|
|
717
|
+
...(agentId ? { agentId } : {}),
|
|
718
|
+
};
|
|
719
|
+
});
|
|
677
720
|
// W7-1: 崩溃/强杀中断的 turn 没有 completionStatus 落库——签名是
|
|
678
721
|
// settings_kv 里残留的 turn_active 标记(turn 开始前写、回复落库后
|
|
679
722
|
// 才清)。cancelled/failed 都是活进程写下的终态,标记已清,不在此列。
|
|
@@ -1944,8 +1987,14 @@ export class LocalBackendRouter {
|
|
|
1944
1987
|
// likewise appends nothing: the crashed turn's user message is already
|
|
1945
1988
|
// persisted (and already in the durable record).
|
|
1946
1989
|
let currentUserMessageId;
|
|
1990
|
+
// 本轮被点名的智能体:菜单点选带来的 id 优先,手打的 `@名字` 从正文
|
|
1991
|
+
// 解析补齐(两者缺一,提及就在后端消失,只剩前端徽章)。
|
|
1992
|
+
const mentionedAgentIds = resolveMentionedAgentIds(payload, userMessageText, await this.store.listChatAgents());
|
|
1947
1993
|
if (!regenerateMatch && !isResume) {
|
|
1948
|
-
const
|
|
1994
|
+
const userMeta = mentionedAgentIds.length > 0
|
|
1995
|
+
? JSON.stringify({ mentionedAgentIds })
|
|
1996
|
+
: null;
|
|
1997
|
+
const userMessage = await this.store.addMessage(chatId, 'user', userMessageText, userMeta);
|
|
1949
1998
|
currentUserMessageId = userMessage.id;
|
|
1950
1999
|
emit(this.sseData({ type: 'user_message', message: userMessage }));
|
|
1951
2000
|
}
|
|
@@ -1954,7 +2003,7 @@ export class LocalBackendRouter {
|
|
|
1954
2003
|
const chatMode = payload.mode === 'plan' ? 'plan' : 'agent';
|
|
1955
2004
|
// 本轮生效的智能体:人设、技能勾选、工具策略同源。必须先解析——工具
|
|
1956
2005
|
// 策略决定工具列表,工具列表又决定技能的触发条件。
|
|
1957
|
-
const turnAgents = await this.resolveTurnAgents(chatId, payload);
|
|
2006
|
+
const turnAgents = await this.resolveTurnAgents(chatId, payload, mentionedAgentIds);
|
|
1958
2007
|
// Wave 2 工具分层:模型可见列表只出 direct 层(内置工具 + tool_search
|
|
1959
2008
|
// 发现缝);MCP 动态工具全在 deferred 层,经 tool_search 命中即调。
|
|
1960
2009
|
// 附带收益:MCP 目录增删不再改动系统提示词里的工具名录,前缀更稳。
|
|
@@ -1998,7 +2047,8 @@ export class LocalBackendRouter {
|
|
|
1998
2047
|
available: turnTools.some((t) => t.name === token),
|
|
1999
2048
|
};
|
|
2000
2049
|
}
|
|
2001
|
-
const
|
|
2050
|
+
const mentionRoster = await buildMentionDelegateRoster(turnAgents.delegates, turnTools.map((tool) => tool.name));
|
|
2051
|
+
const { systemPrompt, messages, skillContext } = await this.buildConversationMessages(chatId, cleanUserMessageText, payload, turnTools, turnAgents, forcedSkillName, chatMode, forcedMcpTool, currentUserMessageId, mentionRoster);
|
|
2002
2052
|
// A4: the sidecar-hosted CoreLoop is the only chat path (the TS loop
|
|
2003
2053
|
// was deleted 2026-08-26 after default-on + canary verification). Tools
|
|
2004
2054
|
// round-trip back to this process over the reverse channel. If the
|
|
@@ -2021,6 +2071,8 @@ export class LocalBackendRouter {
|
|
|
2021
2071
|
shouldGenerateTitle,
|
|
2022
2072
|
firstUserMessageForTitle,
|
|
2023
2073
|
resume: isResume,
|
|
2074
|
+
subagent: mergeTurnSubagentParam(Object.fromEntries(mentionRoster.map((row) => [row.profileName, row.profile]))),
|
|
2075
|
+
parentAgentId: turnAgents.parent?.id ?? null,
|
|
2024
2076
|
});
|
|
2025
2077
|
}
|
|
2026
2078
|
/**
|
|
@@ -2123,48 +2175,38 @@ export class LocalBackendRouter {
|
|
|
2123
2175
|
/**
|
|
2124
2176
|
* 解析本轮生效的智能体:人设前言、技能勾选、工具策略同源于这一次解析。
|
|
2125
2177
|
*
|
|
2126
|
-
*
|
|
2127
|
-
*
|
|
2128
|
-
*
|
|
2129
|
-
*
|
|
2178
|
+
* 父代理 = payload.agentId(专家下拉)> chat.agentId。`@` 提及只进
|
|
2179
|
+
* delegates,不再顶替人设。点到父自己也照样进 delegates:`@` 的语义是
|
|
2180
|
+
* 「起一个子代理」,点自己就是起一个自己的独立副本,父代理仍是拆分与
|
|
2181
|
+
* 汇总的那一个。
|
|
2130
2182
|
*
|
|
2131
2183
|
* 必须在拼装工具列表之前调用:`toolPolicy` 决定每轮 `turnTools`,
|
|
2132
2184
|
* 而工具列表又反过来决定技能的触发条件。
|
|
2133
2185
|
*
|
|
2134
2186
|
* @param chatId 当前对话 id。
|
|
2135
2187
|
* @param payload 前端提交的流请求体。
|
|
2136
|
-
* @returns
|
|
2188
|
+
* @returns 父代理、委派对象、人设前言与父的能力面。
|
|
2137
2189
|
*/
|
|
2138
|
-
async resolveTurnAgents(chatId, payload) {
|
|
2139
|
-
const
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
const
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
const fallbackId = (typeof payload.agentId === 'string' && payload.agentId) ||
|
|
2150
|
-
(await this.store.getChat(chatId))?.agentId ||
|
|
2151
|
-
null;
|
|
2152
|
-
if (fallbackId)
|
|
2153
|
-
orderedAgentIds.push(fallbackId);
|
|
2154
|
-
}
|
|
2155
|
-
const agents = (await Promise.all(orderedAgentIds.map((id) => this.store.getChatAgent(id))))
|
|
2156
|
-
.filter((agent) => Boolean(agent));
|
|
2190
|
+
async resolveTurnAgents(chatId, payload, mentionedAgentIds) {
|
|
2191
|
+
const parentId = (typeof payload.agentId === 'string' && payload.agentId) ||
|
|
2192
|
+
(await this.store.getChat(chatId))?.agentId ||
|
|
2193
|
+
null;
|
|
2194
|
+
const parent = parentId ? await this.store.getChatAgent(parentId) : null;
|
|
2195
|
+
const delegates = [];
|
|
2196
|
+
for (const id of mentionedAgentIds) {
|
|
2197
|
+
const agent = await this.store.getChatAgent(id);
|
|
2198
|
+
if (agent)
|
|
2199
|
+
delegates.push(agent);
|
|
2200
|
+
}
|
|
2157
2201
|
return {
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
identityName: agents[0]?.name.trim() || null,
|
|
2164
|
-
capability: mergeAgentCapabilities(agents),
|
|
2202
|
+
parent,
|
|
2203
|
+
delegates,
|
|
2204
|
+
personaAgents: parent?.rolePrompt ? [parent] : [],
|
|
2205
|
+
identityName: parent?.name.trim() || null,
|
|
2206
|
+
capability: mergeAgentCapabilities(parent ? [parent] : []),
|
|
2165
2207
|
};
|
|
2166
2208
|
}
|
|
2167
|
-
async buildConversationMessages(chatId, latestUserMessage, payload, turnTools, turnAgents, forcedSkillName, chatMode = 'agent', forcedMcpTool, currentUserMessageId) {
|
|
2209
|
+
async buildConversationMessages(chatId, latestUserMessage, payload, turnTools, turnAgents, forcedSkillName, chatMode = 'agent', forcedMcpTool, currentUserMessageId, mentionRoster = []) {
|
|
2168
2210
|
// 跨轮压缩由框架 CoreLoop 拥有:token 压力触发 CompactionHooks(已接真实
|
|
2169
2211
|
// summarizer),压缩边界持久化到 durable record,W6-10 透视 reconcile 保证
|
|
2170
2212
|
// 压缩跨轮存活。桌面把全量原始历史作为种子发给框架——不再维护桌面侧滚动
|
|
@@ -2365,6 +2407,14 @@ export class LocalBackendRouter {
|
|
|
2365
2407
|
if (finalUserImages.notes.length > 0) {
|
|
2366
2408
|
finalUserContent = `${finalUserContent}\n\n【附件图片】\n${finalUserImages.notes.join('\n')}`;
|
|
2367
2409
|
}
|
|
2410
|
+
if (mentionRoster.length > 0) {
|
|
2411
|
+
finalUserContent = `${finalUserContent}\n\n${buildDelegateDispatchInstruction(mentionRoster.map((row) => ({
|
|
2412
|
+
name: row.name,
|
|
2413
|
+
profileName: row.profileName,
|
|
2414
|
+
toolFilter: row.profile.toolFilter,
|
|
2415
|
+
isSelf: row.agentId === turnAgents.parent?.id,
|
|
2416
|
+
})))}`;
|
|
2417
|
+
}
|
|
2368
2418
|
return {
|
|
2369
2419
|
// W2.8.2: the system prompt travels as a typed fragment param (cap
|
|
2370
2420
|
// enforced sidecar-side), not as a leading system message.
|
|
@@ -2766,7 +2816,7 @@ export class LocalBackendRouter {
|
|
|
2766
2816
|
* the reverse-channel handler registered in main.ts.
|
|
2767
2817
|
*/
|
|
2768
2818
|
async handleCoreLoopTurn(args) {
|
|
2769
|
-
const { chatId, systemPrompt, messages, skillContext, turnTools, toolPolicy, chatMode, payload, emit, signal, shouldGenerateTitle, firstUserMessageForTitle, resume, } = args;
|
|
2819
|
+
const { chatId, systemPrompt, messages, skillContext, turnTools, toolPolicy, chatMode, payload, emit, signal, shouldGenerateTitle, firstUserMessageForTitle, resume, subagent, parentAgentId, } = args;
|
|
2770
2820
|
const supervisor = getSidecarSupervisor();
|
|
2771
2821
|
if (!supervisor) {
|
|
2772
2822
|
emit(this.sse('error', { message: 'coreloop enabled but sidecar is not running' }));
|
|
@@ -2931,7 +2981,7 @@ export class LocalBackendRouter {
|
|
|
2931
2981
|
// 带工具域/轮次/并发/系统提示,模型按画像 description 选委派
|
|
2932
2982
|
// 对象;per-profile model 留给设置面。子代理作为 AgentPool 池化
|
|
2933
2983
|
// 子运行执行,生命周期经 onChildEvent → SSE 进 UI 编排卡片。
|
|
2934
|
-
subagent
|
|
2984
|
+
subagent,
|
|
2935
2985
|
// 六件套(agent_spawn/send/wait/close/list/interrupt)收缩为
|
|
2936
2986
|
// opt-in 高级模式:STEERABLE_ORCHESTRATION=1 开启;开启后与
|
|
2937
2987
|
// delegate 共享同一 AgentPool(同一 maxParallel 预算)。
|
|
@@ -3003,13 +3053,39 @@ export class LocalBackendRouter {
|
|
|
3003
3053
|
budget: { kind: budgetKind },
|
|
3004
3054
|
message: budgetKind ? `budget_exhausted: ${budgetKind}` : 'budget_exhausted',
|
|
3005
3055
|
}));
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
3058
|
+
// 轮次边界:封住当前思考/文本段,避免下一轮 reasoning delta
|
|
3059
|
+
// 拼进上一段,把中间的工具结果挤没。
|
|
3060
|
+
if (kind === 'round_end' || isRoundBoundaryHook(notice)) {
|
|
3061
|
+
sealLastTimelineBlock(timeline);
|
|
3062
|
+
emit(this.sseData({
|
|
3063
|
+
type: 'completion',
|
|
3064
|
+
status: 'executing',
|
|
3065
|
+
...(typeof notice?.round === 'number' ? { round: notice.round } : {}),
|
|
3066
|
+
}));
|
|
3006
3067
|
}
|
|
3007
3068
|
},
|
|
3008
3069
|
onChildEvent: (event) => {
|
|
3009
3070
|
// P3.1: forward child-agent lifecycle to the renderer; it renders
|
|
3010
3071
|
// the orchestration card (spawn → running → terminal status).
|
|
3011
|
-
|
|
3012
|
-
|
|
3072
|
+
// Narrowed to the lifecycle fields: the sidecar's `agent.child`
|
|
3073
|
+
// notification also carries its per-stream `streamId`, which has no
|
|
3074
|
+
// meaning once the stream ends and must not reach the persisted
|
|
3075
|
+
// assistant metadata below.
|
|
3076
|
+
const lifecycle = {
|
|
3077
|
+
kind: event.kind,
|
|
3078
|
+
childId: event.childId,
|
|
3079
|
+
...(event.task !== undefined ? { task: event.task } : {}),
|
|
3080
|
+
...(event.depth !== undefined ? { depth: event.depth } : {}),
|
|
3081
|
+
...(event.status !== undefined ? { status: event.status } : {}),
|
|
3082
|
+
...(event.error !== undefined ? { error: event.error } : {}),
|
|
3083
|
+
...(event.profile !== undefined ? { profile: event.profile } : {}),
|
|
3084
|
+
// 子代理自己的 durable record:右侧过程面板据此回看它的推理与工具。
|
|
3085
|
+
...(event.recordId !== undefined ? { recordId: event.recordId } : {}),
|
|
3086
|
+
};
|
|
3087
|
+
children.push(lifecycle);
|
|
3088
|
+
emit(this.sseData({ type: 'orchestration_child', ...lifecycle }));
|
|
3013
3089
|
},
|
|
3014
3090
|
};
|
|
3015
3091
|
// 自动续跑:`budget_exhausted` 是「被预算墙截停」而不是「模型认为做
|
|
@@ -3053,6 +3129,8 @@ export class LocalBackendRouter {
|
|
|
3053
3129
|
}
|
|
3054
3130
|
},
|
|
3055
3131
|
onContinuation: (pass, max) => {
|
|
3132
|
+
sealLastTimelineBlock(timeline);
|
|
3133
|
+
emit(this.sseData({ type: 'completion', status: 'executing' }));
|
|
3056
3134
|
console.log('[local-backend] coreloop budget exhausted — auto-continuing', {
|
|
3057
3135
|
chatId,
|
|
3058
3136
|
pass,
|
|
@@ -3104,6 +3182,7 @@ export class LocalBackendRouter {
|
|
|
3104
3182
|
catch (turnFilesErr) {
|
|
3105
3183
|
console.warn('[local-backend] collect turn files failed', turnFilesErr);
|
|
3106
3184
|
}
|
|
3185
|
+
freezeTimelineReasoning(timeline);
|
|
3107
3186
|
const assistant = await this.store.addMessage(chatId, 'assistant', assistantText, JSON.stringify({
|
|
3108
3187
|
completionStatus,
|
|
3109
3188
|
completionReason,
|
|
@@ -3112,6 +3191,8 @@ export class LocalBackendRouter {
|
|
|
3112
3191
|
mode: chatMode,
|
|
3113
3192
|
coreloop: true,
|
|
3114
3193
|
traceId: outcomeTraceId,
|
|
3194
|
+
...(parentAgentId ? { agentId: parentAgentId } : {}),
|
|
3195
|
+
...(children.length > 0 ? { orchestrationChildEvents: children } : {}),
|
|
3115
3196
|
...(durationMs != null ? { durationMs } : {}),
|
|
3116
3197
|
...(autoContinuations > 0 ? { autoContinuations } : {}),
|
|
3117
3198
|
...(turnFiles.length > 0 ? { turnFiles } : {}),
|
|
@@ -3205,8 +3286,8 @@ export class LocalBackendRouter {
|
|
|
3205
3286
|
return { status: 200 };
|
|
3206
3287
|
}
|
|
3207
3288
|
/**
|
|
3208
|
-
*
|
|
3209
|
-
|
|
3289
|
+
* 回合结束后生成下一轮用户输入建议。只走一次 LLM 判断
|
|
3290
|
+
*(来源:`[next_steps]` 或最后一段),只广播一次最终结果。
|
|
3210
3291
|
* 走 broadcast 而不是 SSE:`[DONE]` 之后渲染端已经不再监听这条流。
|
|
3211
3292
|
*/
|
|
3212
3293
|
runSuggestedRepliesInBackground(args) {
|
|
@@ -3215,27 +3296,11 @@ export class LocalBackendRouter {
|
|
|
3215
3296
|
return;
|
|
3216
3297
|
if (!assistantText.trim())
|
|
3217
3298
|
return;
|
|
3218
|
-
const fallback = fallbackSuggestedReplies(userText, assistantText);
|
|
3219
|
-
this.publishSuggestedReplies(chatId, messageId, fallback);
|
|
3220
3299
|
void (async () => {
|
|
3221
3300
|
try {
|
|
3222
|
-
const skillContents = await this.loadUserSkillSuggestionTexts();
|
|
3223
|
-
const enriched = fallbackSuggestedReplies(userText, assistantText, { skillContents });
|
|
3224
|
-
if (enriched.length !== fallback.length ||
|
|
3225
|
-
enriched.some((item, i) => item !== fallback[i])) {
|
|
3226
|
-
this.publishSuggestedReplies(chatId, messageId, enriched);
|
|
3227
|
-
}
|
|
3228
3301
|
const result = await generateSuggestedReplies(userText, assistantText, {
|
|
3229
3302
|
perAttemptTimeoutMs: 60_000,
|
|
3230
|
-
skillContents,
|
|
3231
3303
|
});
|
|
3232
|
-
if (result.usedFallback)
|
|
3233
|
-
return;
|
|
3234
|
-
const baseline = enriched.length > 0 ? enriched : fallback;
|
|
3235
|
-
const same = result.suggestions.length === baseline.length &&
|
|
3236
|
-
result.suggestions.every((item, i) => item === baseline[i]);
|
|
3237
|
-
if (same)
|
|
3238
|
-
return;
|
|
3239
3304
|
this.publishSuggestedReplies(chatId, messageId, result.suggestions);
|
|
3240
3305
|
}
|
|
3241
3306
|
catch (err) {
|
|
@@ -3243,26 +3308,6 @@ export class LocalBackendRouter {
|
|
|
3243
3308
|
}
|
|
3244
3309
|
})();
|
|
3245
3310
|
}
|
|
3246
|
-
/** 用户/工作区技能正文,供追问建议抽出「下一步」。内置技能不参与。 */
|
|
3247
|
-
async loadUserSkillSuggestionTexts() {
|
|
3248
|
-
try {
|
|
3249
|
-
const modules = await loadSkills({ ignoreConditions: true });
|
|
3250
|
-
return modules
|
|
3251
|
-
.filter((module) => classifySkillOrigin(module.skillsDir) !== 'builtin')
|
|
3252
|
-
.map((module) => {
|
|
3253
|
-
const title = (module.displayName || module.name || module.dirName || '').trim();
|
|
3254
|
-
const body = (module.content ?? '').trim();
|
|
3255
|
-
if (!body)
|
|
3256
|
-
return '';
|
|
3257
|
-
return title ? `# ${title}\n${body}` : body;
|
|
3258
|
-
})
|
|
3259
|
-
.filter(Boolean);
|
|
3260
|
-
}
|
|
3261
|
-
catch (err) {
|
|
3262
|
-
console.warn('[local-backend] suggested-replies skill load failed', err);
|
|
3263
|
-
return [];
|
|
3264
|
-
}
|
|
3265
|
-
}
|
|
3266
3311
|
async publishSuggestedReplies(chatId, messageId, suggestions) {
|
|
3267
3312
|
if (suggestions.length === 0)
|
|
3268
3313
|
return;
|
|
@@ -22,6 +22,17 @@ tags: [identity, base]
|
|
|
22
22
|
- 工具失败或无权限时,把错误如实告诉用户,并建议下一步动作。
|
|
23
23
|
- 回复保持简洁;超过 6-8 行的长内容用 markdown 列表或代码块组织。
|
|
24
24
|
|
|
25
|
+
## 下一步建议
|
|
26
|
+
|
|
27
|
+
回合结束且用户还能接着做时,把建议写在回复最后一段,并用标签包起来(标签本身不会显示,只显示里面的内容):
|
|
28
|
+
|
|
29
|
+
[next_steps]
|
|
30
|
+
- 调整封面配色
|
|
31
|
+
- 把第 2 页写具体
|
|
32
|
+
[/next_steps]
|
|
33
|
+
|
|
34
|
+
只写真实的下一步。本轮已完成的汇报(文件位置、页数、内容结构等)不要放进标签。没有下一步就不要写这个标签。
|
|
35
|
+
|
|
25
36
|
## 结构化提问(ask_user)使用规范
|
|
26
37
|
|
|
27
38
|
当你需要向用户收集结构化信息(提供选项让用户选择)时,使用 `ask_user` 工具,并遵守:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 内置 subagent_type 画像集(CC `.claude/agents` parity
|
|
2
|
+
* 内置 subagent_type 画像集(CC `.claude/agents` parity)以及
|
|
3
|
+
* ChatAgent → 提及子代理画像的映射(`@` 委派 W1)。
|
|
3
4
|
*
|
|
4
5
|
* 桌面不再向 sidecar 传裸 `subagent: true`——命名画像让每个委派带
|
|
5
6
|
* 工具域 / 轮次上限 / 并发性 / 系统提示,模型按画像 description 选择
|
|
@@ -11,7 +12,9 @@
|
|
|
11
12
|
* - per-profile model 刻意不设:跟随父模型;模型路由留给设置面。
|
|
12
13
|
* - 画像系统提示是种进子 loop 的第一条 system 消息(子代理看不到
|
|
13
14
|
* 主对话的系统提示,画像提示就是它的全部角色设定)。
|
|
15
|
+
* - W2 落地前 toolFilter 必须与父本轮工具面取交集(子 ⊆ 父)。
|
|
14
16
|
*/
|
|
17
|
+
import { type AgentCapabilityInput, type AgentToolPolicy } from './agent-capability.js';
|
|
15
18
|
export interface BuiltinSubagentProfile {
|
|
16
19
|
toolFilter?: string[];
|
|
17
20
|
maxRounds?: number;
|
|
@@ -19,6 +22,22 @@ export interface BuiltinSubagentProfile {
|
|
|
19
22
|
description: string;
|
|
20
23
|
systemPrompt?: string;
|
|
21
24
|
}
|
|
25
|
+
/** 映射用的智能体子集(身份 + 能力面),不依赖存储层类型。 */
|
|
26
|
+
export interface SubagentAgentInput extends AgentCapabilityInput {
|
|
27
|
+
id: string;
|
|
28
|
+
slug: string | null;
|
|
29
|
+
name: string;
|
|
30
|
+
rolePrompt: string | null;
|
|
31
|
+
description?: string | null;
|
|
32
|
+
}
|
|
33
|
+
/** 一次 `@` 提及转出的画像,连同显示名,供派发指令与 sidecar 参数共用。 */
|
|
34
|
+
export interface MentionDelegateProfile {
|
|
35
|
+
/** 被提及智能体的 id——调用方据此认出自提及那一行。 */
|
|
36
|
+
agentId: string;
|
|
37
|
+
name: string;
|
|
38
|
+
profileName: string;
|
|
39
|
+
profile: BuiltinSubagentProfile;
|
|
40
|
+
}
|
|
22
41
|
export declare const BUILTIN_SUBAGENT_PROFILES: Record<string, BuiltinSubagentProfile>;
|
|
23
42
|
/**
|
|
24
43
|
* chat.stream 的 `subagent` 参数:内置画像集。调用方(普通回合与
|
|
@@ -28,3 +47,71 @@ export declare const BUILTIN_SUBAGENT_PROFILES: Record<string, BuiltinSubagentPr
|
|
|
28
47
|
export declare function builtinSubagentParam(): {
|
|
29
48
|
profiles: Record<string, BuiltinSubagentProfile>;
|
|
30
49
|
};
|
|
50
|
+
/**
|
|
51
|
+
* 画像名:优先稳定 ASCII slug,否则英文名 slugify,再否则 `agent-<id>`。
|
|
52
|
+
*
|
|
53
|
+
* @param agent 被提及的智能体。
|
|
54
|
+
* @returns 可作为 `subagent_type` enum 的画像名。
|
|
55
|
+
*/
|
|
56
|
+
export declare function profileNameForAgent(agent: {
|
|
57
|
+
id: string;
|
|
58
|
+
slug: string | null;
|
|
59
|
+
name: string;
|
|
60
|
+
}): string;
|
|
61
|
+
/**
|
|
62
|
+
* W2 过渡期:子策略放行的工具与父本轮工具面取交集。
|
|
63
|
+
* 交集等于父全集时不设 filter(子继承父的工具域)。
|
|
64
|
+
*
|
|
65
|
+
* @param childPolicy 子智能体自己的工具策略。
|
|
66
|
+
* @param parentToolNames 父本轮已经广告给模型的工具名。
|
|
67
|
+
* @returns 子代理 toolFilter;`undefined` 表示不比父更窄。
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveChildToolFilter(childPolicy: AgentToolPolicy, parentToolNames: readonly string[]): string[] | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* 把本轮被 `@` 的智能体译成 sidecar 画像(含 W1 夹紧)。
|
|
72
|
+
*
|
|
73
|
+
* @param delegates 被 `@` 的智能体,按提及顺序(含被点名的父代理本身)。
|
|
74
|
+
* @param parentToolNames 父本轮模型可见工具名。
|
|
75
|
+
* @returns 画像名 → 画像。
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildMentionSubagentProfiles(delegates: readonly SubagentAgentInput[], parentToolNames: readonly string[]): Promise<Record<string, BuiltinSubagentProfile>>;
|
|
78
|
+
/**
|
|
79
|
+
* 与 {@link buildMentionSubagentProfiles} 同源,额外带显示名供派发指令使用。
|
|
80
|
+
*
|
|
81
|
+
* @param delegates 被 `@` 的智能体,按提及顺序(含被点名的父代理本身)。
|
|
82
|
+
* @param parentToolNames 父本轮模型可见工具名。
|
|
83
|
+
* @returns 提及顺序的画像花名册。
|
|
84
|
+
*/
|
|
85
|
+
export declare function buildMentionDelegateRoster(delegates: readonly SubagentAgentInput[], parentToolNames: readonly string[]): Promise<MentionDelegateProfile[]>;
|
|
86
|
+
/**
|
|
87
|
+
* 注入本轮最后一条用户消息的强制派发指令。
|
|
88
|
+
*
|
|
89
|
+
* 被点名的父代理自己也在名单里(`isSelf`):它要起一个独立副本去跑那份
|
|
90
|
+
* 子任务,而不是顺手自己做——否则 `requiredProfiles` 完成门会一直把回合
|
|
91
|
+
* 退回来。
|
|
92
|
+
*
|
|
93
|
+
* @param delegates 被点名的子代理(显示名 + 画像名 + 实际工具面 + 是否是自己)。
|
|
94
|
+
* @returns 派发指令正文。
|
|
95
|
+
*/
|
|
96
|
+
export declare function buildDelegateDispatchInstruction(delegates: ReadonlyArray<{
|
|
97
|
+
name: string;
|
|
98
|
+
profileName: string;
|
|
99
|
+
toolFilter?: string[];
|
|
100
|
+
isSelf?: boolean;
|
|
101
|
+
}>): string;
|
|
102
|
+
export interface TurnSubagentParam {
|
|
103
|
+
profiles: Record<string, BuiltinSubagentProfile>;
|
|
104
|
+
maxParallel?: number;
|
|
105
|
+
/** 本轮必须收到委派的画像(被 `@` 的智能体);sidecar 据此挂完成门。 */
|
|
106
|
+
requiredProfiles?: string[];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 内置画像 + 本轮提及画像。被 `@` 超过 4 个时抬高池的并行上限。
|
|
110
|
+
*
|
|
111
|
+
* 提及画像同时作为 `requiredProfiles` 下发:强制派发此前只是提示词里的
|
|
112
|
+
* 一句话,模型跑了别的工具再叙述「已启动」就能蒙过所有既有纪律检查。
|
|
113
|
+
*
|
|
114
|
+
* @param mentionProfiles 本轮提及转出的画像;空对象则与 {@link builtinSubagentParam} 相同。
|
|
115
|
+
* @returns 下发给 sidecar 的 `subagent` 参数。
|
|
116
|
+
*/
|
|
117
|
+
export declare function mergeTurnSubagentParam(mentionProfiles: Record<string, BuiltinSubagentProfile>): TurnSubagentParam;
|