@swarmclawai/swarmclaw 0.9.2 → 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/agents/page.tsx +2 -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/app/globals.css +28 -0
- package/src/app/home/page.tsx +11 -0
- package/src/app/settings/page.tsx +12 -5
- package/src/components/agents/agent-sheet.tsx +5 -5
- package/src/components/connectors/connector-list.tsx +2 -5
- package/src/components/logs/log-list.tsx +2 -5
- package/src/components/providers/provider-list.tsx +2 -5
- package/src/components/runs/run-list.tsx +2 -6
- package/src/components/schedules/schedule-list.tsx +7 -1
- package/src/components/ui/full-screen-loader.tsx +0 -29
- package/src/components/ui/page-loader.tsx +69 -0
- package/src/lib/runtime/runtime-loop.ts +21 -1
- 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-utils.test.ts +56 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +24 -0
- package/src/lib/server/chat-execution/chat-execution.ts +116 -29
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +1 -38
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +67 -76
- package/src/lib/server/chat-execution/stream-agent-chat.ts +119 -110
- package/src/lib/server/chat-execution/stream-continuation.ts +1 -1
- 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 +41 -10
- 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/memory-policy.test.ts +5 -15
- package/src/lib/server/memory/memory-policy.ts +11 -41
- package/src/lib/server/memory/session-archive-memory.ts +2 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +46 -0
- package/src/lib/server/runtime/heartbeat-service.ts +5 -1
- package/src/lib/server/runtime/runtime-settings.test.ts +4 -4
- package/src/lib/server/runtime/runtime-settings.ts +4 -0
- package/src/lib/server/runtime/session-run-manager.ts +2 -0
- 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/delegate.ts +3 -3
- 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 +209 -48
- 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 +30 -2
- package/src/views/settings/section-runtime-loop.tsx +38 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import { HumanMessage, AIMessage } from '@langchain/core/messages'
|
|
3
3
|
import { createReactAgent } from '@langchain/langgraph/prebuilt'
|
|
4
|
+
import { MemorySaver } from '@langchain/langgraph'
|
|
4
5
|
import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/runtime/heartbeat-defaults'
|
|
5
6
|
import { buildSessionTools } from '@/lib/server/session-tools'
|
|
6
7
|
import { buildChatModel } from '@/lib/server/build-llm'
|
|
@@ -8,8 +9,7 @@ import { loadSettings, loadAgents, loadSkills, appendUsage } from '@/lib/server/
|
|
|
8
9
|
import { estimateCost, buildPluginDefinitionCosts } from '@/lib/server/cost'
|
|
9
10
|
import { getPluginManager } from '@/lib/server/plugins'
|
|
10
11
|
import { loadRuntimeSettings, getAgentLoopRecursionLimit } from '@/lib/server/runtime/runtime-settings'
|
|
11
|
-
import {
|
|
12
|
-
import { buildDiscoveredSkillPromptText, collectPluginMatchedDiscoveredSkills } from '@/lib/server/skills/discovered-skill-prompt'
|
|
12
|
+
import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
|
|
13
13
|
|
|
14
14
|
import { logExecution } from '@/lib/server/execution-log'
|
|
15
15
|
import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
|
|
@@ -21,6 +21,7 @@ import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
|
|
|
21
21
|
import { resolveActiveProjectContext } from '@/lib/server/project-context'
|
|
22
22
|
import { resolveImagePath } from '@/lib/server/resolve-image'
|
|
23
23
|
import { routeTaskIntent } from '@/lib/server/capability-router'
|
|
24
|
+
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
24
25
|
import {
|
|
25
26
|
getEnabledToolPlanningView,
|
|
26
27
|
getFirstToolForCapability,
|
|
@@ -51,31 +52,43 @@ import {
|
|
|
51
52
|
import type { ContinuationType } from '@/lib/server/chat-execution/stream-continuation'
|
|
52
53
|
import { dedup, errorMessage, sleep } from '@/lib/shared-utils'
|
|
53
54
|
import { perf } from '@/lib/server/runtime/perf'
|
|
54
|
-
import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
|
|
55
55
|
import {
|
|
56
56
|
compactThreadRecallText,
|
|
57
57
|
getExplicitRequiredToolNames,
|
|
58
58
|
getWalletApprovalBoundaryAction,
|
|
59
|
-
isNarrowDirectMemoryWriteTurn,
|
|
60
59
|
isWalletSimulationResult,
|
|
61
60
|
resolveToolAction,
|
|
62
|
-
shouldAllowToolForDirectMemoryWrite,
|
|
63
|
-
shouldAllowToolForCurrentThreadRecall,
|
|
64
61
|
shouldForceExternalServiceSummary,
|
|
65
62
|
shouldTerminateOnSuccessfulMemoryMutation,
|
|
66
63
|
updateStreamedToolEvents,
|
|
67
64
|
} from '@/lib/server/chat-execution/chat-streaming-utils'
|
|
68
65
|
import { LangGraphToolEventTracker } from '@/lib/server/chat-execution/tool-event-tracker'
|
|
69
66
|
|
|
67
|
+
// LangGraph's streamEvents leaves dangling internal promises when the for-await
|
|
68
|
+
// loop exits early (break on tool loop detection, execution boundary, etc.).
|
|
69
|
+
// These promises may later reject with GraphRecursionError or AbortError.
|
|
70
|
+
// Register a permanent handler to prevent process crashes from these expected
|
|
71
|
+
// background rejections. Only LangGraph-specific errors (identified by
|
|
72
|
+
// pregelTaskId or lc_error_code) are suppressed; all other rejections propagate
|
|
73
|
+
// normally.
|
|
74
|
+
process.on('unhandledRejection', (err: unknown) => {
|
|
75
|
+
if (
|
|
76
|
+
err && typeof err === 'object'
|
|
77
|
+
&& ('pregelTaskId' in err
|
|
78
|
+
|| (err instanceof Error && (err.name === 'AbortError' || err.name === 'GraphRecursionError'))
|
|
79
|
+
|| (err as Record<string, unknown>).lc_error_code === 'GRAPH_RECURSION_LIMIT')
|
|
80
|
+
) {
|
|
81
|
+
// Silently suppress — expected background rejection from LangGraph
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
70
86
|
// Re-export continuation functions so existing consumers don't need to change imports
|
|
71
87
|
export {
|
|
72
88
|
getExplicitRequiredToolNames,
|
|
73
|
-
isNarrowDirectMemoryWriteTurn,
|
|
74
89
|
isWalletSimulationResult,
|
|
75
90
|
looksLikeOpenEndedDeliverableTask,
|
|
76
91
|
shouldForceRecoverableToolErrorFollowthrough,
|
|
77
|
-
shouldAllowToolForDirectMemoryWrite,
|
|
78
|
-
shouldAllowToolForCurrentThreadRecall,
|
|
79
92
|
shouldForceExternalExecutionFollowthrough,
|
|
80
93
|
shouldForceDeliverableFollowthrough,
|
|
81
94
|
shouldForceExternalServiceSummary,
|
|
@@ -84,6 +97,30 @@ export {
|
|
|
84
97
|
resolveContinuationAssistantText,
|
|
85
98
|
}
|
|
86
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
|
+
|
|
87
124
|
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
88
125
|
interface StreamAgentChatOpts {
|
|
89
126
|
session: Session
|
|
@@ -101,9 +138,6 @@ interface StreamAgentChatOpts {
|
|
|
101
138
|
|
|
102
139
|
// LangGraph uses this internal configurable key to bypass subgraph lookup when
|
|
103
140
|
// resolving state from a namespaced checkpoint. It is not exported publicly in
|
|
104
|
-
// @langchain/langgraph 1.x, so keep the literal here instead of importing a
|
|
105
|
-
// non-exported symbol that breaks Next compilation.
|
|
106
|
-
const LANGGRAPH_CHECKPOINTER_CONFIG_KEY = '__pregel_checkpointer'
|
|
107
141
|
const CONTEXT_WARNING_OVERHEAD_TOKENS = 192
|
|
108
142
|
|
|
109
143
|
/** Extract HTTP status code and Retry-After from provider error objects (OpenAI SDK, etc.) */
|
|
@@ -341,7 +375,6 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
341
375
|
const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
|
|
342
376
|
const toolDisciplineLines = buildToolDisciplineLines(opts.enabledPlugins)
|
|
343
377
|
const hasMemoryTools = opts.enabledPlugins.some((toolId) => (canonicalizePluginId(toolId) || toolId) === 'memory')
|
|
344
|
-
const directMemoryWriteOnlyTurn = Boolean(opts.userMessage && isNarrowDirectMemoryWriteTurn(opts.userMessage))
|
|
345
378
|
|
|
346
379
|
const parts: string[] = []
|
|
347
380
|
|
|
@@ -371,10 +404,21 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
371
404
|
'Do not use `manage_tasks`, `manage_agents`, or `delegate` as a substitute for a direct memory write or recall step.',
|
|
372
405
|
)
|
|
373
406
|
}
|
|
374
|
-
if (
|
|
375
|
-
parts.push(
|
|
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
|
+
)
|
|
376
421
|
}
|
|
377
|
-
|
|
378
422
|
if (opts.hasAttachmentContext) {
|
|
379
423
|
parts.push(
|
|
380
424
|
'## Attachments',
|
|
@@ -393,6 +437,7 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
393
437
|
'Execute by default — only confirm on high-risk actions.',
|
|
394
438
|
'If a tool errors, retry or explain the blocker. Never claim success without evidence.',
|
|
395
439
|
'Keep responses concise. Bullet points over prose. After file operations, confirm the result briefly (path and status) without echoing the full file contents.',
|
|
440
|
+
'Do not end every reply with a question. Only ask when a specific missing detail blocks progress. When a task is done, state the result and stop.',
|
|
396
441
|
opts.responseStyle === 'concise'
|
|
397
442
|
? `IMPORTANT: Be extremely concise.${opts.responseMaxChars ? ` Keep responses under ${opts.responseMaxChars} characters.` : ' Target under 500 characters.'} Lead with the answer, skip preamble.`
|
|
398
443
|
: opts.responseStyle === 'detailed'
|
|
@@ -458,16 +503,6 @@ function buildCurrentThreadRecallBlock(history: Message[]): string {
|
|
|
458
503
|
return lines.join('\n')
|
|
459
504
|
}
|
|
460
505
|
|
|
461
|
-
function buildDirectMemoryWriteBlock(): string {
|
|
462
|
-
return [
|
|
463
|
-
'## Direct Memory Write',
|
|
464
|
-
'This turn is a direct request to remember, store, or correct a durable fact.',
|
|
465
|
-
'Call `memory_store` or `memory_update` immediately, then confirm the stored value succinctly.',
|
|
466
|
-
'If the user bundled several related facts into one remember request, store them together in one canonical memory write unless they explicitly asked for separate entries.',
|
|
467
|
-
'Do not inspect skills, browse the workspace, request capabilities, manage tasks, manage agents, or delegate before the direct memory write is complete.',
|
|
468
|
-
].join('\n')
|
|
469
|
-
}
|
|
470
|
-
|
|
471
506
|
export interface StreamAgentChatResult {
|
|
472
507
|
/** All text accumulated across every LLM turn (for SSE / web UI history). */
|
|
473
508
|
fullText: string
|
|
@@ -496,7 +531,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
496
531
|
async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
|
|
497
532
|
const startTs = Date.now()
|
|
498
533
|
const { session, message, imagePath, imageUrl, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
|
|
499
|
-
const isConnectorSession =
|
|
534
|
+
const isConnectorSession = isDirectConnectorSession(session)
|
|
500
535
|
const rawPlugins = Array.isArray(session.plugins) ? session.plugins : []
|
|
501
536
|
const hasShellCapability = rawPlugins.some((toolId) => ['shell', 'execute_command'].includes(String(toolId)))
|
|
502
537
|
const sessionPlugins = expandPluginIds([
|
|
@@ -543,8 +578,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
543
578
|
|
|
544
579
|
const promptParts: string[] = []
|
|
545
580
|
const hasProvidedSystemPrompt = typeof systemPrompt === 'string' && systemPrompt.trim().length > 0
|
|
546
|
-
const
|
|
547
|
-
const currentThreadRecallRequest = !directMemoryWriteOnlyTurn && isCurrentThreadRecallRequest(message)
|
|
581
|
+
const currentThreadRecallRequest = isCurrentThreadRecallRequest(message)
|
|
548
582
|
const hasAttachmentContext = Boolean(
|
|
549
583
|
imagePath
|
|
550
584
|
|| attachedFiles?.length
|
|
@@ -587,27 +621,16 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
587
621
|
if (continuityBlock) promptParts.push(continuityBlock)
|
|
588
622
|
if (agent?.soul) promptParts.push(agent.soul)
|
|
589
623
|
if (agent?.systemPrompt) promptParts.push(agent.systemPrompt)
|
|
590
|
-
const allSkills = loadSkills()
|
|
591
|
-
if (agent?.skillIds?.length) {
|
|
592
|
-
const skillPromptText = buildSkillPromptText(allSkills, agent.skillIds)
|
|
593
|
-
if (skillPromptText) promptParts.push(skillPromptText)
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Auto-discover workspace/bundled skills. If one matches an enabled plugin,
|
|
597
|
-
// inject the full skill content so the agent can use that tool more precisely.
|
|
598
624
|
try {
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
.join('\n')
|
|
609
|
-
if (discoveredBlock) promptParts.push(`## Available Skills\n${discoveredBlock}`)
|
|
610
|
-
}
|
|
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))
|
|
611
634
|
} catch { /* non-critical */ }
|
|
612
635
|
}
|
|
613
636
|
}
|
|
@@ -653,8 +676,8 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
653
676
|
try {
|
|
654
677
|
const pluginContextParts = await getPluginManager().collectAgentContext(session, sessionPlugins, message, history)
|
|
655
678
|
promptParts.push(...pluginContextParts)
|
|
656
|
-
} catch {
|
|
657
|
-
|
|
679
|
+
} catch (err: unknown) {
|
|
680
|
+
console.error('[stream-agent-chat] Plugin context injection failed:', err instanceof Error ? err.message : String(err))
|
|
658
681
|
}
|
|
659
682
|
|
|
660
683
|
if (!hasProvidedSystemPrompt && activeProjectContext.projectId) {
|
|
@@ -757,7 +780,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
757
780
|
|
|
758
781
|
// Proactive memory recall: inject relevant memories into context before LLM invocation
|
|
759
782
|
// Skips heartbeat polls, very short messages, and thread-recall requests (which use chat history instead)
|
|
760
|
-
if (session.agentId && !
|
|
783
|
+
if (session.agentId && !currentThreadRecallRequest && message.length > 12) {
|
|
761
784
|
try {
|
|
762
785
|
const agents = loadAgents()
|
|
763
786
|
const agentForMemory = agents[session.agentId]
|
|
@@ -794,23 +817,6 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
794
817
|
memoryScopeMode: agentMemoryScopeMode,
|
|
795
818
|
})
|
|
796
819
|
endToolBuildPerf({ toolCount: tools.length })
|
|
797
|
-
const toolsForTurn = currentThreadRecallRequest
|
|
798
|
-
? tools.filter((tool) => {
|
|
799
|
-
const toolName = typeof (tool as { name?: unknown }).name === 'string'
|
|
800
|
-
? String((tool as { name?: unknown }).name)
|
|
801
|
-
: ''
|
|
802
|
-
return shouldAllowToolForCurrentThreadRecall(toolName)
|
|
803
|
-
})
|
|
804
|
-
: directMemoryWriteOnlyTurn
|
|
805
|
-
? tools.filter((tool) => {
|
|
806
|
-
const toolName = typeof (tool as { name?: unknown }).name === 'string'
|
|
807
|
-
? String((tool as { name?: unknown }).name)
|
|
808
|
-
: ''
|
|
809
|
-
return shouldAllowToolForDirectMemoryWrite(toolName)
|
|
810
|
-
})
|
|
811
|
-
: tools
|
|
812
|
-
const checkpointNamespace = `chat:${startTs}`
|
|
813
|
-
const checkpointSaver = getCheckpointSaver()
|
|
814
820
|
const recursionLimit = getAgentLoopRecursionLimit(runtime)
|
|
815
821
|
|
|
816
822
|
// Build message history for context
|
|
@@ -997,11 +1003,17 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
997
1003
|
// Warning failure is non-critical
|
|
998
1004
|
}
|
|
999
1005
|
|
|
1006
|
+
// Use a fresh in-memory checkpointer instead of the SQLite one. We manage
|
|
1007
|
+
// conversation history externally via langchainMessages — each iteration
|
|
1008
|
+
// receives full history, so no cross-iteration checkpoint state is needed.
|
|
1009
|
+
// MemorySaver avoids the SQLite serde round-trip that dropped tool_call IDs
|
|
1010
|
+
// or ToolMessages, causing OpenAI to reject with "tool_calls must be
|
|
1011
|
+
// followed by tool messages" errors.
|
|
1000
1012
|
const agent = createReactAgent({
|
|
1001
1013
|
llm,
|
|
1002
|
-
tools
|
|
1014
|
+
tools,
|
|
1003
1015
|
prompt,
|
|
1004
|
-
checkpointer:
|
|
1016
|
+
checkpointer: new MemorySaver(),
|
|
1005
1017
|
})
|
|
1006
1018
|
|
|
1007
1019
|
const langchainMessages: Array<HumanMessage | AIMessage> = []
|
|
@@ -1018,7 +1030,6 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1018
1030
|
const currentContent = await buildLangChainContent(message, imagePath, attachedFiles)
|
|
1019
1031
|
langchainMessages.push(new HumanMessage({ content: currentContent }))
|
|
1020
1032
|
let pendingGraphMessages = [...langchainMessages]
|
|
1021
|
-
let currentCheckpointId: string | undefined
|
|
1022
1033
|
|
|
1023
1034
|
let fullText = ''
|
|
1024
1035
|
let lastSegment = ''
|
|
@@ -1073,7 +1084,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1073
1084
|
MAX_TOOL_SUMMARY_RETRIES = 1
|
|
1074
1085
|
MAX_UNFINISHED_TOOL_FOLLOWTHROUGHS = 1
|
|
1075
1086
|
}
|
|
1076
|
-
const REQUIRED_TOOL_KICKOFF_TIMEOUT_MS =
|
|
1087
|
+
const REQUIRED_TOOL_KICKOFF_TIMEOUT_MS = runtime.requiredToolKickoffMs
|
|
1077
1088
|
let autoContinueCount = 0
|
|
1078
1089
|
let transientRetryCount = 0
|
|
1079
1090
|
let pendingRetryAfterMs: number | null = null
|
|
@@ -1154,7 +1165,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1154
1165
|
idleTimer = setTimeout(() => {
|
|
1155
1166
|
idleTimedOut = true
|
|
1156
1167
|
iterationController.abort()
|
|
1157
|
-
},
|
|
1168
|
+
}, runtime.streamIdleStallMs)
|
|
1158
1169
|
}
|
|
1159
1170
|
|
|
1160
1171
|
const armRequiredToolKickoff = () => {
|
|
@@ -1175,23 +1186,21 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1175
1186
|
const toolPerfEnds = new Map<string, (extra?: Record<string, unknown>) => number>()
|
|
1176
1187
|
const iterationInputMessages = pendingGraphMessages
|
|
1177
1188
|
let iterationSucceeded = false
|
|
1189
|
+
const eventStream = agent.streamEvents(
|
|
1190
|
+
{ messages: iterationInputMessages },
|
|
1191
|
+
{
|
|
1192
|
+
version: 'v2',
|
|
1193
|
+
recursionLimit,
|
|
1194
|
+
signal: iterationController.signal,
|
|
1195
|
+
configurable: {
|
|
1196
|
+
thread_id: `${session.id}:${startTs}:${iteration}`,
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
)
|
|
1178
1200
|
|
|
1179
1201
|
try {
|
|
1180
1202
|
armIdleWatchdog()
|
|
1181
1203
|
armRequiredToolKickoff()
|
|
1182
|
-
const eventStream = agent.streamEvents(
|
|
1183
|
-
{ messages: iterationInputMessages },
|
|
1184
|
-
{
|
|
1185
|
-
version: 'v2',
|
|
1186
|
-
recursionLimit,
|
|
1187
|
-
signal: iterationController.signal,
|
|
1188
|
-
configurable: {
|
|
1189
|
-
thread_id: session.id,
|
|
1190
|
-
checkpoint_ns: checkpointNamespace,
|
|
1191
|
-
...(currentCheckpointId ? { checkpoint_id: currentCheckpointId } : {}),
|
|
1192
|
-
},
|
|
1193
|
-
},
|
|
1194
|
-
)
|
|
1195
1204
|
|
|
1196
1205
|
for await (const event of eventStream) {
|
|
1197
1206
|
const kind = event.event
|
|
@@ -1434,7 +1443,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1434
1443
|
} catch (innerErr: unknown) {
|
|
1435
1444
|
const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
|
|
1436
1445
|
const errMsg = idleTimedOut
|
|
1437
|
-
?
|
|
1446
|
+
? `Model stream stalled without emitting text or tool results for ${Math.trunc(runtime.streamIdleStallMs / 1000)} seconds.`
|
|
1438
1447
|
: requiredToolKickoffTimedOut
|
|
1439
1448
|
? `The turn did not start the required workspace tool step within ${Math.trunc(REQUIRED_TOOL_KICKOFF_TIMEOUT_MS / 1000)} seconds.`
|
|
1440
1449
|
: errorMessage(innerErr)
|
|
@@ -1539,24 +1548,6 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1539
1548
|
abortController.signal.removeEventListener('abort', onParentAbort)
|
|
1540
1549
|
}
|
|
1541
1550
|
|
|
1542
|
-
if (iterationSucceeded) {
|
|
1543
|
-
try {
|
|
1544
|
-
const state = await agent.getState({
|
|
1545
|
-
configurable: {
|
|
1546
|
-
thread_id: session.id,
|
|
1547
|
-
checkpoint_ns: checkpointNamespace,
|
|
1548
|
-
[LANGGRAPH_CHECKPOINTER_CONFIG_KEY]: checkpointSaver,
|
|
1549
|
-
},
|
|
1550
|
-
}) as { config?: { configurable?: { checkpoint_id?: unknown } } } | null
|
|
1551
|
-
const latestCheckpointId = state?.config?.configurable?.checkpoint_id
|
|
1552
|
-
if (typeof latestCheckpointId === 'string' && latestCheckpointId.trim()) {
|
|
1553
|
-
currentCheckpointId = latestCheckpointId
|
|
1554
|
-
}
|
|
1555
|
-
} catch (checkpointErr) {
|
|
1556
|
-
console.warn('[stream-agent-chat] Failed to refresh latest checkpoint state:', errorMessage(checkpointErr))
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
1551
|
if (reachedExecutionBoundary) break
|
|
1561
1552
|
|
|
1562
1553
|
if (!shouldContinue
|
|
@@ -1592,7 +1583,16 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1592
1583
|
// However, if tools already produced results but the model has no/trivial text,
|
|
1593
1584
|
// we attempt a tool_summary continuation instead of just erroring out.
|
|
1594
1585
|
if (loopDetectionTriggered) {
|
|
1595
|
-
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
|
+
)
|
|
1596
1596
|
if (loopTextIsTrivial && streamedToolEvents.length > 0 && toolSummaryRetryCount < MAX_TOOL_SUMMARY_RETRIES) {
|
|
1597
1597
|
// Override: let the tool_summary check below handle it instead of breaking
|
|
1598
1598
|
loopDetectionTriggered = null
|
|
@@ -1752,8 +1752,14 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1752
1752
|
// Triggers when: (a) text is empty, or (b) text is trivially short (< 150 chars)
|
|
1753
1753
|
// and multiple tools ran — the agent likely emitted a "I'll do X" preamble but
|
|
1754
1754
|
// never synthesized the tool outputs into a real response.
|
|
1755
|
+
const skipToolSummaryForShortResponse = shouldSkipToolSummaryForShortResponse({
|
|
1756
|
+
fullText,
|
|
1757
|
+
toolEvents: streamedToolEvents,
|
|
1758
|
+
isConnectorSession,
|
|
1759
|
+
})
|
|
1755
1760
|
const textIsTrivial = !fullText.trim() || (
|
|
1756
|
-
!
|
|
1761
|
+
!skipToolSummaryForShortResponse
|
|
1762
|
+
&& !isConnectorSession && fullText.trim().length < 150
|
|
1757
1763
|
&& (
|
|
1758
1764
|
streamedToolEvents.length >= 2
|
|
1759
1765
|
|| likelyResearchSynthesisTask
|
|
@@ -1765,6 +1771,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1765
1771
|
&& hasToolCalls
|
|
1766
1772
|
&& textIsTrivial
|
|
1767
1773
|
&& streamedToolEvents.length > 0
|
|
1774
|
+
&& !skipToolSummaryForShortResponse
|
|
1768
1775
|
&& toolSummaryRetryCount < MAX_TOOL_SUMMARY_RETRIES
|
|
1769
1776
|
) {
|
|
1770
1777
|
shouldContinue = 'tool_summary'
|
|
@@ -1808,7 +1815,9 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1808
1815
|
const promptMessage = new HumanMessage({ content: continuationPrompt })
|
|
1809
1816
|
langchainMessages.push(promptMessage)
|
|
1810
1817
|
continuationMessages.push(promptMessage)
|
|
1811
|
-
|
|
1818
|
+
// Provide full conversation history since the agent has no checkpointer
|
|
1819
|
+
// and each iteration starts with only the messages we explicitly pass.
|
|
1820
|
+
pendingGraphMessages = [...langchainMessages]
|
|
1812
1821
|
lastSegment = ''
|
|
1813
1822
|
} else if (shouldContinue === 'transient') {
|
|
1814
1823
|
// Exponential backoff before retrying transient errors; respect Retry-After if present
|
|
@@ -1896,7 +1905,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1896
1905
|
const totalTokens = totalInputTokens + totalOutputTokens
|
|
1897
1906
|
if (totalTokens > 0) {
|
|
1898
1907
|
const cost = estimateCost(session.model, totalInputTokens, totalOutputTokens)
|
|
1899
|
-
const pluginDefinitionCosts = buildPluginDefinitionCosts(
|
|
1908
|
+
const pluginDefinitionCosts = buildPluginDefinitionCosts(tools, toolToPluginMap)
|
|
1900
1909
|
const usageRecord: UsageRecord = {
|
|
1901
1910
|
sessionId: session.id,
|
|
1902
1911
|
messageIndex: history.length,
|
|
@@ -100,7 +100,7 @@ function looksLikeIncompleteDeliverableResponse(text: string): boolean {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const ARTIFACT_PATH_EXT_RE = /\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|ts|tsx|js|jsx|mjs|cjs|py|sql|sh)$/i
|
|
103
|
-
const EXPLICIT_ARTIFACT_OUTPUT_RE = /\b(?:save|write|output|export)\b[^.!?\n]{0,80}\b(?:to|as|at|in)\b[^.!?\n]{0,60}(\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|~\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|\.\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|[a-z0-9._/-]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)\b)/i
|
|
103
|
+
const EXPLICIT_ARTIFACT_OUTPUT_RE = /\b(?:save|write|output|export|create|generate)\b[^.!?\n]{0,80}\b(?:to|as|at|in)\b[^.!?\n]{0,60}(\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|~\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|\.\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|[a-z0-9._/-]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)\b)/i
|
|
104
104
|
|
|
105
105
|
function hasExplicitFileOutputRequest(text: string): boolean {
|
|
106
106
|
const normalized = text.toLowerCase()
|
|
@@ -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
|
+
}
|