@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
|
@@ -24,6 +24,10 @@ const LEGACY_BUNDLED_SKILLS_DIR = path.join(DATA_DIR, 'skills')
|
|
|
24
24
|
|
|
25
25
|
let cache: DiscoveryCache | null = null
|
|
26
26
|
|
|
27
|
+
export function clearDiscoveredSkillsCache(): void {
|
|
28
|
+
cache = null
|
|
29
|
+
}
|
|
30
|
+
|
|
27
31
|
function buildCacheKey(cwd?: string): string {
|
|
28
32
|
return `${cwd || ''}`
|
|
29
33
|
}
|
|
@@ -16,6 +16,8 @@ metadata:
|
|
|
16
16
|
bins:
|
|
17
17
|
- curl
|
|
18
18
|
primaryEnv: GITHUB_TOKEN
|
|
19
|
+
toolNames: [github_workspace, github-cli]
|
|
20
|
+
capabilities: [github, pull-requests]
|
|
19
21
|
homepage: https://example.com/github-sync
|
|
20
22
|
install:
|
|
21
23
|
- kind: brew
|
|
@@ -32,6 +34,8 @@ Use the GitHub API.`,
|
|
|
32
34
|
assert.equal(normalized.version, '1.2.3')
|
|
33
35
|
assert.equal(normalized.primaryEnv, 'GITHUB_TOKEN')
|
|
34
36
|
assert.equal(normalized.homepage, 'https://example.com/github-sync')
|
|
37
|
+
assert.deepEqual(normalized.toolNames, ['github_workspace', 'github-cli'])
|
|
38
|
+
assert.deepEqual(normalized.capabilities, ['github', 'pull-requests'])
|
|
35
39
|
assert.equal(normalized.sourceFormat, 'openclaw')
|
|
36
40
|
assert.match(normalized.content, /# Sync issues/)
|
|
37
41
|
assert.equal(normalized.skillRequirements?.env?.[0], 'GITHUB_TOKEN')
|
|
@@ -52,3 +56,27 @@ Run with \`process.env.GITHUB_TOKEN\` and \`process.env.OPENAI_API_KEY\`.`,
|
|
|
52
56
|
assert.ok(normalized.security?.missingDeclarations?.includes('GITHUB_TOKEN'))
|
|
53
57
|
assert.ok(normalized.security?.missingDeclarations?.includes('OPENAI_API_KEY'))
|
|
54
58
|
})
|
|
59
|
+
|
|
60
|
+
test('normalizeSkillPayload parses executable invocation metadata from frontmatter', () => {
|
|
61
|
+
const normalized = normalizeSkillPayload({
|
|
62
|
+
content: `---
|
|
63
|
+
name: task-triage
|
|
64
|
+
description: Route task triage through manage_tasks.
|
|
65
|
+
invocation:
|
|
66
|
+
userInvocable: false
|
|
67
|
+
command-dispatch: tool
|
|
68
|
+
command-tool: manage_tasks
|
|
69
|
+
command-arg-mode: raw
|
|
70
|
+
---
|
|
71
|
+
# Task Triage
|
|
72
|
+
|
|
73
|
+
Route the request through manage_tasks.`,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
assert.deepEqual(normalized.invocation, { userInvocable: false })
|
|
77
|
+
assert.deepEqual(normalized.commandDispatch, {
|
|
78
|
+
kind: 'tool',
|
|
79
|
+
toolName: 'manage_tasks',
|
|
80
|
+
argMode: 'raw',
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
SkillCommandDispatch,
|
|
4
|
+
SkillInstallOption,
|
|
5
|
+
SkillInvocationConfig,
|
|
6
|
+
SkillRequirements,
|
|
7
|
+
SkillSecuritySummary,
|
|
8
|
+
} from '@/types'
|
|
3
9
|
import { dedup } from '@/lib/shared-utils'
|
|
4
10
|
|
|
5
11
|
export type SkillSourceFormat = 'openclaw' | 'plain'
|
|
@@ -17,11 +23,15 @@ type NormalizeSkillInput = {
|
|
|
17
23
|
homepage?: unknown
|
|
18
24
|
primaryEnv?: unknown
|
|
19
25
|
skillKey?: unknown
|
|
26
|
+
toolNames?: unknown
|
|
27
|
+
capabilities?: unknown
|
|
20
28
|
always?: unknown
|
|
21
29
|
installOptions?: unknown
|
|
22
30
|
skillRequirements?: unknown
|
|
23
31
|
detectedEnvVars?: unknown
|
|
24
32
|
security?: unknown
|
|
33
|
+
invocation?: unknown
|
|
34
|
+
commandDispatch?: unknown
|
|
25
35
|
frontmatter?: unknown
|
|
26
36
|
}
|
|
27
37
|
|
|
@@ -38,11 +48,15 @@ export type NormalizedSkill = {
|
|
|
38
48
|
homepage?: string
|
|
39
49
|
primaryEnv?: string | null
|
|
40
50
|
skillKey?: string | null
|
|
51
|
+
toolNames?: string[]
|
|
52
|
+
capabilities?: string[]
|
|
41
53
|
always?: boolean
|
|
42
54
|
installOptions?: SkillInstallOption[]
|
|
43
55
|
skillRequirements?: SkillRequirements
|
|
44
56
|
detectedEnvVars?: string[]
|
|
45
57
|
security?: SkillSecuritySummary | null
|
|
58
|
+
invocation?: SkillInvocationConfig | null
|
|
59
|
+
commandDispatch?: SkillCommandDispatch | null
|
|
46
60
|
frontmatter?: Record<string, unknown> | null
|
|
47
61
|
}
|
|
48
62
|
|
|
@@ -279,6 +293,65 @@ function normalizeRequirements(value: unknown): SkillRequirements | undefined {
|
|
|
279
293
|
return normalized
|
|
280
294
|
}
|
|
281
295
|
|
|
296
|
+
function normalizeInvocationConfig(value: unknown): SkillInvocationConfig | null {
|
|
297
|
+
const source = asObject(value)
|
|
298
|
+
if (!source) return null
|
|
299
|
+
const userInvocable = asBoolean(source.userInvocable ?? source.user_invocable)
|
|
300
|
+
if (userInvocable === undefined) return null
|
|
301
|
+
return { userInvocable }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeCommandDispatch(params: {
|
|
305
|
+
frontmatter?: Record<string, unknown> | null
|
|
306
|
+
runtimeMeta?: Record<string, unknown> | null
|
|
307
|
+
input?: NormalizeSkillInput
|
|
308
|
+
}): SkillCommandDispatch | null {
|
|
309
|
+
const inputDispatch = asObject(params.input?.commandDispatch)
|
|
310
|
+
const inlineDispatch = asObject(params.frontmatter?.commandDispatch)
|
|
311
|
+
|| asObject(params.frontmatter?.command_dispatch)
|
|
312
|
+
|| asObject(params.runtimeMeta?.commandDispatch)
|
|
313
|
+
|| asObject(params.runtimeMeta?.command_dispatch)
|
|
314
|
+
|
|
315
|
+
const kindRaw = asTrimmedString(
|
|
316
|
+
inputDispatch?.kind
|
|
317
|
+
?? inlineDispatch?.kind
|
|
318
|
+
?? params.frontmatter?.['command-dispatch']
|
|
319
|
+
?? params.frontmatter?.command_dispatch
|
|
320
|
+
?? params.runtimeMeta?.['command-dispatch']
|
|
321
|
+
?? params.runtimeMeta?.command_dispatch,
|
|
322
|
+
)
|
|
323
|
+
if (!kindRaw || kindRaw.toLowerCase() !== 'tool') return null
|
|
324
|
+
|
|
325
|
+
const toolName = asTrimmedString(
|
|
326
|
+
inputDispatch?.toolName
|
|
327
|
+
?? inputDispatch?.tool_name
|
|
328
|
+
?? inlineDispatch?.toolName
|
|
329
|
+
?? inlineDispatch?.tool_name
|
|
330
|
+
?? params.frontmatter?.['command-tool']
|
|
331
|
+
?? params.frontmatter?.command_tool
|
|
332
|
+
?? params.runtimeMeta?.['command-tool']
|
|
333
|
+
?? params.runtimeMeta?.command_tool,
|
|
334
|
+
)
|
|
335
|
+
if (!toolName) return null
|
|
336
|
+
|
|
337
|
+
const argModeRaw = asTrimmedString(
|
|
338
|
+
inputDispatch?.argMode
|
|
339
|
+
?? inputDispatch?.arg_mode
|
|
340
|
+
?? inlineDispatch?.argMode
|
|
341
|
+
?? inlineDispatch?.arg_mode
|
|
342
|
+
?? params.frontmatter?.['command-arg-mode']
|
|
343
|
+
?? params.frontmatter?.command_arg_mode
|
|
344
|
+
?? params.runtimeMeta?.['command-arg-mode']
|
|
345
|
+
?? params.runtimeMeta?.command_arg_mode,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
kind: 'tool',
|
|
350
|
+
toolName,
|
|
351
|
+
argMode: argModeRaw && argModeRaw.toLowerCase() === 'raw' ? 'raw' : 'raw',
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
282
355
|
function pickRuntimeMetadata(frontmatter: Record<string, unknown>): Record<string, unknown> | null {
|
|
283
356
|
const metadata = asObject(frontmatter.metadata)
|
|
284
357
|
if (metadata) {
|
|
@@ -395,11 +468,26 @@ export function normalizeSkillPayload(input: NormalizeSkillInput): NormalizedSki
|
|
|
395
468
|
const skillKey = asTrimmedString(runtimeMeta?.skillKey)
|
|
396
469
|
|| asTrimmedString(input.skillKey)
|
|
397
470
|
|| null
|
|
471
|
+
const toolNames = asStringArray(runtimeMeta?.toolNames)
|
|
472
|
+
|| asStringArray(runtimeMeta?.tools)
|
|
473
|
+
|| asStringArray(input.toolNames)
|
|
474
|
+
|| undefined
|
|
475
|
+
const capabilities = asStringArray(runtimeMeta?.capabilities)
|
|
476
|
+
|| asStringArray(input.capabilities)
|
|
477
|
+
|| undefined
|
|
398
478
|
const always = asBoolean(runtimeMeta?.always) ?? asBoolean(input.always)
|
|
399
479
|
const installOptions = normalizeInstallOptions(runtimeMeta?.install)
|
|
400
480
|
|| normalizeInstallOptions(input.installOptions)
|
|
401
481
|
const skillRequirements = normalizeRequirements(runtimeMeta)
|
|
402
482
|
|| normalizeRequirements(input.skillRequirements)
|
|
483
|
+
const invocation = normalizeInvocationConfig(input.invocation)
|
|
484
|
+
|| normalizeInvocationConfig(frontmatter?.invocation)
|
|
485
|
+
|| normalizeInvocationConfig(runtimeMeta?.invocation)
|
|
486
|
+
const commandDispatch = normalizeCommandDispatch({
|
|
487
|
+
frontmatter,
|
|
488
|
+
runtimeMeta,
|
|
489
|
+
input,
|
|
490
|
+
})
|
|
403
491
|
|
|
404
492
|
const sourceUrl = asTrimmedString(input.sourceUrl) || undefined
|
|
405
493
|
const initialFilename = asTrimmedString(input.filename)
|
|
@@ -467,11 +555,15 @@ export function normalizeSkillPayload(input: NormalizeSkillInput): NormalizedSki
|
|
|
467
555
|
homepage: homepage || undefined,
|
|
468
556
|
primaryEnv,
|
|
469
557
|
skillKey,
|
|
558
|
+
toolNames,
|
|
559
|
+
capabilities,
|
|
470
560
|
always,
|
|
471
561
|
installOptions,
|
|
472
562
|
skillRequirements,
|
|
473
563
|
detectedEnvVars: detectedEnvVars.length ? detectedEnvVars : preservedDetectedEnvVars,
|
|
474
564
|
security,
|
|
565
|
+
invocation,
|
|
566
|
+
commandDispatch,
|
|
475
567
|
frontmatter,
|
|
476
568
|
}
|
|
477
569
|
}
|
|
@@ -724,7 +724,7 @@ if (!IS_BUILD_BOOTSTRAP) {
|
|
|
724
724
|
- **Providers** — Configure LLM backends in Settings → Providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, OpenClaw, or custom OpenAI-compatible endpoints.
|
|
725
725
|
- **Tasks** — The Task Board tracks work items. Assign agents and they'll execute autonomously.
|
|
726
726
|
- **Schedules** — Cron-based recurring jobs that run agents or tasks automatically.
|
|
727
|
-
- **Skills** — Reusable markdown instruction files
|
|
727
|
+
- **Skills** — Reusable markdown instruction files agents can discover and use by default; pin them to keep favorite workflows always-on.
|
|
728
728
|
- **Connectors** — Bridge agents to Discord, Slack, Telegram, or WhatsApp.
|
|
729
729
|
- **Secrets** — Encrypted vault for API keys (Settings → Secrets).
|
|
730
730
|
|
|
@@ -279,6 +279,8 @@ describe('task-followups', () => {
|
|
|
279
279
|
const sessions = {
|
|
280
280
|
'sess-1': {
|
|
281
281
|
id: 'sess-1',
|
|
282
|
+
name: 'connector:slack:dm',
|
|
283
|
+
user: 'connector',
|
|
282
284
|
connectorContext: {
|
|
283
285
|
connectorId: 'conn-1',
|
|
284
286
|
channelId: 'ch-ctx',
|
|
@@ -471,6 +473,128 @@ describe('task-followups', () => {
|
|
|
471
473
|
})
|
|
472
474
|
})
|
|
473
475
|
|
|
476
|
+
describe('taskAlreadyDeliveredToConnectorTarget', () => {
|
|
477
|
+
it('returns true when the task session already delivered to the same connector target', () => {
|
|
478
|
+
const task = {
|
|
479
|
+
id: 'task-delivered',
|
|
480
|
+
title: 'Delivered task',
|
|
481
|
+
description: '',
|
|
482
|
+
agentId: 'agent-1',
|
|
483
|
+
sessionId: 'task-session',
|
|
484
|
+
status: 'completed' as const,
|
|
485
|
+
createdAt: Date.now(),
|
|
486
|
+
updatedAt: Date.now(),
|
|
487
|
+
}
|
|
488
|
+
const sessions = {
|
|
489
|
+
'task-session': {
|
|
490
|
+
id: 'task-session',
|
|
491
|
+
messages: [
|
|
492
|
+
{
|
|
493
|
+
role: 'assistant',
|
|
494
|
+
text: 'Sent it.',
|
|
495
|
+
toolEvents: [
|
|
496
|
+
{
|
|
497
|
+
name: 'connector_message_tool',
|
|
498
|
+
input: '{}',
|
|
499
|
+
output: JSON.stringify({
|
|
500
|
+
status: 'voice_sent',
|
|
501
|
+
connectorId: 'conn-wa',
|
|
502
|
+
to: '447700900111@s.whatsapp.net',
|
|
503
|
+
messageId: 'msg-1',
|
|
504
|
+
}),
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
},
|
|
510
|
+
}
|
|
511
|
+
const connectors = {
|
|
512
|
+
'conn-wa': {
|
|
513
|
+
id: 'conn-wa',
|
|
514
|
+
name: 'WA',
|
|
515
|
+
platform: 'whatsapp' as const,
|
|
516
|
+
agentId: 'agent-1',
|
|
517
|
+
config: {},
|
|
518
|
+
enabled: true,
|
|
519
|
+
createdAt: Date.now(),
|
|
520
|
+
updatedAt: Date.now(),
|
|
521
|
+
},
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const delivered = mod.taskAlreadyDeliveredToConnectorTarget({
|
|
525
|
+
task: task as import('@/types').BoardTask,
|
|
526
|
+
target: {
|
|
527
|
+
connectorId: 'conn-wa',
|
|
528
|
+
channelId: '+44 7700 900111',
|
|
529
|
+
},
|
|
530
|
+
sessions: sessions as Record<string, import('@/lib/server/tasks/task-followups').SessionLike>,
|
|
531
|
+
connectors: connectors as Record<string, import('@/types').Connector>,
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
assert.equal(delivered, true)
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
it('returns false when connector delivery was to a different target', () => {
|
|
538
|
+
const task = {
|
|
539
|
+
id: 'task-other-target',
|
|
540
|
+
title: 'Other target',
|
|
541
|
+
description: '',
|
|
542
|
+
agentId: 'agent-1',
|
|
543
|
+
sessionId: 'task-session',
|
|
544
|
+
status: 'completed' as const,
|
|
545
|
+
createdAt: Date.now(),
|
|
546
|
+
updatedAt: Date.now(),
|
|
547
|
+
}
|
|
548
|
+
const sessions = {
|
|
549
|
+
'task-session': {
|
|
550
|
+
id: 'task-session',
|
|
551
|
+
messages: [
|
|
552
|
+
{
|
|
553
|
+
role: 'assistant',
|
|
554
|
+
text: 'Sent it.',
|
|
555
|
+
toolEvents: [
|
|
556
|
+
{
|
|
557
|
+
name: 'connector_message_tool',
|
|
558
|
+
input: '{}',
|
|
559
|
+
output: JSON.stringify({
|
|
560
|
+
status: 'sent',
|
|
561
|
+
connectorId: 'conn-wa',
|
|
562
|
+
to: '447700900222@s.whatsapp.net',
|
|
563
|
+
messageId: 'msg-2',
|
|
564
|
+
}),
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
},
|
|
570
|
+
}
|
|
571
|
+
const connectors = {
|
|
572
|
+
'conn-wa': {
|
|
573
|
+
id: 'conn-wa',
|
|
574
|
+
name: 'WA',
|
|
575
|
+
platform: 'whatsapp' as const,
|
|
576
|
+
agentId: 'agent-1',
|
|
577
|
+
config: {},
|
|
578
|
+
enabled: true,
|
|
579
|
+
createdAt: Date.now(),
|
|
580
|
+
updatedAt: Date.now(),
|
|
581
|
+
},
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const delivered = mod.taskAlreadyDeliveredToConnectorTarget({
|
|
585
|
+
task: task as import('@/types').BoardTask,
|
|
586
|
+
target: {
|
|
587
|
+
connectorId: 'conn-wa',
|
|
588
|
+
channelId: '+44 7700 900111',
|
|
589
|
+
},
|
|
590
|
+
sessions: sessions as Record<string, import('@/lib/server/tasks/task-followups').SessionLike>,
|
|
591
|
+
connectors: connectors as Record<string, import('@/types').Connector>,
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
assert.equal(delivered, false)
|
|
595
|
+
})
|
|
596
|
+
})
|
|
597
|
+
|
|
474
598
|
// ---- isSendableAttachment ----
|
|
475
599
|
|
|
476
600
|
describe('isSendableAttachment', () => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import type { BoardTask, Connector } from '@/types'
|
|
3
|
+
import type { BoardTask, Connector, MessageToolEvent } from '@/types'
|
|
4
4
|
import { normalizeWhatsappTarget } from '@/lib/server/connectors/response-media'
|
|
5
|
+
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
5
6
|
import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
6
7
|
import { loadConnectors, loadSessions, UPLOAD_DIR } from '@/lib/server/storage'
|
|
7
8
|
import { errorMessage } from '@/lib/shared-utils'
|
|
@@ -64,6 +65,8 @@ export interface ConnectorTaskFollowupTarget {
|
|
|
64
65
|
threadId?: string | null
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
const CONNECTOR_DELIVERY_STATUSES = new Set(['sent', 'voice_sent'])
|
|
69
|
+
|
|
67
70
|
function isEnabledFlag(value: unknown): boolean {
|
|
68
71
|
if (typeof value === 'boolean') return value
|
|
69
72
|
if (typeof value !== 'string') return false
|
|
@@ -223,18 +226,20 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
223
226
|
const sourceSession = sessions[sourceSessionId]
|
|
224
227
|
if (!sourceSession) return null
|
|
225
228
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
229
|
+
if (isDirectConnectorSession(sourceSession)) {
|
|
230
|
+
const sessionContextTarget = normalizeTarget({
|
|
231
|
+
connectorId: typeof sourceSession.connectorContext?.connectorId === 'string'
|
|
232
|
+
? sourceSession.connectorContext.connectorId
|
|
233
|
+
: null,
|
|
234
|
+
channelId: typeof sourceSession.connectorContext?.channelId === 'string'
|
|
235
|
+
? sourceSession.connectorContext.channelId
|
|
236
|
+
: null,
|
|
237
|
+
threadId: typeof sourceSession.connectorContext?.threadId === 'string'
|
|
238
|
+
? sourceSession.connectorContext.threadId
|
|
239
|
+
: null,
|
|
240
|
+
})
|
|
241
|
+
if (sessionContextTarget) return sessionContextTarget
|
|
242
|
+
}
|
|
238
243
|
|
|
239
244
|
if (!Array.isArray(sourceSession.messages)) return null
|
|
240
245
|
|
|
@@ -312,6 +317,68 @@ export function collectTaskConnectorFollowupTargets(params: {
|
|
|
312
317
|
return targets
|
|
313
318
|
}
|
|
314
319
|
|
|
320
|
+
function normalizeFollowupChannelForConnector(connector: Connector | undefined, channelId: string | null | undefined): string {
|
|
321
|
+
const raw = typeof channelId === 'string' ? channelId.trim() : ''
|
|
322
|
+
if (!raw) return ''
|
|
323
|
+
return connector?.platform === 'whatsapp' ? normalizeWhatsappTarget(raw) : raw
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function extractDeliveredConnectorTarget(event: MessageToolEvent | null | undefined): {
|
|
327
|
+
connectorId: string
|
|
328
|
+
channelId: string
|
|
329
|
+
} | null {
|
|
330
|
+
if (!event || event.name !== 'connector_message_tool' || event.error === true || !event.output) return null
|
|
331
|
+
try {
|
|
332
|
+
const parsed = JSON.parse(event.output) as Record<string, unknown>
|
|
333
|
+
const status = typeof parsed.status === 'string' ? parsed.status.trim().toLowerCase() : ''
|
|
334
|
+
const connectorId = typeof parsed.connectorId === 'string' ? parsed.connectorId.trim() : ''
|
|
335
|
+
const channelId = typeof parsed.to === 'string' ? parsed.to.trim() : ''
|
|
336
|
+
if (!CONNECTOR_DELIVERY_STATUSES.has(status) || !connectorId || !channelId) return null
|
|
337
|
+
return { connectorId, channelId }
|
|
338
|
+
} catch {
|
|
339
|
+
return null
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function taskAlreadyDeliveredToConnectorTarget(params: {
|
|
344
|
+
task: BoardTask
|
|
345
|
+
target: ConnectorTaskFollowupTarget
|
|
346
|
+
sessions: Record<string, SessionLike>
|
|
347
|
+
connectors: Record<string, Connector>
|
|
348
|
+
}): boolean {
|
|
349
|
+
const taskRecord = params.task as BoardTask & {
|
|
350
|
+
checkpoint?: {
|
|
351
|
+
lastSessionId?: string | null
|
|
352
|
+
} | null
|
|
353
|
+
}
|
|
354
|
+
const taskSessionId = typeof taskRecord.sessionId === 'string' && taskRecord.sessionId.trim()
|
|
355
|
+
? taskRecord.sessionId.trim()
|
|
356
|
+
: typeof taskRecord.checkpoint?.lastSessionId === 'string' && taskRecord.checkpoint.lastSessionId.trim()
|
|
357
|
+
? taskRecord.checkpoint.lastSessionId.trim()
|
|
358
|
+
: ''
|
|
359
|
+
if (!taskSessionId) return false
|
|
360
|
+
const session = params.sessions[taskSessionId]
|
|
361
|
+
if (!session || !Array.isArray(session.messages)) return false
|
|
362
|
+
|
|
363
|
+
const connector = params.connectors[params.target.connectorId]
|
|
364
|
+
const normalizedTargetChannel = normalizeFollowupChannelForConnector(connector, params.target.channelId)
|
|
365
|
+
if (!normalizedTargetChannel) return false
|
|
366
|
+
|
|
367
|
+
for (let index = session.messages.length - 1; index >= 0; index -= 1) {
|
|
368
|
+
const message = session.messages[index]
|
|
369
|
+
if (!message || message.role !== 'assistant' || !Array.isArray(message.toolEvents)) continue
|
|
370
|
+
for (const event of message.toolEvents) {
|
|
371
|
+
const delivered = extractDeliveredConnectorTarget(event as MessageToolEvent)
|
|
372
|
+
if (!delivered) continue
|
|
373
|
+
if (delivered.connectorId !== params.target.connectorId) continue
|
|
374
|
+
const normalizedDeliveredChannel = normalizeFollowupChannelForConnector(connector, delivered.channelId)
|
|
375
|
+
if (normalizedDeliveredChannel === normalizedTargetChannel) return true
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return false
|
|
380
|
+
}
|
|
381
|
+
|
|
315
382
|
export async function notifyConnectorTaskFollowups(params: {
|
|
316
383
|
task: BoardTask
|
|
317
384
|
statusLabel: string
|
|
@@ -347,6 +414,14 @@ export async function notifyConnectorTaskFollowups(params: {
|
|
|
347
414
|
for (const target of targets) {
|
|
348
415
|
const connector = connectors[target.connectorId]
|
|
349
416
|
if (!connector) continue
|
|
417
|
+
if (taskAlreadyDeliveredToConnectorTarget({
|
|
418
|
+
task,
|
|
419
|
+
target,
|
|
420
|
+
sessions: sessions as Record<string, SessionLike>,
|
|
421
|
+
connectors: connectors as Record<string, Connector>,
|
|
422
|
+
})) {
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
350
425
|
|
|
351
426
|
const template = typeof (connector as any).config?.taskFollowupTemplate === 'string'
|
|
352
427
|
? (connector as any).config.taskFollowupTemplate.trim()
|
package/src/types/index.ts
CHANGED
|
@@ -49,6 +49,15 @@ export interface SessionArchiveState {
|
|
|
49
49
|
exportPath?: string | null
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export interface SessionSkillRuntimeState {
|
|
53
|
+
selectedSkillId?: string | null
|
|
54
|
+
selectedSkillName?: string | null
|
|
55
|
+
selectedAt?: number | null
|
|
56
|
+
lastAction?: 'select' | 'load' | 'run' | null
|
|
57
|
+
lastRunAt?: number | null
|
|
58
|
+
lastRunToolName?: string | null
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
export interface CanvasMetricItem {
|
|
53
62
|
label: string
|
|
54
63
|
value: string
|
|
@@ -234,6 +243,7 @@ export interface Session {
|
|
|
234
243
|
lastSessionResetReason?: string | null
|
|
235
244
|
identityState?: IdentityContinuityState | null
|
|
236
245
|
sessionArchiveState?: SessionArchiveState | null
|
|
246
|
+
skillRuntimeState?: SessionSkillRuntimeState | null
|
|
237
247
|
pinned?: boolean
|
|
238
248
|
file?: string | null
|
|
239
249
|
queuedCount?: number
|
|
@@ -642,8 +652,8 @@ export interface Agent {
|
|
|
642
652
|
plugins?: string[] // e.g. ['browser', 'memory'] — enabled plugin IDs
|
|
643
653
|
/** @deprecated Use `plugins` instead. Kept for backward compat with stored data. */
|
|
644
654
|
tools?: string[]
|
|
645
|
-
skills?: string[] // e.g. ['frontend-design'] — Claude Code skills to
|
|
646
|
-
skillIds?: string[] // IDs of
|
|
655
|
+
skills?: string[] // e.g. ['frontend-design'] — pinned Claude Code skills to mention explicitly
|
|
656
|
+
skillIds?: string[] // IDs of pinned managed skills to keep always-on for this agent
|
|
647
657
|
mcpServerIds?: string[] // IDs of configured MCP servers to inject tools from
|
|
648
658
|
mcpDisabledTools?: string[] // MCP tool names disabled for this agent (denylist)
|
|
649
659
|
capabilities?: string[] // e.g. ['frontend', 'screenshots', 'research', 'devops']
|
|
@@ -660,6 +670,7 @@ export interface Agent {
|
|
|
660
670
|
heartbeatTarget?: 'last' | 'none' | string | null
|
|
661
671
|
heartbeatGoal?: string | null
|
|
662
672
|
heartbeatNextAction?: string | null
|
|
673
|
+
heartbeatLightContext?: boolean | null
|
|
663
674
|
sessionResetMode?: SessionResetMode | null
|
|
664
675
|
sessionIdleTimeoutSec?: number | null
|
|
665
676
|
sessionMaxAgeSec?: number | null
|
|
@@ -1277,6 +1288,8 @@ export interface AppSettings {
|
|
|
1277
1288
|
shellCommandTimeoutSec?: number
|
|
1278
1289
|
claudeCodeTimeoutSec?: number
|
|
1279
1290
|
cliProcessTimeoutSec?: number
|
|
1291
|
+
streamIdleStallSec?: number
|
|
1292
|
+
requiredToolKickoffSec?: number
|
|
1280
1293
|
userAvatarSeed?: string
|
|
1281
1294
|
elevenLabsEnabled?: boolean
|
|
1282
1295
|
elevenLabsApiKey?: string | null
|
|
@@ -1298,6 +1311,7 @@ export interface AppSettings {
|
|
|
1298
1311
|
heartbeatActiveStart?: string | null
|
|
1299
1312
|
heartbeatActiveEnd?: string | null
|
|
1300
1313
|
heartbeatTimezone?: string | null
|
|
1314
|
+
heartbeatLightContext?: boolean | null
|
|
1301
1315
|
sessionResetMode?: SessionResetMode | null
|
|
1302
1316
|
sessionIdleTimeoutSec?: number | null
|
|
1303
1317
|
sessionMaxAgeSec?: number | null
|
|
@@ -1671,11 +1685,15 @@ export interface Skill {
|
|
|
1671
1685
|
homepage?: string
|
|
1672
1686
|
primaryEnv?: string | null
|
|
1673
1687
|
skillKey?: string | null
|
|
1688
|
+
toolNames?: string[]
|
|
1689
|
+
capabilities?: string[]
|
|
1674
1690
|
always?: boolean
|
|
1675
1691
|
installOptions?: SkillInstallOption[]
|
|
1676
1692
|
skillRequirements?: SkillRequirements
|
|
1677
1693
|
detectedEnvVars?: string[]
|
|
1678
1694
|
security?: SkillSecuritySummary | null
|
|
1695
|
+
invocation?: SkillInvocationConfig | null
|
|
1696
|
+
commandDispatch?: SkillCommandDispatch | null
|
|
1679
1697
|
frontmatter?: Record<string, unknown> | null
|
|
1680
1698
|
scope?: 'global' | 'agent'
|
|
1681
1699
|
agentIds?: string[]
|
|
@@ -1683,6 +1701,16 @@ export interface Skill {
|
|
|
1683
1701
|
updatedAt: number
|
|
1684
1702
|
}
|
|
1685
1703
|
|
|
1704
|
+
export interface SkillInvocationConfig {
|
|
1705
|
+
userInvocable?: boolean
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
export interface SkillCommandDispatch {
|
|
1709
|
+
kind: 'tool'
|
|
1710
|
+
toolName: string
|
|
1711
|
+
argMode?: 'raw'
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1686
1714
|
export interface SkillSecuritySummary {
|
|
1687
1715
|
level: 'low' | 'medium' | 'high'
|
|
1688
1716
|
notes: string[]
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES,
|
|
11
11
|
DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT,
|
|
12
12
|
DEFAULT_SHELL_COMMAND_TIMEOUT_SEC,
|
|
13
|
+
DEFAULT_STREAM_IDLE_STALL_SEC,
|
|
14
|
+
DEFAULT_REQUIRED_TOOL_KICKOFF_SEC,
|
|
13
15
|
} from '@/lib/runtime/runtime-loop'
|
|
14
16
|
import type { LoopMode } from '@/types'
|
|
15
17
|
import type { SettingsSectionProps } from './types'
|
|
@@ -212,6 +214,42 @@ export function RuntimeLoopSection({ appSettings, patchSettings, inputClass }: S
|
|
|
212
214
|
</div>
|
|
213
215
|
</div>
|
|
214
216
|
|
|
217
|
+
<label className="flex items-center gap-1.5 font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mt-5 mb-3">Stream & Kickoff Timeouts (Seconds) <HintTip text="Controls how long to wait for model output and required tool usage before aborting a turn" /></label>
|
|
218
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
219
|
+
<div>
|
|
220
|
+
<label className="block text-[11px] text-text-3 mb-2">Idle Stall Timeout</label>
|
|
221
|
+
<input
|
|
222
|
+
type="number"
|
|
223
|
+
min={30}
|
|
224
|
+
max={600}
|
|
225
|
+
value={appSettings.streamIdleStallSec ?? DEFAULT_STREAM_IDLE_STALL_SEC}
|
|
226
|
+
onChange={(e) => {
|
|
227
|
+
const n = Number.parseInt(e.target.value, 10)
|
|
228
|
+
patchSettings({ streamIdleStallSec: Number.isFinite(n) ? n : DEFAULT_STREAM_IDLE_STALL_SEC })
|
|
229
|
+
}}
|
|
230
|
+
className={inputClass}
|
|
231
|
+
style={{ fontFamily: 'inherit' }}
|
|
232
|
+
/>
|
|
233
|
+
<p className="text-[11px] text-text-3/60 mt-2">Aborts a turn if no tokens arrive for this long. Raise for slow local models.</p>
|
|
234
|
+
</div>
|
|
235
|
+
<div>
|
|
236
|
+
<label className="block text-[11px] text-text-3 mb-2">Required Tool Kickoff</label>
|
|
237
|
+
<input
|
|
238
|
+
type="number"
|
|
239
|
+
min={10}
|
|
240
|
+
max={120}
|
|
241
|
+
value={appSettings.requiredToolKickoffSec ?? DEFAULT_REQUIRED_TOOL_KICKOFF_SEC}
|
|
242
|
+
onChange={(e) => {
|
|
243
|
+
const n = Number.parseInt(e.target.value, 10)
|
|
244
|
+
patchSettings({ requiredToolKickoffSec: Number.isFinite(n) ? n : DEFAULT_REQUIRED_TOOL_KICKOFF_SEC })
|
|
245
|
+
}}
|
|
246
|
+
className={inputClass}
|
|
247
|
+
style={{ fontFamily: 'inherit' }}
|
|
248
|
+
/>
|
|
249
|
+
<p className="text-[11px] text-text-3/60 mt-2">Max wait for a required tool call before forcing a continuation.</p>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
215
253
|
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mt-6 mb-3">LLM Response Cache</label>
|
|
216
254
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-5">
|
|
217
255
|
<div className="md:col-span-3 flex items-center gap-3">
|