@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.
Files changed (75) hide show
  1. package/README.md +12 -10
  2. package/bundled-skills/google-workspace/SKILL.md +2 -0
  3. package/package.json +1 -1
  4. package/src/app/agents/page.tsx +2 -1
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +1 -1
  6. package/src/app/api/clawhub/install/route.ts +2 -0
  7. package/src/app/api/skills/[id]/route.ts +4 -0
  8. package/src/app/api/skills/route.ts +4 -0
  9. package/src/app/globals.css +28 -0
  10. package/src/app/home/page.tsx +11 -0
  11. package/src/app/settings/page.tsx +12 -5
  12. package/src/components/agents/agent-sheet.tsx +5 -5
  13. package/src/components/connectors/connector-list.tsx +2 -5
  14. package/src/components/logs/log-list.tsx +2 -5
  15. package/src/components/providers/provider-list.tsx +2 -5
  16. package/src/components/runs/run-list.tsx +2 -6
  17. package/src/components/schedules/schedule-list.tsx +7 -1
  18. package/src/components/ui/full-screen-loader.tsx +0 -29
  19. package/src/components/ui/page-loader.tsx +69 -0
  20. package/src/lib/runtime/runtime-loop.ts +21 -1
  21. package/src/lib/server/agents/agent-thread-session.test.ts +64 -0
  22. package/src/lib/server/agents/agent-thread-session.ts +1 -1
  23. package/src/lib/server/agents/main-agent-loop-advanced.test.ts +77 -0
  24. package/src/lib/server/agents/main-agent-loop.ts +259 -0
  25. package/src/lib/server/agents/orchestrator-lg.ts +12 -8
  26. package/src/lib/server/agents/orchestrator.ts +11 -7
  27. package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +11 -10
  28. package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +116 -3
  29. package/src/lib/server/chat-execution/chat-execution-utils.test.ts +56 -0
  30. package/src/lib/server/chat-execution/chat-execution-utils.ts +24 -0
  31. package/src/lib/server/chat-execution/chat-execution.ts +116 -29
  32. package/src/lib/server/chat-execution/chat-streaming-utils.ts +1 -38
  33. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +67 -76
  34. package/src/lib/server/chat-execution/stream-agent-chat.ts +119 -110
  35. package/src/lib/server/chat-execution/stream-continuation.ts +1 -1
  36. package/src/lib/server/chatrooms/chatroom-helpers.test.ts +26 -0
  37. package/src/lib/server/chatrooms/chatroom-helpers.ts +11 -8
  38. package/src/lib/server/connectors/contact-boundaries.ts +101 -0
  39. package/src/lib/server/connectors/manager.test.ts +504 -73
  40. package/src/lib/server/connectors/manager.ts +41 -10
  41. package/src/lib/server/connectors/session-consolidation.ts +2 -0
  42. package/src/lib/server/connectors/session-kind.ts +7 -0
  43. package/src/lib/server/connectors/session.test.ts +104 -0
  44. package/src/lib/server/connectors/session.ts +5 -2
  45. package/src/lib/server/identity-continuity.test.ts +4 -3
  46. package/src/lib/server/identity-continuity.ts +8 -4
  47. package/src/lib/server/memory/memory-policy.test.ts +5 -15
  48. package/src/lib/server/memory/memory-policy.ts +11 -41
  49. package/src/lib/server/memory/session-archive-memory.ts +2 -1
  50. package/src/lib/server/runtime/heartbeat-service.test.ts +46 -0
  51. package/src/lib/server/runtime/heartbeat-service.ts +5 -1
  52. package/src/lib/server/runtime/runtime-settings.test.ts +4 -4
  53. package/src/lib/server/runtime/runtime-settings.ts +4 -0
  54. package/src/lib/server/runtime/session-run-manager.ts +2 -0
  55. package/src/lib/server/session-reset-policy.test.ts +17 -3
  56. package/src/lib/server/session-reset-policy.ts +4 -2
  57. package/src/lib/server/session-tools/connector.ts +11 -10
  58. package/src/lib/server/session-tools/crud.ts +41 -7
  59. package/src/lib/server/session-tools/delegate.ts +3 -3
  60. package/src/lib/server/session-tools/index.ts +2 -0
  61. package/src/lib/server/session-tools/manage-skills.test.ts +194 -0
  62. package/src/lib/server/session-tools/memory.ts +209 -48
  63. package/src/lib/server/session-tools/skill-runtime.test.ts +175 -0
  64. package/src/lib/server/session-tools/skill-runtime.ts +382 -0
  65. package/src/lib/server/session-tools/skills.ts +575 -0
  66. package/src/lib/server/skills/runtime-skill-resolver.test.ts +162 -0
  67. package/src/lib/server/skills/runtime-skill-resolver.ts +750 -0
  68. package/src/lib/server/skills/skill-discovery.ts +4 -0
  69. package/src/lib/server/skills/skills-normalize.test.ts +28 -0
  70. package/src/lib/server/skills/skills-normalize.ts +93 -1
  71. package/src/lib/server/storage.ts +1 -1
  72. package/src/lib/server/tasks/task-followups.test.ts +124 -0
  73. package/src/lib/server/tasks/task-followups.ts +88 -13
  74. package/src/types/index.ts +30 -2
  75. package/src/views/settings/section-runtime-loop.tsx +38 -0
@@ -0,0 +1,382 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import type { Agent, Session, SessionSkillRuntimeState } from '@/types'
4
+ import { errorMessage } from '@/lib/shared-utils'
5
+ import { loadAgent, loadSkills, patchSession } from '@/lib/server/storage'
6
+ import {
7
+ findResolvedSkill,
8
+ recommendRuntimeSkillsForTask,
9
+ resolveRuntimeSkills,
10
+ type ResolvedRuntimeSkill,
11
+ type RuntimeSkillRecommendation,
12
+ type RuntimeSkillSnapshot,
13
+ } from '@/lib/server/skills/runtime-skill-resolver'
14
+ import type { ToolBuildContext } from './context'
15
+ import { normalizeToolInputArgs } from './normalize-tool-args'
16
+
17
+ function resolveActiveAgent(bctx: ToolBuildContext): Agent | null {
18
+ const agentId = bctx.ctx?.agentId
19
+ if (!agentId) return null
20
+ return loadAgent(agentId) as Agent | null
21
+ }
22
+
23
+ function resolveCurrentSession(bctx: ToolBuildContext): Session | null {
24
+ const session = bctx.resolveCurrentSession?.()
25
+ if (!session || typeof session !== 'object') return null
26
+ return session as Session
27
+ }
28
+
29
+ function selectedSkillIdFromSession(session: Session | null): string | null {
30
+ const selectedSkillId = typeof session?.skillRuntimeState?.selectedSkillId === 'string'
31
+ ? session.skillRuntimeState.selectedSkillId.trim()
32
+ : ''
33
+ return selectedSkillId || null
34
+ }
35
+
36
+ function buildRuntimeSnapshot(bctx: ToolBuildContext): RuntimeSkillSnapshot {
37
+ const session = resolveCurrentSession(bctx)
38
+ const activeAgent = resolveActiveAgent(bctx)
39
+ return resolveRuntimeSkills({
40
+ cwd: bctx.cwd,
41
+ enabledPlugins: bctx.activePlugins,
42
+ agentSkillIds: activeAgent?.skillIds || [],
43
+ storedSkills: loadSkills(),
44
+ selectedSkillId: selectedSkillIdFromSession(session),
45
+ })
46
+ }
47
+
48
+ function summarizeRuntimeSkill(skill: ResolvedRuntimeSkill): Record<string, unknown> {
49
+ return {
50
+ id: skill.id,
51
+ storageId: skill.storageId || null,
52
+ key: skill.key,
53
+ name: skill.name,
54
+ description: skill.description || '',
55
+ status: skill.status,
56
+ source: skill.source,
57
+ attached: skill.attached,
58
+ selected: skill.selected,
59
+ eligible: skill.eligible,
60
+ missing: skill.missing,
61
+ toolNames: skill.toolNames,
62
+ capabilities: skill.capabilities,
63
+ executionMode: skill.executionMode,
64
+ runnable: skill.runnable,
65
+ invocation: skill.invocation || null,
66
+ commandDispatch: skill.commandDispatch || null,
67
+ dispatchBlocker: skill.dispatchBlocker || null,
68
+ sourcePath: skill.sourcePath || null,
69
+ sourceUrl: skill.sourceUrl || null,
70
+ }
71
+ }
72
+
73
+ function parseSearchLimit(raw: unknown, fallback = 8): number {
74
+ const parsed = typeof raw === 'number' ? raw : Number.parseInt(String(raw || ''), 10)
75
+ if (!Number.isFinite(parsed)) return fallback
76
+ return Math.max(1, Math.min(20, Math.trunc(parsed)))
77
+ }
78
+
79
+ function resolveSkillSelector(rawArgs: Record<string, unknown>): string {
80
+ for (const key of ['id', 'skillId', 'name']) {
81
+ const value = rawArgs[key]
82
+ if (typeof value === 'string' && value.trim()) return value.trim()
83
+ }
84
+ return ''
85
+ }
86
+
87
+ function resolveTargetSkill(params: {
88
+ rawArgs: Record<string, unknown>
89
+ snapshot: RuntimeSkillSnapshot
90
+ requireExplicit?: boolean
91
+ }): ResolvedRuntimeSkill | null {
92
+ const selector = resolveSkillSelector(params.rawArgs)
93
+ if (selector) return findResolvedSkill(params.snapshot.skills, selector)
94
+
95
+ const query = typeof params.rawArgs.query === 'string' ? params.rawArgs.query.trim() : ''
96
+ if (query) {
97
+ const ranked = recommendRuntimeSkillsForTask(params.snapshot.skills, query)
98
+ const top = ranked.find((entry) => entry.skill.eligible || entry.skill.runnable)
99
+ || ranked[0]
100
+ return top?.skill || null
101
+ }
102
+
103
+ if (!params.requireExplicit && params.snapshot.selectedSkill) return params.snapshot.selectedSkill
104
+ return null
105
+ }
106
+
107
+ function persistSkillRuntimeState(params: {
108
+ bctx: ToolBuildContext
109
+ skill: ResolvedRuntimeSkill
110
+ action: NonNullable<SessionSkillRuntimeState['lastAction']>
111
+ toolName?: string | null
112
+ }): void {
113
+ if (!params.bctx.ctx?.sessionId) return
114
+ patchSession(params.bctx.ctx.sessionId, (currentSession) => {
115
+ if (!currentSession) return currentSession
116
+ const current = currentSession.skillRuntimeState && typeof currentSession.skillRuntimeState === 'object'
117
+ ? currentSession.skillRuntimeState
118
+ : {}
119
+ currentSession.skillRuntimeState = {
120
+ ...current,
121
+ selectedSkillId: params.skill.id,
122
+ selectedSkillName: params.skill.name,
123
+ selectedAt: current.selectedSkillId === params.skill.id ? current.selectedAt || Date.now() : Date.now(),
124
+ lastAction: params.action,
125
+ lastRunAt: params.action === 'run' ? Date.now() : current.lastRunAt || null,
126
+ lastRunToolName: params.action === 'run' ? params.toolName || null : current.lastRunToolName || null,
127
+ }
128
+ currentSession.updatedAt = Date.now()
129
+ return currentSession
130
+ })
131
+ }
132
+
133
+ function parseJsonValue(value: string): unknown {
134
+ try {
135
+ return JSON.parse(value)
136
+ } catch {
137
+ return null
138
+ }
139
+ }
140
+
141
+ function parseJsonObject(value: string): Record<string, unknown> | null {
142
+ const parsed = parseJsonValue(value)
143
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed as Record<string, unknown>
144
+ return null
145
+ }
146
+
147
+ function normalizeDispatchArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
148
+ const reserved = new Set([
149
+ 'action',
150
+ 'id',
151
+ 'skillId',
152
+ 'name',
153
+ 'query',
154
+ 'args',
155
+ 'payload',
156
+ 'parameters',
157
+ 'toolArgs',
158
+ 'input',
159
+ 'limit',
160
+ ])
161
+
162
+ for (const key of ['toolArgs', 'args', 'parameters', 'payload']) {
163
+ const value = rawArgs[key]
164
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>
165
+ if (typeof value === 'string' && value.trim()) {
166
+ const parsed = parseJsonObject(value)
167
+ if (parsed) return parsed
168
+ return { input: value.trim() }
169
+ }
170
+ }
171
+
172
+ const directFields = Object.fromEntries(
173
+ Object.entries(rawArgs).filter(([key, value]) => !reserved.has(key) && value !== undefined),
174
+ )
175
+ if (Object.keys(directFields).length > 0) return directFields
176
+
177
+ if (typeof rawArgs.input === 'string' && rawArgs.input.trim()) {
178
+ const parsed = parseJsonObject(rawArgs.input)
179
+ if (parsed) return parsed
180
+ return { input: rawArgs.input.trim() }
181
+ }
182
+
183
+ return {}
184
+ }
185
+
186
+ function explainSkillBlocker(skill: ResolvedRuntimeSkill | null): Record<string, unknown> {
187
+ if (!skill) {
188
+ return {
189
+ ok: false,
190
+ blocker: 'No selected skill is available for this run.',
191
+ }
192
+ }
193
+
194
+ if (skill.executionMode === 'prompt') {
195
+ return {
196
+ ok: false,
197
+ skill: summarizeRuntimeSkill(skill),
198
+ blocker: 'This skill does not expose executable dispatch metadata.',
199
+ nextAction: 'Call use_skill with action="load" once, then follow the loaded guidance.',
200
+ }
201
+ }
202
+
203
+ if (!skill.runnable) {
204
+ return {
205
+ ok: false,
206
+ skill: summarizeRuntimeSkill(skill),
207
+ blocker: skill.dispatchBlocker || 'The selected skill is not runnable in the current session.',
208
+ missing: skill.missing,
209
+ }
210
+ }
211
+
212
+ return {
213
+ ok: true,
214
+ skill: summarizeRuntimeSkill(skill),
215
+ blocker: null,
216
+ }
217
+ }
218
+
219
+ async function dispatchSkillRun(params: {
220
+ bctx: ToolBuildContext
221
+ skill: ResolvedRuntimeSkill
222
+ rawArgs: Record<string, unknown>
223
+ }): Promise<string> {
224
+ const dispatch = params.skill.commandDispatch
225
+ if (!dispatch || dispatch.kind !== 'tool') {
226
+ return JSON.stringify({
227
+ ok: true,
228
+ executed: false,
229
+ mode: 'prompt_guidance',
230
+ skill: summarizeRuntimeSkill(params.skill),
231
+ guidance: params.skill.content,
232
+ message: 'This skill has no executable dispatch surface. Follow the loaded guidance in the next step.',
233
+ })
234
+ }
235
+
236
+ if (!params.skill.runnable) {
237
+ return JSON.stringify({
238
+ ok: false,
239
+ executed: false,
240
+ mode: 'dispatch_blocked',
241
+ skill: summarizeRuntimeSkill(params.skill),
242
+ blocker: params.skill.dispatchBlocker || 'The selected skill is not runnable in this session.',
243
+ missing: params.skill.missing,
244
+ })
245
+ }
246
+
247
+ if (dispatch.toolName === 'use_skill') {
248
+ return JSON.stringify({
249
+ ok: false,
250
+ executed: false,
251
+ mode: 'dispatch_blocked',
252
+ skill: summarizeRuntimeSkill(params.skill),
253
+ blocker: 'A skill cannot dispatch back into use_skill.',
254
+ })
255
+ }
256
+
257
+ const toolArgs = normalizeDispatchArgs(params.rawArgs)
258
+ const { buildSessionTools } = await import('./index')
259
+ const built = await buildSessionTools(params.bctx.cwd, params.bctx.activePlugins, params.bctx.ctx)
260
+ try {
261
+ const targetTool = built.tools.find((entry) => entry.name === dispatch.toolName)
262
+ if (!targetTool) {
263
+ return JSON.stringify({
264
+ ok: false,
265
+ executed: false,
266
+ mode: 'dispatch_blocked',
267
+ skill: summarizeRuntimeSkill(params.skill),
268
+ blocker: `Dispatch tool "${dispatch.toolName}" is not available in this session.`,
269
+ })
270
+ }
271
+
272
+ const toolOutput = await targetTool.invoke(toolArgs)
273
+ persistSkillRuntimeState({
274
+ bctx: params.bctx,
275
+ skill: params.skill,
276
+ action: 'run',
277
+ toolName: dispatch.toolName,
278
+ })
279
+
280
+ return JSON.stringify({
281
+ ok: true,
282
+ executed: true,
283
+ mode: 'dispatch',
284
+ skill: summarizeRuntimeSkill(params.skill),
285
+ dispatchedTool: dispatch.toolName,
286
+ toolArgs,
287
+ toolOutput: typeof toolOutput === 'string'
288
+ ? (parseJsonValue(toolOutput) ?? toolOutput)
289
+ : toolOutput,
290
+ })
291
+ } finally {
292
+ await built.cleanup()
293
+ }
294
+ }
295
+
296
+ export function buildSkillRuntimeTools(bctx: ToolBuildContext): StructuredToolInterface[] {
297
+ return [
298
+ tool(
299
+ async (rawArgs) => {
300
+ const normalized = normalizeToolInputArgs((rawArgs ?? {}) as Record<string, unknown>)
301
+ const action = typeof normalized.action === 'string' ? normalized.action.trim().toLowerCase() : ''
302
+ const snapshot = buildRuntimeSnapshot(bctx)
303
+
304
+ try {
305
+ switch (action) {
306
+ case 'list': {
307
+ const query = typeof normalized.query === 'string' ? normalized.query.trim() : ''
308
+ const limit = parseSearchLimit(normalized.limit, 12)
309
+ const ranked: RuntimeSkillRecommendation[] = query
310
+ ? recommendRuntimeSkillsForTask(snapshot.skills, query, bctx.activePlugins)
311
+ : snapshot.skills.map((skill) => ({ skill, score: skill.score, reasons: skill.matchReasons }))
312
+ return JSON.stringify({
313
+ selectedSkillId: snapshot.selectedSkill?.id || null,
314
+ skills: ranked.slice(0, limit).map((entry) => ({
315
+ ...summarizeRuntimeSkill(entry.skill),
316
+ score: entry.score,
317
+ reasons: entry.reasons,
318
+ })),
319
+ })
320
+ }
321
+ case 'select': {
322
+ const target = resolveTargetSkill({ rawArgs: normalized, snapshot, requireExplicit: true })
323
+ if (!target) return JSON.stringify({ ok: false, blocker: 'No matching skill found to select.' })
324
+ persistSkillRuntimeState({ bctx, skill: target, action: 'select' })
325
+ return JSON.stringify({
326
+ ok: true,
327
+ selected: true,
328
+ skill: summarizeRuntimeSkill(target),
329
+ })
330
+ }
331
+ case 'load': {
332
+ const target = resolveTargetSkill({ rawArgs: normalized, snapshot })
333
+ if (!target) return JSON.stringify({ ok: false, blocker: 'No selected or matching skill found to load.' })
334
+ persistSkillRuntimeState({ bctx, skill: target, action: 'load' })
335
+ return JSON.stringify({
336
+ ok: true,
337
+ loaded: true,
338
+ skill: summarizeRuntimeSkill(target),
339
+ guidance: target.content,
340
+ })
341
+ }
342
+ case 'run': {
343
+ const target = resolveTargetSkill({ rawArgs: normalized, snapshot })
344
+ if (!target) return JSON.stringify({ ok: false, blocker: 'No selected or matching skill found to run.' })
345
+ persistSkillRuntimeState({ bctx, skill: target, action: 'select' })
346
+ return dispatchSkillRun({ bctx, skill: target, rawArgs: normalized })
347
+ }
348
+ case 'explain_blocker': {
349
+ const target = resolveTargetSkill({ rawArgs: normalized, snapshot })
350
+ return JSON.stringify(explainSkillBlocker(target))
351
+ }
352
+ default:
353
+ return `Error: Unknown action "${action}".`
354
+ }
355
+ } catch (err: unknown) {
356
+ return `Error: ${errorMessage(err)}`
357
+ }
358
+ },
359
+ {
360
+ name: 'use_skill',
361
+ description: [
362
+ 'Runtime skill selection and execution surface.',
363
+ 'Use `list` to inspect available skills, `select` to persist one for the current task, `load` to fetch its guidance, `run` to dispatch executable skills through their bound tools, and `explain_blocker` to understand why a selected skill cannot run.',
364
+ 'Prefer this tool over stuffing many skill bodies into the prompt.',
365
+ ].join('\n\n'),
366
+ schema: z.object({
367
+ action: z.enum(['list', 'select', 'load', 'run', 'explain_blocker']),
368
+ id: z.string().optional().describe('Skill runtime id or stored skill id'),
369
+ skillId: z.string().optional().describe('Alternate skill selector'),
370
+ name: z.string().optional().describe('Skill name selector'),
371
+ query: z.string().optional().describe('Task query used to rank/select a skill'),
372
+ limit: z.number().optional().describe('Maximum number of listed skills'),
373
+ input: z.string().optional().describe('String input forwarded to the dispatched tool when the skill uses raw dispatch'),
374
+ args: z.union([z.string(), z.record(z.string(), z.unknown())]).optional().describe('Tool arguments for action="run"'),
375
+ parameters: z.union([z.string(), z.record(z.string(), z.unknown())]).optional().describe('Alternate tool arguments for action="run"'),
376
+ payload: z.union([z.string(), z.record(z.string(), z.unknown())]).optional().describe('Alternate tool arguments for action="run"'),
377
+ toolArgs: z.record(z.string(), z.unknown()).optional().describe('Object args forwarded directly to the dispatched tool'),
378
+ }).passthrough(),
379
+ },
380
+ ),
381
+ ]
382
+ }