fix group chat agent mentions (#1111)
This commit is contained in:
@@ -72,6 +72,12 @@ export function groupContextTokensWithFixedOverhead(
|
|||||||
return Math.floor(fixedContextTokens) + estimateGroupHistoryMessageTokens(history)
|
return Math.floor(fixedContextTokens) + estimateGroupHistoryMessageTokens(history)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function groupBridgeReasoningDeltaFromEvent(event: Record<string, unknown>): string | null {
|
||||||
|
if (String(event.event || '') !== 'reasoning.delta') return null
|
||||||
|
const text = String(event.text || '')
|
||||||
|
return text ? text : null
|
||||||
|
}
|
||||||
|
|
||||||
interface MemberData {
|
interface MemberData {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -698,10 +704,12 @@ class AgentClient {
|
|||||||
approval_id: (ev as any).approval_id,
|
approval_id: (ev as any).approval_id,
|
||||||
choice: (ev as any).choice,
|
choice: (ev as any).choice,
|
||||||
})
|
})
|
||||||
} else if (eventType === 'reasoning.delta' || eventType === 'thinking.delta') {
|
} else {
|
||||||
const text = String((ev as any)?.text || '')
|
const text = groupBridgeReasoningDeltaFromEvent(ev as Record<string, unknown>)
|
||||||
reasoning += text
|
if (text) {
|
||||||
this.emitMessageReasoningDelta(roomId, getCurrentMessageId(), text)
|
reasoning += text
|
||||||
|
this.emitMessageReasoningDelta(roomId, getCurrentMessageId(), text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reasoning
|
return reasoning
|
||||||
|
|||||||
@@ -142,6 +142,12 @@ function normalizeMentionDepth(depth: unknown): number {
|
|||||||
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maxAgentMentionDepth(): number {
|
||||||
|
const value = Number(process.env.HERMES_GROUP_CHAT_MAX_AGENT_MENTION_DEPTH)
|
||||||
|
if (!Number.isFinite(value) || value <= 0) return 4
|
||||||
|
return Math.min(10, Math.floor(value))
|
||||||
|
}
|
||||||
|
|
||||||
function groupRunOrder(id: string): { baseId: string; phase: number } {
|
function groupRunOrder(id: string): { baseId: string; phase: number } {
|
||||||
const value = String(id || '')
|
const value = String(id || '')
|
||||||
const partMatch = value.match(/^(.*)_part_(\d+)(?:_(toolcall|toolresult)_.+)?$/)
|
const partMatch = value.match(/^(.*)_part_(\d+)(?:_(toolcall|toolresult)_.+)?$/)
|
||||||
@@ -965,10 +971,14 @@ export class GroupChatServer {
|
|||||||
ack?.({ id: savedMsg.id })
|
ack?.({ id: savedMsg.id })
|
||||||
|
|
||||||
const mentionDepth = normalizeMentionDepth(data.mentionDepth)
|
const mentionDepth = normalizeMentionDepth(data.mentionDepth)
|
||||||
const shouldRouteMentions = savedMsg.role === 'user'
|
const isAgentReply = savedMsg.role === 'assistant' && member?.source === 'agent'
|
||||||
|
const shouldRouteMentions = savedMsg.role === 'user' ||
|
||||||
|
(isAgentReply && mentionDepth < maxAgentMentionDepth())
|
||||||
|
|
||||||
if (shouldRouteMentions) {
|
if (shouldRouteMentions) {
|
||||||
// Server-side @mention routing — parse user mentions and invoke agents directly.
|
// Server-side @mention routing — parse mentions and invoke agents directly.
|
||||||
|
// Agent replies are allowed to mention other agents, but mentionDepth
|
||||||
|
// bounds chained agent-to-agent handoffs so one prompt cannot loop forever.
|
||||||
this.agentClients.processMentions(roomId, {
|
this.agentClients.processMentions(roomId, {
|
||||||
content: contentToText(savedMsg.content),
|
content: contentToText(savedMsg.content),
|
||||||
input: Array.isArray(data.content) ? data.content : undefined,
|
input: Array.isArray(data.content) ? data.content : undefined,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import { countTokens } from '../../packages/server/src/lib/context-compressor'
|
import { countTokens } from '../../packages/server/src/lib/context-compressor'
|
||||||
import {
|
import {
|
||||||
estimateGroupHistoryMessageTokens,
|
estimateGroupHistoryMessageTokens,
|
||||||
|
groupBridgeReasoningDeltaFromEvent,
|
||||||
groupContextTokensWithFixedOverhead,
|
groupContextTokensWithFixedOverhead,
|
||||||
} from '../../packages/server/src/services/hermes/group-chat/agent-clients'
|
} from '../../packages/server/src/services/hermes/group-chat/agent-clients'
|
||||||
|
|
||||||
@@ -22,4 +23,19 @@ describe('group chat fixed context cache helpers', () => {
|
|||||||
expect(groupContextTokensWithFixedOverhead(undefined, [{ content: 'hello' }])).toBeUndefined()
|
expect(groupContextTokensWithFixedOverhead(undefined, [{ content: 'hello' }])).toBeUndefined()
|
||||||
expect(groupContextTokensWithFixedOverhead(null, [{ content: 'hello' }])).toBeUndefined()
|
expect(groupContextTokensWithFixedOverhead(null, [{ content: 'hello' }])).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps spinner thinking events out of persisted group-chat reasoning', () => {
|
||||||
|
expect(groupBridgeReasoningDeltaFromEvent({
|
||||||
|
event: 'thinking.delta',
|
||||||
|
text: '(◕‿◕✿) pondering...',
|
||||||
|
})).toBeNull()
|
||||||
|
expect(groupBridgeReasoningDeltaFromEvent({
|
||||||
|
event: 'reasoning.delta',
|
||||||
|
text: 'real reasoning',
|
||||||
|
})).toBe('real reasoning')
|
||||||
|
expect(groupBridgeReasoningDeltaFromEvent({
|
||||||
|
event: 'reasoning.delta',
|
||||||
|
text: '',
|
||||||
|
})).toBeNull()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -270,15 +270,15 @@ describe('Group Chat member/agent identity sync', () => {
|
|||||||
expect(ctx.body).toEqual({ rooms })
|
expect(ctx.body).toEqual({ rooms })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('routes @mentions only from user messages, not agent replies', () => {
|
it('routes @mentions from users and bounded agent replies', () => {
|
||||||
const server = Object.create(GroupChatServer.prototype) as any
|
const server = Object.create(GroupChatServer.prototype) as any
|
||||||
const emit = vi.fn()
|
const emit = vi.fn()
|
||||||
server.rooms = new Map([
|
server.rooms = new Map([
|
||||||
['room-1', {
|
['room-1', {
|
||||||
hasOnlineMember: vi.fn(() => true),
|
hasOnlineMember: vi.fn(() => true),
|
||||||
getOnlineMemberBySocketId: vi.fn((socketId: string) => socketId === 'agent-socket'
|
getOnlineMemberBySocketId: vi.fn((socketId: string) => socketId === 'agent-socket'
|
||||||
? { userId: 'agent-1', name: '丫鬟' }
|
? { userId: 'agent-1', name: '丫鬟', source: 'agent' }
|
||||||
: { userId: 'human-1', name: 'Human' }),
|
: { userId: 'human-1', name: 'Human', source: 'human' }),
|
||||||
}],
|
}],
|
||||||
])
|
])
|
||||||
server.socketUserMap = new Map([
|
server.socketUserMap = new Map([
|
||||||
@@ -297,9 +297,23 @@ describe('Group Chat member/agent identity sync', () => {
|
|||||||
|
|
||||||
server.handleMessage({ id: 'human-socket' }, { roomId: 'room-1', content: '@all hi', role: 'user' }, vi.fn())
|
server.handleMessage({ id: 'human-socket' }, { roomId: 'room-1', content: '@all hi', role: 'user' }, vi.fn())
|
||||||
expect(server.agentClients.processMentions).toHaveBeenCalledTimes(1)
|
expect(server.agentClients.processMentions).toHaveBeenCalledTimes(1)
|
||||||
|
expect(server.agentClients.processMentions).toHaveBeenLastCalledWith('room-1', expect.objectContaining({
|
||||||
|
content: '@all hi',
|
||||||
|
senderId: 'human-1',
|
||||||
|
mentionDepth: 0,
|
||||||
|
}))
|
||||||
|
|
||||||
server.agentClients.processMentions.mockClear()
|
server.agentClients.processMentions.mockClear()
|
||||||
server.handleMessage({ id: 'agent-socket' }, { roomId: 'room-1', content: '@all agent says hi', role: 'assistant', mentionDepth: 1 }, vi.fn())
|
server.handleMessage({ id: 'agent-socket' }, { roomId: 'room-1', content: '@all agent says hi', role: 'assistant', mentionDepth: 1 }, vi.fn())
|
||||||
|
expect(server.agentClients.processMentions).toHaveBeenCalledTimes(1)
|
||||||
|
expect(server.agentClients.processMentions).toHaveBeenLastCalledWith('room-1', expect.objectContaining({
|
||||||
|
content: '@all agent says hi',
|
||||||
|
senderId: 'agent-1',
|
||||||
|
mentionDepth: 1,
|
||||||
|
}))
|
||||||
|
|
||||||
|
server.agentClients.processMentions.mockClear()
|
||||||
|
server.handleMessage({ id: 'agent-socket' }, { roomId: 'room-1', content: '@all too deep', role: 'assistant', mentionDepth: 4 }, vi.fn())
|
||||||
expect(server.agentClients.processMentions).not.toHaveBeenCalled()
|
expect(server.agentClients.processMentions).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user