@swarmclawai/swarmclaw 0.6.7 → 0.7.0
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 +82 -39
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +37 -7
- package/src/components/home/home-view.tsx +54 -24
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +87 -19
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +28 -9
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +224 -0
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +72 -48
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +115 -16
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +32 -2
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +78 -0
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +24 -5
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +155 -10
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { normalizeTaskQualityGate } from './task-quality-gate.ts'
|
|
4
|
+
|
|
5
|
+
test('normalizeTaskQualityGate uses defaults when unset', () => {
|
|
6
|
+
const gate = normalizeTaskQualityGate(undefined, undefined)
|
|
7
|
+
assert.equal(gate.enabled, true)
|
|
8
|
+
assert.equal(gate.minResultChars, 80)
|
|
9
|
+
assert.equal(gate.minEvidenceItems, 2)
|
|
10
|
+
assert.equal(gate.requireVerification, false)
|
|
11
|
+
assert.equal(gate.requireArtifact, false)
|
|
12
|
+
assert.equal(gate.requireReport, false)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('normalizeTaskQualityGate respects app settings defaults', () => {
|
|
16
|
+
const gate = normalizeTaskQualityGate(null, {
|
|
17
|
+
taskQualityGateEnabled: false,
|
|
18
|
+
taskQualityGateMinResultChars: 120,
|
|
19
|
+
taskQualityGateMinEvidenceItems: 1,
|
|
20
|
+
taskQualityGateRequireVerification: true,
|
|
21
|
+
})
|
|
22
|
+
assert.equal(gate.enabled, false)
|
|
23
|
+
assert.equal(gate.minResultChars, 120)
|
|
24
|
+
assert.equal(gate.minEvidenceItems, 1)
|
|
25
|
+
assert.equal(gate.requireVerification, true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('normalizeTaskQualityGate allows per-task overrides on top of settings', () => {
|
|
29
|
+
const gate = normalizeTaskQualityGate({
|
|
30
|
+
enabled: true,
|
|
31
|
+
minResultChars: 64,
|
|
32
|
+
minEvidenceItems: 3,
|
|
33
|
+
requireArtifact: true,
|
|
34
|
+
}, {
|
|
35
|
+
taskQualityGateEnabled: false,
|
|
36
|
+
taskQualityGateMinResultChars: 120,
|
|
37
|
+
taskQualityGateMinEvidenceItems: 1,
|
|
38
|
+
taskQualityGateRequireArtifact: false,
|
|
39
|
+
})
|
|
40
|
+
assert.equal(gate.enabled, true)
|
|
41
|
+
assert.equal(gate.minResultChars, 64)
|
|
42
|
+
assert.equal(gate.minEvidenceItems, 3)
|
|
43
|
+
assert.equal(gate.requireArtifact, true)
|
|
44
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AppSettings, TaskQualityGateConfig } from '@/types'
|
|
2
|
+
|
|
3
|
+
export interface NormalizedTaskQualityGate {
|
|
4
|
+
enabled: boolean
|
|
5
|
+
minResultChars: number
|
|
6
|
+
minEvidenceItems: number
|
|
7
|
+
requireVerification: boolean
|
|
8
|
+
requireArtifact: boolean
|
|
9
|
+
requireReport: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_TASK_QUALITY_GATE: NormalizedTaskQualityGate = {
|
|
13
|
+
enabled: true,
|
|
14
|
+
minResultChars: 80,
|
|
15
|
+
minEvidenceItems: 2,
|
|
16
|
+
requireVerification: false,
|
|
17
|
+
requireArtifact: false,
|
|
18
|
+
requireReport: false,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeInt(value: unknown, fallback: number, min: number, max: number): number {
|
|
22
|
+
const parsed = typeof value === 'number'
|
|
23
|
+
? value
|
|
24
|
+
: typeof value === 'string'
|
|
25
|
+
? Number.parseInt(value, 10)
|
|
26
|
+
: Number.NaN
|
|
27
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
28
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeBool(value: unknown, fallback: boolean): boolean {
|
|
32
|
+
if (typeof value === 'boolean') return value
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
const normalized = value.trim().toLowerCase()
|
|
35
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
|
|
36
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false
|
|
37
|
+
}
|
|
38
|
+
return fallback
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeSettingsDefaults(settings?: AppSettings | Record<string, unknown> | null): NormalizedTaskQualityGate {
|
|
42
|
+
const raw = settings && typeof settings === 'object' ? settings as Record<string, unknown> : {}
|
|
43
|
+
return {
|
|
44
|
+
enabled: normalizeBool(raw.taskQualityGateEnabled, DEFAULT_TASK_QUALITY_GATE.enabled),
|
|
45
|
+
minResultChars: normalizeInt(raw.taskQualityGateMinResultChars, DEFAULT_TASK_QUALITY_GATE.minResultChars, 10, 2000),
|
|
46
|
+
minEvidenceItems: normalizeInt(raw.taskQualityGateMinEvidenceItems, DEFAULT_TASK_QUALITY_GATE.minEvidenceItems, 0, 8),
|
|
47
|
+
requireVerification: normalizeBool(raw.taskQualityGateRequireVerification, DEFAULT_TASK_QUALITY_GATE.requireVerification),
|
|
48
|
+
requireArtifact: normalizeBool(raw.taskQualityGateRequireArtifact, DEFAULT_TASK_QUALITY_GATE.requireArtifact),
|
|
49
|
+
requireReport: normalizeBool(raw.taskQualityGateRequireReport, DEFAULT_TASK_QUALITY_GATE.requireReport),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeTaskQualityGate(
|
|
54
|
+
rawGate?: TaskQualityGateConfig | Record<string, unknown> | null,
|
|
55
|
+
settings?: AppSettings | Record<string, unknown> | null,
|
|
56
|
+
): NormalizedTaskQualityGate {
|
|
57
|
+
const defaults = normalizeSettingsDefaults(settings)
|
|
58
|
+
const raw = rawGate && typeof rawGate === 'object' ? rawGate as Record<string, unknown> : {}
|
|
59
|
+
return {
|
|
60
|
+
enabled: normalizeBool(raw.enabled, defaults.enabled),
|
|
61
|
+
minResultChars: normalizeInt(raw.minResultChars, defaults.minResultChars, 10, 2000),
|
|
62
|
+
minEvidenceItems: normalizeInt(raw.minEvidenceItems, defaults.minEvidenceItems, 0, 8),
|
|
63
|
+
requireVerification: normalizeBool(raw.requireVerification, defaults.requireVerification),
|
|
64
|
+
requireArtifact: normalizeBool(raw.requireArtifact, defaults.requireArtifact),
|
|
65
|
+
requireReport: normalizeBool(raw.requireReport, defaults.requireReport),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -48,3 +48,81 @@ test('validateTaskCompletion still enforces stricter minimum for implementation
|
|
|
48
48
|
assert.equal(validation.ok, false)
|
|
49
49
|
assert.ok(validation.reasons.some((reason) => reason.includes('Result summary is too short')))
|
|
50
50
|
})
|
|
51
|
+
|
|
52
|
+
test('validateTaskCompletion fails implementation task with unfinished next-step language', () => {
|
|
53
|
+
const validation = validateTaskCompletion({
|
|
54
|
+
title: 'Build weather dashboard',
|
|
55
|
+
description: 'Implement dashboard and run dev server.',
|
|
56
|
+
result: 'I prepared an outline. Next I will run the server once access is granted.',
|
|
57
|
+
error: null,
|
|
58
|
+
} as Partial<BoardTask>)
|
|
59
|
+
|
|
60
|
+
assert.equal(validation.ok, false)
|
|
61
|
+
assert.ok(validation.reasons.some((reason) => reason.includes('unfinished work')))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('validateTaskCompletion fails implementation task that requests shell access', () => {
|
|
65
|
+
const validation = validateTaskCompletion({
|
|
66
|
+
title: 'Create blog and run server',
|
|
67
|
+
description: 'Create markdown blog and serve it.',
|
|
68
|
+
result: 'I created the blog file at data/workspace/blog/swarmclaw-blog.md, but I need access to the shell to proceed. Once the access is granted, I will finish setup.',
|
|
69
|
+
error: null,
|
|
70
|
+
} as Partial<BoardTask>)
|
|
71
|
+
|
|
72
|
+
assert.equal(validation.ok, false)
|
|
73
|
+
assert.ok(validation.reasons.some((reason) => reason.includes('unfinished work')))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('validateTaskCompletion fails untitled tasks with empty metadata', () => {
|
|
77
|
+
const validation = validateTaskCompletion({
|
|
78
|
+
title: 'Untitled Task',
|
|
79
|
+
description: '',
|
|
80
|
+
result: 'Could you provide more information about what you need?',
|
|
81
|
+
error: null,
|
|
82
|
+
} as Partial<BoardTask>)
|
|
83
|
+
|
|
84
|
+
assert.equal(validation.ok, false)
|
|
85
|
+
assert.ok(validation.reasons.some((reason) => reason.includes('metadata is too vague')))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('validateTaskCompletion enforces explicit quality gate evidence requirements', () => {
|
|
89
|
+
const validation = validateTaskCompletion({
|
|
90
|
+
title: 'Ship API migration summary',
|
|
91
|
+
description: 'Summarize the migration outcome.',
|
|
92
|
+
result: 'Migration summary completed successfully with no extra artifacts included.',
|
|
93
|
+
qualityGate: {
|
|
94
|
+
enabled: true,
|
|
95
|
+
minResultChars: 20,
|
|
96
|
+
minEvidenceItems: 2,
|
|
97
|
+
requireArtifact: true,
|
|
98
|
+
},
|
|
99
|
+
error: null,
|
|
100
|
+
} as Partial<BoardTask>)
|
|
101
|
+
|
|
102
|
+
assert.equal(validation.ok, false)
|
|
103
|
+
assert.ok(validation.reasons.some((reason) => reason.includes('insufficient completion evidence')))
|
|
104
|
+
assert.ok(validation.reasons.some((reason) => reason.includes('artifact evidence is required')))
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('validateTaskCompletion passes explicit quality gate when evidence checks are met', () => {
|
|
108
|
+
const validation = validateTaskCompletion({
|
|
109
|
+
title: 'Ship API migration summary',
|
|
110
|
+
description: 'Summarize the migration outcome.',
|
|
111
|
+
result: 'Ran npm test and tests passed. Updated src/api/migrate.ts. Uploaded evidence: sandbox:/api/uploads/migration-proof.png.',
|
|
112
|
+
artifacts: [{
|
|
113
|
+
url: 'sandbox:/api/uploads/migration-proof.png',
|
|
114
|
+
type: 'image',
|
|
115
|
+
filename: 'migration-proof.png',
|
|
116
|
+
}],
|
|
117
|
+
qualityGate: {
|
|
118
|
+
enabled: true,
|
|
119
|
+
minResultChars: 20,
|
|
120
|
+
minEvidenceItems: 2,
|
|
121
|
+
requireArtifact: true,
|
|
122
|
+
requireVerification: true,
|
|
123
|
+
},
|
|
124
|
+
error: null,
|
|
125
|
+
} as Partial<BoardTask>)
|
|
126
|
+
|
|
127
|
+
assert.equal(validation.ok, true)
|
|
128
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BoardTask } from '@/types'
|
|
2
2
|
import type { TaskReportArtifact } from './task-reports'
|
|
3
|
+
import { normalizeTaskQualityGate } from './task-quality-gate'
|
|
3
4
|
|
|
4
5
|
export interface TaskCompletionValidation {
|
|
5
6
|
ok: boolean
|
|
@@ -9,6 +10,7 @@ export interface TaskCompletionValidation {
|
|
|
9
10
|
|
|
10
11
|
interface TaskCompletionValidationOptions {
|
|
11
12
|
report?: TaskReportArtifact | null
|
|
13
|
+
settings?: Record<string, unknown> | null
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const MIN_RESULT_CHARS_IMPLEMENTATION = 40
|
|
@@ -24,8 +26,26 @@ const WEAK_RESULT_PATTERNS: RegExp[] = [
|
|
|
24
26
|
/\bzero typescript errors\b/i,
|
|
25
27
|
]
|
|
26
28
|
|
|
29
|
+
const INCOMPLETE_RESULT_PATTERNS: RegExp[] = [
|
|
30
|
+
/\b(?:next|then)\s*,?\s*i\s+(?:will|can|am going to)\b/i,
|
|
31
|
+
/\b(?:i(?:'| a)?ll|let me)\s+(?:start|begin|proceed|continue)\b/i,
|
|
32
|
+
/\b(?:once|when|after)\s+(?:the\s+)?(?:access|approval|permission)\s+(?:is|has been)\s+granted\b/i,
|
|
33
|
+
/\bneed (?:more )?(?:details|information|context)\b/i,
|
|
34
|
+
/\b(?:i|we)\s+(?:need|require)\s+(?:access|approval|permission)\b/i,
|
|
35
|
+
/\brequested\s+(?:access|approval|permission)\b/i,
|
|
36
|
+
/\bneed access to (?:the )?(?:shell|terminal|command line)\b/i,
|
|
37
|
+
/\battempted to\b[^.]{0,120}\b(?:but|however)\b/i,
|
|
38
|
+
/\bcould you provide\b/i,
|
|
39
|
+
/\blet me know once\b/i,
|
|
40
|
+
/\bthere (?:aren't|are not) any specific details\b/i,
|
|
41
|
+
]
|
|
42
|
+
|
|
27
43
|
const IMPLEMENTATION_HINT = /\b(add|build|create|fix|implement|integrat|refactor|update|write)\b/i
|
|
28
|
-
const
|
|
44
|
+
const EXECUTION_ACTION_HINT = /\b(changed|updated|added|modified|implemented|refactored|fixed|ran|executed|verified)\b/i
|
|
45
|
+
const COMMAND_EVIDENCE_HINT = /\b(npm|pnpm|yarn|bun|node|npx|pytest|vitest|jest|playwright|go test|cargo test|deno test|python|pip|uv|docker|git)\b/i
|
|
46
|
+
const FILE_PATH_EVIDENCE_HINT = /\b[\w./-]+\.(ts|tsx|js|jsx|mjs|cjs|json|md|css|scss|html|yml|yaml|sh|py|go|rs|java|kt|swift|rb|php|sql|txt)\b/i
|
|
47
|
+
const ARTIFACT_EVIDENCE_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf|zip)\b/i
|
|
48
|
+
const VERIFICATION_EVIDENCE_HINT = /\b(test|tests|lint|typecheck|build)\b[^.]{0,40}\b(pass(?:ed)?|fail(?:ed)?|ok|success)\b/i
|
|
29
49
|
const SCREENSHOT_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i
|
|
30
50
|
const DELIVERY_HINT = /\b(send|deliver|return|share|upload|post|message)\b/i
|
|
31
51
|
const SCREENSHOT_ARTIFACT_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf)\b/i
|
|
@@ -46,9 +66,14 @@ export function validateTaskCompletion(
|
|
|
46
66
|
const result = normalizeText(task.result)
|
|
47
67
|
const error = normalizeText(task.error)
|
|
48
68
|
const report = options.report || null
|
|
69
|
+
const hasExplicitQualityGate = !!task.qualityGate && typeof task.qualityGate === 'object'
|
|
70
|
+
const qualityGate = normalizeTaskQualityGate(task.qualityGate || null, options.settings || null)
|
|
49
71
|
const implementationTask = IMPLEMENTATION_HINT.test(title) || IMPLEMENTATION_HINT.test(description)
|
|
50
72
|
|
|
51
73
|
if (error) reasons.push('Task has a non-empty error field.')
|
|
74
|
+
if (/^untitled task$/i.test(title) && !description) {
|
|
75
|
+
reasons.push('Task metadata is too vague (untitled title with empty description).')
|
|
76
|
+
}
|
|
52
77
|
|
|
53
78
|
if (!result) reasons.push('Result summary is empty.')
|
|
54
79
|
else {
|
|
@@ -57,11 +82,20 @@ export function validateTaskCompletion(
|
|
|
57
82
|
if (WEAK_RESULT_PATTERNS.some((rx) => rx.test(result))) {
|
|
58
83
|
reasons.push('Result contains placeholder/planning language instead of completion evidence.')
|
|
59
84
|
}
|
|
85
|
+
if (INCOMPLETE_RESULT_PATTERNS.some((rx) => rx.test(result))) {
|
|
86
|
+
reasons.push('Result indicates unfinished work or missing inputs instead of completed execution.')
|
|
87
|
+
}
|
|
60
88
|
}
|
|
61
89
|
|
|
62
90
|
// If task description/title suggests implementation work, require concrete evidence in
|
|
63
91
|
// the result summary OR task report.
|
|
64
|
-
const hasResultEvidence =
|
|
92
|
+
const hasResultEvidence = (
|
|
93
|
+
COMMAND_EVIDENCE_HINT.test(result)
|
|
94
|
+
|| ARTIFACT_EVIDENCE_HINT.test(result)
|
|
95
|
+
|| VERIFICATION_EVIDENCE_HINT.test(result)
|
|
96
|
+
|| (EXECUTION_ACTION_HINT.test(result)
|
|
97
|
+
&& (/\b(command|test|lint|typecheck|build|file|artifact)\b/i.test(result) || FILE_PATH_EVIDENCE_HINT.test(result)))
|
|
98
|
+
)
|
|
65
99
|
const hasReportEvidence = report?.evidence.hasEvidence === true
|
|
66
100
|
if (implementationTask && !hasResultEvidence && !hasReportEvidence) {
|
|
67
101
|
if (report?.relativePath) {
|
|
@@ -80,6 +114,37 @@ export function validateTaskCompletion(
|
|
|
80
114
|
}
|
|
81
115
|
}
|
|
82
116
|
|
|
117
|
+
if (qualityGate.enabled && (implementationTask || hasExplicitQualityGate)) {
|
|
118
|
+
if (result && result.length < qualityGate.minResultChars) {
|
|
119
|
+
reasons.push(`Quality gate: result summary is shorter than required minimum (${result.length} chars; min ${qualityGate.minResultChars}).`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const hasCommandEvidence = COMMAND_EVIDENCE_HINT.test(result) || (report?.evidence.commandsRun.length || 0) > 0
|
|
123
|
+
const hasFileEvidence = FILE_PATH_EVIDENCE_HINT.test(result) || (report?.evidence.changedFiles.length || 0) > 0
|
|
124
|
+
const hasVerificationEvidence = VERIFICATION_EVIDENCE_HINT.test(result) || (report?.evidence.verification.length || 0) > 0
|
|
125
|
+
const hasArtifactEvidence = ARTIFACT_EVIDENCE_HINT.test(result) || ((task.artifacts?.length || 0) > 0)
|
|
126
|
+
|
|
127
|
+
const evidenceSignals = [
|
|
128
|
+
hasCommandEvidence,
|
|
129
|
+
hasFileEvidence,
|
|
130
|
+
hasVerificationEvidence,
|
|
131
|
+
hasArtifactEvidence,
|
|
132
|
+
].filter(Boolean).length
|
|
133
|
+
|
|
134
|
+
if (evidenceSignals < qualityGate.minEvidenceItems) {
|
|
135
|
+
reasons.push(`Quality gate: insufficient completion evidence (${evidenceSignals}/${qualityGate.minEvidenceItems} required evidence signals).`)
|
|
136
|
+
}
|
|
137
|
+
if (qualityGate.requireVerification && !hasVerificationEvidence) {
|
|
138
|
+
reasons.push('Quality gate: verification evidence is required (tests/lint/build/check output missing).')
|
|
139
|
+
}
|
|
140
|
+
if (qualityGate.requireArtifact && !hasArtifactEvidence) {
|
|
141
|
+
reasons.push('Quality gate: artifact evidence is required (artifact URL/upload or structured artifacts list missing).')
|
|
142
|
+
}
|
|
143
|
+
if (qualityGate.requireReport && !report?.relativePath) {
|
|
144
|
+
reasons.push('Quality gate: task completion report is required but missing.')
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
83
148
|
return {
|
|
84
149
|
ok: reasons.length === 0,
|
|
85
150
|
reasons,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const TOOL_ALIAS_GROUPS: string[][] = [
|
|
2
|
+
['shell', 'execute_command', 'process_tool', 'process'],
|
|
3
|
+
['files', 'read_file', 'write_file', 'list_files', 'copy_file', 'move_file', 'delete_file', 'send_file'],
|
|
4
|
+
['edit_file'],
|
|
5
|
+
['web', 'web_search', 'web_fetch'],
|
|
6
|
+
['browser', 'openclaw_browser'],
|
|
7
|
+
['delegate', 'claude_code', 'codex_cli', 'opencode_cli', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli'],
|
|
8
|
+
['manage_platform', 'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_secrets', 'manage_sessions'],
|
|
9
|
+
['manage_connectors', 'connectors', 'connector_message_tool'],
|
|
10
|
+
['manage_chatrooms', 'chatroom'],
|
|
11
|
+
['spawn_subagent', 'subagent', 'delegate_to_agent'],
|
|
12
|
+
['manage_sessions', 'session_info', 'sessions_tool', 'whoami_tool', 'search_history_tool'],
|
|
13
|
+
['schedule_wake', 'schedule'],
|
|
14
|
+
['http_request', 'http'],
|
|
15
|
+
['memory', 'memory_tool'],
|
|
16
|
+
['sandbox', 'sandbox_exec', 'sandbox_list_runtimes'],
|
|
17
|
+
['wallet', 'wallet_tool'],
|
|
18
|
+
['monitor', 'monitor_tool'],
|
|
19
|
+
['sample_ui', 'show_plugin_card'],
|
|
20
|
+
['context_mgmt', 'context_status', 'context_summarize'],
|
|
21
|
+
['openclaw_workspace'],
|
|
22
|
+
['openclaw_nodes'],
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const TOOL_ALIAS_MAP = (() => {
|
|
26
|
+
const map = new Map<string, Set<string>>()
|
|
27
|
+
for (const group of TOOL_ALIAS_GROUPS) {
|
|
28
|
+
const normalized = group.map((tool) => tool.trim().toLowerCase()).filter(Boolean)
|
|
29
|
+
for (const tool of normalized) {
|
|
30
|
+
const current = map.get(tool) || new Set<string>()
|
|
31
|
+
for (const alias of normalized) current.add(alias)
|
|
32
|
+
map.set(tool, current)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return map
|
|
36
|
+
})()
|
|
37
|
+
|
|
38
|
+
export function normalizeToolId(value: unknown): string {
|
|
39
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function expandToolIds(values: string[] | null | undefined): string[] {
|
|
43
|
+
if (!Array.isArray(values) || values.length === 0) return []
|
|
44
|
+
const expanded = new Set<string>()
|
|
45
|
+
const queue: string[] = values
|
|
46
|
+
.map((tool) => normalizeToolId(tool))
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
|
|
49
|
+
while (queue.length > 0) {
|
|
50
|
+
const next = queue.shift()!
|
|
51
|
+
if (expanded.has(next)) continue
|
|
52
|
+
expanded.add(next)
|
|
53
|
+
const aliases = TOOL_ALIAS_MAP.get(next)
|
|
54
|
+
if (!aliases) continue
|
|
55
|
+
for (const alias of aliases) {
|
|
56
|
+
if (!expanded.has(alias)) queue.push(alias)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Array.from(expanded)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function toolIdMatches(enabledTools: string[] | null | undefined, toolId: string): boolean {
|
|
64
|
+
const normalized = normalizeToolId(toolId)
|
|
65
|
+
if (!normalized) return false
|
|
66
|
+
return expandToolIds(enabledTools).includes(normalized)
|
|
67
|
+
}
|
|
68
|
+
|
|
@@ -32,9 +32,9 @@ interface ToolDescriptor {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
|
|
35
|
-
shell: { categories: ['execution'], concreteTools: ['execute_command'] },
|
|
36
|
-
process: { categories: ['execution'], concreteTools: ['process_tool'] },
|
|
37
|
-
files: { categories: ['filesystem'], concreteTools: ['read_file', 'write_file', 'list_files', 'send_file'] },
|
|
35
|
+
shell: { categories: ['execution'], concreteTools: ['shell', 'execute_command'] },
|
|
36
|
+
process: { categories: ['execution'], concreteTools: ['process', 'process_tool'] },
|
|
37
|
+
files: { categories: ['filesystem'], concreteTools: ['files', 'read_file', 'write_file', 'list_files', 'send_file'] },
|
|
38
38
|
read_file: { categories: ['filesystem'], concreteTools: ['read_file'] },
|
|
39
39
|
write_file: { categories: ['filesystem'], concreteTools: ['write_file'] },
|
|
40
40
|
list_files: { categories: ['filesystem'], concreteTools: ['list_files'] },
|
|
@@ -43,22 +43,41 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
|
|
|
43
43
|
move_file: { categories: ['filesystem'], concreteTools: ['move_file'] },
|
|
44
44
|
edit_file: { categories: ['filesystem'], concreteTools: ['edit_file'] },
|
|
45
45
|
delete_file: { categories: ['filesystem'], concreteTools: ['delete_file'], destructive: true },
|
|
46
|
+
web: { categories: ['network'], concreteTools: ['web', 'web_search', 'web_fetch'] },
|
|
46
47
|
web_search: { categories: ['network'], concreteTools: ['web_search'] },
|
|
47
48
|
web_fetch: { categories: ['network'], concreteTools: ['web_fetch'] },
|
|
48
|
-
browser: { categories: ['browser', 'network'], concreteTools: ['browser'] },
|
|
49
|
+
browser: { categories: ['browser', 'network'], concreteTools: ['browser', 'openclaw_browser'] },
|
|
50
|
+
delegate: { categories: ['delegation', 'execution'], concreteTools: ['delegate', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli'] },
|
|
49
51
|
claude_code: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_claude_code'] },
|
|
50
52
|
codex_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_codex_cli'] },
|
|
51
53
|
opencode_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_opencode_cli'] },
|
|
52
|
-
memory: { categories: ['memory'], concreteTools: ['memory_tool', 'context_status', 'context_summarize'] },
|
|
54
|
+
memory: { categories: ['memory'], concreteTools: ['memory', 'memory_tool', 'context_status', 'context_summarize'] },
|
|
55
|
+
sandbox: { categories: ['execution', 'filesystem'], concreteTools: ['sandbox', 'sandbox_exec', 'sandbox_list_runtimes', 'openclaw_sandbox'] },
|
|
56
|
+
git: { categories: ['execution', 'filesystem'], concreteTools: ['git'] },
|
|
57
|
+
http_request: { categories: ['network'], concreteTools: ['http_request'] },
|
|
58
|
+
canvas: { categories: ['filesystem'], concreteTools: ['canvas'] },
|
|
59
|
+
wallet: { categories: ['outbound'], concreteTools: ['wallet', 'wallet_tool'] },
|
|
60
|
+
monitor: { categories: ['execution'], concreteTools: ['monitor', 'monitor_tool'] },
|
|
61
|
+
openclaw_workspace: { categories: ['filesystem', 'platform'], concreteTools: ['openclaw_workspace'] },
|
|
62
|
+
openclaw_nodes: { categories: ['platform'], concreteTools: ['openclaw_nodes'] },
|
|
63
|
+
manage_platform: { categories: ['platform'], concreteTools: ['manage_platform', 'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_connectors', 'manage_sessions', 'manage_secrets'] },
|
|
53
64
|
manage_agents: { categories: ['platform'], concreteTools: ['manage_agents'] },
|
|
54
65
|
manage_tasks: { categories: ['platform'], concreteTools: ['manage_tasks'] },
|
|
55
66
|
manage_schedules: { categories: ['platform'], concreteTools: ['manage_schedules'] },
|
|
67
|
+
schedule_wake: { categories: ['platform'], concreteTools: ['schedule_wake'] },
|
|
56
68
|
manage_skills: { categories: ['platform'], concreteTools: ['manage_skills'] },
|
|
57
69
|
manage_documents: { categories: ['platform'], concreteTools: ['manage_documents'] },
|
|
58
70
|
manage_webhooks: { categories: ['platform', 'network'], concreteTools: ['manage_webhooks'] },
|
|
71
|
+
connectors: { categories: ['platform', 'outbound'], concreteTools: ['connectors', 'connector_message_tool'] },
|
|
59
72
|
manage_connectors: { categories: ['platform', 'outbound'], concreteTools: ['manage_connectors', 'connector_message_tool'] },
|
|
73
|
+
session_info: { categories: ['platform'], concreteTools: ['session_info', 'sessions_tool', 'search_history_tool', 'whoami_tool'] },
|
|
60
74
|
manage_sessions: { categories: ['platform'], concreteTools: ['manage_sessions', 'sessions_tool', 'search_history_tool', 'whoami_tool'] },
|
|
61
75
|
manage_secrets: { categories: ['platform'], concreteTools: ['manage_secrets'] },
|
|
76
|
+
manage_chatrooms: { categories: ['platform'], concreteTools: ['manage_chatrooms', 'chatroom'] },
|
|
77
|
+
spawn_subagent: { categories: ['delegation', 'platform'], concreteTools: ['spawn_subagent', 'delegate_to_agent'] },
|
|
78
|
+
context_mgmt: { categories: ['memory'], concreteTools: ['context_mgmt', 'context_status', 'context_summarize'] },
|
|
79
|
+
plugin_creator: { categories: ['filesystem', 'execution'], concreteTools: ['plugin_creator', 'plugin_creator_tool'] },
|
|
80
|
+
sample_ui: { categories: ['platform'], concreteTools: ['sample_ui', 'show_plugin_card'] },
|
|
62
81
|
}
|
|
63
82
|
|
|
64
83
|
const CONCRETE_TOOL_TO_SESSION_TOOL = new Map<string, string>()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured retry with exponential backoff for transient tool failures.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface RetryOptions {
|
|
6
|
+
maxAttempts?: number
|
|
7
|
+
backoffMs?: number
|
|
8
|
+
retryable?: RegExp[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DEFAULT_RETRYABLE: RegExp[] = [
|
|
12
|
+
/timeout/i,
|
|
13
|
+
/ECONNRESET/i,
|
|
14
|
+
/ENOTFOUND/i,
|
|
15
|
+
/429/,
|
|
16
|
+
/503/,
|
|
17
|
+
/rate.?limit/i,
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const DEFAULT_MAX_ATTEMPTS = 3
|
|
21
|
+
const DEFAULT_BACKOFF_MS = 2000
|
|
22
|
+
|
|
23
|
+
function isRetryableError(error: string, patterns: RegExp[]): boolean {
|
|
24
|
+
return patterns.some((p) => p.test(error))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sleep(ms: number): Promise<void> {
|
|
28
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Wraps a tool handler function with retry logic for transient failures.
|
|
33
|
+
* The wrapped function must return a string (tool output).
|
|
34
|
+
* Retries only when the returned string matches a retryable pattern
|
|
35
|
+
* (tool handlers typically return error strings rather than throwing).
|
|
36
|
+
*/
|
|
37
|
+
export async function withRetry<TArgs>(
|
|
38
|
+
fn: (args: TArgs) => Promise<string>,
|
|
39
|
+
args: TArgs,
|
|
40
|
+
opts?: RetryOptions,
|
|
41
|
+
): Promise<string> {
|
|
42
|
+
const maxAttempts = opts?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS
|
|
43
|
+
const backoffMs = opts?.backoffMs ?? DEFAULT_BACKOFF_MS
|
|
44
|
+
const retryable = opts?.retryable ?? DEFAULT_RETRYABLE
|
|
45
|
+
|
|
46
|
+
let lastResult = ''
|
|
47
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
48
|
+
lastResult = await fn(args)
|
|
49
|
+
|
|
50
|
+
// Only retry if the result looks like a retryable error
|
|
51
|
+
if (attempt < maxAttempts && isRetryableError(lastResult, retryable)) {
|
|
52
|
+
const delay = backoffMs * Math.pow(2, attempt - 1)
|
|
53
|
+
console.warn(
|
|
54
|
+
`[tool-retry] Attempt ${attempt}/${maxAttempts} matched retryable pattern, retrying in ${delay}ms`,
|
|
55
|
+
)
|
|
56
|
+
await sleep(delay)
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
return lastResult
|
|
60
|
+
}
|
|
61
|
+
return lastResult
|
|
62
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Message } from '@/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Repairs a conversation transcript by ensuring that tool events remain associated
|
|
5
|
+
* with their parent assistant messages during pruning or manipulation.
|
|
6
|
+
*
|
|
7
|
+
* In SwarmClaw, toolEvents are nested within the Message object, so "orphaning"
|
|
8
|
+
* is less of a structural risk than in OpenClaw, but we still need to ensure
|
|
9
|
+
* consistency during context management.
|
|
10
|
+
*/
|
|
11
|
+
export function repairTranscriptConsistency(messages: Message[]): Message[] {
|
|
12
|
+
// SwarmClaw specific: ensure that 'system' messages like [Context Summary]
|
|
13
|
+
// are preserved correctly and that nested toolEvents are valid.
|
|
14
|
+
return messages.map(m => {
|
|
15
|
+
if (m.role === 'assistant' && m.toolEvents) {
|
|
16
|
+
// Filter out empty or malformed tool events that might cause LLM confusion
|
|
17
|
+
const validTools = m.toolEvents.filter(t => t.name && t.input)
|
|
18
|
+
if (validTools.length !== m.toolEvents.length) {
|
|
19
|
+
return { ...m, toolEvents: validTools }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return m
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks for and repairs common transcript issues that cause LLM provider errors.
|
|
28
|
+
* (e.g. consecutive user messages, trailing assistant messages without text).
|
|
29
|
+
*/
|
|
30
|
+
export function finalProviderTranscriptSanityCheck(messages: Message[]): Message[] {
|
|
31
|
+
if (messages.length === 0) return []
|
|
32
|
+
|
|
33
|
+
const out: Message[] = []
|
|
34
|
+
for (let i = 0; i < messages.length; i++) {
|
|
35
|
+
const m = messages[i]
|
|
36
|
+
|
|
37
|
+
// 1. Skip messages marked as suppressed
|
|
38
|
+
if (m.suppressed) continue
|
|
39
|
+
|
|
40
|
+
// 2. Prevent consecutive messages of same role (some providers are strict)
|
|
41
|
+
const prev = out.at(-1)
|
|
42
|
+
if (prev && prev.role === m.role) {
|
|
43
|
+
if (m.role === 'user') {
|
|
44
|
+
// Merge consecutive user messages
|
|
45
|
+
prev.text = `${prev.text}\n\n${m.text}`
|
|
46
|
+
if (m.imagePath) prev.imagePath = m.imagePath
|
|
47
|
+
if (m.imageUrl) prev.imageUrl = m.imageUrl
|
|
48
|
+
continue
|
|
49
|
+
} else {
|
|
50
|
+
// Assistant consecutive? Keep the one with tool events or the longer one
|
|
51
|
+
const mTools = m.toolEvents?.length || 0
|
|
52
|
+
const pTools = prev.toolEvents?.length || 0
|
|
53
|
+
if (mTools > pTools || m.text.length > prev.text.length) {
|
|
54
|
+
out[out.length - 1] = m
|
|
55
|
+
}
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
out.push(m)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 3. Ensure the transcript doesn't end with an empty assistant message
|
|
64
|
+
if (out.length > 0 && out.at(-1)?.role === 'assistant') {
|
|
65
|
+
const last = out.at(-1)!
|
|
66
|
+
if (!last.text.trim() && (!last.toolEvents || last.toolEvents.length === 0)) {
|
|
67
|
+
out.pop()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return out
|
|
72
|
+
}
|
package/src/lib/tasks.ts
CHANGED
|
@@ -4,7 +4,13 @@ import type { BoardTask } from '../types'
|
|
|
4
4
|
export const fetchTasks = (includeArchived = false) =>
|
|
5
5
|
api<Record<string, BoardTask>>('GET', `/tasks${includeArchived ? '?includeArchived=true' : ''}`)
|
|
6
6
|
|
|
7
|
-
export const createTask = (data: {
|
|
7
|
+
export const createTask = (data: {
|
|
8
|
+
title: string
|
|
9
|
+
description: string
|
|
10
|
+
agentId: string
|
|
11
|
+
status?: string
|
|
12
|
+
qualityGate?: BoardTask['qualityGate']
|
|
13
|
+
}) =>
|
|
8
14
|
api<BoardTask>('POST', '/tasks', data)
|
|
9
15
|
|
|
10
16
|
export const updateTask = (id: string, data: Partial<BoardTask>) =>
|