feat(agent): loop detection (#152)

* feat(loop-detection): add configurable text and tool loop guards

* style(web): remove duplicate separator in bot settings
This commit is contained in:
Ringo.Typowriter
2026-03-02 15:00:09 +08:00
committed by GitHub
parent 04bce702b7
commit d3edd17d90
13 changed files with 1381 additions and 53 deletions
+5 -1
View File
@@ -51,6 +51,10 @@ export const HeartbeatModel = z.object({
interval: z.number().int().positive().default(30),
})
export const LoopDetectionModel = z.object({
enabled: z.boolean().default(false),
}).optional()
export const AttachmentModel = z.object({
contentHash: z.string().optional(),
type: z.string().min(1, 'Attachment type is required'),
@@ -93,4 +97,4 @@ export const InboxItemModel = z.object({
header: z.record(z.string(), z.unknown()).default({}),
content: z.string().default(''),
createdAt: z.string(),
})
})
+12 -6
View File
@@ -3,7 +3,7 @@ import z from 'zod'
import { createAgent, ModelConfig, allActions } from '@memoh/agent'
import { createAuthFetcher, getBaseUrl } from '../index'
import { bearerMiddleware } from '../middlewares/bearer'
import { AgentSkillModel, AllowedActionModel, AttachmentModel, HeartbeatModel, IdentityContextModel, InboxItemModel, MCPConnectionModel, ModelConfigModel, ScheduleModel } from '../models'
import { AgentSkillModel, AllowedActionModel, AttachmentModel, HeartbeatModel, IdentityContextModel, InboxItemModel, LoopDetectionModel, MCPConnectionModel, ModelConfigModel, ScheduleModel } from '../models'
import { sseChunked } from '../utils/sse'
const AgentModel = z.object({
@@ -19,6 +19,7 @@ const AgentModel = z.object({
attachments: z.array(AttachmentModel).optional().default([]),
mcpConnections: z.array(MCPConnectionModel).optional().default([]),
inbox: z.array(InboxItemModel).optional().default([]),
loopDetection: LoopDetectionModel,
})
export const chatModule = new Elysia({ prefix: '/chat' })
@@ -41,6 +42,7 @@ export const chatModule = new Elysia({ prefix: '/chat' })
skills: body.usableSkills,
mcpConnections: body.mcpConnections,
inbox: body.inbox,
loopDetection: body.loopDetection,
}, authFetcher)
return ask({
query: body.query,
@@ -72,6 +74,7 @@ export const chatModule = new Elysia({ prefix: '/chat' })
skills: body.usableSkills,
mcpConnections: body.mcpConnections,
inbox: body.inbox,
loopDetection: body.loopDetection,
}, authFetcher)
for await (const action of stream({
query: body.query,
@@ -113,6 +116,7 @@ export const chatModule = new Elysia({ prefix: '/chat' })
skills: body.usableSkills,
mcpConnections: body.mcpConnections,
inbox: body.inbox,
loopDetection: body.loopDetection,
}, authFetcher)
return triggerSchedule({
schedule: body.schedule,
@@ -126,20 +130,22 @@ export const chatModule = new Elysia({ prefix: '/chat' })
})
.post('/trigger-heartbeat', async ({ body, bearer }) => {
console.log('trigger-heartbeat', body)
const authFetcher = createAuthFetcher(bearer)
const auth = {
bearer: bearer!,
baseUrl: getBaseUrl(),
}
const authFetcher = createAuthFetcher(auth)
const { triggerHeartbeat } = createAgent({
model: body.model as ModelConfig,
activeContextTime: body.activeContextTime,
channels: body.channels,
currentChannel: body.currentChannel,
identity: body.identity,
auth: {
bearer: bearer!,
baseUrl: getBaseUrl(),
},
auth,
skills: body.usableSkills,
mcpConnections: body.mcpConnections,
inbox: body.inbox,
loopDetection: body.loopDetection,
}, authFetcher)
return triggerHeartbeat({
heartbeat: body.heartbeat,