@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
|
@@ -30,6 +30,7 @@ import { markProviderFailure, markProviderSuccess } from '../provider-health'
|
|
|
30
30
|
import { syncSessionArchiveMemory } from '@/lib/server/memory/session-archive-memory'
|
|
31
31
|
import { buildIdentityContinuityContext } from '../identity-continuity'
|
|
32
32
|
import { ensureAgentThreadSession } from '@/lib/server/agents/agent-thread-session'
|
|
33
|
+
import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
|
|
33
34
|
import { getProvider } from '@/lib/providers'
|
|
34
35
|
import type { Agent, Connector, MessageSource, Chatroom, ChatroomMessage, Session } from '@/types'
|
|
35
36
|
import type { ConnectorInstance, InboundMessage } from './types'
|
|
@@ -57,6 +58,8 @@ import {
|
|
|
57
58
|
textMentionsAlias,
|
|
58
59
|
} from './policy'
|
|
59
60
|
import { buildConnectorThreadContextBlock } from './thread-context'
|
|
61
|
+
import { isDirectConnectorSession } from './session-kind'
|
|
62
|
+
import { enforceSenderQuietBoundary } from './contact-boundaries'
|
|
60
63
|
import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from '@/lib/server/agents/assistant-control'
|
|
61
64
|
import {
|
|
62
65
|
buildInboundApprovalSubject as buildInboundApprovalSubjectHelper,
|
|
@@ -1198,7 +1201,7 @@ async function routeMessageToChatroom(connector: Connector, msg: InboundMessage)
|
|
|
1198
1201
|
}
|
|
1199
1202
|
|
|
1200
1203
|
const syntheticSession = ensureSyntheticSession(agent, chatroomId)
|
|
1201
|
-
const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
|
|
1204
|
+
const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent, syntheticSession.cwd)
|
|
1202
1205
|
const chatroomContext = buildChatroomSystemPrompt(freshChatroom, agents, agent.id)
|
|
1203
1206
|
const fullSystemPrompt = [agentSystemPrompt, chatroomContext, threadContextBlock].filter(Boolean).join('\n\n')
|
|
1204
1207
|
const history = buildHistoryForAgent(freshChatroom, agent.id)
|
|
@@ -1418,6 +1421,26 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1418
1421
|
return NO_MESSAGE_SENTINEL
|
|
1419
1422
|
}
|
|
1420
1423
|
|
|
1424
|
+
const quietBoundary = enforceSenderQuietBoundary({
|
|
1425
|
+
agent,
|
|
1426
|
+
connector,
|
|
1427
|
+
session,
|
|
1428
|
+
msg,
|
|
1429
|
+
})
|
|
1430
|
+
if (quietBoundary.suppress) {
|
|
1431
|
+
logExecution(session.id, 'decision', 'Connector inbound suppressed by sender quiet boundary', {
|
|
1432
|
+
agentId: agent.id,
|
|
1433
|
+
detail: {
|
|
1434
|
+
platform: msg.platform,
|
|
1435
|
+
channelId: msg.channelId,
|
|
1436
|
+
senderId: msg.senderId,
|
|
1437
|
+
senderName: msg.senderName,
|
|
1438
|
+
memoryTitle: quietBoundary.memoryTitle || null,
|
|
1439
|
+
},
|
|
1440
|
+
})
|
|
1441
|
+
return NO_MESSAGE_SENTINEL
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1421
1444
|
if (parsedCommand) {
|
|
1422
1445
|
const commandResult = await handleConnectorCommand({
|
|
1423
1446
|
command: parsedCommand,
|
|
@@ -1520,13 +1543,21 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1520
1543
|
promptParts.push(buildCurrentDateTimePromptContext())
|
|
1521
1544
|
if (agent.soul) promptParts.push(agent.soul)
|
|
1522
1545
|
if (agent.systemPrompt) promptParts.push(agent.systemPrompt)
|
|
1523
|
-
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1546
|
+
try {
|
|
1547
|
+
const enabledPlugins = dedup([
|
|
1548
|
+
...(Array.isArray(session.plugins) ? session.plugins : []),
|
|
1549
|
+
...(Array.isArray(agent.plugins) ? agent.plugins : []),
|
|
1550
|
+
...(Array.isArray(agent.tools) ? agent.tools : []),
|
|
1551
|
+
])
|
|
1552
|
+
const runtimeSkills = resolveRuntimeSkills({
|
|
1553
|
+
cwd: session.cwd,
|
|
1554
|
+
enabledPlugins,
|
|
1555
|
+
agentSkillIds: agent.skillIds || [],
|
|
1556
|
+
storedSkills: loadSkills(),
|
|
1557
|
+
selectedSkillId: session.skillRuntimeState?.selectedSkillId || null,
|
|
1558
|
+
})
|
|
1559
|
+
promptParts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
|
|
1560
|
+
} catch { /* non-critical */ }
|
|
1530
1561
|
const thinkLevel = resolveConnectorSessionPolicy(connector, msg, session).thinkingLevel || ''
|
|
1531
1562
|
if (thinkLevel) {
|
|
1532
1563
|
promptParts.push(`Connector thinking guidance: ${thinkLevel}. Keep responses concise and useful for chat.`)
|
|
@@ -2343,7 +2374,7 @@ export async function sendConnectorMessage(params: {
|
|
|
2343
2374
|
if (params.sessionId) {
|
|
2344
2375
|
const sessions = loadSessions()
|
|
2345
2376
|
const session = sessions[params.sessionId]
|
|
2346
|
-
if (session) {
|
|
2377
|
+
if (session && isDirectConnectorSession(session)) {
|
|
2347
2378
|
session.connectorContext = {
|
|
2348
2379
|
...(session.connectorContext || {}),
|
|
2349
2380
|
connectorId,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { loadSessions, loadSettings, saveSettings, upsertStoredItem } from '../storage'
|
|
6
6
|
import type { Session } from '@/types'
|
|
7
|
+
import { isDirectConnectorSession } from './session-kind'
|
|
7
8
|
|
|
8
9
|
const MIGRATION_FLAG = '_migration_allKnownPeerIds'
|
|
9
10
|
|
|
@@ -17,6 +18,7 @@ export function backfillAllKnownPeerIds(): { migrated: number; skipped: boolean
|
|
|
17
18
|
let migrated = 0
|
|
18
19
|
|
|
19
20
|
for (const session of Object.values(sessions)) {
|
|
21
|
+
if (!isDirectConnectorSession(session)) continue
|
|
20
22
|
const ctx = session?.connectorContext
|
|
21
23
|
if (!ctx?.connectorId) continue
|
|
22
24
|
if (Array.isArray(ctx.allKnownPeerIds) && ctx.allKnownPeerIds.length > 0) continue
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function isDirectConnectorSession(session?: unknown): boolean {
|
|
2
|
+
if (!session || typeof session !== 'object') return false
|
|
3
|
+
const record = session as Record<string, unknown>
|
|
4
|
+
const user = typeof record.user === 'string' ? record.user.trim() : ''
|
|
5
|
+
const name = typeof record.name === 'string' ? record.name.trim() : ''
|
|
6
|
+
return user === 'connector' || name.startsWith('connector:')
|
|
7
|
+
}
|
|
@@ -193,6 +193,110 @@ describe('connectors/session', () => {
|
|
|
193
193
|
assert.ok(found)
|
|
194
194
|
assert.equal(found!.id, created.session.id)
|
|
195
195
|
})
|
|
196
|
+
|
|
197
|
+
it('returns null when multiple direct sessions match without a unique thread or sender match', () => {
|
|
198
|
+
const now = Date.now()
|
|
199
|
+
storage.saveSessions({
|
|
200
|
+
'sess-a': {
|
|
201
|
+
id: 'sess-a',
|
|
202
|
+
name: 'connector:discord:alice',
|
|
203
|
+
user: 'connector',
|
|
204
|
+
provider: 'anthropic',
|
|
205
|
+
model: 'claude-3',
|
|
206
|
+
messages: [],
|
|
207
|
+
createdAt: now,
|
|
208
|
+
lastActiveAt: now,
|
|
209
|
+
agentId: 'agent-1',
|
|
210
|
+
connectorContext: {
|
|
211
|
+
connectorId: 'conn-1',
|
|
212
|
+
channelId: 'shared-channel',
|
|
213
|
+
senderId: 'alice-1',
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
'sess-b': {
|
|
217
|
+
id: 'sess-b',
|
|
218
|
+
name: 'connector:discord:bob',
|
|
219
|
+
user: 'connector',
|
|
220
|
+
provider: 'anthropic',
|
|
221
|
+
model: 'claude-3',
|
|
222
|
+
messages: [],
|
|
223
|
+
createdAt: now,
|
|
224
|
+
lastActiveAt: now,
|
|
225
|
+
agentId: 'agent-1',
|
|
226
|
+
connectorContext: {
|
|
227
|
+
connectorId: 'conn-1',
|
|
228
|
+
channelId: 'shared-channel',
|
|
229
|
+
senderId: 'bob-1',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const connector = {
|
|
235
|
+
id: 'conn-1',
|
|
236
|
+
name: 'Test',
|
|
237
|
+
platform: 'discord' as const,
|
|
238
|
+
agentId: 'agent-1',
|
|
239
|
+
config: {},
|
|
240
|
+
enabled: true,
|
|
241
|
+
createdAt: now,
|
|
242
|
+
updatedAt: now,
|
|
243
|
+
} as unknown as Connector
|
|
244
|
+
|
|
245
|
+
const result = mod.findDirectSessionForInbound(connector, {
|
|
246
|
+
platform: 'discord',
|
|
247
|
+
channelId: 'shared-channel',
|
|
248
|
+
senderId: 'unknown-user',
|
|
249
|
+
senderName: 'Unknown',
|
|
250
|
+
text: 'hello',
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
assert.equal(result, null)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('returns null when the inbound thread id does not match an existing direct thread session', () => {
|
|
257
|
+
const now = Date.now()
|
|
258
|
+
storage.saveSessions({
|
|
259
|
+
'sess-thread': {
|
|
260
|
+
id: 'sess-thread',
|
|
261
|
+
name: 'connector:discord:thread',
|
|
262
|
+
user: 'connector',
|
|
263
|
+
provider: 'anthropic',
|
|
264
|
+
model: 'claude-3',
|
|
265
|
+
messages: [],
|
|
266
|
+
createdAt: now,
|
|
267
|
+
lastActiveAt: now,
|
|
268
|
+
agentId: 'agent-1',
|
|
269
|
+
connectorContext: {
|
|
270
|
+
connectorId: 'conn-1',
|
|
271
|
+
channelId: 'channel-1',
|
|
272
|
+
senderId: 'user-1',
|
|
273
|
+
threadId: 'thread-1',
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const connector = {
|
|
279
|
+
id: 'conn-1',
|
|
280
|
+
name: 'Test',
|
|
281
|
+
platform: 'discord' as const,
|
|
282
|
+
agentId: 'agent-1',
|
|
283
|
+
config: {},
|
|
284
|
+
enabled: true,
|
|
285
|
+
createdAt: now,
|
|
286
|
+
updatedAt: now,
|
|
287
|
+
} as unknown as Connector
|
|
288
|
+
|
|
289
|
+
const result = mod.findDirectSessionForInbound(connector, {
|
|
290
|
+
platform: 'discord',
|
|
291
|
+
channelId: 'channel-1',
|
|
292
|
+
senderId: 'user-1',
|
|
293
|
+
senderName: 'User',
|
|
294
|
+
text: 'hello',
|
|
295
|
+
threadId: 'thread-2',
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
assert.equal(result, null)
|
|
299
|
+
})
|
|
196
300
|
})
|
|
197
301
|
|
|
198
302
|
// ---- updateSessionConnectorContext ----
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
resetConnectorSessionRuntime,
|
|
13
13
|
resolveConnectorSessionPolicy,
|
|
14
14
|
} from './policy'
|
|
15
|
+
import { isDirectConnectorSession } from './session-kind'
|
|
15
16
|
import { resolveThreadPersonaLabel } from './thread-context'
|
|
16
17
|
import type { InboundMessage } from './types'
|
|
17
18
|
|
|
@@ -40,7 +41,8 @@ export function findDirectSessionForInbound(connector: Connector, msg: InboundMe
|
|
|
40
41
|
const senderIds = new Set([msg.senderId, msg.senderIdAlt].filter(Boolean))
|
|
41
42
|
const sessions = Object.values(loadSessions() as Record<string, ConnectorSession>)
|
|
42
43
|
const candidates = sessions.filter((session) =>
|
|
43
|
-
session
|
|
44
|
+
isDirectConnectorSession(session)
|
|
45
|
+
&& session?.agentId === effectiveAgentId
|
|
44
46
|
&& session?.connectorContext?.connectorId === connector.id
|
|
45
47
|
&& (
|
|
46
48
|
channelIds.has(session?.connectorContext?.channelId || '')
|
|
@@ -51,6 +53,7 @@ export function findDirectSessionForInbound(connector: Connector, msg: InboundMe
|
|
|
51
53
|
if (msg.threadId) {
|
|
52
54
|
const threadExact = candidates.find((session) => session?.connectorContext?.threadId === msg.threadId)
|
|
53
55
|
if (threadExact) return threadExact
|
|
56
|
+
return null
|
|
54
57
|
}
|
|
55
58
|
const senderExact = candidates.find((session) =>
|
|
56
59
|
senderIds.has(session?.connectorContext?.senderId || '')
|
|
@@ -64,7 +67,7 @@ export function findDirectSessionForInbound(connector: Connector, msg: InboundMe
|
|
|
64
67
|
)
|
|
65
68
|
if (peerIdMatch) return peerIdMatch
|
|
66
69
|
|
|
67
|
-
return candidates[0]
|
|
70
|
+
return candidates.length === 1 ? candidates[0] : null
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
export function persistSessionRecord(session: ConnectorSession): void {
|
|
@@ -32,9 +32,9 @@ test('buildIdentityContinuityContext merges agent and session continuity', () =>
|
|
|
32
32
|
test('refreshSessionIdentityState derives fallback continuity fields', () => {
|
|
33
33
|
const session = {
|
|
34
34
|
id: 's1',
|
|
35
|
-
name: '
|
|
35
|
+
name: 'connector:checkout-bug',
|
|
36
36
|
cwd: process.cwd(),
|
|
37
|
-
user: '
|
|
37
|
+
user: 'connector',
|
|
38
38
|
provider: 'openai',
|
|
39
39
|
model: 'gpt-4.1',
|
|
40
40
|
claudeSessionId: null,
|
|
@@ -61,7 +61,8 @@ test('refreshSessionIdentityState derives fallback continuity fields', () => {
|
|
|
61
61
|
test('buildIdentityContinuityContext prefers thread persona labels from connector context', () => {
|
|
62
62
|
const block = buildIdentityContinuityContext(
|
|
63
63
|
{
|
|
64
|
-
name: '
|
|
64
|
+
name: 'connector:session',
|
|
65
|
+
user: 'connector',
|
|
65
66
|
connectorContext: {
|
|
66
67
|
threadId: 'thread-9',
|
|
67
68
|
threadPersonaLabel: 'Checkout Incident',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Agent, IdentityContinuityState, Session } from '@/types'
|
|
2
|
+
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
2
3
|
|
|
3
4
|
function normalizeText(value: unknown, maxChars: number): string | null {
|
|
4
5
|
if (typeof value !== 'string') return null
|
|
@@ -49,11 +50,12 @@ function fallbackSelfSummary(agent?: Partial<Agent> | null): string | null {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function fallbackPersonaLabel(session?: Partial<Session> | null, agent?: Partial<Agent> | null): string | null {
|
|
52
|
-
const
|
|
53
|
+
const connectorContext = isDirectConnectorSession(session) ? session?.connectorContext : null
|
|
54
|
+
const threadPersona = normalizeText(connectorContext?.threadPersonaLabel, 120)
|
|
53
55
|
if (threadPersona) return threadPersona
|
|
54
|
-
const threadTitle = normalizeText(
|
|
56
|
+
const threadTitle = normalizeText(connectorContext?.threadTitle, 120)
|
|
55
57
|
if (threadTitle) return threadTitle
|
|
56
|
-
const threadId = normalizeText(
|
|
58
|
+
const threadId = normalizeText(connectorContext?.threadId, 80)
|
|
57
59
|
if (threadId) return `${agent?.name || 'Agent'} thread ${threadId}`
|
|
58
60
|
const sessionName = normalizeText(session?.name, 120)
|
|
59
61
|
if (sessionName && !/^new chat$/i.test(sessionName)) return sessionName
|
|
@@ -61,7 +63,9 @@ function fallbackPersonaLabel(session?: Partial<Session> | null, agent?: Partial
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
function fallbackRelationshipSummary(session?: Partial<Session> | null): string | null {
|
|
64
|
-
const sender =
|
|
66
|
+
const sender = isDirectConnectorSession(session)
|
|
67
|
+
? normalizeText(session?.connectorContext?.senderName, 80)
|
|
68
|
+
: null
|
|
65
69
|
if (sender) return `Ongoing conversation with ${sender}.`
|
|
66
70
|
const user = normalizeText(session?.user, 80)
|
|
67
71
|
if (user && user !== 'user') return `Ongoing conversation with ${user}.`
|
|
@@ -5,6 +5,7 @@ import type { Agent, MemoryEntry, MemoryReference, Session } from '@/types'
|
|
|
5
5
|
import { getMemoryDb } from '@/lib/server/memory/memory-db'
|
|
6
6
|
import { loadAgents, loadSessions, saveSessions } from '@/lib/server/storage'
|
|
7
7
|
import { DATA_DIR } from '@/lib/server/data-dir'
|
|
8
|
+
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
8
9
|
|
|
9
10
|
const MAX_ARCHIVE_MESSAGES = 36
|
|
10
11
|
const MAX_ARCHIVE_LINE_CHARS = 320
|
|
@@ -16,7 +17,7 @@ function toOneLine(value: unknown, maxChars: number): string {
|
|
|
16
17
|
|
|
17
18
|
function messageSpeaker(session: Session, agent: Partial<Agent> | null | undefined, message: Session['messages'][number]): string {
|
|
18
19
|
if (message.role === 'assistant') return agent?.name || 'assistant'
|
|
19
|
-
return session.connectorContext?.senderName || session.user || 'user'
|
|
20
|
+
return (isDirectConnectorSession(session) ? session.connectorContext?.senderName : null) || session.user || 'user'
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function slugifySegment(value: string, fallback: string): string {
|
|
@@ -28,8 +28,16 @@ function makeSession(overrides: Partial<Session> = {}): Session {
|
|
|
28
28
|
|
|
29
29
|
test('inferSessionResetType distinguishes direct, group, and thread sessions', () => {
|
|
30
30
|
assert.equal(inferSessionResetType(makeSession()), 'direct')
|
|
31
|
-
assert.equal(inferSessionResetType(makeSession({
|
|
32
|
-
|
|
31
|
+
assert.equal(inferSessionResetType(makeSession({
|
|
32
|
+
name: 'connector:group',
|
|
33
|
+
user: 'connector',
|
|
34
|
+
connectorContext: { isGroup: true },
|
|
35
|
+
})), 'group')
|
|
36
|
+
assert.equal(inferSessionResetType(makeSession({
|
|
37
|
+
name: 'connector:thread',
|
|
38
|
+
user: 'connector',
|
|
39
|
+
connectorContext: { threadId: 'thread-1' },
|
|
40
|
+
})), 'thread')
|
|
33
41
|
})
|
|
34
42
|
|
|
35
43
|
test('resolveSessionResetPolicy falls back to type defaults', () => {
|
|
@@ -37,7 +45,13 @@ test('resolveSessionResetPolicy falls back to type defaults', () => {
|
|
|
37
45
|
assert.equal(direct.mode, 'idle')
|
|
38
46
|
assert.equal(direct.idleTimeoutSec, 12 * 60 * 60)
|
|
39
47
|
|
|
40
|
-
const thread = resolveSessionResetPolicy({
|
|
48
|
+
const thread = resolveSessionResetPolicy({
|
|
49
|
+
session: makeSession({
|
|
50
|
+
name: 'connector:thread',
|
|
51
|
+
user: 'connector',
|
|
52
|
+
connectorContext: { threadId: 'thread-1' },
|
|
53
|
+
}),
|
|
54
|
+
})
|
|
41
55
|
assert.equal(thread.mode, 'idle')
|
|
42
56
|
assert.equal(thread.idleTimeoutSec, 4 * 60 * 60)
|
|
43
57
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Agent, AppSettings, Session, SessionResetMode, SessionResetType } from '@/types'
|
|
2
|
+
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
2
3
|
|
|
3
4
|
export interface ResolvedSessionResetPolicy {
|
|
4
5
|
type: SessionResetType
|
|
@@ -140,9 +141,10 @@ export function inferSessionResetType(
|
|
|
140
141
|
opts?: { isGroup?: boolean | null; threadId?: string | null },
|
|
141
142
|
): SessionResetType {
|
|
142
143
|
if ((session?.sessionType as string | undefined) === 'orchestrated') return 'main'
|
|
143
|
-
const
|
|
144
|
+
const connectorContext = isDirectConnectorSession(session) ? session?.connectorContext : null
|
|
145
|
+
const threadId = opts?.threadId ?? connectorContext?.threadId ?? null
|
|
144
146
|
if (threadId) return 'thread'
|
|
145
|
-
const isGroup = opts?.isGroup ??
|
|
147
|
+
const isGroup = opts?.isGroup ?? connectorContext?.isGroup ?? false
|
|
146
148
|
return isGroup ? 'group' : 'direct'
|
|
147
149
|
}
|
|
148
150
|
|
|
@@ -13,6 +13,7 @@ import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
|
13
13
|
import { safeJsonParseObject } from '../json-utils'
|
|
14
14
|
import { tryResolvePathWithinBaseDir } from '../path-utils'
|
|
15
15
|
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
16
|
+
import { isDirectConnectorSession } from '../connectors/session-kind'
|
|
16
17
|
|
|
17
18
|
const CONNECTOR_ACTION_DEDUPE_TTL_MS = 30_000
|
|
18
19
|
const CONNECTOR_TURN_SEND_TTL_MS = 180_000
|
|
@@ -341,11 +342,14 @@ function trimToString(value: unknown): string {
|
|
|
341
342
|
|
|
342
343
|
function resolveSessionConnectorTargets(
|
|
343
344
|
session: {
|
|
345
|
+
user?: string
|
|
346
|
+
name?: string
|
|
344
347
|
connectorContext?: Record<string, unknown>
|
|
345
348
|
messages?: Array<Record<string, unknown>>
|
|
346
349
|
} | null | undefined,
|
|
347
350
|
connectorId: string,
|
|
348
351
|
): Array<{ channelId: string; senderId?: string; senderName?: string }> {
|
|
352
|
+
if (!isDirectConnectorSession(session)) return []
|
|
349
353
|
const targets: Array<{ channelId: string; senderId?: string; senderName?: string }> = []
|
|
350
354
|
const seen = new Set<string>()
|
|
351
355
|
const pushTarget = (target: { channelId: string; senderId?: string; senderName?: string } | null) => {
|
|
@@ -368,6 +372,7 @@ function resolveSessionConnectorTargets(
|
|
|
368
372
|
|
|
369
373
|
const messages = Array.isArray(session?.messages) ? session.messages : []
|
|
370
374
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
375
|
+
if (messages[i]?.historyExcluded === true) continue
|
|
371
376
|
const source = messages[i]?.source as Record<string, unknown> | undefined
|
|
372
377
|
if (!source || trimToString(source.connectorId) !== connectorId) continue
|
|
373
378
|
const channelId = trimToString(source.channelId)
|
|
@@ -386,8 +391,9 @@ function pickChannelTarget(params: {
|
|
|
386
391
|
connector: { config?: Record<string, string> }
|
|
387
392
|
connectorId: string
|
|
388
393
|
to?: string
|
|
389
|
-
recentChannelId: string | null
|
|
390
394
|
currentSession?: {
|
|
395
|
+
user?: string
|
|
396
|
+
name?: string
|
|
391
397
|
connectorContext?: Record<string, unknown>
|
|
392
398
|
messages?: Array<Record<string, unknown>>
|
|
393
399
|
} | null
|
|
@@ -421,9 +427,6 @@ function pickChannelTarget(params: {
|
|
|
421
427
|
const outbound = connector.config?.outboundTarget?.trim()
|
|
422
428
|
if (outbound) channelId = outbound
|
|
423
429
|
}
|
|
424
|
-
if (!channelId && params.recentChannelId) {
|
|
425
|
-
channelId = params.recentChannelId
|
|
426
|
-
}
|
|
427
430
|
if (!channelId) {
|
|
428
431
|
const allowed = parseCsv(connector.config?.allowedJids)
|
|
429
432
|
if (allowed.length) channelId = allowed[0]
|
|
@@ -562,7 +565,6 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
562
565
|
const {
|
|
563
566
|
listRunningConnectors,
|
|
564
567
|
sendConnectorMessage,
|
|
565
|
-
getConnectorRecentChannelId,
|
|
566
568
|
scheduleConnectorFollowUp,
|
|
567
569
|
performConnectorMessageAction,
|
|
568
570
|
} = await import('../connectors/manager')
|
|
@@ -646,6 +648,7 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
646
648
|
|
|
647
649
|
const currentSession = bctx.resolveCurrentSession?.()
|
|
648
650
|
const sessionId = bctx.ctx?.sessionId || currentSession?.id || undefined
|
|
651
|
+
const connectorScopedSessionId = isDirectConnectorSession(currentSession) ? sessionId : undefined
|
|
649
652
|
|
|
650
653
|
if (actionName === 'send' || actionName === 'send_voice_note' || actionName === 'schedule_followup') {
|
|
651
654
|
const settings = loadSettings()
|
|
@@ -662,7 +665,6 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
662
665
|
connector,
|
|
663
666
|
connectorId: selected.id,
|
|
664
667
|
to,
|
|
665
|
-
recentChannelId: getConnectorRecentChannelId(selected.id),
|
|
666
668
|
currentSession,
|
|
667
669
|
})
|
|
668
670
|
if (target.error) return target.error
|
|
@@ -728,7 +730,7 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
728
730
|
const sent = await sendConnectorMessage({
|
|
729
731
|
connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType: outboundMimeType,
|
|
730
732
|
fileName: fileName?.trim() || 'voicenote.mp3', caption: caption?.trim() || undefined, ptt: ptt ?? true,
|
|
731
|
-
sessionId,
|
|
733
|
+
sessionId: connectorScopedSessionId,
|
|
732
734
|
replyToMessageId: replyToMessageId?.trim() || undefined,
|
|
733
735
|
threadId: threadId?.trim() || undefined,
|
|
734
736
|
})
|
|
@@ -768,7 +770,7 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
768
770
|
connectorId: selected.id,
|
|
769
771
|
channelId,
|
|
770
772
|
text: followupText,
|
|
771
|
-
sessionId,
|
|
773
|
+
sessionId: connectorScopedSessionId,
|
|
772
774
|
delaySec: followupDelay,
|
|
773
775
|
dedupeKey: dedupeKey?.trim() || undefined,
|
|
774
776
|
imageUrl: media.imageUrl,
|
|
@@ -793,7 +795,7 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
793
795
|
|
|
794
796
|
const sent = await sendConnectorMessage({
|
|
795
797
|
connectorId: selected.id, channelId, text: message?.trim() || '',
|
|
796
|
-
sessionId,
|
|
798
|
+
sessionId: connectorScopedSessionId,
|
|
797
799
|
imageUrl: media.imageUrl, fileUrl: media.fileUrl, mediaPath: media.mediaPath,
|
|
798
800
|
mimeType: mimeType?.trim() || undefined, fileName: fileName?.trim() || undefined,
|
|
799
801
|
caption: caption?.trim() || undefined,
|
|
@@ -815,7 +817,6 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
815
817
|
connector: resolved.connector,
|
|
816
818
|
connectorId: selected.id,
|
|
817
819
|
to,
|
|
818
|
-
recentChannelId: getConnectorRecentChannelId(selected.id),
|
|
819
820
|
currentSession,
|
|
820
821
|
})
|
|
821
822
|
if (target.error) return target.error
|
|
@@ -46,6 +46,8 @@ import { safePath, findBinaryOnPath } from './context'
|
|
|
46
46
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
47
47
|
import type { BoardTask } from '@/types'
|
|
48
48
|
import { dedup } from '@/lib/shared-utils'
|
|
49
|
+
import { isDirectConnectorSession } from '../connectors/session-kind'
|
|
50
|
+
import { buildManageSkillsDescription, executeManageSkillsAction } from './skills'
|
|
49
51
|
|
|
50
52
|
// ---------------------------------------------------------------------------
|
|
51
53
|
// Document helpers
|
|
@@ -258,8 +260,10 @@ function deriveScheduleFollowupTarget(sessionId: string | null | undefined): {
|
|
|
258
260
|
}
|
|
259
261
|
}
|
|
260
262
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
if (isDirectConnectorSession(session as { user?: string; name?: string })) {
|
|
264
|
+
const contextTarget = pickSourceFields(session.connectorContext || undefined)
|
|
265
|
+
if (contextTarget.followupConnectorId && contextTarget.followupChannelId) return contextTarget
|
|
266
|
+
}
|
|
263
267
|
|
|
264
268
|
const messages = Array.isArray(session.messages) ? session.messages : []
|
|
265
269
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
@@ -447,12 +451,17 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
447
451
|
if (ctx?.projectId) {
|
|
448
452
|
description += `\n\nCurrent project context: "${ctx.projectName || ctx.projectId}" (projectId "${ctx.projectId}"). Omit "projectId" to link the secret to this active project.`
|
|
449
453
|
}
|
|
454
|
+
} else if (toolKey === 'manage_skills') {
|
|
455
|
+
description = buildManageSkillsDescription()
|
|
450
456
|
}
|
|
451
457
|
|
|
452
458
|
tools.push(
|
|
453
459
|
tool(
|
|
454
460
|
async (rawArgs) => {
|
|
455
461
|
const normalized = normalizeToolInputArgs((rawArgs ?? {}) as Record<string, unknown>)
|
|
462
|
+
if (toolKey === 'manage_skills') {
|
|
463
|
+
return executeManageSkillsAction(normalized, bctx)
|
|
464
|
+
}
|
|
456
465
|
const action = normalized.action as string | undefined
|
|
457
466
|
const id = normalized.id as string | undefined
|
|
458
467
|
const data = normalized.data as string | undefined
|
|
@@ -881,11 +890,36 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
881
890
|
{
|
|
882
891
|
name: toolKey,
|
|
883
892
|
description,
|
|
884
|
-
schema:
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
893
|
+
schema: toolKey === 'manage_skills'
|
|
894
|
+
? z.object({
|
|
895
|
+
action: z.enum([
|
|
896
|
+
'list',
|
|
897
|
+
'get',
|
|
898
|
+
'create',
|
|
899
|
+
'update',
|
|
900
|
+
'delete',
|
|
901
|
+
'status',
|
|
902
|
+
'search_available',
|
|
903
|
+
'recommend_for_task',
|
|
904
|
+
'attach',
|
|
905
|
+
'install',
|
|
906
|
+
]).describe('The manage_skills action to perform'),
|
|
907
|
+
id: z.string().optional().describe('Stored skill ID or runtime skill selector'),
|
|
908
|
+
skillId: z.string().optional().describe('Alternate skill selector'),
|
|
909
|
+
name: z.string().optional().describe('Skill name or marketplace name'),
|
|
910
|
+
query: z.string().optional().describe('Search query or task description'),
|
|
911
|
+
task: z.string().optional().describe('Task description for skill recommendation'),
|
|
912
|
+
url: z.string().optional().describe('Remote skill URL for install'),
|
|
913
|
+
approvalId: z.string().optional().describe('Approved install request id'),
|
|
914
|
+
attach: z.boolean().optional().describe('Attach the skill to the current agent after install'),
|
|
915
|
+
agentId: z.string().optional().describe('Target agent id for attach'),
|
|
916
|
+
data: z.string().optional().describe('JSON string of fields for create/update'),
|
|
917
|
+
}).passthrough()
|
|
918
|
+
: z.object({
|
|
919
|
+
action: z.enum(['list', 'get', 'create', 'update', 'delete']).describe('The CRUD action to perform'),
|
|
920
|
+
id: z.string().optional().describe('Resource ID (required for get, update, delete)'),
|
|
921
|
+
data: z.string().optional().describe('JSON string of fields for create/update'),
|
|
922
|
+
}).passthrough(),
|
|
889
923
|
},
|
|
890
924
|
),
|
|
891
925
|
)
|
|
@@ -45,6 +45,7 @@ import { buildExtractTools } from './extract'
|
|
|
45
45
|
import { buildTableTools } from './table'
|
|
46
46
|
import { buildCrawlTools } from './crawl'
|
|
47
47
|
import { buildGoogleWorkspaceTools } from './google-workspace'
|
|
48
|
+
import { buildSkillRuntimeTools } from './skill-runtime'
|
|
48
49
|
import './connector'
|
|
49
50
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
50
51
|
import { enforceFileAccessPolicy } from './file-access-policy'
|
|
@@ -172,6 +173,7 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
172
173
|
['calendar', buildCalendarTools],
|
|
173
174
|
['replicate', buildReplicateTools],
|
|
174
175
|
['google_workspace', buildGoogleWorkspaceTools],
|
|
176
|
+
['use_skill', buildSkillRuntimeTools],
|
|
175
177
|
['mailbox', buildMailboxTools],
|
|
176
178
|
['ask_human', buildHumanLoopTools],
|
|
177
179
|
['document', buildDocumentTools],
|