@swarmclawai/swarmclaw 0.9.3 → 0.9.4
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/README.md +12 -10
- package/bundled-skills/google-workspace/SKILL.md +2 -0
- package/package.json +1 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +1 -1
- package/src/app/api/clawhub/install/route.ts +2 -0
- package/src/app/api/skills/[id]/route.ts +4 -0
- package/src/app/api/skills/route.ts +4 -0
- package/src/components/agents/agent-sheet.tsx +5 -5
- package/src/lib/server/agents/agent-thread-session.test.ts +64 -0
- package/src/lib/server/agents/agent-thread-session.ts +1 -1
- package/src/lib/server/agents/main-agent-loop-advanced.test.ts +77 -0
- package/src/lib/server/agents/main-agent-loop.ts +259 -0
- package/src/lib/server/agents/orchestrator-lg.ts +12 -8
- package/src/lib/server/agents/orchestrator.ts +11 -7
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +11 -10
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +116 -3
- package/src/lib/server/chat-execution/chat-execution.ts +74 -26
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +65 -30
- package/src/lib/server/chat-execution/stream-agent-chat.ts +69 -25
- package/src/lib/server/chatrooms/chatroom-helpers.test.ts +26 -0
- package/src/lib/server/chatrooms/chatroom-helpers.ts +11 -8
- package/src/lib/server/connectors/contact-boundaries.ts +101 -0
- package/src/lib/server/connectors/manager.test.ts +504 -73
- package/src/lib/server/connectors/manager.ts +40 -9
- package/src/lib/server/connectors/session-consolidation.ts +2 -0
- package/src/lib/server/connectors/session-kind.ts +7 -0
- package/src/lib/server/connectors/session.test.ts +104 -0
- package/src/lib/server/connectors/session.ts +5 -2
- package/src/lib/server/identity-continuity.test.ts +4 -3
- package/src/lib/server/identity-continuity.ts +8 -4
- package/src/lib/server/memory/session-archive-memory.ts +2 -1
- package/src/lib/server/session-reset-policy.test.ts +17 -3
- package/src/lib/server/session-reset-policy.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +11 -10
- package/src/lib/server/session-tools/crud.ts +41 -7
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/manage-skills.test.ts +194 -0
- package/src/lib/server/session-tools/memory.ts +12 -23
- package/src/lib/server/session-tools/skill-runtime.test.ts +175 -0
- package/src/lib/server/session-tools/skill-runtime.ts +382 -0
- package/src/lib/server/session-tools/skills.ts +575 -0
- package/src/lib/server/skills/runtime-skill-resolver.test.ts +162 -0
- package/src/lib/server/skills/runtime-skill-resolver.ts +750 -0
- package/src/lib/server/skills/skill-discovery.ts +4 -0
- package/src/lib/server/skills/skills-normalize.test.ts +28 -0
- package/src/lib/server/skills/skills-normalize.ts +93 -1
- package/src/lib/server/storage.ts +1 -1
- package/src/lib/server/tasks/task-followups.test.ts +124 -0
- package/src/lib/server/tasks/task-followups.ts +88 -13
- package/src/types/index.ts +26 -2
|
@@ -9,8 +9,7 @@ import { loadSettings, loadAgents, loadSkills, appendUsage } from '@/lib/server/
|
|
|
9
9
|
import { estimateCost, buildPluginDefinitionCosts } from '@/lib/server/cost'
|
|
10
10
|
import { getPluginManager } from '@/lib/server/plugins'
|
|
11
11
|
import { loadRuntimeSettings, getAgentLoopRecursionLimit } from '@/lib/server/runtime/runtime-settings'
|
|
12
|
-
import {
|
|
13
|
-
import { buildDiscoveredSkillPromptText, collectPluginMatchedDiscoveredSkills } from '@/lib/server/skills/discovered-skill-prompt'
|
|
12
|
+
import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
|
|
14
13
|
|
|
15
14
|
import { logExecution } from '@/lib/server/execution-log'
|
|
16
15
|
import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
|
|
@@ -22,6 +21,7 @@ import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
|
|
|
22
21
|
import { resolveActiveProjectContext } from '@/lib/server/project-context'
|
|
23
22
|
import { resolveImagePath } from '@/lib/server/resolve-image'
|
|
24
23
|
import { routeTaskIntent } from '@/lib/server/capability-router'
|
|
24
|
+
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
25
25
|
import {
|
|
26
26
|
getEnabledToolPlanningView,
|
|
27
27
|
getFirstToolForCapability,
|
|
@@ -97,6 +97,30 @@ export {
|
|
|
97
97
|
resolveContinuationAssistantText,
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
const TOOL_SUMMARY_SHORT_RESPONSE_EXEMPT_TOOLS = new Set([
|
|
101
|
+
'use_skill',
|
|
102
|
+
])
|
|
103
|
+
|
|
104
|
+
export function shouldSkipToolSummaryForShortResponse(params: {
|
|
105
|
+
fullText: string
|
|
106
|
+
toolEvents: MessageToolEvent[]
|
|
107
|
+
isConnectorSession?: boolean
|
|
108
|
+
}): boolean {
|
|
109
|
+
if (params.isConnectorSession) return false
|
|
110
|
+
if (!params.fullText.trim()) return false
|
|
111
|
+
if (!Array.isArray(params.toolEvents) || params.toolEvents.length === 0) return false
|
|
112
|
+
const toolNames = Array.from(new Set(
|
|
113
|
+
params.toolEvents
|
|
114
|
+
.map((event) => canonicalizePluginId(event.name) || event.name)
|
|
115
|
+
.filter((name): name is string => typeof name === 'string' && name.trim().length > 0),
|
|
116
|
+
))
|
|
117
|
+
if (toolNames.length === 0) return false
|
|
118
|
+
// Skill runtime tools load guidance into context rather than producing external
|
|
119
|
+
// evidence that needs a forced synthesis pass. A short exact answer after those
|
|
120
|
+
// calls can already be the correct completion.
|
|
121
|
+
return toolNames.every((toolName) => TOOL_SUMMARY_SHORT_RESPONSE_EXEMPT_TOOLS.has(toolName))
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
101
125
|
interface StreamAgentChatOpts {
|
|
102
126
|
session: Session
|
|
@@ -380,6 +404,21 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
380
404
|
'Do not use `manage_tasks`, `manage_agents`, or `delegate` as a substitute for a direct memory write or recall step.',
|
|
381
405
|
)
|
|
382
406
|
}
|
|
407
|
+
if (hasTooling) {
|
|
408
|
+
parts.push(
|
|
409
|
+
'## Skill Runtime',
|
|
410
|
+
'When the skill runtime section lists a fitting reusable workflow, use `use_skill` to select it before falling back to generic exploration.',
|
|
411
|
+
'Prefer `use_skill` action `run` for executable skills and `use_skill` action `load` only when the skill is guidance-only.',
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
if (opts.enabledPlugins.some((toolId) => (canonicalizePluginId(toolId) || toolId) === 'manage_skills')) {
|
|
415
|
+
parts.push(
|
|
416
|
+
'## Skill Resolution',
|
|
417
|
+
'When you are blocked on a missing capability, binary, or environment setup, call `manage_skills` before repeating generic exploration.',
|
|
418
|
+
'Use `manage_skills` action `recommend_for_task` or `status` to find a fitting local skill. If a fitting skill needs installation, request the explicit install approval through `manage_skills` and stop retrying the same blocker.',
|
|
419
|
+
'Do not silently install skills during autonomous runs. Installation is explicit and approval-gated.',
|
|
420
|
+
)
|
|
421
|
+
}
|
|
383
422
|
if (opts.hasAttachmentContext) {
|
|
384
423
|
parts.push(
|
|
385
424
|
'## Attachments',
|
|
@@ -492,7 +531,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
492
531
|
async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
|
|
493
532
|
const startTs = Date.now()
|
|
494
533
|
const { session, message, imagePath, imageUrl, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
|
|
495
|
-
const isConnectorSession =
|
|
534
|
+
const isConnectorSession = isDirectConnectorSession(session)
|
|
496
535
|
const rawPlugins = Array.isArray(session.plugins) ? session.plugins : []
|
|
497
536
|
const hasShellCapability = rawPlugins.some((toolId) => ['shell', 'execute_command'].includes(String(toolId)))
|
|
498
537
|
const sessionPlugins = expandPluginIds([
|
|
@@ -582,27 +621,16 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
582
621
|
if (continuityBlock) promptParts.push(continuityBlock)
|
|
583
622
|
if (agent?.soul) promptParts.push(agent.soul)
|
|
584
623
|
if (agent?.systemPrompt) promptParts.push(agent.systemPrompt)
|
|
585
|
-
const allSkills = loadSkills()
|
|
586
|
-
if (agent?.skillIds?.length) {
|
|
587
|
-
const skillPromptText = buildSkillPromptText(allSkills, agent.skillIds)
|
|
588
|
-
if (skillPromptText) promptParts.push(skillPromptText)
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Auto-discover workspace/bundled skills. If one matches an enabled plugin,
|
|
592
|
-
// inject the full skill content so the agent can use that tool more precisely.
|
|
593
624
|
try {
|
|
594
|
-
const
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
.join('\n')
|
|
604
|
-
if (discoveredBlock) promptParts.push(`## Available Skills\n${discoveredBlock}`)
|
|
605
|
-
}
|
|
625
|
+
const allSkills = loadSkills()
|
|
626
|
+
const runtimeSkills = resolveRuntimeSkills({
|
|
627
|
+
cwd: session.cwd,
|
|
628
|
+
enabledPlugins: sessionPlugins,
|
|
629
|
+
agentSkillIds: agent?.skillIds || [],
|
|
630
|
+
storedSkills: allSkills,
|
|
631
|
+
selectedSkillId: session.skillRuntimeState?.selectedSkillId || null,
|
|
632
|
+
})
|
|
633
|
+
promptParts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
|
|
606
634
|
} catch { /* non-critical */ }
|
|
607
635
|
}
|
|
608
636
|
}
|
|
@@ -1555,7 +1583,16 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1555
1583
|
// However, if tools already produced results but the model has no/trivial text,
|
|
1556
1584
|
// we attempt a tool_summary continuation instead of just erroring out.
|
|
1557
1585
|
if (loopDetectionTriggered) {
|
|
1558
|
-
const
|
|
1586
|
+
const skipToolSummaryForShortResponse = shouldSkipToolSummaryForShortResponse({
|
|
1587
|
+
fullText,
|
|
1588
|
+
toolEvents: streamedToolEvents,
|
|
1589
|
+
isConnectorSession,
|
|
1590
|
+
})
|
|
1591
|
+
const loopTextIsTrivial = !fullText.trim() || (
|
|
1592
|
+
!skipToolSummaryForShortResponse
|
|
1593
|
+
&& fullText.trim().length < 150
|
|
1594
|
+
&& streamedToolEvents.length >= 2
|
|
1595
|
+
)
|
|
1559
1596
|
if (loopTextIsTrivial && streamedToolEvents.length > 0 && toolSummaryRetryCount < MAX_TOOL_SUMMARY_RETRIES) {
|
|
1560
1597
|
// Override: let the tool_summary check below handle it instead of breaking
|
|
1561
1598
|
loopDetectionTriggered = null
|
|
@@ -1715,8 +1752,14 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1715
1752
|
// Triggers when: (a) text is empty, or (b) text is trivially short (< 150 chars)
|
|
1716
1753
|
// and multiple tools ran — the agent likely emitted a "I'll do X" preamble but
|
|
1717
1754
|
// never synthesized the tool outputs into a real response.
|
|
1755
|
+
const skipToolSummaryForShortResponse = shouldSkipToolSummaryForShortResponse({
|
|
1756
|
+
fullText,
|
|
1757
|
+
toolEvents: streamedToolEvents,
|
|
1758
|
+
isConnectorSession,
|
|
1759
|
+
})
|
|
1718
1760
|
const textIsTrivial = !fullText.trim() || (
|
|
1719
|
-
!
|
|
1761
|
+
!skipToolSummaryForShortResponse
|
|
1762
|
+
&& !isConnectorSession && fullText.trim().length < 150
|
|
1720
1763
|
&& (
|
|
1721
1764
|
streamedToolEvents.length >= 2
|
|
1722
1765
|
|| likelyResearchSynthesisTask
|
|
@@ -1728,6 +1771,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1728
1771
|
&& hasToolCalls
|
|
1729
1772
|
&& textIsTrivial
|
|
1730
1773
|
&& streamedToolEvents.length > 0
|
|
1774
|
+
&& !skipToolSummaryForShortResponse
|
|
1731
1775
|
&& toolSummaryRetryCount < MAX_TOOL_SUMMARY_RETRIES
|
|
1732
1776
|
) {
|
|
1733
1777
|
shouldContinue = 'tool_summary'
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
1
4
|
import { describe, it } from 'node:test'
|
|
2
5
|
import assert from 'node:assert/strict'
|
|
3
6
|
import type { Agent, Chatroom } from '@/types'
|
|
@@ -9,6 +12,7 @@ import {
|
|
|
9
12
|
resolveChatroomWorkspaceDir,
|
|
10
13
|
resolveAgentApiEndpoint,
|
|
11
14
|
resolveReplyTargetAgentId,
|
|
15
|
+
buildAgentSystemPromptForChatroom,
|
|
12
16
|
} from '@/lib/server/chatrooms/chatroom-helpers'
|
|
13
17
|
|
|
14
18
|
function makeAgents(): Record<string, Agent> {
|
|
@@ -163,4 +167,26 @@ describe('chatroom-helpers', () => {
|
|
|
163
167
|
assert.equal(cwd, resolveChatroomWorkspaceDir('room-safe'))
|
|
164
168
|
assert.match(cwd, /chatrooms[\/\\]room-safe$/)
|
|
165
169
|
})
|
|
170
|
+
|
|
171
|
+
it('includes discoverable local skills in chatroom prompts even when none are pinned', () => {
|
|
172
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chatroom-skill-'))
|
|
173
|
+
try {
|
|
174
|
+
const skillDir = path.join(cwd, 'skills', 'chatroom-default-skill')
|
|
175
|
+
fs.mkdirSync(skillDir, { recursive: true })
|
|
176
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), `---
|
|
177
|
+
name: chatroom-default-skill
|
|
178
|
+
description: Local chatroom skill.
|
|
179
|
+
---
|
|
180
|
+
# Chatroom Default Skill
|
|
181
|
+
|
|
182
|
+
Prefer this chatroom workflow when it fits.
|
|
183
|
+
`)
|
|
184
|
+
|
|
185
|
+
const prompt = buildAgentSystemPromptForChatroom(makeAgents().default, cwd)
|
|
186
|
+
assert.match(prompt, /discoverable by default/i)
|
|
187
|
+
assert.match(prompt, /chatroom-default-skill/i)
|
|
188
|
+
} finally {
|
|
189
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
190
|
+
}
|
|
191
|
+
})
|
|
166
192
|
})
|
|
@@ -9,6 +9,7 @@ import { getProvider } from '@/lib/providers'
|
|
|
9
9
|
import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
|
|
10
10
|
import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
11
11
|
import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
|
|
12
|
+
import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
|
|
12
13
|
import type { Chatroom, ChatroomMember, Agent, Session, Message, ChatroomMessage } from '@/types'
|
|
13
14
|
|
|
14
15
|
/** Resolve API key from an agent's credentialId */
|
|
@@ -324,7 +325,7 @@ export function appendSyntheticSessionMessage(
|
|
|
324
325
|
}
|
|
325
326
|
|
|
326
327
|
/** Build agent's system prompt including skills and identity context */
|
|
327
|
-
export function buildAgentSystemPromptForChatroom(agent: Agent): string {
|
|
328
|
+
export function buildAgentSystemPromptForChatroom(agent: Agent, cwd?: string | null): string {
|
|
328
329
|
const settings = loadSettings()
|
|
329
330
|
const parts: string[] = []
|
|
330
331
|
|
|
@@ -358,13 +359,15 @@ export function buildAgentSystemPromptForChatroom(agent: Agent): string {
|
|
|
358
359
|
if (agent.systemPrompt) parts.push(`## System Prompt\n${agent.systemPrompt}`)
|
|
359
360
|
|
|
360
361
|
// 5. Skills (SwarmClaw Core)
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
362
|
+
try {
|
|
363
|
+
const runtimeSkills = resolveRuntimeSkills({
|
|
364
|
+
cwd,
|
|
365
|
+
enabledPlugins: agent.plugins || agent.tools || [],
|
|
366
|
+
agentSkillIds: agent.skillIds || [],
|
|
367
|
+
storedSkills: loadSkills(),
|
|
368
|
+
})
|
|
369
|
+
parts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
|
|
370
|
+
} catch { /* non-critical */ }
|
|
368
371
|
|
|
369
372
|
// 6. Thinking & Output Format (OpenClaw Style)
|
|
370
373
|
const thinkingHint = [
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Agent, Session, MemoryEntry, Connector } from '@/types'
|
|
2
|
+
import { getMemoryDb } from '@/lib/server/memory/memory-db'
|
|
3
|
+
import { dedup } from '@/lib/shared-utils'
|
|
4
|
+
import { isReplyToLastOutbound, textMentionsAlias } from './policy'
|
|
5
|
+
import type { InboundMessage } from './types'
|
|
6
|
+
|
|
7
|
+
function toDigits(raw: string): string {
|
|
8
|
+
const stripped = raw.replace(/@.*$/, '').replace(/[^\d]/g, '')
|
|
9
|
+
if (stripped.startsWith('0') && stripped.length >= 10) return `44${stripped.slice(1)}`
|
|
10
|
+
return stripped
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function collectSenderIds(
|
|
14
|
+
msg: InboundMessage,
|
|
15
|
+
session?: Partial<Session> | null,
|
|
16
|
+
): string[] {
|
|
17
|
+
return dedup([
|
|
18
|
+
msg.senderId,
|
|
19
|
+
msg.senderIdAlt,
|
|
20
|
+
msg.channelId,
|
|
21
|
+
msg.channelIdAlt,
|
|
22
|
+
...(Array.isArray(session?.connectorContext?.allKnownPeerIds) ? session.connectorContext.allKnownPeerIds : []),
|
|
23
|
+
].filter((value): value is string => typeof value === 'string' && value.trim().length > 0))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function memoryMatchesSender(entry: MemoryEntry, senderIds: string[], senderName: string): boolean {
|
|
27
|
+
const title = String(entry.title || '').toLowerCase()
|
|
28
|
+
const content = String(entry.content || '').toLowerCase()
|
|
29
|
+
const normalizedSenderName = senderName.trim().toLowerCase()
|
|
30
|
+
|
|
31
|
+
for (const rawId of senderIds) {
|
|
32
|
+
const lowered = rawId.toLowerCase()
|
|
33
|
+
if (lowered && (title.includes(lowered) || content.includes(lowered))) return true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const senderDigits = new Set(senderIds.map(toDigits).filter((value) => value.length >= 6))
|
|
37
|
+
const memoryDigits = [
|
|
38
|
+
...(String(entry.content || '').match(/(?:\+?\d[\d\s\-().]{6,}\d)/g) || []).map(toDigits),
|
|
39
|
+
...(Array.isArray((entry.metadata as Record<string, unknown> | undefined)?.identifiers)
|
|
40
|
+
? ((entry.metadata as Record<string, unknown>).identifiers as unknown[])
|
|
41
|
+
.filter((value): value is string => typeof value === 'string')
|
|
42
|
+
.map(toDigits)
|
|
43
|
+
: []),
|
|
44
|
+
].filter((value) => value.length >= 6)
|
|
45
|
+
|
|
46
|
+
for (const memoryDigit of memoryDigits) {
|
|
47
|
+
for (const senderDigit of senderDigits) {
|
|
48
|
+
if (senderDigit.endsWith(memoryDigit) || memoryDigit.endsWith(senderDigit)) return true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!normalizedSenderName) return false
|
|
53
|
+
return title.includes(normalizedSenderName) || content.includes(normalizedSenderName)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function memoryDefinesQuietBoundary(entry: MemoryEntry): boolean {
|
|
57
|
+
const text = `${entry.title || ''}\n${entry.content || ''}`.toLowerCase()
|
|
58
|
+
const boundaryRule = /\b(?:do not respond|do not reply|don't respond|don't reply|no replies|stay quiet|stay silent|remain quiet|be quiet)\b[\s\S]{0,140}\bunless\b/i
|
|
59
|
+
const directAddressRule = /\b(?:address(?:es|ed)?|mention(?:s|ed)?|refer(?:s|red)?|talk(?:ing)? to)\b[\s\S]{0,80}\bhal\b/i
|
|
60
|
+
const verifyRule = /\bverify whether\b[\s\S]{0,80}\b(?:wayde|hal)\b/i
|
|
61
|
+
return boundaryRule.test(text) && (directAddressRule.test(text) || verifyRule.test(text))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildDirectAddressAliases(agent: Partial<Agent> | null | undefined, connector: Partial<Connector> | null | undefined): string[] {
|
|
65
|
+
const agentName = typeof agent?.name === 'string' ? agent.name.trim() : ''
|
|
66
|
+
const connectorName = typeof connector?.name === 'string' ? connector.name.trim() : ''
|
|
67
|
+
const aliases = [agentName, connectorName]
|
|
68
|
+
const firstWord = agentName.split(/\s+/)[0] || ''
|
|
69
|
+
if (firstWord) aliases.push(firstWord)
|
|
70
|
+
if (agentName.toLowerCase().includes('hal')) aliases.push('Hal')
|
|
71
|
+
return dedup(aliases.filter(Boolean))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function enforceSenderQuietBoundary(params: {
|
|
75
|
+
agent?: Partial<Agent> | null
|
|
76
|
+
connector?: Partial<Connector> | null
|
|
77
|
+
session?: Partial<Session> | null
|
|
78
|
+
msg: InboundMessage
|
|
79
|
+
}): { suppress: boolean; memoryTitle?: string } {
|
|
80
|
+
const { agent, connector, session, msg } = params
|
|
81
|
+
if (!agent?.id || msg.isGroup) return { suppress: false }
|
|
82
|
+
|
|
83
|
+
const senderIds = collectSenderIds(msg, session)
|
|
84
|
+
const senderName = typeof msg.senderName === 'string' ? msg.senderName : ''
|
|
85
|
+
if (senderIds.length === 0 && !senderName.trim()) return { suppress: false }
|
|
86
|
+
|
|
87
|
+
const memDb = getMemoryDb()
|
|
88
|
+
const memories = memDb.list(agent.id, 200).filter((entry) =>
|
|
89
|
+
entry.category?.startsWith('identity/')
|
|
90
|
+
&& memoryMatchesSender(entry, senderIds, senderName),
|
|
91
|
+
)
|
|
92
|
+
const matchedBoundary = memories.find(memoryDefinesQuietBoundary)
|
|
93
|
+
if (!matchedBoundary) return { suppress: false }
|
|
94
|
+
|
|
95
|
+
const explicitlyAddressed = textMentionsAlias(msg.text || '', buildDirectAddressAliases(agent, connector))
|
|
96
|
+
|| isReplyToLastOutbound(msg, session)
|
|
97
|
+
|
|
98
|
+
return explicitlyAddressed
|
|
99
|
+
? { suppress: false }
|
|
100
|
+
: { suppress: true, memoryTitle: matchedBoundary.title }
|
|
101
|
+
}
|