@steerable/agent-shell-web 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/package.json +4 -4
- package/src/components/AgentSidebar.tsx +27 -27
- package/src/components/BrandLockup.tsx +20 -4
- package/src/components/ChatHeader.tsx +5 -15
- package/src/components/LocalLlmSettingsModal.tsx +5 -5
- package/src/components/TerminalPanel.tsx +1 -1
- package/src/components/chat/ApprovalModal.tsx +5 -5
- package/src/components/chat/AskUserQuestionMenu.tsx +13 -13
- package/src/components/chat/AssistantMessage.tsx +122 -36
- package/src/components/chat/ChatInput.tsx +34 -26
- package/src/components/chat/ExecPolicyPicker.tsx +4 -4
- package/src/components/chat/ExecutedActionsCard.tsx +398 -29
- package/src/components/chat/FilePathCode.tsx +2 -1
- package/src/components/chat/LocalChatPanel.tsx +42 -11
- package/src/components/chat/Markdown.tsx +15 -14
- package/src/components/chat/MessageList.tsx +23 -5
- package/src/components/chat/ModelPicker.tsx +6 -6
- package/src/components/chat/OrchestrationChildrenCard.tsx +69 -5
- package/src/components/chat/SessionTodoList.tsx +234 -0
- package/src/components/chat/SessionTreeModal.tsx +3 -3
- package/src/components/chat/SuggestedReplies.tsx +14 -5
- package/src/components/chat/TaskPanelModal.tsx +3 -3
- package/src/components/chat/TaskProcessPanel.tsx +67 -21
- package/src/components/chat/TurnFilesCard.tsx +2 -2
- package/src/components/chat/TurnProcessGroup.tsx +252 -99
- package/src/components/chat/UserMessage.tsx +7 -7
- package/src/components/chat/executed-actions-model.ts +469 -0
- package/src/components/chat/next-steps-tags.ts +28 -0
- package/src/components/chat/orchestration-children-model.ts +120 -0
- package/src/components/chat/process-status.ts +125 -53
- package/src/components/chat/todo-list-model.ts +167 -0
- package/src/components/chat/turn-timeline.ts +123 -11
- package/src/components/settings/AgentsSettingsPanel.tsx +2 -2
- package/src/components/settings/AppearanceSettingsPanel.tsx +3 -3
- package/src/components/settings/DiagnoseSettingsPanel.tsx +2 -2
- package/src/components/settings/InsightsSettingsPanel.tsx +4 -4
- package/src/components/settings/LlmSettingsPanel.tsx +10 -10
- package/src/components/settings/McpSettingsPanel.tsx +3 -3
- package/src/components/settings/ModelIdCombobox.tsx +2 -2
- package/src/components/settings/SecuritySettingsPanel.tsx +1 -1
- package/src/components/settings/SettingsSaveButton.tsx +1 -1
- package/src/components/settings/SkillsSettingsPanel.tsx +2 -2
- package/src/components/settings/TelemetrySettingsPanel.tsx +2 -2
- package/src/components/settings/UsagePanel.tsx +14 -10
- package/src/components/settings/WebSearchSettingsPanel.tsx +2 -2
- package/src/layouts/AgentLayout.tsx +3 -3
- package/src/lib/chat-transport.ts +68 -12
- package/src/lib/local-api.ts +11 -0
- package/src/lib/show-thinking-content.ts +4 -4
- package/src/pages/AgentPage.tsx +82 -11
- package/src/pages/ChatPanelPreviewPage.tsx +26 -4
- package/src/pages/SettingsPage.tsx +39 -39
- package/src/styles/index.css +12 -2
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* result_count / status / bytes / truncated 这些小字段原样保留。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { findAgentForProfile } from './orchestration-children-model';
|
|
12
|
+
|
|
11
13
|
interface WebToolResultData {
|
|
12
14
|
result_count?: unknown;
|
|
13
15
|
status?: unknown;
|
|
@@ -121,6 +123,473 @@ export interface ExecutedActionLike {
|
|
|
121
123
|
sandbox?: { backend?: string; enforcement: string };
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
export interface DelegateSubagentView {
|
|
127
|
+
profile: string | null;
|
|
128
|
+
task: string | null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const BUILTIN_DELEGATE_LABELS: Record<string, string> = {
|
|
132
|
+
explore: '探索',
|
|
133
|
+
research: '调研',
|
|
134
|
+
coder: '编程',
|
|
135
|
+
'general-purpose': '通用',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const BUILTIN_DELEGATE_COLORS: Record<string, string> = {
|
|
139
|
+
explore: '#0ea5e9',
|
|
140
|
+
research: '#2563eb',
|
|
141
|
+
coder: '#16a34a',
|
|
142
|
+
'general-purpose': '#7c3aed',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export interface TurnParticipant {
|
|
146
|
+
key: string;
|
|
147
|
+
name: string;
|
|
148
|
+
color: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type AgentRef = { id: string; slug: string | null; name: string; color?: string | null };
|
|
152
|
+
|
|
153
|
+
function participantFromProfile(
|
|
154
|
+
profile: string,
|
|
155
|
+
agents: ReadonlyArray<AgentRef>,
|
|
156
|
+
): TurnParticipant {
|
|
157
|
+
const match = findAgentForProfile(profile, agents);
|
|
158
|
+
return {
|
|
159
|
+
key: match?.id ?? profile,
|
|
160
|
+
name: match?.name ?? BUILTIN_DELEGATE_LABELS[profile] ?? profile,
|
|
161
|
+
color: match?.color || BUILTIN_DELEGATE_COLORS[profile] || '#7c3aed',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 本回合顶栏要画的智能体。用户 `@提及` 了谁就画谁(含父代理若未点名则
|
|
167
|
+
* 仍放最前);没有提及才回落到实际 `delegate_subagent` / 子代理生命周期,
|
|
168
|
+
* 避免内置画像「探索」顶替用户点名的智能助手、日程规划。
|
|
169
|
+
*/
|
|
170
|
+
export function collectTurnParticipants(
|
|
171
|
+
parent: { id: string; name: string; color?: string | null } | null,
|
|
172
|
+
input: {
|
|
173
|
+
agents?: ReadonlyArray<AgentRef>;
|
|
174
|
+
children?: ReadonlyArray<{ profile?: string }>;
|
|
175
|
+
actions?: ReadonlyArray<{ tool: string; arguments?: unknown }>;
|
|
176
|
+
mentionedAgentIds?: string[];
|
|
177
|
+
},
|
|
178
|
+
): TurnParticipant[] {
|
|
179
|
+
const agents = input.agents ?? [];
|
|
180
|
+
const out: TurnParticipant[] = [];
|
|
181
|
+
const seen = new Set<string>();
|
|
182
|
+
const push = (item: TurnParticipant) => {
|
|
183
|
+
if (seen.has(item.key)) return;
|
|
184
|
+
seen.add(item.key);
|
|
185
|
+
out.push(item);
|
|
186
|
+
};
|
|
187
|
+
const pushAgent = (agent: AgentRef) => {
|
|
188
|
+
push({
|
|
189
|
+
key: agent.id,
|
|
190
|
+
name: agent.name,
|
|
191
|
+
color: agent.color || '#7c3aed',
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
const mentioned = (input.mentionedAgentIds ?? []).filter((id) => id.length > 0);
|
|
195
|
+
if (mentioned.length > 0) {
|
|
196
|
+
if (parent && !mentioned.includes(parent.id)) {
|
|
197
|
+
push({
|
|
198
|
+
key: parent.id,
|
|
199
|
+
name: parent.name,
|
|
200
|
+
color: parent.color || '#7c3aed',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
for (const id of mentioned) {
|
|
204
|
+
const agent = agents.find((item) => item.id === id);
|
|
205
|
+
if (agent) pushAgent(agent);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
if (parent) {
|
|
210
|
+
push({
|
|
211
|
+
key: parent.id,
|
|
212
|
+
name: parent.name,
|
|
213
|
+
color: parent.color || '#7c3aed',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
for (const child of input.children ?? []) {
|
|
217
|
+
if (child.profile) push(participantFromProfile(child.profile, agents));
|
|
218
|
+
}
|
|
219
|
+
for (const action of input.actions ?? []) {
|
|
220
|
+
if (action.tool !== 'delegate_subagent') continue;
|
|
221
|
+
const { profile } = parseDelegateSubagent(action.arguments);
|
|
222
|
+
if (profile) push(participantFromProfile(profile, agents));
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const MENTION_TOKEN = /(?<=^|\s)@([^\s@]+)/g;
|
|
228
|
+
|
|
229
|
+
function mentionedIdsFromMetadata(raw: unknown): string[] {
|
|
230
|
+
if (typeof raw !== 'string' || !raw) return [];
|
|
231
|
+
try {
|
|
232
|
+
const meta = JSON.parse(raw) as { mentionedAgentIds?: unknown };
|
|
233
|
+
if (!Array.isArray(meta.mentionedAgentIds)) return [];
|
|
234
|
+
return meta.mentionedAgentIds.filter(
|
|
235
|
+
(item): item is string => typeof item === 'string' && item.length > 0,
|
|
236
|
+
);
|
|
237
|
+
} catch {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** 上一轮用户消息里点名的智能体:优先 metadata,否则按正文 `@名` 扫描。 */
|
|
243
|
+
export function extractMentionedAgentIds(
|
|
244
|
+
user: { content?: string; messageMetadata?: string | null } | null | undefined,
|
|
245
|
+
agents: ReadonlyArray<AgentRef>,
|
|
246
|
+
): string[] {
|
|
247
|
+
if (!user) return [];
|
|
248
|
+
const fromMeta = mentionedIdsFromMetadata(user.messageMetadata);
|
|
249
|
+
if (fromMeta.length > 0) {
|
|
250
|
+
const known = new Set(agents.map((agent) => agent.id));
|
|
251
|
+
return fromMeta.filter((id) => known.has(id));
|
|
252
|
+
}
|
|
253
|
+
const content = user.content ?? '';
|
|
254
|
+
const seen = new Set<string>();
|
|
255
|
+
const ids: string[] = [];
|
|
256
|
+
for (const match of content.matchAll(MENTION_TOKEN)) {
|
|
257
|
+
const name = match[1];
|
|
258
|
+
const agent = agents.find((item) => item.name === name || item.slug === name);
|
|
259
|
+
if (!agent || seen.has(agent.id)) continue;
|
|
260
|
+
seen.add(agent.id);
|
|
261
|
+
ids.push(agent.id);
|
|
262
|
+
}
|
|
263
|
+
return ids;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* `delegate_subagent` 的参数投影:画像名 + 自包含任务正文。
|
|
268
|
+
*/
|
|
269
|
+
export function parseDelegateSubagent(args: unknown): DelegateSubagentView {
|
|
270
|
+
if (!args || typeof args !== 'object') return { profile: null, task: null };
|
|
271
|
+
const obj = args as Record<string, unknown>;
|
|
272
|
+
const profile =
|
|
273
|
+
typeof obj.subagent_type === 'string' && obj.subagent_type.trim()
|
|
274
|
+
? obj.subagent_type.trim()
|
|
275
|
+
: null;
|
|
276
|
+
const task =
|
|
277
|
+
typeof obj.task === 'string' && obj.task.trim() ? obj.task.trim() : null;
|
|
278
|
+
return { profile, task };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 把 `subagent_type` 映射成卡片上的显示名:先匹配会话智能体,再退回内置画像中文名,再退回画像原文。
|
|
283
|
+
*/
|
|
284
|
+
export function delegateSubagentDisplayName(
|
|
285
|
+
profile: string | null,
|
|
286
|
+
agents: ReadonlyArray<{ id: string; slug: string | null; name: string }>,
|
|
287
|
+
): string {
|
|
288
|
+
if (!profile) return '子代理';
|
|
289
|
+
const match = findAgentForProfile(profile, agents);
|
|
290
|
+
if (match) return match.name;
|
|
291
|
+
return BUILTIN_DELEGATE_LABELS[profile] ?? profile;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** 任务正文作行摘要;没有任务时返回 null,调用方不再回落 JSON。 */
|
|
295
|
+
export function summarizeDelegateSubagent(args: unknown): string | null {
|
|
296
|
+
const { task } = parseDelegateSubagent(args);
|
|
297
|
+
if (!task) return null;
|
|
298
|
+
return clip(task);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function clip(text: string, max = 60): string {
|
|
302
|
+
return text.length > max ? `${text.slice(0, max - 3)}…` : text;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function asObject(value: unknown): Record<string, unknown> | null {
|
|
306
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
307
|
+
? (value as Record<string, unknown>)
|
|
308
|
+
: null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function stringField(obj: Record<string, unknown> | null, key: string): string | null {
|
|
312
|
+
if (!obj) return null;
|
|
313
|
+
const value = obj[key];
|
|
314
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const ENVELOPE_KEYS = new Set([
|
|
318
|
+
'success',
|
|
319
|
+
'error',
|
|
320
|
+
'message',
|
|
321
|
+
'data',
|
|
322
|
+
'needsFollowup',
|
|
323
|
+
'terminal',
|
|
324
|
+
'nextAction',
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
/** Sidecar `ToolResult` plus JSON 字符串;`fields` 是 data 与顶层业务字段的合并。 */
|
|
328
|
+
export interface ToolEnvelope {
|
|
329
|
+
success: boolean | null;
|
|
330
|
+
error: string | null;
|
|
331
|
+
message: string | null;
|
|
332
|
+
fields: Record<string, unknown>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function parseJsonValue(value: unknown): unknown {
|
|
336
|
+
if (typeof value !== 'string') return value;
|
|
337
|
+
const trimmed = value.trim();
|
|
338
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return value;
|
|
339
|
+
try {
|
|
340
|
+
return JSON.parse(trimmed) as unknown;
|
|
341
|
+
} catch {
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function parseToolEnvelope(result: unknown): ToolEnvelope {
|
|
347
|
+
const parsed = parseJsonValue(result);
|
|
348
|
+
const obj = asObject(parsed);
|
|
349
|
+
if (!obj) {
|
|
350
|
+
return {
|
|
351
|
+
success: null,
|
|
352
|
+
error: null,
|
|
353
|
+
message: typeof parsed === 'string' && parsed.trim() ? parsed.trim() : null,
|
|
354
|
+
fields: {},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const data = asObject(obj.data);
|
|
358
|
+
const fields: Record<string, unknown> = {};
|
|
359
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
360
|
+
if (!ENVELOPE_KEYS.has(key) && value !== undefined) fields[key] = value;
|
|
361
|
+
}
|
|
362
|
+
if (data) {
|
|
363
|
+
for (const [key, value] of Object.entries(data)) {
|
|
364
|
+
if (key === 'durationMs' || value === undefined) continue;
|
|
365
|
+
fields[key] = value;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const dataText = typeof obj.data === 'string' && obj.data.trim() ? obj.data.trim() : null;
|
|
369
|
+
return {
|
|
370
|
+
success: typeof obj.success === 'boolean' ? obj.success : null,
|
|
371
|
+
error: stringField(obj, 'error'),
|
|
372
|
+
message: stringField(obj, 'message') ?? dataText,
|
|
373
|
+
fields,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const DELEGATE_STATUS_ZH: Record<string, string> = {
|
|
378
|
+
budget_exhausted: '额度耗尽',
|
|
379
|
+
failed: '失败',
|
|
380
|
+
cancelled: '已取消',
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
/** 把 sidecar 的英文失败句收成行头/错误栏可读的中文。 */
|
|
384
|
+
export function humanizeDelegateError(error: string | null): string | null {
|
|
385
|
+
if (!error) return null;
|
|
386
|
+
if (error.includes('orchestration_budget_exceeded')) return '编排额度已用尽';
|
|
387
|
+
const ended = error.match(/sub-agent ended with status:\s*(\S+)/i);
|
|
388
|
+
if (ended) {
|
|
389
|
+
const status = ended[1].replace(/[.,;]+$/, '');
|
|
390
|
+
return `子代理因${DELEGATE_STATUS_ZH[status] ?? status}结束`;
|
|
391
|
+
}
|
|
392
|
+
if (error.includes('budget_exhausted')) return '子代理因额度耗尽结束';
|
|
393
|
+
return error;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export interface TaskSnapshotView {
|
|
397
|
+
taskId: string | null;
|
|
398
|
+
status: string | null;
|
|
399
|
+
task: string | null;
|
|
400
|
+
answer: string | null;
|
|
401
|
+
error: string | null;
|
|
402
|
+
worktreeLabel: string | null;
|
|
403
|
+
hint: string | null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function worktreeLabel(obj: Record<string, unknown>): string | null {
|
|
407
|
+
const path = stringField(obj, 'worktreePath');
|
|
408
|
+
if (path) return path;
|
|
409
|
+
const wt = asObject(obj.worktree);
|
|
410
|
+
if (!wt) return null;
|
|
411
|
+
const wtPath = stringField(wt, 'path');
|
|
412
|
+
const branch = stringField(wt, 'branch');
|
|
413
|
+
if (wtPath && branch) return `${wtPath} · ${branch}`;
|
|
414
|
+
return wtPath ?? branch;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function snapshotFromRecord(obj: Record<string, unknown>): TaskSnapshotView {
|
|
418
|
+
return {
|
|
419
|
+
taskId: stringField(obj, 'taskId'),
|
|
420
|
+
status: stringField(obj, 'status'),
|
|
421
|
+
task: stringField(obj, 'task'),
|
|
422
|
+
answer: stringField(obj, 'answer') ?? stringField(obj, 'answerPreview'),
|
|
423
|
+
error: stringField(obj, 'error'),
|
|
424
|
+
worktreeLabel: worktreeLabel(obj),
|
|
425
|
+
hint: stringField(obj, 'hint'),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function snapshotUseful(view: TaskSnapshotView): boolean {
|
|
430
|
+
return Boolean(
|
|
431
|
+
view.taskId || view.status || view.task || view.answer || view.worktreeLabel || view.hint,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** `task_*` 工具结果:兼容扁平返回和 CoreLoop `{success, data:{task}}`。 */
|
|
436
|
+
export function parseTaskToolOutput(result: unknown): {
|
|
437
|
+
items: TaskSnapshotView[];
|
|
438
|
+
hint: string | null;
|
|
439
|
+
error: string | null;
|
|
440
|
+
} {
|
|
441
|
+
const env = parseToolEnvelope(result);
|
|
442
|
+
const nested = asObject(env.fields.task);
|
|
443
|
+
const items: TaskSnapshotView[] = [];
|
|
444
|
+
if (Array.isArray(env.fields.tasks)) {
|
|
445
|
+
for (const row of env.fields.tasks) {
|
|
446
|
+
const obj = asObject(row);
|
|
447
|
+
if (!obj) continue;
|
|
448
|
+
const view = snapshotFromRecord(obj);
|
|
449
|
+
if (snapshotUseful(view)) items.push(view);
|
|
450
|
+
}
|
|
451
|
+
} else if (nested) {
|
|
452
|
+
const view = snapshotFromRecord(nested);
|
|
453
|
+
if (snapshotUseful(view)) items.push(view);
|
|
454
|
+
} else {
|
|
455
|
+
const view = snapshotFromRecord(env.fields);
|
|
456
|
+
if (snapshotUseful(view)) items.push(view);
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
items,
|
|
460
|
+
hint: stringField(env.fields, 'hint'),
|
|
461
|
+
error: env.error,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** 右侧过程栏的打开目标。终态卡用 `task` 当标题。 */
|
|
466
|
+
export interface InspectTaskInput {
|
|
467
|
+
id: string;
|
|
468
|
+
chatId: string;
|
|
469
|
+
title?: string;
|
|
470
|
+
task?: string;
|
|
471
|
+
/** 子代理过程:按它自己的 durable record 读,而不是任务表。 */
|
|
472
|
+
recordId?: string;
|
|
473
|
+
/** 目标仍在运行——面板据此轮询并显示「正在推理」。 */
|
|
474
|
+
live?: boolean;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function inspectTaskTitle(input: InspectTaskInput): string {
|
|
478
|
+
return input.title ?? input.task ?? '后台任务';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** 工具行能打开右侧「后台推理」时抽出 taskId 和标题。 */
|
|
482
|
+
export function inspectableTaskFromAction(
|
|
483
|
+
tool: string,
|
|
484
|
+
args: unknown,
|
|
485
|
+
result?: unknown,
|
|
486
|
+
): { id: string; title: string } | null {
|
|
487
|
+
if (!isTaskFamilyTool(tool)) return null;
|
|
488
|
+
const parsed = parseTaskToolOutput(result);
|
|
489
|
+
const argId = stringField(asObject(args), 'taskId');
|
|
490
|
+
const id = parsed.items[0]?.taskId ?? argId;
|
|
491
|
+
if (!id) return null;
|
|
492
|
+
const title =
|
|
493
|
+
parseTaskRun(args).task
|
|
494
|
+
?? parsed.items[0]?.task
|
|
495
|
+
?? parsed.items[0]?.hint
|
|
496
|
+
?? '后台任务';
|
|
497
|
+
return { id, title };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export interface TaskRunView {
|
|
501
|
+
task: string | null;
|
|
502
|
+
worktree: boolean;
|
|
503
|
+
worktreeName: string | null;
|
|
504
|
+
dependsOn: string[];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** `task_run` 的参数投影。 */
|
|
508
|
+
export function parseTaskRun(args: unknown): TaskRunView {
|
|
509
|
+
const obj = asObject(args);
|
|
510
|
+
const dependsOn = Array.isArray(obj?.dependsOn)
|
|
511
|
+
? obj.dependsOn.filter((item): item is string => typeof item === 'string' && item.length > 0)
|
|
512
|
+
: [];
|
|
513
|
+
return {
|
|
514
|
+
task: stringField(obj, 'task'),
|
|
515
|
+
worktree: obj?.worktree === true,
|
|
516
|
+
worktreeName: stringField(obj, 'worktreeName'),
|
|
517
|
+
dependsOn,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function shortTaskId(id: string): string {
|
|
522
|
+
return id.slice(0, 8);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export const TASK_FAMILY_TOOLS = [
|
|
526
|
+
'task_run',
|
|
527
|
+
'task_send',
|
|
528
|
+
'task_status',
|
|
529
|
+
'task_result',
|
|
530
|
+
] as const;
|
|
531
|
+
|
|
532
|
+
export type TaskFamilyTool = (typeof TASK_FAMILY_TOOLS)[number];
|
|
533
|
+
|
|
534
|
+
export function isTaskFamilyTool(tool: string): tool is TaskFamilyTool {
|
|
535
|
+
return (TASK_FAMILY_TOOLS as readonly string[]).includes(tool);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** 行头标题:不露出 `task_run` 这类工具原名。 */
|
|
539
|
+
export function taskToolLabel(tool: string, args: unknown): string | null {
|
|
540
|
+
switch (tool) {
|
|
541
|
+
case 'task_run':
|
|
542
|
+
return parseTaskRun(args).worktree ? '后台任务 · 隔离' : '后台任务';
|
|
543
|
+
case 'task_send':
|
|
544
|
+
return '转达任务';
|
|
545
|
+
case 'task_status':
|
|
546
|
+
return '查询任务';
|
|
547
|
+
case 'task_result':
|
|
548
|
+
return '收取结果';
|
|
549
|
+
default:
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function taskRunStatus(result: unknown): string | null {
|
|
555
|
+
const env = parseToolEnvelope(result);
|
|
556
|
+
const direct = stringField(env.fields, 'status');
|
|
557
|
+
if (direct) return direct;
|
|
558
|
+
const nested = asObject(env.fields.task);
|
|
559
|
+
return nested ? stringField(nested, 'status') : null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** `task_*` 家族的行摘要。 */
|
|
563
|
+
export function summarizeTaskAction(
|
|
564
|
+
tool: string,
|
|
565
|
+
args: unknown,
|
|
566
|
+
result?: unknown,
|
|
567
|
+
): string | null {
|
|
568
|
+
if (tool === 'task_run') {
|
|
569
|
+
const parsed = parseTaskRun(args);
|
|
570
|
+
if (!parsed.task) return null;
|
|
571
|
+
const head = clip(parsed.task);
|
|
572
|
+
if (taskRunStatus(result) === 'blocked') return `${head} · 等待依赖`;
|
|
573
|
+
if (parsed.worktree) return `${head} · 隔离工作区`;
|
|
574
|
+
return head;
|
|
575
|
+
}
|
|
576
|
+
if (tool === 'task_send') {
|
|
577
|
+
const message = stringField(asObject(args), 'message');
|
|
578
|
+
return message ? clip(message) : null;
|
|
579
|
+
}
|
|
580
|
+
if (tool === 'task_status') {
|
|
581
|
+
const id = stringField(asObject(args), 'taskId');
|
|
582
|
+
if (id) return shortTaskId(id);
|
|
583
|
+
const total = asObject(result)?.total;
|
|
584
|
+
return typeof total === 'number' ? `${total} 个任务` : '本会话';
|
|
585
|
+
}
|
|
586
|
+
if (tool === 'task_result') {
|
|
587
|
+
const id = stringField(asObject(args), 'taskId');
|
|
588
|
+
return id ? shortTaskId(id) : null;
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
|
|
124
593
|
/** Flatten a run_code action into the outer program plus inner tool rows. */
|
|
125
594
|
export function expandRunCodeActions<T extends ExecutedActionLike>(actions: T[]): T[] {
|
|
126
595
|
const out: T[] = [];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 助手回复里的自定义 `[next_steps]...[/next_steps]` 标签:
|
|
3
|
+
* 给追问建议当判断来源,气泡里只显示标签内的正文,不显示标签本身。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const COMPLETE_BLOCK_RE = /\[next_steps\]([\s\S]*?)\[\/next_steps\]/gi;
|
|
7
|
+
const OPEN_TAG = 'next_steps]';
|
|
8
|
+
const CLOSE_TAG = '/next_steps]';
|
|
9
|
+
|
|
10
|
+
function isIncompleteTagSuffix(suffix: string): boolean {
|
|
11
|
+
const body = suffix.slice(1).toLowerCase();
|
|
12
|
+
if (!body) return false;
|
|
13
|
+
return OPEN_TAG.startsWith(body) || CLOSE_TAG.startsWith(body);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stripIncompleteTrailingTag(text: string): string {
|
|
17
|
+
const idx = text.lastIndexOf('[');
|
|
18
|
+
if (idx < 0) return text;
|
|
19
|
+
const suffix = text.slice(idx);
|
|
20
|
+
return isIncompleteTagSuffix(suffix) ? text.slice(0, idx) : text;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 去掉自定义标签,保留里面的建议正文。 */
|
|
24
|
+
export function stripNextStepsTags(text: string): string {
|
|
25
|
+
let out = (text ?? '').replace(COMPLETE_BLOCK_RE, '$1');
|
|
26
|
+
out = out.replace(/\[\/?next_steps\]/gi, '');
|
|
27
|
+
return stripIncompleteTrailingTag(out);
|
|
28
|
+
}
|
|
@@ -22,6 +22,80 @@ export interface ChildInfo {
|
|
|
22
22
|
profile?: string;
|
|
23
23
|
/** running | completed | failed | cancelled | interrupted */
|
|
24
24
|
status: string;
|
|
25
|
+
/** 子代理自己的 durable record;右侧过程面板据此回看它的推理与工具。 */
|
|
26
|
+
recordId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 把一次 `delegate_subagent` 调用对上它的子代理。
|
|
31
|
+
*
|
|
32
|
+
* 工具行只有画像名与任务正文,子代理的 record 在生命周期事件里;`task` 就是
|
|
33
|
+
* 这次调用的 `task` 参数,所以两者精确相等。同一画像被派了多份活时按顺序
|
|
34
|
+
* 消费,`taken` 记下已配对的 childId。
|
|
35
|
+
*
|
|
36
|
+
* @param call 这次委派的画像名与任务正文。
|
|
37
|
+
* @param children 本回合累积的子代理列表。
|
|
38
|
+
* @param taken 已被前面的行配对掉的 childId。
|
|
39
|
+
* @returns 命中的子代理,或 undefined(历史回合没有事件、六件套子代理等)。
|
|
40
|
+
*/
|
|
41
|
+
export function matchChildForDelegation(
|
|
42
|
+
call: { profile: string | null; task: string | null },
|
|
43
|
+
children: ReadonlyArray<ChildInfo>,
|
|
44
|
+
taken: ReadonlySet<string> = new Set(),
|
|
45
|
+
): ChildInfo | undefined {
|
|
46
|
+
return children.find((child) => {
|
|
47
|
+
if (taken.has(child.childId)) return false;
|
|
48
|
+
if (call.task && child.task && child.task.trim() !== call.task.trim()) return false;
|
|
49
|
+
if (!call.task && child.task) return false;
|
|
50
|
+
if (call.profile && child.profile && child.profile !== call.profile) return false;
|
|
51
|
+
return true;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 画像名的派生规则,与宿主 `local-backend/subagent-profiles.ts` 的
|
|
57
|
+
* `profileNameForAgent` 一致(两包不能互相 import:那边带 fs / sqlite)。
|
|
58
|
+
*
|
|
59
|
+
* 没有 slug 的自建智能体(中文名 slugify 后为空)会拿到
|
|
60
|
+
* `agent-<id 前 16 位字母数字>` 这种派生名——只按 slug/id 反查就映射不回来,
|
|
61
|
+
* 行头只能显示这串 id。所以这里把宿主的整条链都列成候选。
|
|
62
|
+
*/
|
|
63
|
+
function profileNameCandidates(agent: {
|
|
64
|
+
id: string;
|
|
65
|
+
slug: string | null;
|
|
66
|
+
name: string;
|
|
67
|
+
}): string[] {
|
|
68
|
+
const candidates = [agent.id];
|
|
69
|
+
const slug = agent.slug?.trim() ?? '';
|
|
70
|
+
if (slug) candidates.push(slug);
|
|
71
|
+
if (/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(slug)) return candidates;
|
|
72
|
+
const fromName = agent.name
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
75
|
+
.replace(/^-+|-+$/g, '')
|
|
76
|
+
.slice(0, 40);
|
|
77
|
+
const alnumId = agent.id.replace(/[^a-zA-Z0-9]/g, '');
|
|
78
|
+
const derived = fromName || (alnumId ? `agent-${alnumId.slice(0, 16)}` : 'agent');
|
|
79
|
+
candidates.push(derived);
|
|
80
|
+
// 同名画像在花名册里会加 id 后缀去重(`buildMentionDelegateRoster`)。
|
|
81
|
+
if (alnumId) candidates.push(`${derived}-${alnumId.slice(0, 8)}`);
|
|
82
|
+
return candidates;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 把画像名(slug / 派生名 / lineage id)映射回智能体记录,调用方据此取显示名
|
|
87
|
+
* 与颜色。没有匹配(编排六件套的子代理、已删除的智能体)时返回 undefined,
|
|
88
|
+
* 由调用方退回画像原文。
|
|
89
|
+
*
|
|
90
|
+
* @param profileOrId `child_spawned.profile`、`subagent_type`,或六件套的 lineage childId。
|
|
91
|
+
* @param agents 当前会话可见的智能体列表。
|
|
92
|
+
*/
|
|
93
|
+
export function findAgentForProfile<
|
|
94
|
+
T extends { id: string; slug: string | null; name: string },
|
|
95
|
+
>(profileOrId: string, agents: ReadonlyArray<T>): T | undefined {
|
|
96
|
+
return agents.find((agent) =>
|
|
97
|
+
profileNameCandidates(agent).includes(profileOrId),
|
|
98
|
+
);
|
|
25
99
|
}
|
|
26
100
|
|
|
27
101
|
const STATUS_MAP: Record<string, OrchestrationTaskStatus> = {
|
|
@@ -58,6 +132,14 @@ export function childrenToCardModel(children: ChildInfo[]): {
|
|
|
58
132
|
};
|
|
59
133
|
}
|
|
60
134
|
|
|
135
|
+
/**
|
|
136
|
+
* 子代理看板只在「不止一个孩子」时有用。单次 `delegate_subagent` 已经
|
|
137
|
+
* 出现在工具行「委派 ·」,再画「编排计划 · 1 个子任务」是同一件事。
|
|
138
|
+
*/
|
|
139
|
+
export function shouldShowOrchestrationBoard(children: ChildInfo[]): boolean {
|
|
140
|
+
return children.length >= 2;
|
|
141
|
+
}
|
|
142
|
+
|
|
61
143
|
/**
|
|
62
144
|
* Fold the accumulated raw sidecar `agent.child` lifecycle events (as
|
|
63
145
|
* forwarded by the backend's `orchestration_child` SSE payload) back into a
|
|
@@ -81,6 +163,9 @@ export function foldOrchestrationChildEvents(
|
|
|
81
163
|
childId,
|
|
82
164
|
task: typeof ev.task === 'string' ? ev.task : undefined,
|
|
83
165
|
depth: typeof ev.depth === 'number' ? ev.depth : undefined,
|
|
166
|
+
profile: typeof ev.profile === 'string' ? ev.profile : undefined,
|
|
167
|
+
// 子代理自己的 durable record:委派行靠它跳转右侧过程栏。
|
|
168
|
+
recordId: typeof ev.recordId === 'string' ? ev.recordId : undefined,
|
|
84
169
|
status: 'running',
|
|
85
170
|
});
|
|
86
171
|
continue;
|
|
@@ -102,3 +187,38 @@ export function foldOrchestrationChildEvents(
|
|
|
102
187
|
}
|
|
103
188
|
return list;
|
|
104
189
|
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 从助手消息 `messageMetadata.orchestrationChildEvents` 水合子代理卡片。
|
|
193
|
+
* 刷新后没有 live SSE,只能靠落库事件经 `foldOrchestrationChildEvents` 重建。
|
|
194
|
+
*/
|
|
195
|
+
export function extractPersistedOrchestrationChildren(
|
|
196
|
+
messages:
|
|
197
|
+
| ReadonlyArray<{
|
|
198
|
+
id: string;
|
|
199
|
+
role?: string;
|
|
200
|
+
messageMetadata?: string | null;
|
|
201
|
+
}>
|
|
202
|
+
| undefined,
|
|
203
|
+
): Record<string, ChildInfo[]> {
|
|
204
|
+
const seeded: Record<string, ChildInfo[]> = {};
|
|
205
|
+
if (!messages || messages.length === 0) return seeded;
|
|
206
|
+
for (const message of messages) {
|
|
207
|
+
if (message.role !== 'assistant' || !message.messageMetadata) continue;
|
|
208
|
+
try {
|
|
209
|
+
const metadata = JSON.parse(message.messageMetadata) as {
|
|
210
|
+
orchestrationChildEvents?: unknown;
|
|
211
|
+
};
|
|
212
|
+
if (!Array.isArray(metadata.orchestrationChildEvents)) continue;
|
|
213
|
+
const events = metadata.orchestrationChildEvents.filter(
|
|
214
|
+
(item): item is Record<string, unknown> =>
|
|
215
|
+
Boolean(item) && typeof item === 'object' && !Array.isArray(item),
|
|
216
|
+
);
|
|
217
|
+
const list = foldOrchestrationChildEvents(events);
|
|
218
|
+
if (list.length > 0) seeded[message.id] = list;
|
|
219
|
+
} catch {
|
|
220
|
+
// Ignore malformed legacy metadata.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return seeded;
|
|
224
|
+
}
|