@swarmclawai/swarmclaw 0.7.6 → 0.7.8
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 +19 -10
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +16 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/route.ts +21 -1
- package/src/app/api/chats/route.ts +13 -1
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
- package/src/app/api/external-agents/[id]/route.ts +38 -6
- package/src/app/api/external-agents/route.ts +17 -1
- package/src/app/api/gateways/[id]/health/route.ts +8 -0
- package/src/app/api/gateways/[id]/route.ts +53 -1
- package/src/app/api/gateways/route.ts +53 -0
- package/src/app/api/openclaw/deploy/route.ts +139 -0
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/cli/index.js +40 -0
- package/src/cli/index.test.js +68 -0
- package/src/cli/spec.js +60 -0
- package/src/components/agents/agent-sheet.tsx +281 -33
- package/src/components/auth/setup-wizard.tsx +75 -2
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-header.tsx +4 -0
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/gateways/gateway-sheet.tsx +140 -8
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +221 -17
- package/src/components/shared/settings/section-capability-policy.tsx +38 -0
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/tasks/approvals-panel.tsx +177 -18
- package/src/components/tasks/task-board.tsx +137 -23
- package/src/components/tasks/task-card.tsx +29 -0
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/lib/server/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- package/src/lib/server/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +33 -3
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.ts +99 -74
- package/src/lib/server/daemon-state.ts +83 -46
- package/src/lib/server/elevenlabs.test.ts +59 -1
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/openclaw-deploy.test.ts +8 -0
- package/src/lib/server/openclaw-deploy.ts +679 -19
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +11 -0
- package/src/lib/server/plugins.ts +6 -1
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-followups.test.ts +147 -2
- package/src/lib/server/queue.ts +278 -8
- package/src/lib/server/session-run-manager.ts +31 -0
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.ts +26 -1
- package/src/lib/server/session-tools/context.ts +5 -0
- package/src/lib/server/session-tools/crud.ts +265 -76
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +38 -2
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.ts +14 -2
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
- package/src/lib/server/session-tools/web.ts +153 -6
- package/src/lib/server/stream-agent-chat.test.ts +27 -2
- package/src/lib/server/stream-agent-chat.ts +104 -30
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +269 -0
- package/src/lib/setup-defaults.ts +2 -2
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +104 -0
|
@@ -26,6 +26,11 @@ export interface ResolvedAgentRoute {
|
|
|
26
26
|
source: 'agent' | 'routing-target'
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
interface GatewayRoutePreferences {
|
|
30
|
+
preferredGatewayTags?: string[]
|
|
31
|
+
preferredGatewayUseCase?: string | null
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
interface RouteSeed {
|
|
30
35
|
id: string
|
|
31
36
|
label?: string
|
|
@@ -35,6 +40,8 @@ interface RouteSeed {
|
|
|
35
40
|
fallbackCredentialIds?: string[]
|
|
36
41
|
apiEndpoint?: string | null
|
|
37
42
|
gatewayProfileId?: string | null
|
|
43
|
+
preferredGatewayTags?: string[]
|
|
44
|
+
preferredGatewayUseCase?: string | null
|
|
38
45
|
role?: AgentRoutingTarget['role']
|
|
39
46
|
priority?: number
|
|
40
47
|
source: ResolvedAgentRoute['source']
|
|
@@ -47,6 +54,68 @@ function ensureStringArray(value: unknown): string[] {
|
|
|
47
54
|
.filter(Boolean)
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
function normalizeText(value: unknown): string | null {
|
|
58
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeNullableNumber(value: unknown): number | null {
|
|
62
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeGatewayDeployment(
|
|
66
|
+
value: unknown,
|
|
67
|
+
): GatewayProfile['deployment'] {
|
|
68
|
+
if (!value || typeof value !== 'object') return null
|
|
69
|
+
const deployment = value as Record<string, unknown>
|
|
70
|
+
type DeploymentConfig = NonNullable<GatewayProfile['deployment']>
|
|
71
|
+
return {
|
|
72
|
+
method: normalizeText(deployment.method) as DeploymentConfig['method'],
|
|
73
|
+
provider: normalizeText(deployment.provider) as DeploymentConfig['provider'],
|
|
74
|
+
remoteTarget: normalizeText(deployment.remoteTarget) as DeploymentConfig['remoteTarget'],
|
|
75
|
+
useCase: normalizeText(deployment.useCase) as DeploymentConfig['useCase'],
|
|
76
|
+
exposure: normalizeText(deployment.exposure) as DeploymentConfig['exposure'],
|
|
77
|
+
managedBy: normalizeText(deployment.managedBy) as DeploymentConfig['managedBy'],
|
|
78
|
+
targetHost: normalizeText(deployment.targetHost),
|
|
79
|
+
sshHost: normalizeText(deployment.sshHost),
|
|
80
|
+
sshUser: normalizeText(deployment.sshUser),
|
|
81
|
+
sshPort: normalizeNullableNumber(deployment.sshPort),
|
|
82
|
+
sshKeyPath: normalizeText(deployment.sshKeyPath),
|
|
83
|
+
sshTargetDir: normalizeText(deployment.sshTargetDir),
|
|
84
|
+
image: normalizeText(deployment.image),
|
|
85
|
+
version: normalizeText(deployment.version),
|
|
86
|
+
lastDeployAt: normalizeNullableNumber(deployment.lastDeployAt),
|
|
87
|
+
lastDeployAction: normalizeText(deployment.lastDeployAction),
|
|
88
|
+
lastDeployProcessId: normalizeText(deployment.lastDeployProcessId),
|
|
89
|
+
lastDeploySummary: normalizeText(deployment.lastDeploySummary),
|
|
90
|
+
lastVerifiedAt: normalizeNullableNumber(deployment.lastVerifiedAt),
|
|
91
|
+
lastVerifiedOk: typeof deployment.lastVerifiedOk === 'boolean' ? deployment.lastVerifiedOk : null,
|
|
92
|
+
lastVerifiedMessage: normalizeText(deployment.lastVerifiedMessage),
|
|
93
|
+
lastBackupPath: normalizeText(deployment.lastBackupPath),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeGatewayStats(value: unknown): GatewayProfile['stats'] {
|
|
98
|
+
if (!value || typeof value !== 'object') return null
|
|
99
|
+
const stats = value as Record<string, unknown>
|
|
100
|
+
return {
|
|
101
|
+
nodeCount: normalizeNullableNumber(stats.nodeCount) ?? undefined,
|
|
102
|
+
connectedNodeCount: normalizeNullableNumber(stats.connectedNodeCount) ?? undefined,
|
|
103
|
+
pendingNodePairings: normalizeNullableNumber(stats.pendingNodePairings) ?? undefined,
|
|
104
|
+
pairedDeviceCount: normalizeNullableNumber(stats.pairedDeviceCount) ?? undefined,
|
|
105
|
+
pendingDevicePairings: normalizeNullableNumber(stats.pendingDevicePairings) ?? undefined,
|
|
106
|
+
externalRuntimeCount: normalizeNullableNumber(stats.externalRuntimeCount) ?? undefined,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeRoutePreferences(
|
|
111
|
+
value?: GatewayRoutePreferences | null,
|
|
112
|
+
): GatewayRoutePreferences {
|
|
113
|
+
return {
|
|
114
|
+
preferredGatewayTags: ensureStringArray(value?.preferredGatewayTags),
|
|
115
|
+
preferredGatewayUseCase: normalizeText(value?.preferredGatewayUseCase),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
50
119
|
function normalizeGateway(raw: unknown, id: string): GatewayProfile | null {
|
|
51
120
|
if (!raw || typeof raw !== 'object') return null
|
|
52
121
|
const gateway = raw as Partial<GatewayProfile> & Record<string, unknown>
|
|
@@ -72,6 +141,8 @@ function normalizeGateway(raw: unknown, id: string): GatewayProfile | null {
|
|
|
72
141
|
lastModelCount: typeof gateway.lastModelCount === 'number' ? gateway.lastModelCount : null,
|
|
73
142
|
discoveredHost: typeof gateway.discoveredHost === 'string' ? gateway.discoveredHost : null,
|
|
74
143
|
discoveredPort: typeof gateway.discoveredPort === 'number' ? gateway.discoveredPort : null,
|
|
144
|
+
deployment: normalizeGatewayDeployment(gateway.deployment),
|
|
145
|
+
stats: normalizeGatewayStats(gateway.stats),
|
|
75
146
|
isDefault: gateway.isDefault === true,
|
|
76
147
|
createdAt: typeof gateway.createdAt === 'number' ? gateway.createdAt : Date.now(),
|
|
77
148
|
updatedAt: typeof gateway.updatedAt === 'number' ? gateway.updatedAt : Date.now(),
|
|
@@ -107,6 +178,50 @@ function defaultGatewayProfile(gatewayProfiles: GatewayProfile[]): GatewayProfil
|
|
|
107
178
|
return gatewayProfiles.find((profile) => profile.isDefault) || gatewayProfiles[0] || null
|
|
108
179
|
}
|
|
109
180
|
|
|
181
|
+
function gatewayPreferenceScore(
|
|
182
|
+
gatewayProfile: GatewayProfile,
|
|
183
|
+
preferences?: GatewayRoutePreferences | null,
|
|
184
|
+
): number {
|
|
185
|
+
const normalized = normalizeRoutePreferences(preferences)
|
|
186
|
+
const preferredTags = normalized.preferredGatewayTags || []
|
|
187
|
+
const preferredUseCase = normalized.preferredGatewayUseCase || null
|
|
188
|
+
const gatewayTags = new Set(ensureStringArray(gatewayProfile.tags))
|
|
189
|
+
const gatewayUseCase = normalizeText(gatewayProfile.deployment?.useCase)
|
|
190
|
+
|
|
191
|
+
let score = 0
|
|
192
|
+
if (preferredUseCase) {
|
|
193
|
+
if (gatewayUseCase !== preferredUseCase) return -1
|
|
194
|
+
score += 30
|
|
195
|
+
}
|
|
196
|
+
if (preferredTags.length > 0) {
|
|
197
|
+
const matchedTagCount = preferredTags.filter((tag) => gatewayTags.has(tag)).length
|
|
198
|
+
if (matchedTagCount === 0) return -1
|
|
199
|
+
score += matchedTagCount * 10
|
|
200
|
+
if (matchedTagCount === preferredTags.length) score += 8
|
|
201
|
+
}
|
|
202
|
+
if (gatewayProfile.status === 'healthy') score += 4
|
|
203
|
+
else if (gatewayProfile.status === 'degraded') score += 2
|
|
204
|
+
if (gatewayProfile.isDefault) score += 3
|
|
205
|
+
return score
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function pickPreferredGatewayProfile(
|
|
209
|
+
gatewayProfiles: GatewayProfile[],
|
|
210
|
+
preferences?: GatewayRoutePreferences | null,
|
|
211
|
+
): GatewayProfile | null {
|
|
212
|
+
const normalized = normalizeRoutePreferences(preferences)
|
|
213
|
+
if (!(normalized.preferredGatewayTags?.length || normalized.preferredGatewayUseCase)) {
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
return gatewayProfiles
|
|
217
|
+
.map((profile) => ({ profile, score: gatewayPreferenceScore(profile, normalized) }))
|
|
218
|
+
.filter((entry) => entry.score >= 0)
|
|
219
|
+
.sort((left, right) => {
|
|
220
|
+
if (left.score !== right.score) return right.score - left.score
|
|
221
|
+
return left.profile.name.localeCompare(right.profile.name)
|
|
222
|
+
})[0]?.profile || null
|
|
223
|
+
}
|
|
224
|
+
|
|
110
225
|
function roleWeight(strategy: AgentRoutingStrategy, role?: AgentRoutingTarget['role']): number {
|
|
111
226
|
const normalized = role || 'primary'
|
|
112
227
|
const matrix: Record<AgentRoutingStrategy, Record<string, number>> = {
|
|
@@ -135,14 +250,21 @@ function dedupeCredentialIds(primary: string | null | undefined, candidates: str
|
|
|
135
250
|
function buildRouteFromSeed(
|
|
136
251
|
seed: RouteSeed,
|
|
137
252
|
gatewayProfiles: GatewayProfile[],
|
|
253
|
+
routePreferences?: GatewayRoutePreferences | null,
|
|
138
254
|
agentGatewayProfileId?: string | null,
|
|
139
255
|
): ResolvedAgentRoute | null {
|
|
140
256
|
const provider = (seed.provider || 'claude-cli') as ProviderType
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
257
|
+
const mergedPreferences = normalizeRoutePreferences({
|
|
258
|
+
preferredGatewayTags: seed.preferredGatewayTags ?? routePreferences?.preferredGatewayTags,
|
|
259
|
+
preferredGatewayUseCase: seed.preferredGatewayUseCase ?? routePreferences?.preferredGatewayUseCase,
|
|
260
|
+
})
|
|
261
|
+
let gatewayProfile = findGatewayProfile(gatewayProfiles, seed.gatewayProfileId ?? null)
|
|
262
|
+
if (!gatewayProfile && provider === 'openclaw') {
|
|
263
|
+
gatewayProfile = pickPreferredGatewayProfile(gatewayProfiles, mergedPreferences)
|
|
264
|
+
|| findGatewayProfile(gatewayProfiles, agentGatewayProfileId ?? null)
|
|
265
|
+
|| defaultGatewayProfile(gatewayProfiles)
|
|
144
266
|
}
|
|
145
|
-
const
|
|
267
|
+
const gatewayProfileId = gatewayProfile?.id ?? seed.gatewayProfileId ?? agentGatewayProfileId ?? null
|
|
146
268
|
|
|
147
269
|
const providerFromGateway = gatewayProfile?.provider === 'openclaw' ? 'openclaw' : provider
|
|
148
270
|
const apiEndpoint = normalizeProviderEndpoint(
|
|
@@ -189,8 +311,9 @@ function dedupeRoutes(routes: ResolvedAgentRoute[]): ResolvedAgentRoute[] {
|
|
|
189
311
|
export function resolveAgentRouteCandidates(
|
|
190
312
|
agent: Agent | null | undefined,
|
|
191
313
|
preferredStrategy?: AgentRoutingStrategy | null,
|
|
314
|
+
routePreferences?: GatewayRoutePreferences | null,
|
|
192
315
|
): ResolvedAgentRoute[] {
|
|
193
|
-
return resolveAgentRouteCandidatesWithProfiles(agent, getGatewayProfiles('openclaw'), preferredStrategy)
|
|
316
|
+
return resolveAgentRouteCandidatesWithProfiles(agent, getGatewayProfiles('openclaw'), preferredStrategy, undefined, routePreferences)
|
|
194
317
|
}
|
|
195
318
|
|
|
196
319
|
export function resolveAgentRouteCandidatesWithProfiles(
|
|
@@ -198,9 +321,16 @@ export function resolveAgentRouteCandidatesWithProfiles(
|
|
|
198
321
|
gatewayProfiles: GatewayProfile[],
|
|
199
322
|
preferredStrategy?: AgentRoutingStrategy | null,
|
|
200
323
|
isCoolingDown: (providerId: string) => boolean = isProviderCoolingDown,
|
|
324
|
+
routePreferences?: GatewayRoutePreferences | null,
|
|
201
325
|
): ResolvedAgentRoute[] {
|
|
202
326
|
if (!agent) return []
|
|
203
327
|
const strategy = preferredStrategy || agent.routingStrategy || 'single'
|
|
328
|
+
const resolvedPreferences = normalizeRoutePreferences({
|
|
329
|
+
preferredGatewayTags: routePreferences?.preferredGatewayTags?.length
|
|
330
|
+
? routePreferences.preferredGatewayTags
|
|
331
|
+
: agent.preferredGatewayTags,
|
|
332
|
+
preferredGatewayUseCase: routePreferences?.preferredGatewayUseCase || agent.preferredGatewayUseCase,
|
|
333
|
+
})
|
|
204
334
|
const seeds: RouteSeed[] = [
|
|
205
335
|
{
|
|
206
336
|
id: 'base',
|
|
@@ -211,6 +341,8 @@ export function resolveAgentRouteCandidatesWithProfiles(
|
|
|
211
341
|
fallbackCredentialIds: agent.fallbackCredentialIds || [],
|
|
212
342
|
apiEndpoint: agent.apiEndpoint ?? null,
|
|
213
343
|
gatewayProfileId: agent.gatewayProfileId ?? null,
|
|
344
|
+
preferredGatewayTags: agent.preferredGatewayTags || [],
|
|
345
|
+
preferredGatewayUseCase: agent.preferredGatewayUseCase ?? null,
|
|
214
346
|
role: 'primary',
|
|
215
347
|
priority: 0,
|
|
216
348
|
source: 'agent',
|
|
@@ -224,6 +356,8 @@ export function resolveAgentRouteCandidatesWithProfiles(
|
|
|
224
356
|
fallbackCredentialIds: target.fallbackCredentialIds || [],
|
|
225
357
|
apiEndpoint: target.apiEndpoint ?? null,
|
|
226
358
|
gatewayProfileId: target.gatewayProfileId ?? null,
|
|
359
|
+
preferredGatewayTags: target.preferredGatewayTags || [],
|
|
360
|
+
preferredGatewayUseCase: target.preferredGatewayUseCase ?? null,
|
|
227
361
|
role: target.role,
|
|
228
362
|
priority: typeof target.priority === 'number' ? target.priority : index + 1,
|
|
229
363
|
source: 'routing-target' as const,
|
|
@@ -232,7 +366,7 @@ export function resolveAgentRouteCandidatesWithProfiles(
|
|
|
232
366
|
|
|
233
367
|
return dedupeRoutes(
|
|
234
368
|
seeds
|
|
235
|
-
.map((seed) => buildRouteFromSeed(seed, gatewayProfiles, agent.gatewayProfileId ?? null))
|
|
369
|
+
.map((seed) => buildRouteFromSeed(seed, gatewayProfiles, resolvedPreferences, agent.gatewayProfileId ?? null))
|
|
236
370
|
.filter((route): route is ResolvedAgentRoute => Boolean(route)),
|
|
237
371
|
).sort((left, right) => {
|
|
238
372
|
const leftCooling = isCoolingDown(left.provider)
|
|
@@ -249,8 +383,9 @@ export function resolveAgentRouteCandidatesWithProfiles(
|
|
|
249
383
|
export function resolvePrimaryAgentRoute(
|
|
250
384
|
agent: Agent | null | undefined,
|
|
251
385
|
preferredStrategy?: AgentRoutingStrategy | null,
|
|
386
|
+
routePreferences?: GatewayRoutePreferences | null,
|
|
252
387
|
): ResolvedAgentRoute | null {
|
|
253
|
-
return resolveAgentRouteCandidates(agent, preferredStrategy)[0] || null
|
|
388
|
+
return resolveAgentRouteCandidates(agent, preferredStrategy, routePreferences)[0] || null
|
|
254
389
|
}
|
|
255
390
|
|
|
256
391
|
export function applyResolvedRoute<T extends {
|
|
@@ -26,6 +26,8 @@ function buildThreadSession(agent: Agent, sessionId: string, user: string, creat
|
|
|
26
26
|
fallbackCredentialIds: agent.fallbackCredentialIds || [],
|
|
27
27
|
apiEndpoint: agent.apiEndpoint || null,
|
|
28
28
|
gatewayProfileId: agent.gatewayProfileId || null,
|
|
29
|
+
routePreferredGatewayTags: existing?.routePreferredGatewayTags || [],
|
|
30
|
+
routePreferredGatewayUseCase: existing?.routePreferredGatewayUseCase || null,
|
|
29
31
|
claudeSessionId: existing?.claudeSessionId || null,
|
|
30
32
|
codexThreadId: existing?.codexThreadId || null,
|
|
31
33
|
opencodeSessionId: existing?.opencodeSessionId || null,
|
|
@@ -77,7 +79,13 @@ function buildThreadSession(agent: Agent, sessionId: string, user: string, creat
|
|
|
77
79
|
avatar: existing?.avatar,
|
|
78
80
|
canvasContent: existing?.canvasContent || null,
|
|
79
81
|
}
|
|
80
|
-
return applyResolvedRoute(
|
|
82
|
+
return applyResolvedRoute(
|
|
83
|
+
baseSession,
|
|
84
|
+
resolvePrimaryAgentRoute(agent, undefined, {
|
|
85
|
+
preferredGatewayTags: baseSession.routePreferredGatewayTags || [],
|
|
86
|
+
preferredGatewayUseCase: baseSession.routePreferredGatewayUseCase || null,
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
export function ensureAgentThreadSession(agentId: string, user = 'default'): Session | null {
|
|
@@ -19,3 +19,25 @@ test('routeTaskIntent keeps coding prompts prioritized over memory keywords', ()
|
|
|
19
19
|
)
|
|
20
20
|
assert.equal(decision.intent, 'coding')
|
|
21
21
|
})
|
|
22
|
+
|
|
23
|
+
test('routeTaskIntent keeps hybrid research-plus-media prompts in research intent', () => {
|
|
24
|
+
const decision = routeTaskIntent(
|
|
25
|
+
'Can you tell me more if there is any news related to the US-Iran war, and can you send me some screenshots and give me a summary and maybe send me a voice note about it?',
|
|
26
|
+
['web_search', 'web_fetch', 'browser', 'manage_connectors'],
|
|
27
|
+
null,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
assert.equal(decision.intent, 'research')
|
|
31
|
+
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'browser', 'connector_message_tool'])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('routeTaskIntent treats direct voice-note delivery as outreach', () => {
|
|
35
|
+
const decision = routeTaskIntent(
|
|
36
|
+
'Send me a voice note over WhatsApp summarizing what changed.',
|
|
37
|
+
['manage_connectors'],
|
|
38
|
+
null,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
assert.equal(decision.intent, 'outreach')
|
|
42
|
+
assert.deepEqual(decision.preferredTools, ['connector_message_tool'])
|
|
43
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AppSettings } from '@/types'
|
|
2
|
+
import { getToolsForCapability, matchToolCapabilitiesForMessage, TOOL_CAPABILITY } from './tool-planning'
|
|
2
3
|
|
|
3
4
|
export type TaskIntent =
|
|
4
5
|
| 'coding'
|
|
@@ -27,6 +28,15 @@ function containsAny(text: string, terms: string[]): boolean {
|
|
|
27
28
|
return terms.some((term) => text.includes(term))
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function dedupe(values: string[]): string[] {
|
|
32
|
+
return Array.from(new Set(values.filter(Boolean)))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function preferredToolsForCapabilities(enabledPlugins: string[], capabilities: string[], fallback: string[] = []): string[] {
|
|
36
|
+
const preferred = capabilities.flatMap((capability) => getToolsForCapability(enabledPlugins, capability))
|
|
37
|
+
return dedupe(preferred.length > 0 ? preferred : fallback)
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
function normalizeDelegateOrder(value: unknown): DelegateTool[] {
|
|
31
41
|
const fallback: DelegateTool[] = [
|
|
32
42
|
'delegate_to_claude_code',
|
|
@@ -59,6 +69,14 @@ export function routeTaskIntent(
|
|
|
59
69
|
const text = (message || '').toLowerCase()
|
|
60
70
|
const url = findFirstUrl(message || '')
|
|
61
71
|
const delegateOrder = normalizeDelegateOrder(settings?.autonomyPreferredDelegates)
|
|
72
|
+
const matchedCapabilities = matchToolCapabilitiesForMessage(enabledPlugins, message)
|
|
73
|
+
const wantsVoiceNote = matchedCapabilities.has(TOOL_CAPABILITY.deliveryVoiceNote)
|
|
74
|
+
const wantsScreenshots = matchedCapabilities.has(TOOL_CAPABILITY.browserCapture)
|
|
75
|
+
const wantsMediaDelivery = matchedCapabilities.has(TOOL_CAPABILITY.deliveryMedia)
|
|
76
|
+
const wantsChannelDelivery = matchedCapabilities.has(TOOL_CAPABILITY.deliveryMessage)
|
|
77
|
+
const researchLike = matchedCapabilities.has(TOOL_CAPABILITY.researchSearch)
|
|
78
|
+
|| matchedCapabilities.has(TOOL_CAPABILITY.researchFetch)
|
|
79
|
+
|| !!url
|
|
62
80
|
|
|
63
81
|
const coding = containsAny(text, [
|
|
64
82
|
'build',
|
|
@@ -98,12 +116,20 @@ export function routeTaskIntent(
|
|
|
98
116
|
'discord',
|
|
99
117
|
'notify',
|
|
100
118
|
'broadcast',
|
|
101
|
-
])
|
|
119
|
+
]) || (!researchLike && (wantsVoiceNote || wantsMediaDelivery || wantsChannelDelivery))
|
|
102
120
|
if (outreach) {
|
|
103
121
|
return {
|
|
104
122
|
intent: 'outreach',
|
|
105
123
|
confidence: 0.8,
|
|
106
|
-
preferredTools:
|
|
124
|
+
preferredTools: preferredToolsForCapabilities(
|
|
125
|
+
enabledPlugins,
|
|
126
|
+
[
|
|
127
|
+
TOOL_CAPABILITY.deliveryVoiceNote,
|
|
128
|
+
TOOL_CAPABILITY.deliveryMedia,
|
|
129
|
+
TOOL_CAPABILITY.deliveryMessage,
|
|
130
|
+
],
|
|
131
|
+
['connector_message_tool', 'manage_connectors', 'manage_sessions'],
|
|
132
|
+
),
|
|
107
133
|
preferredDelegates: delegateOrder,
|
|
108
134
|
primaryUrl: url,
|
|
109
135
|
}
|
|
@@ -129,36 +155,46 @@ export function routeTaskIntent(
|
|
|
129
155
|
}
|
|
130
156
|
|
|
131
157
|
const browsing = !!url && (
|
|
132
|
-
|
|
133
|
-
||
|
|
158
|
+
matchedCapabilities.has(TOOL_CAPABILITY.browserNavigate)
|
|
159
|
+
|| matchedCapabilities.has(TOOL_CAPABILITY.browserCapture)
|
|
160
|
+
|| getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.browserNavigate).length > 0
|
|
161
|
+
|| getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.browserCapture).length > 0
|
|
134
162
|
)
|
|
135
163
|
if (browsing) {
|
|
136
164
|
return {
|
|
137
165
|
intent: 'browsing',
|
|
138
166
|
confidence: 0.7,
|
|
139
|
-
preferredTools:
|
|
167
|
+
preferredTools: preferredToolsForCapabilities(
|
|
168
|
+
enabledPlugins,
|
|
169
|
+
[
|
|
170
|
+
TOOL_CAPABILITY.browserCapture,
|
|
171
|
+
TOOL_CAPABILITY.browserNavigate,
|
|
172
|
+
TOOL_CAPABILITY.researchFetch,
|
|
173
|
+
],
|
|
174
|
+
['browser', 'web_fetch'],
|
|
175
|
+
),
|
|
140
176
|
preferredDelegates: delegateOrder,
|
|
141
177
|
primaryUrl: url,
|
|
142
178
|
}
|
|
143
179
|
}
|
|
144
180
|
|
|
145
|
-
const research =
|
|
146
|
-
'research',
|
|
147
|
-
'look up',
|
|
148
|
-
'find out',
|
|
149
|
-
'search for',
|
|
150
|
-
'compare',
|
|
151
|
-
'latest',
|
|
152
|
-
'news',
|
|
153
|
-
'wikipedia',
|
|
154
|
-
'summarize this url',
|
|
155
|
-
'analyze website',
|
|
156
|
-
]) || !!url
|
|
181
|
+
const research = researchLike
|
|
157
182
|
if (research) {
|
|
183
|
+
const preferred = preferredToolsForCapabilities(
|
|
184
|
+
enabledPlugins,
|
|
185
|
+
[
|
|
186
|
+
TOOL_CAPABILITY.researchSearch,
|
|
187
|
+
TOOL_CAPABILITY.researchFetch,
|
|
188
|
+
...(wantsScreenshots ? [TOOL_CAPABILITY.browserCapture] : []),
|
|
189
|
+
...(wantsVoiceNote ? [TOOL_CAPABILITY.deliveryVoiceNote] : []),
|
|
190
|
+
...(wantsMediaDelivery || wantsChannelDelivery ? [TOOL_CAPABILITY.deliveryMedia, TOOL_CAPABILITY.deliveryMessage] : []),
|
|
191
|
+
],
|
|
192
|
+
['web_search', 'web_fetch', 'browser'],
|
|
193
|
+
)
|
|
158
194
|
return {
|
|
159
195
|
intent: 'research',
|
|
160
196
|
confidence: 0.7,
|
|
161
|
-
preferredTools:
|
|
197
|
+
preferredTools: preferred,
|
|
162
198
|
preferredDelegates: delegateOrder,
|
|
163
199
|
primaryUrl: url,
|
|
164
200
|
}
|
|
@@ -18,7 +18,7 @@ import { getProvider } from '@/lib/providers'
|
|
|
18
18
|
import { estimateCost, checkAgentBudgetLimits } from './cost'
|
|
19
19
|
import { log } from './logger'
|
|
20
20
|
import { logExecution } from './execution-log'
|
|
21
|
-
import { streamAgentChat } from './stream-agent-chat'
|
|
21
|
+
import { buildToolDisciplineLines, streamAgentChat } from './stream-agent-chat'
|
|
22
22
|
import { runLinkUnderstanding } from './link-understanding'
|
|
23
23
|
import { buildSessionTools } from './session-tools'
|
|
24
24
|
import type { StructuredToolInterface } from '@langchain/core/tools'
|
|
@@ -46,6 +46,7 @@ import { buildIdentityContinuityContext, refreshSessionIdentityState } from './i
|
|
|
46
46
|
import { syncSessionArchiveMemory } from './session-archive-memory'
|
|
47
47
|
import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolicy } from './session-reset-policy'
|
|
48
48
|
import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat-streaming-state'
|
|
49
|
+
import { resolveActiveProjectContext } from './project-context'
|
|
49
50
|
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
50
51
|
|
|
51
52
|
/** Slice history from the most recent context-clear marker forward */
|
|
@@ -191,6 +192,8 @@ function extractDelegateResponse(outputText: string): string | null {
|
|
|
191
192
|
const MANAGE_PLATFORM_RESOURCE_TO_TOOL: Record<string, string> = {
|
|
192
193
|
agent: 'manage_agents',
|
|
193
194
|
agents: 'manage_agents',
|
|
195
|
+
project: 'manage_projects',
|
|
196
|
+
projects: 'manage_projects',
|
|
194
197
|
task: 'manage_tasks',
|
|
195
198
|
tasks: 'manage_tasks',
|
|
196
199
|
schedule: 'manage_schedules',
|
|
@@ -626,7 +629,10 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
626
629
|
if (!agent) return
|
|
627
630
|
|
|
628
631
|
let changed = false
|
|
629
|
-
const route = resolvePrimaryAgentRoute(agent
|
|
632
|
+
const route = resolvePrimaryAgentRoute(agent, undefined, {
|
|
633
|
+
preferredGatewayTags: session.routePreferredGatewayTags || [],
|
|
634
|
+
preferredGatewayUseCase: session.routePreferredGatewayUseCase || null,
|
|
635
|
+
})
|
|
630
636
|
if (!session.provider && agent.provider) { session.provider = agent.provider; changed = true }
|
|
631
637
|
if ((session.model === undefined || session.model === null || session.model === '') && agent.model !== undefined) {
|
|
632
638
|
session.model = agent.model
|
|
@@ -668,6 +674,12 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
668
674
|
}
|
|
669
675
|
const isShortcutChat = session.shortcutForAgentId === agent.id || agent.threadSessionId === sessionId
|
|
670
676
|
if (isShortcutChat) {
|
|
677
|
+
const desiredPlugins = Array.isArray(agent.plugins) ? [...agent.plugins] : []
|
|
678
|
+
const currentPlugins = Array.isArray(session.plugins) ? [...session.plugins] : []
|
|
679
|
+
if (JSON.stringify(currentPlugins) !== JSON.stringify(desiredPlugins)) {
|
|
680
|
+
session.plugins = desiredPlugins
|
|
681
|
+
changed = true
|
|
682
|
+
}
|
|
671
683
|
if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
|
|
672
684
|
if (session.name !== agent.name) { session.name = agent.name; changed = true }
|
|
673
685
|
}
|
|
@@ -734,6 +746,14 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
734
746
|
]
|
|
735
747
|
parts.push(thinkingHint.join('\n'))
|
|
736
748
|
|
|
749
|
+
const enabledPlugins = Array.isArray(session.plugins) ? session.plugins : (Array.isArray(agent.plugins) ? agent.plugins : [])
|
|
750
|
+
const toolDisciplineLines = buildToolDisciplineLines(enabledPlugins)
|
|
751
|
+
if (toolDisciplineLines.length > 0) parts.push(['## Tool Discipline', ...toolDisciplineLines].join('\n'))
|
|
752
|
+
const operatingGuidance = getPluginManager().collectOperatingGuidance(enabledPlugins)
|
|
753
|
+
if (operatingGuidance.length > 0) parts.push(['## Tool Guidance', ...operatingGuidance].join('\n'))
|
|
754
|
+
const capabilityLines = getPluginManager().collectCapabilityDescriptions(enabledPlugins)
|
|
755
|
+
if (capabilityLines.length > 0) parts.push(['## Tool Capabilities', ...capabilityLines].join('\n'))
|
|
756
|
+
|
|
737
757
|
// 7. Heartbeat Guidance
|
|
738
758
|
parts.push([
|
|
739
759
|
'## Heartbeats',
|
|
@@ -849,7 +869,10 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
849
869
|
? session
|
|
850
870
|
: { ...session, plugins: pluginsForRun }
|
|
851
871
|
if (agentForSession) {
|
|
852
|
-
const preferredRoute = resolvePrimaryAgentRoute(agentForSession
|
|
872
|
+
const preferredRoute = resolvePrimaryAgentRoute(agentForSession, undefined, {
|
|
873
|
+
preferredGatewayTags: session.routePreferredGatewayTags || [],
|
|
874
|
+
preferredGatewayUseCase: session.routePreferredGatewayUseCase || null,
|
|
875
|
+
})
|
|
853
876
|
if (preferredRoute) {
|
|
854
877
|
sessionForRun = applyResolvedRoute({ ...sessionForRun }, preferredRoute)
|
|
855
878
|
}
|
|
@@ -1255,12 +1278,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1255
1278
|
return false
|
|
1256
1279
|
}
|
|
1257
1280
|
const agent = session.agentId ? loadAgents()[session.agentId] : null
|
|
1281
|
+
const activeProjectContext = resolveActiveProjectContext(session)
|
|
1258
1282
|
const { tools, cleanup } = await buildSessionTools(session.cwd, sessionForRun.plugins || sessionForRun.tools || [], {
|
|
1259
1283
|
agentId: session.agentId || null,
|
|
1260
1284
|
sessionId,
|
|
1261
1285
|
platformAssignScope: agent?.platformAssignScope || 'self',
|
|
1262
1286
|
mcpServerIds: agent?.mcpServerIds,
|
|
1263
1287
|
mcpDisabledTools: agent?.mcpDisabledTools,
|
|
1288
|
+
projectId: activeProjectContext.projectId,
|
|
1289
|
+
projectRoot: activeProjectContext.projectRoot,
|
|
1290
|
+
projectName: activeProjectContext.project?.name || null,
|
|
1291
|
+
projectDescription: activeProjectContext.project?.description || null,
|
|
1292
|
+
memoryScopeMode: (((session as unknown as Record<string, unknown>).memoryScopeMode as string | null | undefined) ?? agent?.memoryScopeMode ?? null),
|
|
1264
1293
|
})
|
|
1265
1294
|
try {
|
|
1266
1295
|
const directTool = tools.find((t) => t?.name === toolName) as StructuredToolInterface | undefined
|
|
@@ -1502,6 +1531,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1502
1531
|
claudeCode: normalizeResumeId(sr.claudeCode ?? cr.claudeCode),
|
|
1503
1532
|
codex: normalizeResumeId(sr.codex ?? cr.codex),
|
|
1504
1533
|
opencode: normalizeResumeId(sr.opencode ?? cr.opencode),
|
|
1534
|
+
gemini: normalizeResumeId(sr.gemini ?? cr.gemini),
|
|
1505
1535
|
}
|
|
1506
1536
|
if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
|
|
1507
1537
|
current.delegateResumeIds = nextResume
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import {
|
|
4
|
+
advanceConnectorReconnectState,
|
|
5
|
+
createConnectorReconnectState,
|
|
6
|
+
} from './manager'
|
|
7
|
+
|
|
8
|
+
test('advanceConnectorReconnectState applies exponential backoff and exhaustion', () => {
|
|
9
|
+
const policy = {
|
|
10
|
+
initialBackoffMs: 30_000,
|
|
11
|
+
maxBackoffMs: 15 * 60 * 1000,
|
|
12
|
+
maxAttempts: 3,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const initial = createConnectorReconnectState({}, policy)
|
|
16
|
+
|
|
17
|
+
const first = advanceConnectorReconnectState(initial, 'boom-1', 1_000, policy)
|
|
18
|
+
assert.equal(first.attempts, 1)
|
|
19
|
+
assert.equal(first.backoffMs, 30_000)
|
|
20
|
+
assert.equal(first.nextRetryAt, 31_000)
|
|
21
|
+
assert.equal(first.exhausted, false)
|
|
22
|
+
|
|
23
|
+
const second = advanceConnectorReconnectState(first, 'boom-2', 31_000, policy)
|
|
24
|
+
assert.equal(second.attempts, 2)
|
|
25
|
+
assert.equal(second.backoffMs, 60_000)
|
|
26
|
+
assert.equal(second.nextRetryAt, 91_000)
|
|
27
|
+
assert.equal(second.exhausted, false)
|
|
28
|
+
|
|
29
|
+
const third = advanceConnectorReconnectState(second, 'boom-3', 91_000, policy)
|
|
30
|
+
assert.equal(third.attempts, 3)
|
|
31
|
+
assert.equal(third.backoffMs, 120_000)
|
|
32
|
+
assert.equal(third.nextRetryAt, 211_000)
|
|
33
|
+
assert.equal(third.exhausted, true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('createConnectorReconnectState respects custom initial backoff', () => {
|
|
37
|
+
const state = createConnectorReconnectState(
|
|
38
|
+
{ error: 'seeded' },
|
|
39
|
+
{ initialBackoffMs: 45_000 },
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assert.equal(state.attempts, 0)
|
|
43
|
+
assert.equal(state.backoffMs, 45_000)
|
|
44
|
+
assert.equal(state.nextRetryAt, 0)
|
|
45
|
+
assert.equal(state.error, 'seeded')
|
|
46
|
+
assert.equal(state.exhausted, false)
|
|
47
|
+
})
|