@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
|
@@ -11,6 +11,7 @@ import { buildChatModel } from '@/lib/server/build-llm'
|
|
|
11
11
|
import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
|
|
12
12
|
import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
|
|
13
13
|
import { getPluginManager } from '@/lib/server/plugins'
|
|
14
|
+
import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
|
|
14
15
|
import { buildBoardTask } from '@/lib/server/tasks/task-lifecycle'
|
|
15
16
|
import '@/lib/server/builtin-plugins'
|
|
16
17
|
import { genId } from '@/lib/id'
|
|
@@ -401,6 +402,7 @@ export async function executeLangGraphOrchestrator(
|
|
|
401
402
|
taskId?: string,
|
|
402
403
|
): Promise<string> {
|
|
403
404
|
const allAgents = loadAgents()
|
|
405
|
+
const orchestrationSession = loadSessions()[sessionId]
|
|
404
406
|
|
|
405
407
|
// Build available agents list
|
|
406
408
|
const agentIds = orchestrator.subAgentIds || []
|
|
@@ -461,14 +463,16 @@ export async function executeLangGraphOrchestrator(
|
|
|
461
463
|
promptParts.push(buildCurrentDateTimePromptContext())
|
|
462
464
|
if (orchestrator.soul) promptParts.push(orchestrator.soul)
|
|
463
465
|
if (orchestrator.systemPrompt) promptParts.push(orchestrator.systemPrompt)
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
466
|
+
try {
|
|
467
|
+
const runtimeSkills = resolveRuntimeSkills({
|
|
468
|
+
cwd: orchestrationSession?.cwd || WORKSPACE_DIR,
|
|
469
|
+
enabledPlugins: Array.isArray(orchestrator.plugins) ? orchestrator.plugins : [],
|
|
470
|
+
agentSkillIds: orchestrator.skillIds || [],
|
|
471
|
+
storedSkills: loadSkills(),
|
|
472
|
+
selectedSkillId: orchestrationSession?.skillRuntimeState?.selectedSkillId || null,
|
|
473
|
+
})
|
|
474
|
+
promptParts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
|
|
475
|
+
} catch { /* non-critical */ }
|
|
472
476
|
const basePrompt = promptParts.join('\n\n')
|
|
473
477
|
|
|
474
478
|
const systemMessage = [
|
|
@@ -7,6 +7,7 @@ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
|
7
7
|
import { loadRuntimeSettings, getLegacyOrchestratorMaxTurns } from '@/lib/server/runtime/runtime-settings'
|
|
8
8
|
import { getMemoryDb } from '@/lib/server/memory/memory-db'
|
|
9
9
|
import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
|
|
10
|
+
import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
|
|
10
11
|
import { getProvider } from '@/lib/providers'
|
|
11
12
|
import type { Agent } from '@/types'
|
|
12
13
|
|
|
@@ -125,13 +126,16 @@ async function executeOrchestratorLegacy(
|
|
|
125
126
|
promptParts.push(buildCurrentDateTimePromptContext())
|
|
126
127
|
if (orchestrator.soul) promptParts.push(orchestrator.soul)
|
|
127
128
|
if (orchestrator.systemPrompt) promptParts.push(orchestrator.systemPrompt)
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
try {
|
|
130
|
+
const runtimeSkills = resolveRuntimeSkills({
|
|
131
|
+
cwd: session.cwd,
|
|
132
|
+
enabledPlugins: Array.isArray(orchestrator.plugins) ? orchestrator.plugins : [],
|
|
133
|
+
agentSkillIds: orchestrator.skillIds || [],
|
|
134
|
+
storedSkills: loadSkills(),
|
|
135
|
+
selectedSkillId: session.skillRuntimeState?.selectedSkillId || null,
|
|
136
|
+
})
|
|
137
|
+
promptParts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
|
|
138
|
+
} catch { /* non-critical */ }
|
|
135
139
|
const basePrompt = promptParts.join('\n\n')
|
|
136
140
|
|
|
137
141
|
const systemPrompt = [
|
|
@@ -485,26 +485,27 @@ describe('buildToolDisciplineLines advanced', () => {
|
|
|
485
485
|
assert.ok(lines.some((line) => line.includes('Enabled tools')))
|
|
486
486
|
})
|
|
487
487
|
|
|
488
|
-
it('includes
|
|
488
|
+
it('includes direct platform guidance when manage_schedules is enabled without manage_platform', () => {
|
|
489
489
|
const lines = buildToolDisciplineLines(['manage_schedules'])
|
|
490
|
-
assert.ok(lines.some((line) => line.includes('
|
|
490
|
+
assert.ok(lines.some((line) => line.includes('Use direct platform tools exactly as named (`manage_schedules`)')))
|
|
491
|
+
assert.ok(lines.some((line) => line.includes('Do not substitute `manage_platform` unless it is explicitly enabled.')))
|
|
491
492
|
})
|
|
492
493
|
|
|
493
|
-
it('includes
|
|
494
|
+
it('includes local files and shell guidance when coding tools and delegate enabled', () => {
|
|
494
495
|
const lines = buildToolDisciplineLines(['delegate', 'shell', 'files'])
|
|
495
|
-
assert.ok(lines.some((line) => line.includes('
|
|
496
|
+
assert.ok(lines.some((line) => line.includes('{"action":"read","filePath":"path/to/file.md"}')))
|
|
497
|
+
assert.ok(lines.some((line) => line.includes('For `shell`, use `{"action":"execute","command":"..."}`')))
|
|
496
498
|
})
|
|
497
499
|
|
|
498
|
-
it('tells research-capable agents to try another enabled acquisition path before
|
|
500
|
+
it('tells research-capable agents to try another enabled acquisition path before giving up', () => {
|
|
499
501
|
const lines = buildToolDisciplineLines(['web_search', 'web_fetch', 'http_request', 'shell'])
|
|
500
|
-
assert.ok(lines.some((line) => line.includes('
|
|
502
|
+
assert.ok(lines.some((line) => line.includes('If one research path is blocked, try another') && line.includes('`shell`') && line.includes('`http_request`')))
|
|
501
503
|
})
|
|
502
504
|
|
|
503
|
-
it('adds direct
|
|
505
|
+
it('adds direct email and file action guidance when those tools are enabled', () => {
|
|
504
506
|
const lines = buildToolDisciplineLines(['files', 'email', 'spawn_subagent'])
|
|
505
|
-
assert.ok(lines.some((line) => line.includes('
|
|
506
|
-
assert.ok(lines.some((line) => line.includes('
|
|
507
|
-
assert.ok(lines.some((line) => line.includes('returned `swarmId`')))
|
|
507
|
+
assert.ok(lines.some((line) => line.includes('For `email`, send mail with `{"action":"send","to":"user@example.com","subject":"...","body":"..."}`')))
|
|
508
|
+
assert.ok(lines.some((line) => line.includes('{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}')))
|
|
508
509
|
})
|
|
509
510
|
})
|
|
510
511
|
|
|
@@ -84,6 +84,13 @@ test('executeSessionChatTurn syncs updated agent runtime fields onto its thread
|
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
const session = ensureAgentThreadSession('molly')
|
|
87
|
+
const sessionsBefore = storage.loadSessions()
|
|
88
|
+
sessionsBefore[session.id].connectorContext = {
|
|
89
|
+
connectorId: 'conn-stale',
|
|
90
|
+
channelId: 'stale-channel',
|
|
91
|
+
senderId: 'stale-user',
|
|
92
|
+
}
|
|
93
|
+
storage.saveSessions(sessionsBefore)
|
|
87
94
|
const agents = storage.loadAgents()
|
|
88
95
|
agents.molly.provider = 'test-provider'
|
|
89
96
|
agents.molly.model = 'unit'
|
|
@@ -93,7 +100,7 @@ test('executeSessionChatTurn syncs updated agent runtime fields onto its thread
|
|
|
93
100
|
agents.molly.updatedAt = now + 1
|
|
94
101
|
storage.saveAgents(agents)
|
|
95
102
|
|
|
96
|
-
|
|
103
|
+
await executeSessionChatTurn({
|
|
97
104
|
sessionId: session.id,
|
|
98
105
|
message: 'hello',
|
|
99
106
|
runId: 'run-session-sync',
|
|
@@ -101,19 +108,125 @@ test('executeSessionChatTurn syncs updated agent runtime fields onto its thread
|
|
|
101
108
|
|
|
102
109
|
const persisted = storage.loadSession(session.id)
|
|
103
110
|
console.log(JSON.stringify({
|
|
104
|
-
text: result.text || null,
|
|
105
111
|
provider: persisted?.provider || null,
|
|
106
112
|
model: persisted?.model || null,
|
|
107
113
|
plugins: persisted?.plugins || [],
|
|
108
114
|
heartbeatEnabled: persisted?.heartbeatEnabled ?? null,
|
|
109
115
|
heartbeatIntervalSec: persisted?.heartbeatIntervalSec ?? null,
|
|
116
|
+
connectorContext: persisted?.connectorContext || null,
|
|
110
117
|
}))
|
|
111
118
|
`)
|
|
112
119
|
|
|
113
|
-
assert.equal(output.text, 'synced')
|
|
114
120
|
assert.equal(output.provider, 'test-provider')
|
|
115
121
|
assert.equal(output.model, 'unit')
|
|
116
122
|
assert.deepEqual(output.plugins, [])
|
|
117
123
|
assert.equal(output.heartbeatEnabled, true)
|
|
118
124
|
assert.equal(output.heartbeatIntervalSec, 90)
|
|
125
|
+
assert.equal(output.connectorContext, null)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('executeSessionChatTurn keeps tool-only heartbeats off the visible main-thread history and clears stale connector state', () => {
|
|
129
|
+
const output = runWithTempDataDir(`
|
|
130
|
+
const storageMod = await import('@/lib/server/storage')
|
|
131
|
+
const providersMod = await import('@/lib/providers/index')
|
|
132
|
+
const execMod = await import('@/lib/server/chat-execution/chat-execution')
|
|
133
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
134
|
+
const executeSessionChatTurn = execMod.executeSessionChatTurn
|
|
135
|
+
|| execMod.default?.executeSessionChatTurn
|
|
136
|
+
|| execMod['module.exports']?.executeSessionChatTurn
|
|
137
|
+
const providers = providersMod.PROVIDERS
|
|
138
|
+
|| providersMod.default?.PROVIDERS
|
|
139
|
+
|| providersMod['module.exports']?.PROVIDERS
|
|
140
|
+
|
|
141
|
+
providers['test-provider'] = {
|
|
142
|
+
id: 'test-provider',
|
|
143
|
+
name: 'Test Provider',
|
|
144
|
+
models: ['unit'],
|
|
145
|
+
requiresApiKey: false,
|
|
146
|
+
requiresEndpoint: false,
|
|
147
|
+
handler: {
|
|
148
|
+
async streamChat(opts) {
|
|
149
|
+
opts.write('data: ' + JSON.stringify({ t: 'r', text: 'Sent the ferry status to WhatsApp.' }) + '\\n')
|
|
150
|
+
return ''
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const now = Date.now()
|
|
156
|
+
storage.saveAgents({
|
|
157
|
+
hal: {
|
|
158
|
+
id: 'hal',
|
|
159
|
+
name: 'Hal2k',
|
|
160
|
+
description: 'Heartbeat hygiene test',
|
|
161
|
+
provider: 'test-provider',
|
|
162
|
+
model: 'unit',
|
|
163
|
+
credentialId: null,
|
|
164
|
+
apiEndpoint: null,
|
|
165
|
+
fallbackCredentialIds: [],
|
|
166
|
+
disabled: false,
|
|
167
|
+
heartbeatEnabled: true,
|
|
168
|
+
heartbeatIntervalSec: 60,
|
|
169
|
+
plugins: [],
|
|
170
|
+
threadSessionId: 'agent_thread',
|
|
171
|
+
createdAt: now,
|
|
172
|
+
updatedAt: now,
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
storage.saveSessions({
|
|
176
|
+
agent_thread: {
|
|
177
|
+
id: 'agent_thread',
|
|
178
|
+
name: 'Hal2k',
|
|
179
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
180
|
+
user: 'default',
|
|
181
|
+
provider: 'test-provider',
|
|
182
|
+
model: 'unit',
|
|
183
|
+
claudeSessionId: null,
|
|
184
|
+
codexThreadId: null,
|
|
185
|
+
opencodeSessionId: null,
|
|
186
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
187
|
+
messages: [
|
|
188
|
+
{ role: 'user', text: 'seed user message', time: now - 1000 },
|
|
189
|
+
],
|
|
190
|
+
createdAt: now,
|
|
191
|
+
lastActiveAt: now,
|
|
192
|
+
sessionType: 'human',
|
|
193
|
+
agentId: 'hal',
|
|
194
|
+
shortcutForAgentId: 'hal',
|
|
195
|
+
plugins: [],
|
|
196
|
+
connectorContext: {
|
|
197
|
+
connectorId: 'conn-stale',
|
|
198
|
+
channelId: 'wrong-chat',
|
|
199
|
+
senderId: 'wrong-user',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
await executeSessionChatTurn({
|
|
205
|
+
sessionId: 'agent_thread',
|
|
206
|
+
message: 'AGENT_HEARTBEAT_WAKE\\nInternal connector follow-up only',
|
|
207
|
+
internal: true,
|
|
208
|
+
source: 'heartbeat-wake',
|
|
209
|
+
heartbeatConfig: {
|
|
210
|
+
ackMaxChars: 300,
|
|
211
|
+
showOk: false,
|
|
212
|
+
showAlerts: true,
|
|
213
|
+
target: null,
|
|
214
|
+
deliveryMode: 'tool_only',
|
|
215
|
+
},
|
|
216
|
+
runId: 'run-heartbeat-tool-only',
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const persisted = storage.loadSession('agent_thread')
|
|
220
|
+
console.log(JSON.stringify({
|
|
221
|
+
connectorContext: persisted?.connectorContext || null,
|
|
222
|
+
messageCount: persisted?.messages?.length || 0,
|
|
223
|
+
lastMessageText: persisted?.messages?.at(-1)?.text || null,
|
|
224
|
+
heartbeatKinds: (persisted?.messages || []).filter((entry) => entry.kind === 'heartbeat').length,
|
|
225
|
+
}))
|
|
226
|
+
`)
|
|
227
|
+
|
|
228
|
+
assert.equal(output.connectorContext, null)
|
|
229
|
+
assert.equal(output.messageCount, 1)
|
|
230
|
+
assert.equal(output.lastMessageText, 'seed user message')
|
|
231
|
+
assert.equal(output.heartbeatKinds, 0)
|
|
119
232
|
})
|
|
@@ -29,6 +29,7 @@ import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agent
|
|
|
29
29
|
import { resolveSessionToolPolicy } from '@/lib/server/tool-capability-policy'
|
|
30
30
|
import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
|
|
31
31
|
import { buildWorkspaceContext } from '@/lib/server/workspace-context'
|
|
32
|
+
import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
|
|
32
33
|
import { resolveImagePath } from '@/lib/server/resolve-image'
|
|
33
34
|
import {
|
|
34
35
|
applyContextClearBoundary,
|
|
@@ -67,6 +68,7 @@ import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolic
|
|
|
67
68
|
import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat/chat-streaming-state'
|
|
68
69
|
import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from '@/lib/server/agents/assistant-control'
|
|
69
70
|
import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
|
|
71
|
+
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
70
72
|
import { errorMessage as toErrorMessage } from '@/lib/shared-utils'
|
|
71
73
|
import { listUniversalToolAccessPluginIds } from '@/lib/server/universal-tool-access'
|
|
72
74
|
|
|
@@ -109,6 +111,24 @@ export function buildEnabledToolsAutonomyGuidance(): string[] {
|
|
|
109
111
|
]
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
function resolveHeartbeatLastConnectorTarget(session: Session | null | undefined): {
|
|
115
|
+
connectorId?: string
|
|
116
|
+
channelId: string
|
|
117
|
+
} | null {
|
|
118
|
+
if (!isDirectConnectorSession(session)) return null
|
|
119
|
+
const connectorId = typeof session?.connectorContext?.connectorId === 'string'
|
|
120
|
+
? session.connectorContext.connectorId.trim()
|
|
121
|
+
: ''
|
|
122
|
+
const channelId = typeof session?.connectorContext?.channelId === 'string'
|
|
123
|
+
? session.connectorContext.channelId.trim()
|
|
124
|
+
: ''
|
|
125
|
+
if (!channelId) return null
|
|
126
|
+
return {
|
|
127
|
+
connectorId: connectorId || undefined,
|
|
128
|
+
channelId,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
interface SessionWithCredentials {
|
|
113
133
|
credentialId?: string | null
|
|
114
134
|
}
|
|
@@ -130,7 +150,14 @@ export interface ExecuteChatTurnInput {
|
|
|
130
150
|
signal?: AbortSignal
|
|
131
151
|
onEvent?: (event: SSEEvent) => void
|
|
132
152
|
modelOverride?: string
|
|
133
|
-
heartbeatConfig?: {
|
|
153
|
+
heartbeatConfig?: {
|
|
154
|
+
ackMaxChars: number
|
|
155
|
+
showOk: boolean
|
|
156
|
+
showAlerts: boolean
|
|
157
|
+
target: string | null
|
|
158
|
+
lightContext?: boolean
|
|
159
|
+
deliveryMode?: 'default' | 'tool_only'
|
|
160
|
+
}
|
|
134
161
|
replyToId?: string
|
|
135
162
|
}
|
|
136
163
|
|
|
@@ -422,6 +449,10 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
422
449
|
session.openclawAgentId = desiredOpenClawAgentId
|
|
423
450
|
changed = true
|
|
424
451
|
}
|
|
452
|
+
if (session.connectorContext) {
|
|
453
|
+
session.connectorContext = undefined
|
|
454
|
+
changed = true
|
|
455
|
+
}
|
|
425
456
|
}
|
|
426
457
|
|
|
427
458
|
if (changed) {
|
|
@@ -496,13 +527,16 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
496
527
|
if (agent.systemPrompt) parts.push(`## System Prompt\n${agent.systemPrompt}`)
|
|
497
528
|
|
|
498
529
|
// 5. Skills (SwarmClaw Core)
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
530
|
+
try {
|
|
531
|
+
const runtimeSkills = resolveRuntimeSkills({
|
|
532
|
+
cwd: session.cwd,
|
|
533
|
+
enabledPlugins,
|
|
534
|
+
agentSkillIds: agent.skillIds || [],
|
|
535
|
+
storedSkills: loadSkills(),
|
|
536
|
+
selectedSkillId: session.skillRuntimeState?.selectedSkillId || null,
|
|
537
|
+
})
|
|
538
|
+
parts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
|
|
539
|
+
} catch { /* non-critical */ }
|
|
506
540
|
|
|
507
541
|
// 5b. Workspace context files (HEARTBEAT.md, IDENTITY.md, AGENTS.md, etc.)
|
|
508
542
|
try {
|
|
@@ -1228,6 +1262,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1228
1262
|
const shouldPersistAssistant = !hiddenControlOnly
|
|
1229
1263
|
&& hasPersistableAssistantPayload(persistedText, thinkingText, persistedToolEvents)
|
|
1230
1264
|
&& heartbeatClassification !== 'suppress'
|
|
1265
|
+
&& !(isHeartbeatRun && heartbeatConfig?.deliveryMode === 'tool_only' && !isDirectConnectorSession(session))
|
|
1231
1266
|
|
|
1232
1267
|
const normalizeResumeId = (value: unknown): string | null =>
|
|
1233
1268
|
typeof value === 'string' && value.trim() ? value.trim() : null
|
|
@@ -1236,6 +1271,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1236
1271
|
const current = fresh[sessionId]
|
|
1237
1272
|
if (current) {
|
|
1238
1273
|
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1274
|
+
if (!isDirectConnectorSession(current) && current.connectorContext) {
|
|
1275
|
+
current.connectorContext = undefined
|
|
1276
|
+
}
|
|
1239
1277
|
const currentAgent = current.agentId ? loadAgents()[current.agentId] : null
|
|
1240
1278
|
pruneStreamingAssistantArtifacts(current.messages, {
|
|
1241
1279
|
minIndex: runMessageStartIndex,
|
|
@@ -1309,18 +1347,22 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1309
1347
|
}
|
|
1310
1348
|
|
|
1311
1349
|
// Target routing for non-suppressed heartbeat alerts
|
|
1312
|
-
if (
|
|
1350
|
+
if (
|
|
1351
|
+
isHeartbeatRun
|
|
1352
|
+
&& shouldAutoRouteHeartbeatAlerts(heartbeatConfig)
|
|
1353
|
+
&& heartbeatConfig?.target
|
|
1354
|
+
&& heartbeatConfig.target !== 'none'
|
|
1355
|
+
) {
|
|
1313
1356
|
try {
|
|
1314
1357
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1315
|
-
const {
|
|
1358
|
+
const { sendConnectorMessage } = require('../connectors/manager')
|
|
1316
1359
|
let connectorId: string | undefined
|
|
1317
1360
|
let channelId: string | undefined
|
|
1318
1361
|
if (heartbeatConfig.target === 'last') {
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
channelId = first.recentChannelId
|
|
1362
|
+
const lastTarget = resolveHeartbeatLastConnectorTarget(current)
|
|
1363
|
+
if (lastTarget) {
|
|
1364
|
+
connectorId = lastTarget.connectorId
|
|
1365
|
+
channelId = lastTarget.channelId
|
|
1324
1366
|
}
|
|
1325
1367
|
} else if (heartbeatConfig.target.includes(':')) {
|
|
1326
1368
|
const [cId, chId] = heartbeatConfig.target.split(':', 2)
|
|
@@ -1339,19 +1381,25 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1339
1381
|
|
|
1340
1382
|
// Auto-discover connectors linked to this agent when no explicit target is set
|
|
1341
1383
|
// Skip if a real inbound message was handled recently — the agent just responded to it
|
|
1342
|
-
if (
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1384
|
+
if (
|
|
1385
|
+
isHeartbeatRun
|
|
1386
|
+
&& shouldAutoRouteHeartbeatAlerts(heartbeatConfig)
|
|
1387
|
+
&& !heartbeatConfig?.target
|
|
1388
|
+
&& isDirectConnectorSession(current)
|
|
1389
|
+
) {
|
|
1390
|
+
const recentInbound = current.connectorContext?.lastInboundAt
|
|
1391
|
+
&& (Date.now() - current.connectorContext.lastInboundAt) < 60_000
|
|
1392
|
+
const connectorId = typeof current.connectorContext?.connectorId === 'string'
|
|
1393
|
+
? current.connectorContext.connectorId.trim()
|
|
1394
|
+
: ''
|
|
1395
|
+
const channelId = typeof current.connectorContext?.channelId === 'string'
|
|
1396
|
+
? current.connectorContext.channelId.trim()
|
|
1397
|
+
: ''
|
|
1398
|
+
if (!recentInbound && channelId) {
|
|
1346
1399
|
try {
|
|
1347
1400
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1348
|
-
const {
|
|
1349
|
-
|
|
1350
|
-
c.agentId === session.agentId && c.recentChannelId && c.supportsSend
|
|
1351
|
-
)
|
|
1352
|
-
for (const conn of agentConnectors) {
|
|
1353
|
-
sendMsg({ connectorId: conn.id, channelId: conn.recentChannelId, text: persistedText }).catch(() => {})
|
|
1354
|
-
}
|
|
1401
|
+
const { sendConnectorMessage: sendMsg } = require('../connectors/manager')
|
|
1402
|
+
sendMsg({ connectorId: connectorId || undefined, channelId, text: persistedText }).catch(() => {})
|
|
1355
1403
|
} catch {
|
|
1356
1404
|
// Best effort — connector manager may not be loaded
|
|
1357
1405
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
looksLikeOpenEndedDeliverableTask,
|
|
13
13
|
resolveContinuationAssistantText,
|
|
14
14
|
resolveFinalStreamResponseText,
|
|
15
|
+
shouldSkipToolSummaryForShortResponse,
|
|
15
16
|
shouldForceAttachmentFollowthrough,
|
|
16
17
|
shouldForceRecoverableToolErrorFollowthrough,
|
|
17
18
|
shouldTerminateOnSuccessfulMemoryMutation,
|
|
@@ -52,72 +53,66 @@ describe('buildToolDisciplineLines', () => {
|
|
|
52
53
|
assert.ok(lines.every((line) => !line.includes('Do not substitute `manage_platform`')))
|
|
53
54
|
})
|
|
54
55
|
|
|
55
|
-
it('includes concrete files-tool examples for
|
|
56
|
+
it('includes concrete files-tool examples for file work', () => {
|
|
56
57
|
const lines = buildToolDisciplineLines(['files'])
|
|
57
58
|
|
|
58
59
|
assert.ok(lines.some((line) => line.includes('{"action":"read","filePath":"path/to/file.md"}')))
|
|
59
|
-
assert.ok(lines.some((line) => line.includes('
|
|
60
|
-
assert.ok(lines.some((line) => line.includes('
|
|
60
|
+
assert.ok(lines.some((line) => line.includes('{"action":"list","dirPath":"."}')))
|
|
61
|
+
assert.ok(lines.some((line) => line.includes('{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}')))
|
|
61
62
|
})
|
|
62
63
|
|
|
63
|
-
it('
|
|
64
|
+
it('tells the agent to use direct schedule tools when manage_platform is absent', () => {
|
|
64
65
|
const lines = buildToolDisciplineLines(['manage_schedules', 'schedule_wake'])
|
|
65
66
|
|
|
66
|
-
assert.ok(lines.some((line) => line.includes('
|
|
67
|
-
assert.ok(lines.some((line) => line.includes('
|
|
68
|
-
assert.ok(lines.some((line) => line.includes('prefer `schedule_wake` over creating a recurring schedule')))
|
|
67
|
+
assert.ok(lines.some((line) => line.includes('Use direct platform tools exactly as named (`manage_schedules`)')))
|
|
68
|
+
assert.ok(lines.some((line) => line.includes('Do not substitute `manage_platform` unless it is explicitly enabled.')))
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
it('warns browser
|
|
71
|
+
it('warns browser-capable sessions to use current supported tool inputs and sequencing', () => {
|
|
72
72
|
const lines = buildToolDisciplineLines(['web_search', 'web_fetch', 'browser', 'manage_connectors', 'http_request', 'email', 'ask_human', 'manage_secrets'])
|
|
73
73
|
|
|
74
74
|
assert.ok(lines.some((line) => line.includes('Do not invent placeholder URLs')))
|
|
75
75
|
assert.ok(lines.some((line) => line.includes('A shorthand `form` object keyed by input id/name also works')))
|
|
76
|
-
assert.ok(lines.some((line) => line.includes('prefer `fill_form` and `submit_form`')))
|
|
77
76
|
assert.ok(lines.some((line) => line.includes('For current events, breaking news, or "latest" requests, start with `web_search`')))
|
|
78
77
|
assert.ok(lines.some((line) => line.includes('Use `browser` when the user asks for screenshots')))
|
|
79
|
-
assert.ok(lines.some((line) => line.includes('do not capture screenshots') && line.includes('`browser`')))
|
|
80
78
|
assert.ok(lines.some((line) => line.includes('connector_message_tool') && line.includes('list_running')))
|
|
81
79
|
assert.ok(lines.some((line) => line.includes('connector/channel setup is missing')))
|
|
82
|
-
assert.ok(lines.some((line) => line.includes('capture the artifact first with `browser`') && line.includes('`connector_message_tool`')))
|
|
83
80
|
assert.ok(lines.some((line) => line.includes('Keep JSON request bodies as raw JSON strings')))
|
|
84
|
-
assert.ok(lines.some((line) => line.includes('
|
|
85
|
-
assert.ok(lines.some((line) => line.includes('
|
|
81
|
+
assert.ok(lines.some((line) => line.includes('gather sources first, then capture')))
|
|
82
|
+
assert.ok(lines.some((line) => line.includes('If one research path is blocked, try another') && line.includes('`http_request`') && line.includes('`browser`')))
|
|
86
83
|
assert.ok(lines.some((line) => line.includes('{"action":"send","to":"user@example.com","subject":"...","body":"..."}')))
|
|
87
84
|
assert.ok(lines.some((line) => line.includes('do not guess or keep re-submitting blank forms')))
|
|
88
|
-
assert.ok(lines.some((line) => line.includes('
|
|
89
|
-
assert.ok(lines.some((line) => line.includes('Use `manage_secrets` only for sensitive credentials or tokens')))
|
|
85
|
+
assert.ok(lines.some((line) => line.includes('Store secrets (passwords, API keys, tokens) with `manage_secrets`')))
|
|
90
86
|
})
|
|
91
87
|
|
|
92
88
|
it('adds bounded execution guidance for wallet-connected external-service tasks', () => {
|
|
93
89
|
const lines = buildToolDisciplineLines(['wallet', 'browser', 'http_request', 'manage_capabilities'])
|
|
94
90
|
|
|
95
|
-
assert.ok(lines.some((line) => line.includes('inspect the
|
|
96
|
-
assert.ok(lines.some((line) => line.includes('
|
|
97
|
-
assert.ok(lines.some((line) => line.includes('
|
|
98
|
-
assert.ok(lines.some((line) => line.includes('If a direct tool for the job is already enabled in this session, call that tool immediately')))
|
|
91
|
+
assert.ok(lines.some((line) => line.includes('inspect the wallet first with `wallet_tool`')))
|
|
92
|
+
assert.ok(lines.some((line) => line.includes('Use a bounded loop: verify, attempt one reversible step, then execute or state the blocker.')))
|
|
93
|
+
assert.ok(lines.some((line) => line.includes('stop venue-shopping') && line.includes('call_contract')))
|
|
99
94
|
})
|
|
100
95
|
|
|
101
|
-
it('
|
|
96
|
+
it('includes concrete local coding tool guidance when coding tools are already available', () => {
|
|
102
97
|
const lines = buildToolDisciplineLines(['files', 'shell', 'delegate'])
|
|
103
98
|
|
|
104
|
-
assert.ok(lines.some((line) => line.includes('
|
|
99
|
+
assert.ok(lines.some((line) => line.includes('{"action":"read","filePath":"path/to/file.md"}')))
|
|
100
|
+
assert.ok(lines.some((line) => line.includes('For `shell`, use `{"action":"execute","command":"..."}`')))
|
|
105
101
|
})
|
|
106
102
|
|
|
107
|
-
it('adds explicit
|
|
103
|
+
it('adds explicit ask_human request and wait guidance when ask_human is enabled', () => {
|
|
108
104
|
const lines = buildToolDisciplineLines(['browser', 'ask_human'])
|
|
109
105
|
|
|
110
|
-
assert.ok(lines.some((line) => line.includes('request_input') && line.includes('wait_for_reply') && line.includes('
|
|
111
|
-
assert.ok(lines.some((line) => line.includes('
|
|
112
|
-
assert.ok(lines.some((line) => line.includes('Do not loop on `status` without a `watchJobId`')))
|
|
106
|
+
assert.ok(lines.some((line) => line.includes('request_input') && line.includes('wait_for_reply') && line.includes('correlationId')))
|
|
107
|
+
assert.ok(lines.some((line) => line.includes('do not guess or keep re-submitting blank forms')))
|
|
113
108
|
})
|
|
114
109
|
|
|
115
|
-
it('tells agents to
|
|
110
|
+
it('tells agents how to send email and write files when those tools are enabled', () => {
|
|
116
111
|
const lines = buildToolDisciplineLines(['files', 'email', 'spawn_subagent'])
|
|
117
112
|
|
|
118
|
-
assert.ok(lines.some((line) => line.includes('
|
|
119
|
-
assert.ok(lines.some((line) => line.includes('
|
|
120
|
-
assert.ok(lines.some((line) => line.includes('
|
|
113
|
+
assert.ok(lines.some((line) => line.includes('For `email`, send mail with `{"action":"send","to":"user@example.com","subject":"...","body":"..."}`')))
|
|
114
|
+
assert.ok(lines.some((line) => line.includes('If delivery depends on SMTP setup, check `{"action":"status"}` before claiming success.')))
|
|
115
|
+
assert.ok(lines.some((line) => line.includes('{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}')))
|
|
121
116
|
})
|
|
122
117
|
|
|
123
118
|
it('does not force capability-inferred tools — trusts the LLM to select tools', () => {
|
|
@@ -183,7 +178,7 @@ describe('buildToolDisciplineLines', () => {
|
|
|
183
178
|
assert.ok(streamAgentChatSource.includes('If a task explicitly names an enabled tool, use that tool before declaring success.'))
|
|
184
179
|
assert.ok(streamAgentChatSource.includes('collect required human input through the tool'))
|
|
185
180
|
assert.ok(streamAgentChatSource.includes('## Attachments'))
|
|
186
|
-
assert.ok(
|
|
181
|
+
assert.ok(streamSources.includes('Do not claim that you cannot use images, attachments, or external tools when they are available in this session.'))
|
|
187
182
|
assert.ok(streamSources.includes('You have not yet completed the required explicit tool step(s):'))
|
|
188
183
|
assert.ok(streamSources.includes('attachment_followthrough'))
|
|
189
184
|
assert.ok(streamSources.includes('unfinished_tool_followthrough'))
|
|
@@ -264,6 +259,46 @@ describe('isWalletSimulationResult', () => {
|
|
|
264
259
|
})
|
|
265
260
|
})
|
|
266
261
|
|
|
262
|
+
describe('shouldSkipToolSummaryForShortResponse', () => {
|
|
263
|
+
it('skips forced tool-summary continuation for short responses after pure use_skill calls', () => {
|
|
264
|
+
assert.equal(
|
|
265
|
+
shouldSkipToolSummaryForShortResponse({
|
|
266
|
+
fullText: 'HAL2K_RELEASE_LIVE_OK',
|
|
267
|
+
toolEvents: [
|
|
268
|
+
{ name: 'use_skill', input: '{"action":"list"}', output: '{"ok":true}' },
|
|
269
|
+
{ name: 'use_skill', input: '{"action":"load"}', output: '{"loaded":true}' },
|
|
270
|
+
],
|
|
271
|
+
}),
|
|
272
|
+
true,
|
|
273
|
+
)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('does not skip tool-summary continuation when substantive tools also ran', () => {
|
|
277
|
+
assert.equal(
|
|
278
|
+
shouldSkipToolSummaryForShortResponse({
|
|
279
|
+
fullText: 'Done.',
|
|
280
|
+
toolEvents: [
|
|
281
|
+
{ name: 'use_skill', input: '{"action":"load"}', output: '{"loaded":true}' },
|
|
282
|
+
{ name: 'web', input: '{"q":"latest"}', output: 'results' },
|
|
283
|
+
],
|
|
284
|
+
}),
|
|
285
|
+
false,
|
|
286
|
+
)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('does not skip tool-summary continuation for empty text', () => {
|
|
290
|
+
assert.equal(
|
|
291
|
+
shouldSkipToolSummaryForShortResponse({
|
|
292
|
+
fullText: '',
|
|
293
|
+
toolEvents: [
|
|
294
|
+
{ name: 'use_skill', input: '{"action":"load"}', output: '{"loaded":true}' },
|
|
295
|
+
],
|
|
296
|
+
}),
|
|
297
|
+
false,
|
|
298
|
+
)
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
267
302
|
describe('looksLikeOpenEndedDeliverableTask', () => {
|
|
268
303
|
it('detects open-ended deliverable prompts', () => {
|
|
269
304
|
assert.equal(
|