@lobehub/lobehub 2.0.0-next.288 → 2.0.0-next.289

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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.289](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.288...v2.0.0-next.289)
6
+
7
+ <sup>Released on **2026-01-15**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix page content mismatch when switch quickly.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix page content mismatch when switch quickly, closes [#11505](https://github.com/lobehub/lobe-chat/issues/11505) ([0cb1374](https://github.com/lobehub/lobe-chat/commit/0cb1374))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.288](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.287...v2.0.0-next.288)
6
31
 
7
32
  <sup>Released on **2026-01-15**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix page content mismatch when switch quickly."
6
+ ]
7
+ },
8
+ "date": "2026-01-15",
9
+ "version": "2.0.0-next.289"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "features": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.288",
3
+ "version": "2.0.0-next.289",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -7,6 +7,7 @@ import { ContextEngine } from '../../pipeline';
7
7
  import {
8
8
  AgentCouncilFlattenProcessor,
9
9
  GroupMessageFlattenProcessor,
10
+ GroupOrchestrationFilterProcessor,
10
11
  GroupRoleTransformProcessor,
11
12
  HistoryTruncateProcessor,
12
13
  InputTemplateProcessor,
@@ -274,6 +275,21 @@ export class MessagesEngine {
274
275
  // 15. Supervisor role restore (convert role=supervisor back to role=assistant for model)
275
276
  new SupervisorRoleRestoreProcessor(),
276
277
 
278
+ // 15.5. Group orchestration filter (remove supervisor's orchestration messages like broadcast/speak)
279
+ // This must be BEFORE GroupRoleTransformProcessor so we filter based on original agentId/tools
280
+ ...(isAgentGroupEnabled && agentGroup.agentMap && agentGroup.currentAgentId
281
+ ? [
282
+ new GroupOrchestrationFilterProcessor({
283
+ agentMap: Object.fromEntries(
284
+ Object.entries(agentGroup.agentMap).map(([id, info]) => [id, { role: info.role }]),
285
+ ),
286
+ currentAgentId: agentGroup.currentAgentId,
287
+ // Only enabled when current agent is NOT supervisor (supervisor needs to see orchestration history)
288
+ enabled: agentGroup.currentAgentRole !== 'supervisor',
289
+ }),
290
+ ]
291
+ : []),
292
+
277
293
  // 16. Group role transform (convert other agents' messages to user role with speaker tags)
278
294
  // This must be BEFORE ToolCallProcessor so other agents' tool messages are converted first
279
295
  ...(isAgentGroupEnabled && agentGroup.currentAgentId
@@ -0,0 +1,211 @@
1
+ import debug from 'debug';
2
+
3
+ import { BaseProcessor } from '../base/BaseProcessor';
4
+ import type { Message, PipelineContext, ProcessorOptions } from '../types';
5
+
6
+ const log = debug('context-engine:processor:GroupOrchestrationFilterProcessor');
7
+
8
+ /**
9
+ * Default orchestration tool identifier
10
+ */
11
+ const DEFAULT_ORCHESTRATION_IDENTIFIER = 'lobe-group-management';
12
+
13
+ /**
14
+ * Default orchestration api names that should be filtered
15
+ */
16
+ const DEFAULT_ORCHESTRATION_API_NAMES = ['broadcast', 'speak', 'executeTask', 'executeTasks'];
17
+
18
+ /**
19
+ * Agent info for identifying supervisor
20
+ */
21
+ export interface OrchestrationAgentInfo {
22
+ role: 'supervisor' | 'participant';
23
+ }
24
+
25
+ /**
26
+ * Tool info structure
27
+ */
28
+ interface ToolInfo {
29
+ apiName?: string;
30
+ identifier?: string;
31
+ }
32
+
33
+ /**
34
+ * Configuration for GroupOrchestrationFilterProcessor
35
+ */
36
+ export interface GroupOrchestrationFilterConfig {
37
+ /**
38
+ * Mapping from agentId to agent info
39
+ * Used to identify supervisor messages
40
+ */
41
+ agentMap?: Record<string, OrchestrationAgentInfo>;
42
+ /**
43
+ * The current agent ID that is responding
44
+ * If the current agent is supervisor, filtering will be skipped
45
+ * (Supervisor needs to see its own orchestration history)
46
+ */
47
+ currentAgentId?: string;
48
+ /**
49
+ * Whether to enable filtering
50
+ * @default true
51
+ */
52
+ enabled?: boolean;
53
+ /**
54
+ * Api names of orchestration tools to filter
55
+ * @default ['broadcast', 'speak', 'executeTask', 'executeTasks']
56
+ */
57
+ orchestrationApiNames?: string[];
58
+ /**
59
+ * Tool identifiers that are considered orchestration tools
60
+ * @default ['lobe-group-management']
61
+ */
62
+ orchestrationToolIdentifiers?: string[];
63
+ }
64
+
65
+ /**
66
+ * Group Orchestration Filter Processor
67
+ *
68
+ * Filters out Supervisor's orchestration messages (broadcast, speak, executeTask, etc.)
69
+ * from the context to reduce noise for participant agents.
70
+ *
71
+ * These messages are coordination metadata that participant agents don't need to see.
72
+ * Filtering them reduces context window usage and prevents model confusion.
73
+ *
74
+ * Filtering rules:
75
+ * - Supervisor assistant + orchestration tool_use: REMOVE
76
+ * - Supervisor tool_result for orchestration tools: REMOVE
77
+ * - Supervisor assistant without tools: KEEP (may contain meaningful summaries)
78
+ * - Supervisor assistant + non-orchestration tools: KEEP (e.g., search)
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * const processor = new GroupOrchestrationFilterProcessor({
83
+ * agentMap: {
84
+ * 'supervisor-id': { role: 'supervisor' },
85
+ * 'agent-1': { role: 'participant' },
86
+ * },
87
+ * });
88
+ * ```
89
+ */
90
+ export class GroupOrchestrationFilterProcessor extends BaseProcessor {
91
+ readonly name = 'GroupOrchestrationFilterProcessor';
92
+
93
+ private config: GroupOrchestrationFilterConfig;
94
+ private orchestrationIdentifiers: Set<string>;
95
+ private orchestrationApiNames: Set<string>;
96
+
97
+ constructor(config: GroupOrchestrationFilterConfig = {}, options: ProcessorOptions = {}) {
98
+ super(options);
99
+ this.config = config;
100
+ this.orchestrationIdentifiers = new Set(
101
+ config.orchestrationToolIdentifiers || [DEFAULT_ORCHESTRATION_IDENTIFIER],
102
+ );
103
+ this.orchestrationApiNames = new Set(
104
+ config.orchestrationApiNames || DEFAULT_ORCHESTRATION_API_NAMES,
105
+ );
106
+ }
107
+
108
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
109
+ const clonedContext = this.cloneContext(context);
110
+
111
+ // Skip if disabled or no agentMap provided
112
+ if (this.config.enabled === false || !this.config.agentMap) {
113
+ log('Processor disabled or no agentMap provided, skipping');
114
+ return this.markAsExecuted(clonedContext);
115
+ }
116
+
117
+ // Skip if current agent is supervisor (supervisor needs to see its orchestration history)
118
+ if (this.isCurrentAgentSupervisor()) {
119
+ log('Current agent is supervisor, skipping orchestration filter');
120
+ return this.markAsExecuted(clonedContext);
121
+ }
122
+
123
+ let filteredCount = 0;
124
+ let assistantFiltered = 0;
125
+ let toolFiltered = 0;
126
+
127
+ const filteredMessages = clonedContext.messages.filter((msg: Message) => {
128
+ // Only filter supervisor messages
129
+ if (!this.isSupervisorMessage(msg)) {
130
+ return true;
131
+ }
132
+
133
+ // Check assistant messages with tools
134
+ if (msg.role === 'assistant' && msg.tools && msg.tools.length > 0) {
135
+ const hasOrchestrationTool = msg.tools.some((tool: ToolInfo) =>
136
+ this.isOrchestrationTool(tool),
137
+ );
138
+
139
+ if (hasOrchestrationTool) {
140
+ filteredCount++;
141
+ assistantFiltered++;
142
+ log(`Filtering supervisor orchestration assistant message: ${msg.id}`);
143
+ return false;
144
+ }
145
+ }
146
+
147
+ // Check tool result messages
148
+ if (msg.role === 'tool' && msg.plugin && this.isOrchestrationTool(msg.plugin)) {
149
+ filteredCount++;
150
+ toolFiltered++;
151
+ log(`Filtering supervisor orchestration tool result: ${msg.id}`);
152
+ return false;
153
+ }
154
+
155
+ // Keep other supervisor messages (pure text, non-orchestration tools)
156
+ return true;
157
+ });
158
+
159
+ clonedContext.messages = filteredMessages;
160
+
161
+ // Update metadata
162
+ clonedContext.metadata.orchestrationFilterProcessed = {
163
+ assistantFiltered,
164
+ filteredCount,
165
+ toolFiltered,
166
+ };
167
+
168
+ log(
169
+ `Orchestration filter completed: ${filteredCount} messages filtered (${assistantFiltered} assistant, ${toolFiltered} tool)`,
170
+ );
171
+
172
+ return this.markAsExecuted(clonedContext);
173
+ }
174
+
175
+ /**
176
+ * Check if the current agent is a supervisor
177
+ * Supervisor doesn't need orchestration messages filtered (they need to see their history)
178
+ */
179
+ private isCurrentAgentSupervisor(): boolean {
180
+ if (!this.config.currentAgentId || !this.config.agentMap) {
181
+ return false;
182
+ }
183
+
184
+ const currentAgentInfo = this.config.agentMap[this.config.currentAgentId];
185
+ return currentAgentInfo?.role === 'supervisor';
186
+ }
187
+
188
+ /**
189
+ * Check if a message is from a supervisor agent
190
+ */
191
+ private isSupervisorMessage(msg: Message): boolean {
192
+ if (!msg.agentId || !this.config.agentMap) {
193
+ return false;
194
+ }
195
+
196
+ const agentInfo = this.config.agentMap[msg.agentId];
197
+ return agentInfo?.role === 'supervisor';
198
+ }
199
+
200
+ /**
201
+ * Check if a tool is an orchestration tool that should be filtered
202
+ */
203
+ private isOrchestrationTool(tool: ToolInfo): boolean {
204
+ if (!tool) return false;
205
+
206
+ const identifier = tool.identifier || '';
207
+ const apiName = tool.apiName || '';
208
+
209
+ return this.orchestrationIdentifiers.has(identifier) && this.orchestrationApiNames.has(apiName);
210
+ }
211
+ }
@@ -0,0 +1,770 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { PipelineContext } from '../../types';
4
+ import { GroupOrchestrationFilterProcessor } from '../GroupOrchestrationFilter';
5
+
6
+ describe('GroupOrchestrationFilterProcessor', () => {
7
+ const createContext = (messages: any[]): PipelineContext => ({
8
+ initialState: { messages: [] },
9
+ isAborted: false,
10
+ messages,
11
+ metadata: {},
12
+ });
13
+
14
+ const defaultConfig = {
15
+ agentMap: {
16
+ 'agent-a': { role: 'participant' as const },
17
+ 'agent-b': { role: 'participant' as const },
18
+ 'supervisor': { role: 'supervisor' as const },
19
+ },
20
+ currentAgentId: 'agent-a', // Default to participant agent
21
+ };
22
+
23
+ describe('filtering supervisor orchestration messages', () => {
24
+ it('should filter supervisor assistant message with broadcast tool', async () => {
25
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
26
+ const context = createContext([
27
+ { content: 'User question', id: 'msg_1', role: 'user' },
28
+ {
29
+ agentId: 'supervisor',
30
+ content: 'Let me coordinate the agents...',
31
+ id: 'msg_2',
32
+ role: 'assistant',
33
+ tools: [
34
+ {
35
+ apiName: 'broadcast',
36
+ arguments: '{"agentIds": ["agent-a", "agent-b"], "instruction": "Please respond"}',
37
+ id: 'call_1',
38
+ identifier: 'lobe-group-management',
39
+ },
40
+ ],
41
+ },
42
+ { content: 'Agent response', id: 'msg_3', role: 'assistant' },
43
+ ]);
44
+
45
+ const result = await processor.process(context);
46
+
47
+ expect(result.messages).toHaveLength(2);
48
+ expect(result.messages[0].id).toBe('msg_1');
49
+ expect(result.messages[1].id).toBe('msg_3');
50
+ });
51
+
52
+ it('should filter supervisor assistant message with speak tool', async () => {
53
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
54
+ const context = createContext([
55
+ {
56
+ agentId: 'supervisor',
57
+ content: 'Asking agent-a to respond',
58
+ id: 'msg_1',
59
+ role: 'assistant',
60
+ tools: [
61
+ {
62
+ apiName: 'speak',
63
+ arguments: '{"agentId": "agent-a", "instruction": "Please help"}',
64
+ id: 'call_1',
65
+ identifier: 'lobe-group-management',
66
+ },
67
+ ],
68
+ },
69
+ ]);
70
+
71
+ const result = await processor.process(context);
72
+
73
+ expect(result.messages).toHaveLength(0);
74
+ });
75
+
76
+ it('should filter supervisor assistant message with executeTask tool', async () => {
77
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
78
+ const context = createContext([
79
+ {
80
+ agentId: 'supervisor',
81
+ content: 'Executing task',
82
+ id: 'msg_1',
83
+ role: 'assistant',
84
+ tools: [
85
+ {
86
+ apiName: 'executeTask',
87
+ arguments: '{"task": "do something"}',
88
+ id: 'call_1',
89
+ identifier: 'lobe-group-management',
90
+ },
91
+ ],
92
+ },
93
+ ]);
94
+
95
+ const result = await processor.process(context);
96
+
97
+ expect(result.messages).toHaveLength(0);
98
+ });
99
+
100
+ it('should filter supervisor assistant message with executeTasks tool', async () => {
101
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
102
+ const context = createContext([
103
+ {
104
+ agentId: 'supervisor',
105
+ content: 'Executing multiple tasks',
106
+ id: 'msg_1',
107
+ role: 'assistant',
108
+ tools: [
109
+ {
110
+ apiName: 'executeTasks',
111
+ arguments: '{"tasks": ["task1", "task2"]}',
112
+ id: 'call_1',
113
+ identifier: 'lobe-group-management',
114
+ },
115
+ ],
116
+ },
117
+ ]);
118
+
119
+ const result = await processor.process(context);
120
+
121
+ expect(result.messages).toHaveLength(0);
122
+ });
123
+ });
124
+
125
+ describe('filtering supervisor tool results', () => {
126
+ it('should filter supervisor tool result for broadcast', async () => {
127
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
128
+ const context = createContext([
129
+ { content: 'User question', id: 'msg_1', role: 'user' },
130
+ {
131
+ agentId: 'supervisor',
132
+ content: 'Triggered broadcast to agents: agent-a, agent-b',
133
+ id: 'msg_2',
134
+ plugin: {
135
+ apiName: 'broadcast',
136
+ identifier: 'lobe-group-management',
137
+ },
138
+ role: 'tool',
139
+ tool_call_id: 'call_1',
140
+ },
141
+ { content: 'Instruction from supervisor', id: 'msg_3', role: 'user' },
142
+ ]);
143
+
144
+ const result = await processor.process(context);
145
+
146
+ expect(result.messages).toHaveLength(2);
147
+ expect(result.messages[0].id).toBe('msg_1');
148
+ expect(result.messages[1].id).toBe('msg_3');
149
+ });
150
+
151
+ it('should filter supervisor tool result for speak', async () => {
152
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
153
+ const context = createContext([
154
+ {
155
+ agentId: 'supervisor',
156
+ content: 'Triggered speak to agent-a',
157
+ id: 'msg_1',
158
+ plugin: {
159
+ apiName: 'speak',
160
+ identifier: 'lobe-group-management',
161
+ },
162
+ role: 'tool',
163
+ tool_call_id: 'call_1',
164
+ },
165
+ ]);
166
+
167
+ const result = await processor.process(context);
168
+
169
+ expect(result.messages).toHaveLength(0);
170
+ });
171
+ });
172
+
173
+ describe('keeping non-orchestration messages', () => {
174
+ it('should keep supervisor assistant message without tools', async () => {
175
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
176
+ const context = createContext([
177
+ {
178
+ agentId: 'supervisor',
179
+ content: 'Here is a summary of the discussion...',
180
+ id: 'msg_1',
181
+ role: 'assistant',
182
+ },
183
+ ]);
184
+
185
+ const result = await processor.process(context);
186
+
187
+ expect(result.messages).toHaveLength(1);
188
+ expect(result.messages[0].content).toBe('Here is a summary of the discussion...');
189
+ });
190
+
191
+ it('should keep supervisor assistant message with non-orchestration tools', async () => {
192
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
193
+ const context = createContext([
194
+ {
195
+ agentId: 'supervisor',
196
+ content: 'Let me search for information',
197
+ id: 'msg_1',
198
+ role: 'assistant',
199
+ tools: [
200
+ {
201
+ apiName: 'search',
202
+ arguments: '{"query": "test"}',
203
+ id: 'call_1',
204
+ identifier: 'web-search',
205
+ },
206
+ ],
207
+ },
208
+ ]);
209
+
210
+ const result = await processor.process(context);
211
+
212
+ expect(result.messages).toHaveLength(1);
213
+ expect(result.messages[0].id).toBe('msg_1');
214
+ });
215
+
216
+ it('should keep supervisor tool result for non-orchestration tools', async () => {
217
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
218
+ const context = createContext([
219
+ {
220
+ agentId: 'supervisor',
221
+ content: '{"results": ["item1", "item2"]}',
222
+ id: 'msg_1',
223
+ plugin: {
224
+ apiName: 'search',
225
+ identifier: 'web-search',
226
+ },
227
+ role: 'tool',
228
+ tool_call_id: 'call_1',
229
+ },
230
+ ]);
231
+
232
+ const result = await processor.process(context);
233
+
234
+ expect(result.messages).toHaveLength(1);
235
+ });
236
+
237
+ it('should keep all participant agent messages', async () => {
238
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
239
+ const context = createContext([
240
+ {
241
+ agentId: 'agent-a',
242
+ content: 'Participant response',
243
+ id: 'msg_1',
244
+ role: 'assistant',
245
+ tools: [
246
+ {
247
+ apiName: 'broadcast',
248
+ arguments: '{}',
249
+ id: 'call_1',
250
+ identifier: 'lobe-group-management',
251
+ },
252
+ ],
253
+ },
254
+ {
255
+ agentId: 'agent-b',
256
+ content: 'Tool result',
257
+ id: 'msg_2',
258
+ plugin: {
259
+ apiName: 'broadcast',
260
+ identifier: 'lobe-group-management',
261
+ },
262
+ role: 'tool',
263
+ tool_call_id: 'call_1',
264
+ },
265
+ ]);
266
+
267
+ const result = await processor.process(context);
268
+
269
+ // Participant messages are never filtered, even with orchestration tools
270
+ expect(result.messages).toHaveLength(2);
271
+ });
272
+
273
+ it('should keep user messages unchanged', async () => {
274
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
275
+ const context = createContext([
276
+ { content: 'User question 1', id: 'msg_1', role: 'user' },
277
+ { content: 'User question 2', id: 'msg_2', role: 'user' },
278
+ ]);
279
+
280
+ const result = await processor.process(context);
281
+
282
+ expect(result.messages).toHaveLength(2);
283
+ });
284
+
285
+ it('should keep messages without agentId', async () => {
286
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
287
+ const context = createContext([
288
+ {
289
+ content: 'System message',
290
+ id: 'msg_1',
291
+ role: 'system',
292
+ },
293
+ {
294
+ content: 'Assistant without agentId',
295
+ id: 'msg_2',
296
+ role: 'assistant',
297
+ tools: [
298
+ {
299
+ apiName: 'broadcast',
300
+ arguments: '{}',
301
+ id: 'call_1',
302
+ identifier: 'lobe-group-management',
303
+ },
304
+ ],
305
+ },
306
+ ]);
307
+
308
+ const result = await processor.process(context);
309
+
310
+ expect(result.messages).toHaveLength(2);
311
+ });
312
+ });
313
+
314
+ describe('configuration options', () => {
315
+ it('should skip processing when disabled', async () => {
316
+ const processor = new GroupOrchestrationFilterProcessor({
317
+ ...defaultConfig,
318
+ enabled: false,
319
+ });
320
+ const context = createContext([
321
+ {
322
+ agentId: 'supervisor',
323
+ content: 'Orchestration message',
324
+ id: 'msg_1',
325
+ role: 'assistant',
326
+ tools: [
327
+ {
328
+ apiName: 'broadcast',
329
+ arguments: '{}',
330
+ id: 'call_1',
331
+ identifier: 'lobe-group-management',
332
+ },
333
+ ],
334
+ },
335
+ ]);
336
+
337
+ const result = await processor.process(context);
338
+
339
+ expect(result.messages).toHaveLength(1);
340
+ });
341
+
342
+ it('should skip processing when current agent is supervisor', async () => {
343
+ const processor = new GroupOrchestrationFilterProcessor({
344
+ ...defaultConfig,
345
+ currentAgentId: 'supervisor', // Supervisor as current agent
346
+ });
347
+ const context = createContext([
348
+ {
349
+ agentId: 'supervisor',
350
+ content: 'Orchestration message',
351
+ id: 'msg_1',
352
+ role: 'assistant',
353
+ tools: [
354
+ {
355
+ apiName: 'broadcast',
356
+ arguments: '{}',
357
+ id: 'call_1',
358
+ identifier: 'lobe-group-management',
359
+ },
360
+ ],
361
+ },
362
+ {
363
+ agentId: 'supervisor',
364
+ content: 'Tool result',
365
+ id: 'msg_2',
366
+ plugin: {
367
+ apiName: 'broadcast',
368
+ identifier: 'lobe-group-management',
369
+ },
370
+ role: 'tool',
371
+ tool_call_id: 'call_1',
372
+ },
373
+ ]);
374
+
375
+ const result = await processor.process(context);
376
+
377
+ // Supervisor should see all messages including orchestration ones
378
+ expect(result.messages).toHaveLength(2);
379
+ });
380
+
381
+ it('should skip processing when no agentMap provided', async () => {
382
+ const processor = new GroupOrchestrationFilterProcessor({});
383
+ const context = createContext([
384
+ {
385
+ agentId: 'supervisor',
386
+ content: 'Orchestration message',
387
+ id: 'msg_1',
388
+ role: 'assistant',
389
+ tools: [
390
+ {
391
+ apiName: 'broadcast',
392
+ arguments: '{}',
393
+ id: 'call_1',
394
+ identifier: 'lobe-group-management',
395
+ },
396
+ ],
397
+ },
398
+ ]);
399
+
400
+ const result = await processor.process(context);
401
+
402
+ expect(result.messages).toHaveLength(1);
403
+ });
404
+
405
+ it('should skip processing when no currentAgentId provided', async () => {
406
+ const processor = new GroupOrchestrationFilterProcessor({
407
+ agentMap: defaultConfig.agentMap,
408
+ // No currentAgentId
409
+ });
410
+ const context = createContext([
411
+ {
412
+ agentId: 'supervisor',
413
+ content: 'Orchestration message',
414
+ id: 'msg_1',
415
+ role: 'assistant',
416
+ tools: [
417
+ {
418
+ apiName: 'broadcast',
419
+ arguments: '{}',
420
+ id: 'call_1',
421
+ identifier: 'lobe-group-management',
422
+ },
423
+ ],
424
+ },
425
+ ]);
426
+
427
+ const result = await processor.process(context);
428
+
429
+ // Without currentAgentId, can't determine if supervisor, so treat as participant and filter
430
+ // Actually, isCurrentAgentSupervisor returns false when no currentAgentId, so filtering happens
431
+ expect(result.messages).toHaveLength(0);
432
+ });
433
+
434
+ it('should use custom orchestration tool identifiers', async () => {
435
+ const processor = new GroupOrchestrationFilterProcessor({
436
+ ...defaultConfig,
437
+ orchestrationToolIdentifiers: ['custom-orchestration'],
438
+ });
439
+ const context = createContext([
440
+ {
441
+ agentId: 'supervisor',
442
+ content: 'Custom orchestration',
443
+ id: 'msg_1',
444
+ role: 'assistant',
445
+ tools: [
446
+ {
447
+ apiName: 'broadcast',
448
+ arguments: '{}',
449
+ id: 'call_1',
450
+ identifier: 'custom-orchestration',
451
+ },
452
+ ],
453
+ },
454
+ {
455
+ agentId: 'supervisor',
456
+ content: 'Default orchestration - should not be filtered',
457
+ id: 'msg_2',
458
+ role: 'assistant',
459
+ tools: [
460
+ {
461
+ apiName: 'broadcast',
462
+ arguments: '{}',
463
+ id: 'call_2',
464
+ identifier: 'lobe-group-management',
465
+ },
466
+ ],
467
+ },
468
+ ]);
469
+
470
+ const result = await processor.process(context);
471
+
472
+ expect(result.messages).toHaveLength(1);
473
+ expect(result.messages[0].id).toBe('msg_2');
474
+ });
475
+
476
+ it('should use custom orchestration api names', async () => {
477
+ const processor = new GroupOrchestrationFilterProcessor({
478
+ ...defaultConfig,
479
+ orchestrationApiNames: ['customBroadcast'],
480
+ });
481
+ const context = createContext([
482
+ {
483
+ agentId: 'supervisor',
484
+ content: 'Custom api name',
485
+ id: 'msg_1',
486
+ role: 'assistant',
487
+ tools: [
488
+ {
489
+ apiName: 'customBroadcast',
490
+ arguments: '{}',
491
+ id: 'call_1',
492
+ identifier: 'lobe-group-management',
493
+ },
494
+ ],
495
+ },
496
+ {
497
+ agentId: 'supervisor',
498
+ content: 'Default broadcast - should not be filtered',
499
+ id: 'msg_2',
500
+ role: 'assistant',
501
+ tools: [
502
+ {
503
+ apiName: 'broadcast',
504
+ arguments: '{}',
505
+ id: 'call_2',
506
+ identifier: 'lobe-group-management',
507
+ },
508
+ ],
509
+ },
510
+ ]);
511
+
512
+ const result = await processor.process(context);
513
+
514
+ expect(result.messages).toHaveLength(1);
515
+ expect(result.messages[0].id).toBe('msg_2');
516
+ });
517
+ });
518
+
519
+ describe('edge cases', () => {
520
+ it('should handle empty messages array', async () => {
521
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
522
+ const context = createContext([]);
523
+
524
+ const result = await processor.process(context);
525
+
526
+ expect(result.messages).toHaveLength(0);
527
+ });
528
+
529
+ it('should handle message with empty tools array', async () => {
530
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
531
+ const context = createContext([
532
+ {
533
+ agentId: 'supervisor',
534
+ content: 'Message with empty tools',
535
+ id: 'msg_1',
536
+ role: 'assistant',
537
+ tools: [],
538
+ },
539
+ ]);
540
+
541
+ const result = await processor.process(context);
542
+
543
+ // Empty tools array means no orchestration tools, so message is kept
544
+ expect(result.messages).toHaveLength(1);
545
+ });
546
+
547
+ it('should handle tool without identifier', async () => {
548
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
549
+ const context = createContext([
550
+ {
551
+ agentId: 'supervisor',
552
+ content: 'Tool without identifier',
553
+ id: 'msg_1',
554
+ role: 'assistant',
555
+ tools: [
556
+ {
557
+ apiName: 'broadcast',
558
+ arguments: '{}',
559
+ id: 'call_1',
560
+ // Missing identifier
561
+ },
562
+ ],
563
+ },
564
+ ]);
565
+
566
+ const result = await processor.process(context);
567
+
568
+ // Tool without identifier doesn't match orchestration pattern
569
+ expect(result.messages).toHaveLength(1);
570
+ });
571
+
572
+ it('should handle tool without apiName', async () => {
573
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
574
+ const context = createContext([
575
+ {
576
+ agentId: 'supervisor',
577
+ content: 'Tool without apiName',
578
+ id: 'msg_1',
579
+ role: 'assistant',
580
+ tools: [
581
+ {
582
+ arguments: '{}',
583
+ id: 'call_1',
584
+ identifier: 'lobe-group-management',
585
+ // Missing apiName
586
+ },
587
+ ],
588
+ },
589
+ ]);
590
+
591
+ const result = await processor.process(context);
592
+
593
+ // Tool without apiName doesn't match orchestration pattern
594
+ expect(result.messages).toHaveLength(1);
595
+ });
596
+
597
+ it('should track filter counts in metadata', async () => {
598
+ const processor = new GroupOrchestrationFilterProcessor(defaultConfig);
599
+ const context = createContext([
600
+ {
601
+ agentId: 'supervisor',
602
+ content: 'Broadcast',
603
+ id: 'msg_1',
604
+ role: 'assistant',
605
+ tools: [
606
+ {
607
+ apiName: 'broadcast',
608
+ arguments: '{}',
609
+ id: 'call_1',
610
+ identifier: 'lobe-group-management',
611
+ },
612
+ ],
613
+ },
614
+ {
615
+ agentId: 'supervisor',
616
+ content: 'Tool result',
617
+ id: 'msg_2',
618
+ plugin: {
619
+ apiName: 'broadcast',
620
+ identifier: 'lobe-group-management',
621
+ },
622
+ role: 'tool',
623
+ tool_call_id: 'call_1',
624
+ },
625
+ {
626
+ agentId: 'supervisor',
627
+ content: 'Speak',
628
+ id: 'msg_3',
629
+ role: 'assistant',
630
+ tools: [
631
+ {
632
+ apiName: 'speak',
633
+ arguments: '{}',
634
+ id: 'call_2',
635
+ identifier: 'lobe-group-management',
636
+ },
637
+ ],
638
+ },
639
+ ]);
640
+
641
+ const result = await processor.process(context);
642
+
643
+ expect(result.metadata.orchestrationFilterProcessed).toEqual({
644
+ assistantFiltered: 2,
645
+ filteredCount: 3,
646
+ toolFiltered: 1,
647
+ });
648
+ });
649
+ });
650
+
651
+ describe('comprehensive end-to-end filtering', () => {
652
+ it('should correctly filter a full group conversation with orchestration messages', async () => {
653
+ const processor = new GroupOrchestrationFilterProcessor({
654
+ agentMap: {
655
+ 'agent-a': { role: 'participant' },
656
+ 'agent-b': { role: 'participant' },
657
+ 'supervisor': { role: 'supervisor' },
658
+ },
659
+ });
660
+
661
+ const inputMessages = [
662
+ // 1. User's original question
663
+ { content: '帮我规划杭州行程', id: 'msg_1', role: 'user' },
664
+ // 2. Supervisor broadcasts - SHOULD BE FILTERED
665
+ {
666
+ agentId: 'supervisor',
667
+ content: '好的,让我协调专家们...',
668
+ id: 'msg_2',
669
+ role: 'assistant',
670
+ tools: [
671
+ {
672
+ apiName: 'broadcast',
673
+ arguments: '{"agentIds": ["agent-a", "agent-b"], "instruction": "请给建议"}',
674
+ id: 'call_1',
675
+ identifier: 'lobe-group-management',
676
+ },
677
+ ],
678
+ },
679
+ // 3. Broadcast tool result - SHOULD BE FILTERED
680
+ {
681
+ agentId: 'supervisor',
682
+ content: 'Triggered broadcast to agents: agent-a, agent-b',
683
+ id: 'msg_3',
684
+ plugin: {
685
+ apiName: 'broadcast',
686
+ identifier: 'lobe-group-management',
687
+ },
688
+ role: 'tool',
689
+ tool_call_id: 'call_1',
690
+ },
691
+ // 4. Actual instruction (injected by broadcast) - SHOULD BE KEPT
692
+ { content: '请各位专家给出杭州行程建议', id: 'msg_4', role: 'user' },
693
+ // 5. Agent A response - SHOULD BE KEPT
694
+ { agentId: 'agent-a', content: '推荐西湖景区', id: 'msg_5', role: 'assistant' },
695
+ // 6. Agent B response - SHOULD BE KEPT
696
+ { agentId: 'agent-b', content: '推荐楼外楼', id: 'msg_6', role: 'assistant' },
697
+ // 7. Supervisor uses speak - SHOULD BE FILTERED
698
+ {
699
+ agentId: 'supervisor',
700
+ content: '让 agent-a 总结一下',
701
+ id: 'msg_7',
702
+ role: 'assistant',
703
+ tools: [
704
+ {
705
+ apiName: 'speak',
706
+ arguments: '{"agentId": "agent-a", "instruction": "请总结"}',
707
+ id: 'call_2',
708
+ identifier: 'lobe-group-management',
709
+ },
710
+ ],
711
+ },
712
+ // 8. Speak tool result - SHOULD BE FILTERED
713
+ {
714
+ agentId: 'supervisor',
715
+ content: 'Triggered speak to agent-a',
716
+ id: 'msg_8',
717
+ plugin: {
718
+ apiName: 'speak',
719
+ identifier: 'lobe-group-management',
720
+ },
721
+ role: 'tool',
722
+ tool_call_id: 'call_2',
723
+ },
724
+ // 9. Supervisor's summary (pure text, no tools) - SHOULD BE KEPT
725
+ {
726
+ agentId: 'supervisor',
727
+ content: '以上就是专家们的建议汇总',
728
+ id: 'msg_9',
729
+ role: 'assistant',
730
+ },
731
+ // 10. Supervisor uses search tool - SHOULD BE KEPT
732
+ {
733
+ agentId: 'supervisor',
734
+ content: '让我搜索一下更多信息',
735
+ id: 'msg_10',
736
+ role: 'assistant',
737
+ tools: [
738
+ {
739
+ apiName: 'search',
740
+ arguments: '{"query": "杭州景点"}',
741
+ id: 'call_3',
742
+ identifier: 'web-search',
743
+ },
744
+ ],
745
+ },
746
+ ];
747
+
748
+ const context = createContext(inputMessages);
749
+ const result = await processor.process(context);
750
+
751
+ // Should have: msg_1, msg_4, msg_5, msg_6, msg_9, msg_10 (6 messages)
752
+ expect(result.messages).toHaveLength(6);
753
+ expect(result.messages.map((m) => m.id)).toEqual([
754
+ 'msg_1',
755
+ 'msg_4',
756
+ 'msg_5',
757
+ 'msg_6',
758
+ 'msg_9',
759
+ 'msg_10',
760
+ ]);
761
+
762
+ // Verify metadata
763
+ expect(result.metadata.orchestrationFilterProcessed).toEqual({
764
+ assistantFiltered: 2, // msg_2, msg_7
765
+ filteredCount: 4, // msg_2, msg_3, msg_7, msg_8
766
+ toolFiltered: 2, // msg_3, msg_8
767
+ });
768
+ });
769
+ });
770
+ });
@@ -1,6 +1,11 @@
1
1
  // Transformer processors
2
2
  export { AgentCouncilFlattenProcessor } from './AgentCouncilFlatten';
3
3
  export { GroupMessageFlattenProcessor } from './GroupMessageFlatten';
4
+ export {
5
+ type GroupOrchestrationFilterConfig,
6
+ GroupOrchestrationFilterProcessor,
7
+ type OrchestrationAgentInfo,
8
+ } from './GroupOrchestrationFilter';
4
9
  export { GroupRoleTransformProcessor } from './GroupRoleTransform';
5
10
  export { HistoryTruncateProcessor } from './HistoryTruncate';
6
11
  export { InputTemplateProcessor } from './InputTemplate';
@@ -191,6 +191,14 @@ export const createDocumentSlice: StateCreator<
191
191
  // Both documentId and editor are guaranteed to be defined when this callback is called
192
192
  if (!document || !documentId || !editor) return;
193
193
 
194
+ // Check if this response is still for the current active document
195
+ // This prevents race conditions when quickly switching between documents
196
+ const currentActiveId = get().activeDocumentId;
197
+ if (currentActiveId && currentActiveId !== documentId) {
198
+ // User has already switched to another document, discard this stale response
199
+ return;
200
+ }
201
+
194
202
  // Initialize document with editor
195
203
  get().initDocumentWithEditor({
196
204
  autoSave,