@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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { after, before, describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import type { Agent, Skill } from '@/types'
|
|
7
|
+
|
|
8
|
+
const originalEnv = {
|
|
9
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
10
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
11
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let tempDir = ''
|
|
15
|
+
let workspaceDir = ''
|
|
16
|
+
let buildCrudTools: Awaited<typeof import('./crud')>['buildCrudTools']
|
|
17
|
+
let loadSkills: Awaited<typeof import('../storage')>['loadSkills']
|
|
18
|
+
let loadAgent: Awaited<typeof import('../storage')>['loadAgent']
|
|
19
|
+
let saveAgents: Awaited<typeof import('../storage')>['saveAgents']
|
|
20
|
+
let saveSessions: Awaited<typeof import('../storage')>['saveSessions']
|
|
21
|
+
let upsertApproval: Awaited<typeof import('../storage')>['upsertApproval']
|
|
22
|
+
|
|
23
|
+
function buildManageSkillsTool() {
|
|
24
|
+
const tools = buildCrudTools({
|
|
25
|
+
cwd: workspaceDir,
|
|
26
|
+
ctx: { sessionId: 'skill-session', agentId: 'agent-skill-test', platformAssignScope: 'self' },
|
|
27
|
+
hasPlugin: (name) => name === 'manage_skills',
|
|
28
|
+
hasTool: (name) => name === 'manage_skills',
|
|
29
|
+
cleanupFns: [],
|
|
30
|
+
commandTimeoutMs: 1_000,
|
|
31
|
+
claudeTimeoutMs: 1_000,
|
|
32
|
+
cliProcessTimeoutMs: 1_000,
|
|
33
|
+
persistDelegateResumeId: () => {},
|
|
34
|
+
readStoredDelegateResumeId: () => null,
|
|
35
|
+
resolveCurrentSession: () => null,
|
|
36
|
+
activePlugins: ['manage_skills', 'google_workspace'],
|
|
37
|
+
})
|
|
38
|
+
const tool = tools.find((entry) => entry.name === 'manage_skills')
|
|
39
|
+
assert.ok(tool, 'expected manage_skills tool')
|
|
40
|
+
return tool!
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
before(async () => {
|
|
44
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-manage-skills-'))
|
|
45
|
+
workspaceDir = path.join(tempDir, 'workspace')
|
|
46
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
47
|
+
process.env.WORKSPACE_DIR = workspaceDir
|
|
48
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
49
|
+
fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
|
|
50
|
+
fs.mkdirSync(workspaceDir, { recursive: true })
|
|
51
|
+
|
|
52
|
+
const crudMod = await import('./crud')
|
|
53
|
+
buildCrudTools = crudMod.buildCrudTools
|
|
54
|
+
|
|
55
|
+
const storageMod = await import('../storage')
|
|
56
|
+
loadSkills = storageMod.loadSkills
|
|
57
|
+
loadAgent = storageMod.loadAgent
|
|
58
|
+
saveAgents = storageMod.saveAgents
|
|
59
|
+
saveSessions = storageMod.saveSessions
|
|
60
|
+
upsertApproval = storageMod.upsertApproval
|
|
61
|
+
|
|
62
|
+
saveAgents({
|
|
63
|
+
'agent-skill-test': {
|
|
64
|
+
id: 'agent-skill-test',
|
|
65
|
+
name: 'Skill Tester',
|
|
66
|
+
provider: 'openai',
|
|
67
|
+
model: 'gpt-test',
|
|
68
|
+
plugins: ['manage_skills'],
|
|
69
|
+
tools: ['manage_skills'],
|
|
70
|
+
skillIds: [],
|
|
71
|
+
platformAssignScope: 'self',
|
|
72
|
+
createdAt: Date.now(),
|
|
73
|
+
updatedAt: Date.now(),
|
|
74
|
+
} satisfies Agent,
|
|
75
|
+
})
|
|
76
|
+
saveSessions({
|
|
77
|
+
'skill-session': {
|
|
78
|
+
id: 'skill-session',
|
|
79
|
+
name: 'Skill Session',
|
|
80
|
+
cwd: workspaceDir,
|
|
81
|
+
user: 'tester',
|
|
82
|
+
provider: 'openai',
|
|
83
|
+
model: 'gpt-test',
|
|
84
|
+
messages: [],
|
|
85
|
+
createdAt: Date.now(),
|
|
86
|
+
lastActiveAt: Date.now(),
|
|
87
|
+
sessionType: 'human',
|
|
88
|
+
agentId: 'agent-skill-test',
|
|
89
|
+
plugins: ['manage_skills'],
|
|
90
|
+
heartbeatEnabled: false,
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
after(() => {
|
|
96
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
97
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
98
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
99
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
100
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
101
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
102
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('manage_skills runtime actions', () => {
|
|
106
|
+
it('status reports resolved local skills with eligibility and metadata', async () => {
|
|
107
|
+
const manageSkills = buildManageSkillsTool()
|
|
108
|
+
const created = await manageSkills.invoke({
|
|
109
|
+
action: 'create',
|
|
110
|
+
name: 'workspace-helper',
|
|
111
|
+
description: 'Automate workspace docs.',
|
|
112
|
+
content: '# Workspace Helper\nUse the workspace workflow.',
|
|
113
|
+
toolNames: ['google_workspace'],
|
|
114
|
+
capabilities: ['docs', 'workspace'],
|
|
115
|
+
})
|
|
116
|
+
const createdSkill = JSON.parse(String(created)) as Skill
|
|
117
|
+
|
|
118
|
+
const raw = await manageSkills.invoke({ action: 'status', query: 'workspace docs' })
|
|
119
|
+
const result = JSON.parse(String(raw)) as Array<Record<string, unknown>>
|
|
120
|
+
|
|
121
|
+
const statusEntry = result.find((entry) => entry.storageId === createdSkill.id)
|
|
122
|
+
assert.ok(statusEntry)
|
|
123
|
+
assert.equal(statusEntry?.eligible, true)
|
|
124
|
+
assert.deepEqual(statusEntry?.toolNames, ['google_workspace'])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('attach materializes a discovered project skill and binds it to the current agent', async () => {
|
|
128
|
+
const localSkillDir = path.join(workspaceDir, 'skills', 'project-helper')
|
|
129
|
+
fs.mkdirSync(localSkillDir, { recursive: true })
|
|
130
|
+
fs.writeFileSync(path.join(localSkillDir, 'SKILL.md'), `---
|
|
131
|
+
name: project-helper
|
|
132
|
+
description: Project-local helper.
|
|
133
|
+
metadata:
|
|
134
|
+
openclaw:
|
|
135
|
+
toolNames: [google_workspace]
|
|
136
|
+
---
|
|
137
|
+
# Project Helper
|
|
138
|
+
|
|
139
|
+
Use the project helper.
|
|
140
|
+
`)
|
|
141
|
+
|
|
142
|
+
const manageSkills = buildManageSkillsTool()
|
|
143
|
+
const raw = await manageSkills.invoke({
|
|
144
|
+
action: 'attach',
|
|
145
|
+
name: 'project-helper',
|
|
146
|
+
})
|
|
147
|
+
const result = JSON.parse(String(raw)) as Record<string, unknown>
|
|
148
|
+
const skillId = String(result.skillId || '')
|
|
149
|
+
const agent = loadAgent('agent-skill-test') as Agent
|
|
150
|
+
|
|
151
|
+
assert.ok(skillId)
|
|
152
|
+
assert.ok(loadSkills()[skillId], 'discovered skill copied into managed storage')
|
|
153
|
+
assert.ok(agent.skillIds?.includes(skillId), 'skill attached to current agent')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('install is approval-gated and can install a remote skill after approval', async () => {
|
|
157
|
+
const manageSkills = buildManageSkillsTool()
|
|
158
|
+
const firstRaw = await manageSkills.invoke({
|
|
159
|
+
action: 'install',
|
|
160
|
+
name: 'remote-helper',
|
|
161
|
+
url: 'https://clawhub.ai/skills/remote-helper',
|
|
162
|
+
content: '# Remote Helper\nUse the remote helper.',
|
|
163
|
+
attach: true,
|
|
164
|
+
})
|
|
165
|
+
const first = JSON.parse(String(firstRaw)) as Record<string, unknown>
|
|
166
|
+
const approval = first.approval as { id: string }
|
|
167
|
+
|
|
168
|
+
assert.equal(first.requiresApproval, true)
|
|
169
|
+
assert.ok(approval?.id)
|
|
170
|
+
|
|
171
|
+
upsertApproval(approval.id, {
|
|
172
|
+
...(first.approval as Record<string, unknown>),
|
|
173
|
+
status: 'approved',
|
|
174
|
+
updatedAt: Date.now(),
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const secondRaw = await manageSkills.invoke({
|
|
178
|
+
action: 'install',
|
|
179
|
+
name: 'remote-helper',
|
|
180
|
+
url: 'https://clawhub.ai/skills/remote-helper',
|
|
181
|
+
content: '# Remote Helper\nUse the remote helper.',
|
|
182
|
+
attach: true,
|
|
183
|
+
approvalId: approval.id,
|
|
184
|
+
})
|
|
185
|
+
const second = JSON.parse(String(secondRaw)) as Record<string, unknown>
|
|
186
|
+
const installedSkill = second.skill as Skill
|
|
187
|
+
const agent = loadAgent('agent-skill-test') as Agent
|
|
188
|
+
|
|
189
|
+
assert.equal(second.ok, true)
|
|
190
|
+
assert.ok(installedSkill?.id)
|
|
191
|
+
assert.ok(loadSkills()[installedSkill.id])
|
|
192
|
+
assert.ok(agent.skillIds?.includes(installedSkill.id), 'approved install can attach to the agent')
|
|
193
|
+
})
|
|
194
|
+
})
|
|
@@ -24,43 +24,32 @@ import {
|
|
|
24
24
|
shouldAutoCaptureMemoryTurn,
|
|
25
25
|
shouldInjectMemoryContext,
|
|
26
26
|
} from '@/lib/server/memory/memory-policy'
|
|
27
|
+
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Advanced Database-Backed Memory logic.
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
* Lightweight in-memory cache for per-agent memory lookups (pinned + recent).
|
|
34
|
-
* TTL-based with invalidation on any write operation.
|
|
35
|
-
*/
|
|
36
|
-
const MEMORY_CACHE_TTL_MS = 30_000
|
|
37
|
-
interface AgentMemoryCache {
|
|
33
|
+
type DisabledAgentMemoryCacheEntry = {
|
|
38
34
|
pinned: MemoryEntry[]
|
|
39
35
|
allRecent: MemoryEntry[]
|
|
40
|
-
cachedAt: number
|
|
41
36
|
}
|
|
42
|
-
const agentMemoryCache = new Map<string, AgentMemoryCache>()
|
|
43
37
|
|
|
44
|
-
function getCachedAgentMemories(agentId: string):
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (Date.now() - cached.cachedAt > MEMORY_CACHE_TTL_MS) {
|
|
48
|
-
agentMemoryCache.delete(agentId)
|
|
49
|
-
return null
|
|
50
|
-
}
|
|
51
|
-
return cached
|
|
38
|
+
function getCachedAgentMemories(agentId: string): DisabledAgentMemoryCacheEntry | null {
|
|
39
|
+
void agentId
|
|
40
|
+
return null
|
|
52
41
|
}
|
|
53
42
|
|
|
54
43
|
function setCachedAgentMemories(agentId: string, pinned: MemoryEntry[], allRecent: MemoryEntry[]): void {
|
|
55
|
-
|
|
44
|
+
void agentId
|
|
45
|
+
void pinned
|
|
46
|
+
void allRecent
|
|
47
|
+
// Intentionally disabled until we can prove memory reads stay complete and fresh.
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
function invalidateAgentMemoryCache(agentId?: string | null): void {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
} else {
|
|
62
|
-
agentMemoryCache.clear()
|
|
63
|
-
}
|
|
51
|
+
void agentId
|
|
52
|
+
// Intentionally disabled: the per-agent in-memory cache is not used.
|
|
64
53
|
}
|
|
65
54
|
type MemoryActionContext = Partial<Session> & {
|
|
66
55
|
sessionId?: string | null
|
|
@@ -660,7 +649,7 @@ const MemoryPlugin: Plugin = {
|
|
|
660
649
|
|
|
661
650
|
// QMD scope: identity/* memories and contact resolution are private (DM/peer only).
|
|
662
651
|
// Group channels, threads, and shared "main" sessions don't see them.
|
|
663
|
-
const connCtx = ctx.session.connectorContext
|
|
652
|
+
const connCtx = isDirectConnectorSession(ctx.session) ? ctx.session.connectorContext : null
|
|
664
653
|
const isPrivateContext = !connCtx || !connCtx.isGroup
|
|
665
654
|
|
|
666
655
|
const memDb = getMemoryDb()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { after, before, describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import type { Agent, Skill } from '@/types'
|
|
7
|
+
|
|
8
|
+
const originalEnv = {
|
|
9
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
10
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
11
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let tempDir = ''
|
|
15
|
+
let workspaceDir = ''
|
|
16
|
+
let buildSessionTools: Awaited<typeof import('./index')>['buildSessionTools']
|
|
17
|
+
let saveAgents: Awaited<typeof import('../storage')>['saveAgents']
|
|
18
|
+
let saveSessions: Awaited<typeof import('../storage')>['saveSessions']
|
|
19
|
+
let saveSkills: Awaited<typeof import('../storage')>['saveSkills']
|
|
20
|
+
let loadSession: Awaited<typeof import('../storage')>['loadSession']
|
|
21
|
+
|
|
22
|
+
async function buildUseSkillTool() {
|
|
23
|
+
const built = await buildSessionTools(workspaceDir, ['manage_skills'], {
|
|
24
|
+
sessionId: 'skill-runtime-session',
|
|
25
|
+
agentId: 'skill-runtime-agent',
|
|
26
|
+
platformAssignScope: 'self',
|
|
27
|
+
})
|
|
28
|
+
const tool = built.tools.find((entry) => entry.name === 'use_skill')
|
|
29
|
+
assert.ok(tool, 'expected use_skill tool')
|
|
30
|
+
return { built, tool: tool! }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
before(async () => {
|
|
34
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-skill-runtime-'))
|
|
35
|
+
workspaceDir = path.join(tempDir, 'workspace')
|
|
36
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
37
|
+
process.env.WORKSPACE_DIR = workspaceDir
|
|
38
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
39
|
+
fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
|
|
40
|
+
fs.mkdirSync(workspaceDir, { recursive: true })
|
|
41
|
+
|
|
42
|
+
const toolsMod = await import('./index')
|
|
43
|
+
buildSessionTools = toolsMod.buildSessionTools
|
|
44
|
+
|
|
45
|
+
const storageMod = await import('../storage')
|
|
46
|
+
saveAgents = storageMod.saveAgents
|
|
47
|
+
saveSessions = storageMod.saveSessions
|
|
48
|
+
saveSkills = storageMod.saveSkills
|
|
49
|
+
loadSession = storageMod.loadSession
|
|
50
|
+
|
|
51
|
+
saveAgents({
|
|
52
|
+
'skill-runtime-agent': {
|
|
53
|
+
id: 'skill-runtime-agent',
|
|
54
|
+
name: 'Skill Runtime Tester',
|
|
55
|
+
description: 'Tests runtime skill execution.',
|
|
56
|
+
provider: 'openai',
|
|
57
|
+
model: 'gpt-test',
|
|
58
|
+
plugins: ['manage_skills'],
|
|
59
|
+
tools: ['manage_skills'],
|
|
60
|
+
skillIds: [],
|
|
61
|
+
platformAssignScope: 'self',
|
|
62
|
+
createdAt: Date.now(),
|
|
63
|
+
updatedAt: Date.now(),
|
|
64
|
+
} satisfies Agent,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
saveSessions({
|
|
68
|
+
'skill-runtime-session': {
|
|
69
|
+
id: 'skill-runtime-session',
|
|
70
|
+
name: 'Skill Runtime Session',
|
|
71
|
+
cwd: workspaceDir,
|
|
72
|
+
user: 'tester',
|
|
73
|
+
provider: 'openai',
|
|
74
|
+
model: 'gpt-test',
|
|
75
|
+
messages: [],
|
|
76
|
+
createdAt: Date.now(),
|
|
77
|
+
lastActiveAt: Date.now(),
|
|
78
|
+
sessionType: 'human',
|
|
79
|
+
agentId: 'skill-runtime-agent',
|
|
80
|
+
plugins: ['manage_skills'],
|
|
81
|
+
heartbeatEnabled: false,
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
saveSkills({
|
|
86
|
+
dispatch_skill: {
|
|
87
|
+
id: 'dispatch_skill',
|
|
88
|
+
name: 'dispatch-helper',
|
|
89
|
+
filename: 'dispatch-helper.md',
|
|
90
|
+
description: 'Dispatch through manage_skills status.',
|
|
91
|
+
content: '# Dispatch Helper\nRun manage_skills status.',
|
|
92
|
+
commandDispatch: {
|
|
93
|
+
kind: 'tool',
|
|
94
|
+
toolName: 'manage_skills',
|
|
95
|
+
argMode: 'raw',
|
|
96
|
+
},
|
|
97
|
+
createdAt: Date.now(),
|
|
98
|
+
updatedAt: Date.now(),
|
|
99
|
+
} satisfies Skill,
|
|
100
|
+
prompt_skill: {
|
|
101
|
+
id: 'prompt_skill',
|
|
102
|
+
name: 'prompt-helper',
|
|
103
|
+
filename: 'prompt-helper.md',
|
|
104
|
+
description: 'Guidance-only workflow.',
|
|
105
|
+
content: '# Prompt Helper\nFollow this checklist.',
|
|
106
|
+
createdAt: Date.now(),
|
|
107
|
+
updatedAt: Date.now(),
|
|
108
|
+
} satisfies Skill,
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
after(() => {
|
|
113
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
114
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
115
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
116
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
117
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
118
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
119
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('use_skill runtime tool', () => {
|
|
123
|
+
it('selects a skill and persists the selection on the session', async () => {
|
|
124
|
+
const { built, tool } = await buildUseSkillTool()
|
|
125
|
+
try {
|
|
126
|
+
const raw = await tool.invoke({ action: 'select', name: 'dispatch-helper' })
|
|
127
|
+
const result = JSON.parse(String(raw)) as Record<string, unknown>
|
|
128
|
+
const session = loadSession('skill-runtime-session')
|
|
129
|
+
|
|
130
|
+
assert.equal(result.ok, true)
|
|
131
|
+
assert.equal((result.skill as Record<string, unknown>)?.name, 'dispatch-helper')
|
|
132
|
+
assert.equal(session?.skillRuntimeState?.selectedSkillName, 'dispatch-helper')
|
|
133
|
+
} finally {
|
|
134
|
+
await built.cleanup()
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('runs an executable skill by dispatching into its bound tool', async () => {
|
|
139
|
+
const { built, tool } = await buildUseSkillTool()
|
|
140
|
+
try {
|
|
141
|
+
const raw = await tool.invoke({
|
|
142
|
+
action: 'run',
|
|
143
|
+
name: 'dispatch-helper',
|
|
144
|
+
toolArgs: { action: 'status', query: 'dispatch helper' },
|
|
145
|
+
})
|
|
146
|
+
const result = JSON.parse(String(raw)) as Record<string, unknown>
|
|
147
|
+
const toolOutput = result.toolOutput as Array<Record<string, unknown>>
|
|
148
|
+
const session = loadSession('skill-runtime-session')
|
|
149
|
+
|
|
150
|
+
assert.equal(result.ok, true)
|
|
151
|
+
assert.equal(result.executed, true)
|
|
152
|
+
assert.equal(result.dispatchedTool, 'manage_skills')
|
|
153
|
+
assert.ok(Array.isArray(toolOutput))
|
|
154
|
+
assert.equal(session?.skillRuntimeState?.lastAction, 'run')
|
|
155
|
+
assert.equal(session?.skillRuntimeState?.lastRunToolName, 'manage_skills')
|
|
156
|
+
} finally {
|
|
157
|
+
await built.cleanup()
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('falls back to prompt guidance for non-executable skills', async () => {
|
|
162
|
+
const { built, tool } = await buildUseSkillTool()
|
|
163
|
+
try {
|
|
164
|
+
const raw = await tool.invoke({ action: 'run', name: 'prompt-helper' })
|
|
165
|
+
const result = JSON.parse(String(raw)) as Record<string, unknown>
|
|
166
|
+
|
|
167
|
+
assert.equal(result.ok, true)
|
|
168
|
+
assert.equal(result.executed, false)
|
|
169
|
+
assert.equal(result.mode, 'prompt_guidance')
|
|
170
|
+
assert.match(String(result.guidance || ''), /Prompt Helper/)
|
|
171
|
+
} finally {
|
|
172
|
+
await built.cleanup()
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
})
|