@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
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Skill,
|
|
3
|
+
SkillCommandDispatch,
|
|
4
|
+
SkillInstallOption,
|
|
5
|
+
SkillInvocationConfig,
|
|
6
|
+
SkillRequirements,
|
|
7
|
+
SkillSecuritySummary,
|
|
8
|
+
} from '@/types'
|
|
9
|
+
import { dedup } from '@/lib/shared-utils'
|
|
10
|
+
import { expandPluginIds, getPluginAliases, normalizePluginId } from '@/lib/server/tool-aliases'
|
|
11
|
+
import { loadSettings, loadSkills } from '@/lib/server/storage'
|
|
12
|
+
import { discoverSkills, type DiscoveredSkill } from './skill-discovery'
|
|
13
|
+
import { evaluateSkillEligibility } from './skill-eligibility'
|
|
14
|
+
import {
|
|
15
|
+
MAX_SKILLS_IN_PROMPT,
|
|
16
|
+
MAX_SKILLS_PROMPT_CHARS,
|
|
17
|
+
} from './skill-prompt-budget'
|
|
18
|
+
|
|
19
|
+
export type RuntimeSkillSource = 'stored' | 'bundled' | 'workspace' | 'project'
|
|
20
|
+
|
|
21
|
+
type SkillSeed = {
|
|
22
|
+
runtimeId: string
|
|
23
|
+
storageId?: string
|
|
24
|
+
name: string
|
|
25
|
+
key: string
|
|
26
|
+
filename: string
|
|
27
|
+
content: string
|
|
28
|
+
description?: string
|
|
29
|
+
tags: string[]
|
|
30
|
+
toolNames: string[]
|
|
31
|
+
capabilities: string[]
|
|
32
|
+
source: RuntimeSkillSource
|
|
33
|
+
sourcePath?: string
|
|
34
|
+
sourceUrl?: string
|
|
35
|
+
sourceFormat?: Skill['sourceFormat']
|
|
36
|
+
author?: string
|
|
37
|
+
version?: string
|
|
38
|
+
homepage?: string
|
|
39
|
+
primaryEnv?: string | null
|
|
40
|
+
skillKey?: string | null
|
|
41
|
+
always: boolean
|
|
42
|
+
attached: boolean
|
|
43
|
+
installOptions?: SkillInstallOption[]
|
|
44
|
+
skillRequirements?: SkillRequirements
|
|
45
|
+
detectedEnvVars?: string[]
|
|
46
|
+
security?: SkillSecuritySummary | null
|
|
47
|
+
invocation?: SkillInvocationConfig | null
|
|
48
|
+
commandDispatch?: SkillCommandDispatch | null
|
|
49
|
+
frontmatter?: Record<string, unknown> | null
|
|
50
|
+
priority: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RuntimeSkillConfigCheck {
|
|
54
|
+
key: string
|
|
55
|
+
ok: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface RuntimeSkillStatus {
|
|
59
|
+
eligible: boolean
|
|
60
|
+
missingBins: string[]
|
|
61
|
+
missingAnyBins: string[][]
|
|
62
|
+
missingEnv: string[]
|
|
63
|
+
missingConfig: string[]
|
|
64
|
+
unsupportedOs: boolean
|
|
65
|
+
reasons: string[]
|
|
66
|
+
configChecks: RuntimeSkillConfigCheck[]
|
|
67
|
+
installRequired: boolean
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ResolvedRuntimeSkill {
|
|
71
|
+
id: string
|
|
72
|
+
key: string
|
|
73
|
+
storageId?: string
|
|
74
|
+
name: string
|
|
75
|
+
filename: string
|
|
76
|
+
content: string
|
|
77
|
+
description?: string
|
|
78
|
+
tags: string[]
|
|
79
|
+
toolNames: string[]
|
|
80
|
+
capabilities: string[]
|
|
81
|
+
source: RuntimeSkillSource
|
|
82
|
+
sourcePath?: string
|
|
83
|
+
sourceUrl?: string
|
|
84
|
+
sourceFormat?: Skill['sourceFormat']
|
|
85
|
+
author?: string
|
|
86
|
+
version?: string
|
|
87
|
+
homepage?: string
|
|
88
|
+
primaryEnv?: string | null
|
|
89
|
+
skillKey?: string | null
|
|
90
|
+
always: boolean
|
|
91
|
+
attached: boolean
|
|
92
|
+
managed: boolean
|
|
93
|
+
installOptions?: SkillInstallOption[]
|
|
94
|
+
skillRequirements?: SkillRequirements
|
|
95
|
+
detectedEnvVars?: string[]
|
|
96
|
+
security?: SkillSecuritySummary | null
|
|
97
|
+
invocation?: SkillInvocationConfig | null
|
|
98
|
+
commandDispatch?: SkillCommandDispatch | null
|
|
99
|
+
frontmatter?: Record<string, unknown> | null
|
|
100
|
+
eligible: boolean
|
|
101
|
+
missing: string[]
|
|
102
|
+
reasons: string[]
|
|
103
|
+
status: 'ready' | 'needs_install' | 'blocked'
|
|
104
|
+
configChecks: RuntimeSkillConfigCheck[]
|
|
105
|
+
autoMatch: boolean
|
|
106
|
+
matchReasons: string[]
|
|
107
|
+
score: number
|
|
108
|
+
selected: boolean
|
|
109
|
+
executionMode: 'dispatch' | 'prompt'
|
|
110
|
+
runnable: boolean
|
|
111
|
+
dispatchToolAvailable: boolean
|
|
112
|
+
dispatchBlocker?: string | null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface RuntimeSkillSnapshot {
|
|
116
|
+
skills: ResolvedRuntimeSkill[]
|
|
117
|
+
promptSkills: ResolvedRuntimeSkill[]
|
|
118
|
+
availableSkills: ResolvedRuntimeSkill[]
|
|
119
|
+
autoMatchedSkills: ResolvedRuntimeSkill[]
|
|
120
|
+
attachedSkills: ResolvedRuntimeSkill[]
|
|
121
|
+
selectedSkill: ResolvedRuntimeSkill | null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ResolveRuntimeSkillsOptions {
|
|
125
|
+
cwd?: string | null
|
|
126
|
+
enabledPlugins?: string[] | null
|
|
127
|
+
agentSkillIds?: string[] | null
|
|
128
|
+
storedSkills?: Record<string, Skill>
|
|
129
|
+
selectedSkillId?: string | null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface RuntimeSkillRecommendation {
|
|
133
|
+
skill: ResolvedRuntimeSkill
|
|
134
|
+
score: number
|
|
135
|
+
reasons: string[]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const SOURCE_PRIORITY: Record<RuntimeSkillSource, number> = {
|
|
139
|
+
bundled: 10,
|
|
140
|
+
stored: 20,
|
|
141
|
+
workspace: 30,
|
|
142
|
+
project: 40,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeKey(value: string | null | undefined): string {
|
|
146
|
+
return String(value || '')
|
|
147
|
+
.trim()
|
|
148
|
+
.toLowerCase()
|
|
149
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
150
|
+
.replace(/^_+|_+$/g, '')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function tokenize(value: string | null | undefined): string[] {
|
|
154
|
+
return dedup(
|
|
155
|
+
String(value || '')
|
|
156
|
+
.toLowerCase()
|
|
157
|
+
.split(/[^a-z0-9]+/g)
|
|
158
|
+
.map((part) => part.trim())
|
|
159
|
+
.filter((part) => part.length >= 2),
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function scoreOverlap(a: string[], b: Set<string>): number {
|
|
164
|
+
let total = 0
|
|
165
|
+
for (const token of a) {
|
|
166
|
+
if (b.has(token)) total += 1
|
|
167
|
+
}
|
|
168
|
+
return total
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildSkillKey(input: {
|
|
172
|
+
skillKey?: string | null
|
|
173
|
+
name: string
|
|
174
|
+
filename?: string | null
|
|
175
|
+
}): string {
|
|
176
|
+
return normalizeKey(input.skillKey || input.name || input.filename || 'skill')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function inferToolNames(input: {
|
|
180
|
+
name: string
|
|
181
|
+
skillKey?: string | null
|
|
182
|
+
explicit?: string[]
|
|
183
|
+
}): string[] {
|
|
184
|
+
const explicit = dedup((input.explicit || []).map((value) => normalizePluginId(value)).filter(Boolean))
|
|
185
|
+
if (explicit.length > 0) return explicit
|
|
186
|
+
|
|
187
|
+
const inferred = new Set<string>()
|
|
188
|
+
for (const candidate of [input.skillKey, input.name]) {
|
|
189
|
+
const normalized = normalizeKey(candidate || '')
|
|
190
|
+
if (!normalized) continue
|
|
191
|
+
const aliases = getPluginAliases(normalized)
|
|
192
|
+
if (aliases.length > 1) {
|
|
193
|
+
for (const alias of aliases) inferred.add(normalizePluginId(alias))
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
const dashed = normalized.replace(/_/g, '-')
|
|
197
|
+
const dashedAliases = getPluginAliases(dashed)
|
|
198
|
+
if (dashedAliases.length > 1) {
|
|
199
|
+
for (const alias of dashedAliases) inferred.add(normalizePluginId(alias))
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return [...inferred].filter(Boolean)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function inferCapabilities(input: {
|
|
206
|
+
name: string
|
|
207
|
+
description?: string
|
|
208
|
+
tags?: string[]
|
|
209
|
+
toolNames?: string[]
|
|
210
|
+
explicit?: string[]
|
|
211
|
+
}): string[] {
|
|
212
|
+
return dedup([
|
|
213
|
+
...(input.explicit || []),
|
|
214
|
+
...(input.tags || []),
|
|
215
|
+
...(input.toolNames || []),
|
|
216
|
+
...tokenize(input.name),
|
|
217
|
+
...tokenize(input.description),
|
|
218
|
+
].map((value) => value.trim()).filter(Boolean))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function asPlainRecord(value: unknown): Record<string, unknown> | null {
|
|
222
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
223
|
+
return value as Record<string, unknown>
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readConfigPath(source: Record<string, unknown>, keyPath: string): unknown {
|
|
227
|
+
const parts = keyPath
|
|
228
|
+
.split('.')
|
|
229
|
+
.map((part) => part.trim())
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
let current: unknown = source
|
|
232
|
+
for (const part of parts) {
|
|
233
|
+
const record = asPlainRecord(current)
|
|
234
|
+
if (!record || !Object.prototype.hasOwnProperty.call(record, part)) return undefined
|
|
235
|
+
current = record[part]
|
|
236
|
+
}
|
|
237
|
+
return current
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isTruthyConfigValue(value: unknown): boolean {
|
|
241
|
+
if (value === undefined || value === null) return false
|
|
242
|
+
if (typeof value === 'boolean') return value
|
|
243
|
+
if (typeof value === 'number') return Number.isFinite(value)
|
|
244
|
+
if (typeof value === 'string') return value.trim().length > 0
|
|
245
|
+
if (Array.isArray(value)) return value.length > 0
|
|
246
|
+
if (typeof value === 'object') return Object.keys(value as Record<string, unknown>).length > 0
|
|
247
|
+
return true
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function evaluateSkillStatus(seed: SkillSeed): RuntimeSkillStatus {
|
|
251
|
+
const baseSkill = {
|
|
252
|
+
id: seed.storageId || seed.runtimeId,
|
|
253
|
+
name: seed.name,
|
|
254
|
+
filename: seed.filename,
|
|
255
|
+
content: seed.content,
|
|
256
|
+
skillRequirements: seed.skillRequirements,
|
|
257
|
+
} satisfies Pick<Skill, 'id' | 'name' | 'filename' | 'content' | 'skillRequirements'>
|
|
258
|
+
const eligibility = evaluateSkillEligibility(baseSkill as Skill)
|
|
259
|
+
const settings = loadSettings() as Record<string, unknown>
|
|
260
|
+
const configKeys = Array.isArray(seed.skillRequirements?.config) ? seed.skillRequirements?.config : []
|
|
261
|
+
const configChecks = configKeys.map((key) => ({
|
|
262
|
+
key,
|
|
263
|
+
ok: isTruthyConfigValue(readConfigPath(settings, key)),
|
|
264
|
+
}))
|
|
265
|
+
const missingConfig = configChecks.filter((check) => !check.ok).map((check) => check.key)
|
|
266
|
+
const reasons = [
|
|
267
|
+
...eligibility.reasons,
|
|
268
|
+
...(missingConfig.length > 0 ? [`Missing config: ${missingConfig.join(', ')}`] : []),
|
|
269
|
+
]
|
|
270
|
+
const installRequired = reasons.length > 0
|
|
271
|
+
&& ((seed.installOptions?.length || 0) > 0 || eligibility.missingBins.length > 0 || eligibility.missingAnyBins.length > 0)
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
...eligibility,
|
|
275
|
+
missingConfig,
|
|
276
|
+
reasons,
|
|
277
|
+
configChecks,
|
|
278
|
+
installRequired,
|
|
279
|
+
eligible: eligibility.eligible && missingConfig.length === 0,
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function formatMissing(status: RuntimeSkillStatus): string[] {
|
|
284
|
+
const missing: string[] = []
|
|
285
|
+
for (const value of status.missingBins) missing.push(value)
|
|
286
|
+
for (const group of status.missingAnyBins) {
|
|
287
|
+
if (group.length > 0) missing.push(`one of: ${group.join(' | ')}`)
|
|
288
|
+
}
|
|
289
|
+
for (const value of status.missingEnv) missing.push(`env ${value}`)
|
|
290
|
+
for (const value of status.missingConfig) missing.push(`config ${value}`)
|
|
291
|
+
if (status.unsupportedOs) missing.push(`os ${process.platform}`)
|
|
292
|
+
return missing
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function buildSeedFromStored(skill: Skill, attachedIds: Set<string>): SkillSeed {
|
|
296
|
+
const explicitToolNames = Array.isArray(skill.toolNames) ? skill.toolNames : []
|
|
297
|
+
const toolNames = inferToolNames({
|
|
298
|
+
name: skill.name,
|
|
299
|
+
skillKey: skill.skillKey,
|
|
300
|
+
explicit: explicitToolNames,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
runtimeId: `runtime:stored:${buildSkillKey(skill)}`,
|
|
305
|
+
storageId: skill.id,
|
|
306
|
+
name: skill.name,
|
|
307
|
+
key: buildSkillKey(skill),
|
|
308
|
+
filename: skill.filename,
|
|
309
|
+
content: skill.content || '',
|
|
310
|
+
description: skill.description || '',
|
|
311
|
+
tags: dedup(Array.isArray(skill.tags) ? skill.tags : []),
|
|
312
|
+
toolNames,
|
|
313
|
+
capabilities: inferCapabilities({
|
|
314
|
+
name: skill.name,
|
|
315
|
+
description: skill.description,
|
|
316
|
+
tags: skill.tags,
|
|
317
|
+
toolNames,
|
|
318
|
+
explicit: Array.isArray(skill.capabilities) ? skill.capabilities : [],
|
|
319
|
+
}),
|
|
320
|
+
source: 'stored',
|
|
321
|
+
sourceUrl: skill.sourceUrl,
|
|
322
|
+
sourceFormat: skill.sourceFormat,
|
|
323
|
+
author: skill.author,
|
|
324
|
+
version: skill.version,
|
|
325
|
+
homepage: skill.homepage,
|
|
326
|
+
primaryEnv: skill.primaryEnv,
|
|
327
|
+
skillKey: skill.skillKey,
|
|
328
|
+
always: skill.always === true,
|
|
329
|
+
attached: attachedIds.has(skill.id),
|
|
330
|
+
installOptions: skill.installOptions,
|
|
331
|
+
skillRequirements: skill.skillRequirements,
|
|
332
|
+
detectedEnvVars: skill.detectedEnvVars,
|
|
333
|
+
security: skill.security,
|
|
334
|
+
invocation: skill.invocation,
|
|
335
|
+
commandDispatch: skill.commandDispatch,
|
|
336
|
+
frontmatter: skill.frontmatter,
|
|
337
|
+
priority: SOURCE_PRIORITY.stored,
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildSeedFromDiscovered(skill: DiscoveredSkill): SkillSeed {
|
|
342
|
+
const explicitToolNames = Array.isArray(skill.toolNames) ? skill.toolNames : []
|
|
343
|
+
const toolNames = inferToolNames({
|
|
344
|
+
name: skill.name,
|
|
345
|
+
skillKey: skill.skillKey,
|
|
346
|
+
explicit: explicitToolNames,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
runtimeId: `runtime:${skill.source}:${buildSkillKey(skill)}`,
|
|
351
|
+
name: skill.name,
|
|
352
|
+
key: buildSkillKey(skill),
|
|
353
|
+
filename: skill.filename,
|
|
354
|
+
content: skill.content || '',
|
|
355
|
+
description: skill.description || '',
|
|
356
|
+
tags: dedup(Array.isArray(skill.tags) ? skill.tags : []),
|
|
357
|
+
toolNames,
|
|
358
|
+
capabilities: inferCapabilities({
|
|
359
|
+
name: skill.name,
|
|
360
|
+
description: skill.description,
|
|
361
|
+
tags: skill.tags,
|
|
362
|
+
toolNames,
|
|
363
|
+
explicit: Array.isArray(skill.capabilities) ? skill.capabilities : [],
|
|
364
|
+
}),
|
|
365
|
+
source: skill.source,
|
|
366
|
+
sourcePath: skill.sourcePath,
|
|
367
|
+
sourceUrl: skill.sourceUrl,
|
|
368
|
+
sourceFormat: skill.sourceFormat,
|
|
369
|
+
author: skill.author,
|
|
370
|
+
version: skill.version,
|
|
371
|
+
homepage: skill.homepage,
|
|
372
|
+
primaryEnv: skill.primaryEnv,
|
|
373
|
+
skillKey: skill.skillKey,
|
|
374
|
+
always: skill.always === true,
|
|
375
|
+
attached: false,
|
|
376
|
+
installOptions: skill.installOptions,
|
|
377
|
+
skillRequirements: skill.skillRequirements,
|
|
378
|
+
detectedEnvVars: skill.detectedEnvVars,
|
|
379
|
+
security: skill.security,
|
|
380
|
+
invocation: skill.invocation,
|
|
381
|
+
commandDispatch: skill.commandDispatch,
|
|
382
|
+
frontmatter: skill.frontmatter,
|
|
383
|
+
priority: SOURCE_PRIORITY[skill.source],
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function mergeSeeds(seeds: SkillSeed[]): SkillSeed {
|
|
388
|
+
const ordered = [...seeds].sort((a, b) => a.priority - b.priority)
|
|
389
|
+
const winner = ordered[ordered.length - 1]
|
|
390
|
+
const storedSeed = [...ordered].reverse().find((entry) => entry.storageId)
|
|
391
|
+
return {
|
|
392
|
+
...winner,
|
|
393
|
+
storageId: winner.storageId || storedSeed?.storageId,
|
|
394
|
+
attached: ordered.some((entry) => entry.attached),
|
|
395
|
+
always: ordered.some((entry) => entry.always),
|
|
396
|
+
tags: dedup(ordered.flatMap((entry) => entry.tags || [])),
|
|
397
|
+
toolNames: dedup(ordered.flatMap((entry) => entry.toolNames || [])),
|
|
398
|
+
capabilities: dedup(ordered.flatMap((entry) => entry.capabilities || [])),
|
|
399
|
+
invocation: [...ordered].reverse().find((entry) => entry.invocation)?.invocation || null,
|
|
400
|
+
commandDispatch: [...ordered].reverse().find((entry) => entry.commandDispatch)?.commandDispatch || null,
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function scoreSkillForRuntime(seed: SkillSeed, status: RuntimeSkillStatus, enabledPluginSet: Set<string>): {
|
|
405
|
+
autoMatch: boolean
|
|
406
|
+
score: number
|
|
407
|
+
matchReasons: string[]
|
|
408
|
+
} {
|
|
409
|
+
let score = 0
|
|
410
|
+
const reasons: string[] = []
|
|
411
|
+
const matchingTools = seed.toolNames.filter((toolName) => enabledPluginSet.has(normalizePluginId(toolName)))
|
|
412
|
+
if (matchingTools.length > 0) {
|
|
413
|
+
score += 45 + matchingTools.length
|
|
414
|
+
reasons.push(`matches tools: ${matchingTools.join(', ')}`)
|
|
415
|
+
}
|
|
416
|
+
if (seed.attached) {
|
|
417
|
+
score += 80
|
|
418
|
+
reasons.push('attached to agent')
|
|
419
|
+
}
|
|
420
|
+
if (seed.always) {
|
|
421
|
+
score += 40
|
|
422
|
+
reasons.push('always-on')
|
|
423
|
+
}
|
|
424
|
+
if (status.eligible) score += 8
|
|
425
|
+
if (!status.eligible && status.installRequired) score += 2
|
|
426
|
+
return {
|
|
427
|
+
autoMatch: matchingTools.length > 0,
|
|
428
|
+
score,
|
|
429
|
+
matchReasons: reasons,
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function toResolvedSkill(seed: SkillSeed, status: RuntimeSkillStatus, match: {
|
|
434
|
+
autoMatch: boolean
|
|
435
|
+
score: number
|
|
436
|
+
matchReasons: string[]
|
|
437
|
+
}, options: {
|
|
438
|
+
enabledPluginSet: Set<string>
|
|
439
|
+
selected: boolean
|
|
440
|
+
}): ResolvedRuntimeSkill {
|
|
441
|
+
const missing = formatMissing(status)
|
|
442
|
+
const state: ResolvedRuntimeSkill['status'] = status.eligible
|
|
443
|
+
? 'ready'
|
|
444
|
+
: status.installRequired
|
|
445
|
+
? 'needs_install'
|
|
446
|
+
: 'blocked'
|
|
447
|
+
|
|
448
|
+
const dispatch = seed.commandDispatch?.kind === 'tool' ? seed.commandDispatch : null
|
|
449
|
+
const dispatchToolAvailable = dispatch
|
|
450
|
+
? options.enabledPluginSet.has(normalizePluginId(dispatch.toolName))
|
|
451
|
+
|| options.enabledPluginSet.has(dispatch.toolName)
|
|
452
|
+
: false
|
|
453
|
+
const dispatchBlocker = dispatch
|
|
454
|
+
? !dispatchToolAvailable
|
|
455
|
+
? `dispatch tool ${dispatch.toolName} is not enabled in this session`
|
|
456
|
+
: !status.eligible
|
|
457
|
+
? missing.length > 0
|
|
458
|
+
? `skill is not ready: ${missing.join(', ')}`
|
|
459
|
+
: 'skill is not ready in this environment'
|
|
460
|
+
: null
|
|
461
|
+
: null
|
|
462
|
+
const executionMode: ResolvedRuntimeSkill['executionMode'] = dispatch ? 'dispatch' : 'prompt'
|
|
463
|
+
const runnable = executionMode === 'dispatch' && status.eligible && dispatchToolAvailable
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
id: seed.runtimeId,
|
|
467
|
+
key: seed.key,
|
|
468
|
+
storageId: seed.storageId,
|
|
469
|
+
name: seed.name,
|
|
470
|
+
filename: seed.filename,
|
|
471
|
+
content: seed.content,
|
|
472
|
+
description: seed.description,
|
|
473
|
+
tags: seed.tags,
|
|
474
|
+
toolNames: seed.toolNames,
|
|
475
|
+
capabilities: seed.capabilities,
|
|
476
|
+
source: seed.source,
|
|
477
|
+
sourcePath: seed.sourcePath,
|
|
478
|
+
sourceUrl: seed.sourceUrl,
|
|
479
|
+
sourceFormat: seed.sourceFormat,
|
|
480
|
+
author: seed.author,
|
|
481
|
+
version: seed.version,
|
|
482
|
+
homepage: seed.homepage,
|
|
483
|
+
primaryEnv: seed.primaryEnv,
|
|
484
|
+
skillKey: seed.skillKey,
|
|
485
|
+
always: seed.always,
|
|
486
|
+
attached: seed.attached,
|
|
487
|
+
managed: Boolean(seed.storageId),
|
|
488
|
+
installOptions: seed.installOptions,
|
|
489
|
+
skillRequirements: seed.skillRequirements,
|
|
490
|
+
detectedEnvVars: seed.detectedEnvVars,
|
|
491
|
+
security: seed.security,
|
|
492
|
+
invocation: seed.invocation,
|
|
493
|
+
commandDispatch: seed.commandDispatch,
|
|
494
|
+
frontmatter: seed.frontmatter,
|
|
495
|
+
eligible: status.eligible,
|
|
496
|
+
missing,
|
|
497
|
+
reasons: status.reasons,
|
|
498
|
+
status: state,
|
|
499
|
+
configChecks: status.configChecks,
|
|
500
|
+
autoMatch: match.autoMatch,
|
|
501
|
+
matchReasons: match.matchReasons,
|
|
502
|
+
score: match.score,
|
|
503
|
+
selected: options.selected,
|
|
504
|
+
executionMode,
|
|
505
|
+
runnable,
|
|
506
|
+
dispatchToolAvailable,
|
|
507
|
+
dispatchBlocker,
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function resolveRuntimeSkills(options: ResolveRuntimeSkillsOptions = {}): RuntimeSkillSnapshot {
|
|
512
|
+
const storedSkills = options.storedSkills || loadSkills()
|
|
513
|
+
const attachedIds = new Set(Array.isArray(options.agentSkillIds) ? options.agentSkillIds.filter(Boolean) : [])
|
|
514
|
+
const discovered = discoverSkills({ cwd: options.cwd || undefined })
|
|
515
|
+
const seeds = [
|
|
516
|
+
...Object.values(storedSkills).map((skill) => buildSeedFromStored(skill, attachedIds)),
|
|
517
|
+
...discovered.map((skill) => buildSeedFromDiscovered(skill)),
|
|
518
|
+
]
|
|
519
|
+
|
|
520
|
+
const grouped = new Map<string, SkillSeed[]>()
|
|
521
|
+
for (const seed of seeds) {
|
|
522
|
+
const current = grouped.get(seed.key) || []
|
|
523
|
+
current.push(seed)
|
|
524
|
+
grouped.set(seed.key, current)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const enabledPluginSet = new Set(
|
|
528
|
+
expandPluginIds(Array.isArray(options.enabledPlugins) ? options.enabledPlugins : [])
|
|
529
|
+
.map((entry) => normalizePluginId(entry))
|
|
530
|
+
.filter(Boolean),
|
|
531
|
+
)
|
|
532
|
+
const selectedSkillSelector = normalizeKey(options.selectedSkillId || '')
|
|
533
|
+
|
|
534
|
+
const skills = [...grouped.values()]
|
|
535
|
+
.map((entries) => {
|
|
536
|
+
const merged = mergeSeeds(entries)
|
|
537
|
+
const status = evaluateSkillStatus(merged)
|
|
538
|
+
const match = scoreSkillForRuntime(merged, status, enabledPluginSet)
|
|
539
|
+
const selected = Boolean(
|
|
540
|
+
selectedSkillSelector
|
|
541
|
+
&& [
|
|
542
|
+
merged.runtimeId,
|
|
543
|
+
merged.storageId,
|
|
544
|
+
merged.key,
|
|
545
|
+
merged.name,
|
|
546
|
+
merged.skillKey,
|
|
547
|
+
].some((value) => normalizeKey(value || '') === selectedSkillSelector),
|
|
548
|
+
)
|
|
549
|
+
return toResolvedSkill(merged, status, match, {
|
|
550
|
+
enabledPluginSet,
|
|
551
|
+
selected,
|
|
552
|
+
})
|
|
553
|
+
})
|
|
554
|
+
.sort((a, b) => {
|
|
555
|
+
if (b.score !== a.score) return b.score - a.score
|
|
556
|
+
return a.name.localeCompare(b.name)
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
const promptSkills = selectPromptSkills(skills)
|
|
560
|
+
const selectedSkill = skills.find((skill) => skill.selected) || null
|
|
561
|
+
const promptIds = new Set(promptSkills.map((skill) => skill.id))
|
|
562
|
+
return {
|
|
563
|
+
skills,
|
|
564
|
+
promptSkills,
|
|
565
|
+
selectedSkill,
|
|
566
|
+
attachedSkills: skills.filter((skill) => skill.attached),
|
|
567
|
+
autoMatchedSkills: skills.filter((skill) => skill.autoMatch),
|
|
568
|
+
availableSkills: skills.filter((skill) => !promptIds.has(skill.id) && !skill.selected),
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function selectPromptSkills(skills: ResolvedRuntimeSkill[]): ResolvedRuntimeSkill[] {
|
|
573
|
+
const ordered = [...skills]
|
|
574
|
+
.filter((skill) =>
|
|
575
|
+
(skill.attached || skill.always)
|
|
576
|
+
&& typeof skill.content === 'string'
|
|
577
|
+
&& skill.content.trim(),
|
|
578
|
+
)
|
|
579
|
+
.sort((a, b) => {
|
|
580
|
+
const priorityA = (a.attached ? 1000 : 0) + (a.always ? 500 : 0) + a.score
|
|
581
|
+
const priorityB = (b.attached ? 1000 : 0) + (b.always ? 500 : 0) + b.score
|
|
582
|
+
if (priorityB !== priorityA) return priorityB - priorityA
|
|
583
|
+
return a.name.localeCompare(b.name)
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
const selected: ResolvedRuntimeSkill[] = []
|
|
587
|
+
let totalChars = 0
|
|
588
|
+
for (const skill of ordered) {
|
|
589
|
+
if (selected.length >= MAX_SKILLS_IN_PROMPT) break
|
|
590
|
+
const contentLen = skill.name.length + skill.content.length + 12
|
|
591
|
+
if (totalChars + contentLen > MAX_SKILLS_PROMPT_CHARS) continue
|
|
592
|
+
totalChars += contentLen
|
|
593
|
+
selected.push(skill)
|
|
594
|
+
}
|
|
595
|
+
return selected
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function sectionFromSkills(params: {
|
|
599
|
+
title: string
|
|
600
|
+
preface: string
|
|
601
|
+
skills: ResolvedRuntimeSkill[]
|
|
602
|
+
}): string {
|
|
603
|
+
const usable = params.skills.filter((skill) => skill.content.trim())
|
|
604
|
+
if (usable.length === 0) return ''
|
|
605
|
+
const body = usable
|
|
606
|
+
.map((skill) => `### ${skill.name}\n${skill.content}`)
|
|
607
|
+
.join('\n\n')
|
|
608
|
+
return [params.title, params.preface, '', body].join('\n')
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export function buildRuntimeSkillPromptBlocks(snapshot: RuntimeSkillSnapshot): string[] {
|
|
612
|
+
const selectedId = snapshot.selectedSkill?.id || null
|
|
613
|
+
const blocks = [
|
|
614
|
+
buildSkillRuntimeInstructionBlock(snapshot),
|
|
615
|
+
sectionFromSkills({
|
|
616
|
+
title: '## Active Selected Skill',
|
|
617
|
+
preface: [
|
|
618
|
+
'This skill was already selected for the current task.',
|
|
619
|
+
'Keep using it unless the task materially changes or the tool result proves it is the wrong fit.',
|
|
620
|
+
].join('\n'),
|
|
621
|
+
skills: snapshot.selectedSkill ? [snapshot.selectedSkill] : [],
|
|
622
|
+
}),
|
|
623
|
+
sectionFromSkills({
|
|
624
|
+
title: '## Pinned Skills',
|
|
625
|
+
preface: [
|
|
626
|
+
'Before responding, check these pinned or always-on skills first.',
|
|
627
|
+
'They are the only skills included in the prompt before explicit selection.',
|
|
628
|
+
'Other skills stay discoverable below and should be selected on demand through `use_skill`.',
|
|
629
|
+
].join('\n'),
|
|
630
|
+
skills: snapshot.promptSkills.filter((skill) => !selectedId || skill.id !== selectedId),
|
|
631
|
+
}),
|
|
632
|
+
...buildAvailableSkillBlocks(snapshot.availableSkills),
|
|
633
|
+
]
|
|
634
|
+
return blocks.filter(Boolean)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function buildSkillRuntimeInstructionBlock(snapshot: RuntimeSkillSnapshot): string {
|
|
638
|
+
const availableCount = snapshot.availableSkills.length + (snapshot.selectedSkill ? 1 : 0) + snapshot.promptSkills.length
|
|
639
|
+
if (availableCount === 0) return ''
|
|
640
|
+
return [
|
|
641
|
+
'## Skill Runtime',
|
|
642
|
+
'Before replying: scan the available skill names and descriptions below.',
|
|
643
|
+
'- If exactly one skill clearly applies, call `use_skill` with `action=\"select\"` for that skill.',
|
|
644
|
+
'- If the selected skill shows `mode=dispatch`, call `use_skill` with `action=\"run\"` so it dispatches through its bound tool.',
|
|
645
|
+
'- If the selected skill shows `mode=prompt`, call `use_skill` with `action=\"load\"` once, then follow the loaded guidance.',
|
|
646
|
+
'- Do not load more than one non-pinned skill up front.',
|
|
647
|
+
'- Use `manage_skills` only for installation, attachment, or broader discovery/install management.',
|
|
648
|
+
].join('\n')
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function buildAvailableSkillBlocks(skills: ResolvedRuntimeSkill[]): string[] {
|
|
652
|
+
const lines = skills
|
|
653
|
+
.slice(0, 12)
|
|
654
|
+
.map((skill) => {
|
|
655
|
+
const status = skill.status === 'ready'
|
|
656
|
+
? 'ready'
|
|
657
|
+
: skill.missing.length > 0
|
|
658
|
+
? `needs ${skill.missing.join(', ')}`
|
|
659
|
+
: skill.status
|
|
660
|
+
const hint = skill.description ? `: ${(skill.description || '').slice(0, 120)}` : ''
|
|
661
|
+
const mode = skill.executionMode === 'dispatch'
|
|
662
|
+
? skill.dispatchToolAvailable
|
|
663
|
+
? `mode=dispatch tool=${skill.commandDispatch?.toolName}`
|
|
664
|
+
: `mode=dispatch blocked=${skill.dispatchBlocker || 'tool unavailable'}`
|
|
665
|
+
: 'mode=prompt'
|
|
666
|
+
return `- **${skill.name}** [${status}; ${mode}]${hint}`
|
|
667
|
+
})
|
|
668
|
+
return lines.length > 0
|
|
669
|
+
? [[
|
|
670
|
+
'## Available Skills',
|
|
671
|
+
'Local skills are discoverable by default. Select one on demand with `use_skill` instead of loading many skill bodies into the prompt.',
|
|
672
|
+
'',
|
|
673
|
+
lines.join('\n'),
|
|
674
|
+
].join('\n')]
|
|
675
|
+
: []
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export function recommendRuntimeSkillsForTask(
|
|
679
|
+
skills: ResolvedRuntimeSkill[],
|
|
680
|
+
task: string,
|
|
681
|
+
enabledPlugins?: string[] | null,
|
|
682
|
+
): RuntimeSkillRecommendation[] {
|
|
683
|
+
const queryTerms = new Set(tokenize(task))
|
|
684
|
+
const enabledPluginSet = new Set(
|
|
685
|
+
expandPluginIds(Array.isArray(enabledPlugins) ? enabledPlugins : [])
|
|
686
|
+
.map((entry) => normalizePluginId(entry))
|
|
687
|
+
.filter(Boolean),
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
return skills
|
|
691
|
+
.map((skill) => {
|
|
692
|
+
let score = skill.score
|
|
693
|
+
const reasons = [...skill.matchReasons]
|
|
694
|
+
const exactNameHit = queryTerms.has(normalizeKey(skill.name))
|
|
695
|
+
|| queryTerms.has(normalizeKey(skill.skillKey || ''))
|
|
696
|
+
if (exactNameHit) {
|
|
697
|
+
score += 35
|
|
698
|
+
reasons.push('task mentions the skill directly')
|
|
699
|
+
}
|
|
700
|
+
const nameOverlap = scoreOverlap(tokenize(skill.name), queryTerms)
|
|
701
|
+
if (nameOverlap > 0) {
|
|
702
|
+
score += nameOverlap * 10
|
|
703
|
+
reasons.push('name overlaps task keywords')
|
|
704
|
+
}
|
|
705
|
+
const capabilityOverlap = scoreOverlap(skill.capabilities, queryTerms)
|
|
706
|
+
if (capabilityOverlap > 0) {
|
|
707
|
+
score += capabilityOverlap * 6
|
|
708
|
+
reasons.push('capabilities overlap task keywords')
|
|
709
|
+
}
|
|
710
|
+
const tagOverlap = scoreOverlap(skill.tags, queryTerms)
|
|
711
|
+
if (tagOverlap > 0) {
|
|
712
|
+
score += tagOverlap * 4
|
|
713
|
+
}
|
|
714
|
+
const descriptionOverlap = scoreOverlap(tokenize(skill.description), queryTerms)
|
|
715
|
+
if (descriptionOverlap > 0) {
|
|
716
|
+
score += descriptionOverlap * 3
|
|
717
|
+
}
|
|
718
|
+
const toolOverlap = skill.toolNames.filter((toolName) => enabledPluginSet.has(normalizePluginId(toolName)))
|
|
719
|
+
if (toolOverlap.length > 0) {
|
|
720
|
+
score += toolOverlap.length * 8
|
|
721
|
+
}
|
|
722
|
+
if (skill.attached) score += 12
|
|
723
|
+
if (skill.eligible) score += 6
|
|
724
|
+
return {
|
|
725
|
+
skill,
|
|
726
|
+
score,
|
|
727
|
+
reasons: dedup(reasons),
|
|
728
|
+
}
|
|
729
|
+
})
|
|
730
|
+
.filter((entry) => entry.score > 0)
|
|
731
|
+
.sort((a, b) => {
|
|
732
|
+
if (b.score !== a.score) return b.score - a.score
|
|
733
|
+
return a.skill.name.localeCompare(b.skill.name)
|
|
734
|
+
})
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export function findResolvedSkill(
|
|
738
|
+
skills: ResolvedRuntimeSkill[],
|
|
739
|
+
selector: string,
|
|
740
|
+
): ResolvedRuntimeSkill | null {
|
|
741
|
+
const normalized = normalizeKey(selector)
|
|
742
|
+
if (!normalized) return null
|
|
743
|
+
return skills.find((skill) =>
|
|
744
|
+
normalizeKey(skill.id) === normalized
|
|
745
|
+
|| normalizeKey(skill.storageId || '') === normalized
|
|
746
|
+
|| normalizeKey(skill.key) === normalized
|
|
747
|
+
|| normalizeKey(skill.name) === normalized
|
|
748
|
+
|| normalizeKey(skill.skillKey || '') === normalized,
|
|
749
|
+
) || null
|
|
750
|
+
}
|