@swarmclawai/swarmclaw 1.8.12 → 1.8.13
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 +10 -2
- package/package.json +2 -2
- package/src/lib/server/runtime/queue/core.ts +11 -2
- package/src/lib/server/runtime/queue-retry-policy.test.ts +98 -0
- package/src/lib/server/session-tools/context.ts +2 -0
- package/src/lib/server/session-tools/execute.test.ts +93 -1
- package/src/lib/server/session-tools/execute.ts +4 -3
- package/src/lib/server/session-tools/index.ts +2 -1
- package/src/lib/server/tasks/task-validation.test.ts +33 -0
- package/src/lib/server/tasks/task-validation.ts +2 -1
package/README.md
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
<img src="https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/public/branding/swarmclaw-org-avatar.png" alt="SwarmClaw lobster logo" width="120" />
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
<p align="center"><strong>
|
|
11
|
+
<p align="center"><strong>The self-hosted AI agent runtime and multi-agent framework for autonomous agents.</strong> Open-source agent swarms with durable agent memory, MCP tools, skills, delegation, schedules, and 23+ LLM providers — a practical Claude Code and LangChain alternative.</p>
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<img src="doc/assets/screenshots/org-chart.png" alt="SwarmClaw org chart with delegation and live agent activity" width="900" />
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
-
SwarmClaw is
|
|
17
|
+
SwarmClaw is an open-source, self-hosted AI agent runtime and multi-agent framework. Run autonomous AI agents, agent swarms, and orchestrators with heartbeats, schedules, delegation, agent memory, runtime skills, and reviewed conversation-to-skill learning — across OpenClaw gateways, Claude, GPT, Gemini, OpenRouter, Ollama, and 23+ other providers. Use it as your AI agent dashboard, agent orchestration platform, and home base for self-hosted multi-agent AI workflows.
|
|
18
18
|
|
|
19
19
|
GitHub: https://github.com/swarmclawai/swarmclaw
|
|
20
20
|
Docs: https://swarmclaw.ai/docs
|
|
@@ -399,6 +399,14 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.8.13 Highlights
|
|
403
|
+
|
|
404
|
+
Task retry and host execute hotfix for issues [#68](https://github.com/swarmclawai/swarmclaw/issues/68) and [#69](https://github.com/swarmclawai/swarmclaw/issues/69).
|
|
405
|
+
|
|
406
|
+
- **Per-agent host execute.** Agents configured with `executeConfig.backend = "host"` now pass that setting into the runtime `execute` tool, so `persistent=true` uses the documented host backend.
|
|
407
|
+
- **Scheduled task validation.** Schedule-created tasks no longer get auto-classified as implementation tasks for quality gates unless they explicitly opt into a task quality gate.
|
|
408
|
+
- **Retry loop guard.** A task that fails again with the same retry reason is dead-lettered instead of spending another run on identical work.
|
|
409
|
+
|
|
402
410
|
### v1.8.12 Highlights
|
|
403
411
|
|
|
404
412
|
Gateway Fleet Command release: SwarmClaw now treats OpenClaw gateways as an operator surface instead of a background provider detail.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.13",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
88
88
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
89
89
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
90
|
-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
|
|
90
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/session-tools/execute.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
|
|
91
91
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
92
92
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
93
93
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -124,6 +124,14 @@ function applyTaskPolicyDefaults(task: BoardTask): void {
|
|
|
124
124
|
if (task.deadLetteredAt === undefined) task.deadLetteredAt = null
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function formatRetryError(reason: string): string {
|
|
128
|
+
return `Retry scheduled after failure: ${reason}`.slice(0, 500)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isRepeatedRetryFailure(previousError: unknown, reason: string): boolean {
|
|
132
|
+
return typeof previousError === 'string' && previousError === formatRetryError(reason)
|
|
133
|
+
}
|
|
134
|
+
|
|
127
135
|
export interface TaskResumeState {
|
|
128
136
|
claudeSessionId: string | null
|
|
129
137
|
codexThreadId: string | null
|
|
@@ -971,9 +979,10 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
|
|
|
971
979
|
}
|
|
972
980
|
applyTaskPolicyDefaults(task)
|
|
973
981
|
const now = Date.now()
|
|
982
|
+
const repeatedRetryFailure = isRepeatedRetryFailure(task.error, reason)
|
|
974
983
|
task.attempts = (task.attempts || 0) + 1
|
|
975
984
|
|
|
976
|
-
if ((task.attempts || 0) < (task.maxAttempts || 1)) {
|
|
985
|
+
if ((task.attempts || 0) < (task.maxAttempts || 1) && !repeatedRetryFailure) {
|
|
977
986
|
const delayMs = jitteredBackoff((task.retryBackoffSec || 30) * 1000, Math.max(0, (task.attempts || 1) - 1), 6 * 3600_000)
|
|
978
987
|
task.status = 'queued'
|
|
979
988
|
task.retryScheduledAt = now + delayMs
|
|
@@ -982,7 +991,7 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
|
|
|
982
991
|
// recovery loop burns CPU re-queueing a task that can never run.
|
|
983
992
|
task.checkoutRunId = null
|
|
984
993
|
task.updatedAt = now
|
|
985
|
-
task.error =
|
|
994
|
+
task.error = formatRetryError(reason)
|
|
986
995
|
if (!task.comments) task.comments = []
|
|
987
996
|
task.comments.push({
|
|
988
997
|
id: genId(),
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { test } from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir<T extends Record<string, unknown>>(script: string): T {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-queue-retry-policy-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
SWARMCLAW_BUILD_MODE: '1',
|
|
20
|
+
},
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
timeout: 15000,
|
|
23
|
+
})
|
|
24
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
25
|
+
const lines = (result.stdout || '')
|
|
26
|
+
.trim()
|
|
27
|
+
.split('\n')
|
|
28
|
+
.map((line) => line.trim())
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
31
|
+
return JSON.parse(jsonLine || '{}') as T
|
|
32
|
+
} finally {
|
|
33
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('recoverStalledRunningTasks dead-letters repeated retry failures instead of repeating identical work', () => {
|
|
38
|
+
const output = runWithTempDataDir<{
|
|
39
|
+
result: { recovered: number; deadLettered: number }
|
|
40
|
+
status: string | null
|
|
41
|
+
attempts: number | null
|
|
42
|
+
queued: string[]
|
|
43
|
+
retryScheduledAt: number | null
|
|
44
|
+
deadLetteredAt: number | null
|
|
45
|
+
error: string | null
|
|
46
|
+
}>(`
|
|
47
|
+
const storageMod = await import('@/lib/server/storage')
|
|
48
|
+
const queueMod = await import('@/lib/server/runtime/queue')
|
|
49
|
+
const storage = storageMod.default || storageMod
|
|
50
|
+
const queue = queueMod.default || queueMod
|
|
51
|
+
|
|
52
|
+
const now = Date.now()
|
|
53
|
+
const reason = 'Detected stalled run after 5m without progress'
|
|
54
|
+
storage.saveSettings({
|
|
55
|
+
...storage.loadSettings(),
|
|
56
|
+
taskStallTimeoutMin: 5,
|
|
57
|
+
taskRetryBackoffSec: 30,
|
|
58
|
+
})
|
|
59
|
+
storage.saveTasks({
|
|
60
|
+
repeat: {
|
|
61
|
+
id: 'repeat',
|
|
62
|
+
title: 'Repeated structural failure',
|
|
63
|
+
description: 'A task that already retried the same failure reason',
|
|
64
|
+
status: 'running',
|
|
65
|
+
agentId: 'agent-a',
|
|
66
|
+
startedAt: now - 600_000,
|
|
67
|
+
updatedAt: now - 600_000,
|
|
68
|
+
createdAt: now - 700_000,
|
|
69
|
+
maxAttempts: 3,
|
|
70
|
+
attempts: 1,
|
|
71
|
+
checkoutRunId: 'repeat-run-id',
|
|
72
|
+
error: 'Retry scheduled after failure: ' + reason,
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
storage.saveQueue([])
|
|
76
|
+
|
|
77
|
+
const result = queue.recoverStalledRunningTasks()
|
|
78
|
+
const task = storage.loadTasks().repeat
|
|
79
|
+
console.log(JSON.stringify({
|
|
80
|
+
result,
|
|
81
|
+
status: task?.status ?? null,
|
|
82
|
+
attempts: task?.attempts ?? null,
|
|
83
|
+
queued: storage.loadQueue(),
|
|
84
|
+
retryScheduledAt: task?.retryScheduledAt ?? null,
|
|
85
|
+
deadLetteredAt: task?.deadLetteredAt ?? null,
|
|
86
|
+
error: task?.error ?? null,
|
|
87
|
+
}))
|
|
88
|
+
`)
|
|
89
|
+
|
|
90
|
+
assert.equal(output.result.recovered, 0)
|
|
91
|
+
assert.equal(output.result.deadLettered, 1)
|
|
92
|
+
assert.equal(output.status, 'failed')
|
|
93
|
+
assert.equal(output.attempts, 2)
|
|
94
|
+
assert.deepEqual(output.queued, [])
|
|
95
|
+
assert.equal(output.retryScheduledAt, null)
|
|
96
|
+
assert.equal(typeof output.deadLetteredAt, 'number')
|
|
97
|
+
assert.match(output.error || '', /Dead-lettered after 2\/3 attempts/)
|
|
98
|
+
})
|
|
@@ -94,6 +94,8 @@ export interface ToolBuildContext {
|
|
|
94
94
|
fileAccessPolicy?: { allowedPaths?: string[]; blockedPaths?: string[] } | null
|
|
95
95
|
/** Agent's sandbox config — passed to shell for session-scoped container execution */
|
|
96
96
|
sandboxConfig?: NonNullable<Agent['sandboxConfig']> | null
|
|
97
|
+
/** Loaded agent record for tool builders that need per-agent runtime settings */
|
|
98
|
+
agentRecord?: Agent | null
|
|
97
99
|
/** Agent's filesystem scope — 'machine' allows file access outside the workspace */
|
|
98
100
|
filesystemScope?: 'workspace' | 'machine'
|
|
99
101
|
}
|
|
@@ -1,7 +1,40 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
1
|
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it, test } from 'node:test'
|
|
3
7
|
import { redactSecrets, buildCredentialEnv } from './credential-env'
|
|
4
8
|
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
|
|
10
|
+
|
|
11
|
+
function runWithTempDataDir<T extends Record<string, unknown>>(script: string): T {
|
|
12
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-execute-tool-'))
|
|
13
|
+
try {
|
|
14
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
15
|
+
cwd: repoRoot,
|
|
16
|
+
env: {
|
|
17
|
+
...process.env,
|
|
18
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
19
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
20
|
+
SWARMCLAW_BUILD_MODE: '1',
|
|
21
|
+
},
|
|
22
|
+
encoding: 'utf-8',
|
|
23
|
+
timeout: 15000,
|
|
24
|
+
})
|
|
25
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
26
|
+
const lines = (result.stdout || '')
|
|
27
|
+
.trim()
|
|
28
|
+
.split('\n')
|
|
29
|
+
.map((line) => line.trim())
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
32
|
+
return JSON.parse(jsonLine || '{}') as T
|
|
33
|
+
} finally {
|
|
34
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
5
38
|
describe('credential-env', () => {
|
|
6
39
|
describe('redactSecrets', () => {
|
|
7
40
|
it('redacts secret values from text', () => {
|
|
@@ -56,3 +89,62 @@ describe('credential-env', () => {
|
|
|
56
89
|
})
|
|
57
90
|
})
|
|
58
91
|
})
|
|
92
|
+
|
|
93
|
+
test('buildExecuteTools uses the current agent executeConfig backend', () => {
|
|
94
|
+
const output = runWithTempDataDir<{
|
|
95
|
+
hostOutput: string
|
|
96
|
+
sandboxOutput: string
|
|
97
|
+
}>(`
|
|
98
|
+
const storageMod = await import('@/lib/server/storage')
|
|
99
|
+
const executeModImport = await import('./src/lib/server/session-tools/execute.ts')
|
|
100
|
+
const executeMod = executeModImport.default || executeModImport
|
|
101
|
+
const storage = storageMod.default || storageMod
|
|
102
|
+
const now = Date.now()
|
|
103
|
+
|
|
104
|
+
storage.saveAgents({
|
|
105
|
+
'agent-host': {
|
|
106
|
+
id: 'agent-host',
|
|
107
|
+
name: 'Host Agent',
|
|
108
|
+
provider: 'openai',
|
|
109
|
+
model: 'gpt-test',
|
|
110
|
+
executeConfig: { backend: 'host', network: { enabled: true }, timeout: 30 },
|
|
111
|
+
createdAt: now,
|
|
112
|
+
updatedAt: now,
|
|
113
|
+
},
|
|
114
|
+
'agent-sandbox': {
|
|
115
|
+
id: 'agent-sandbox',
|
|
116
|
+
name: 'Sandbox Agent',
|
|
117
|
+
provider: 'openai',
|
|
118
|
+
model: 'gpt-test',
|
|
119
|
+
createdAt: now,
|
|
120
|
+
updatedAt: now,
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const makeContext = (agentId) => ({
|
|
125
|
+
cwd: process.cwd(),
|
|
126
|
+
ctx: { agentId, sessionId: 'session-' + agentId },
|
|
127
|
+
hasExtension: (name) => name === 'execute',
|
|
128
|
+
hasTool: (name) => name === 'execute',
|
|
129
|
+
cleanupFns: [],
|
|
130
|
+
commandTimeoutMs: 30000,
|
|
131
|
+
claudeTimeoutMs: 30000,
|
|
132
|
+
cliProcessTimeoutMs: 30000,
|
|
133
|
+
persistDelegateResumeId: () => {},
|
|
134
|
+
readStoredDelegateResumeId: () => null,
|
|
135
|
+
resolveCurrentSession: () => ({ id: 'session-' + agentId, agentId }),
|
|
136
|
+
activeExtensions: ['execute'],
|
|
137
|
+
agentRecord: storage.loadAgent(agentId),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const hostTool = executeMod.buildExecuteTools(makeContext('agent-host'))[0]
|
|
141
|
+
const sandboxTool = executeMod.buildExecuteTools(makeContext('agent-sandbox'))[0]
|
|
142
|
+
const hostOutput = await hostTool.invoke({ code: 'printf host-ok', persistent: true })
|
|
143
|
+
const sandboxOutput = await sandboxTool.invoke({ code: 'printf sandbox-no', persistent: true })
|
|
144
|
+
|
|
145
|
+
console.log(JSON.stringify({ hostOutput, sandboxOutput }))
|
|
146
|
+
`)
|
|
147
|
+
|
|
148
|
+
assert.equal(output.hostOutput, 'host-ok')
|
|
149
|
+
assert.match(output.sandboxOutput, /requires `executeConfig\.backend = "host"`/)
|
|
150
|
+
})
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
normalizeAgentExecuteConfig,
|
|
27
27
|
type AgentExecuteConfig,
|
|
28
28
|
} from '@/lib/agent-execute-defaults'
|
|
29
|
+
import { loadAgent } from '../storage'
|
|
29
30
|
|
|
30
31
|
const TAG = 'execute'
|
|
31
32
|
|
|
@@ -310,9 +311,9 @@ registerNativeCapability('execute', ExecuteExtension)
|
|
|
310
311
|
export function buildExecuteTools(bctx: ToolBuildContext) {
|
|
311
312
|
if (!bctx.hasExtension('execute')) return []
|
|
312
313
|
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
314
|
+
const agentId = typeof bctx.ctx?.agentId === 'string' ? bctx.ctx.agentId.trim() : ''
|
|
315
|
+
const agent = (bctx.agentRecord as (Agent & { executeConfig?: ExecuteConfig }) | null | undefined)
|
|
316
|
+
?? (agentId ? (loadAgent(agentId) as (Agent & { executeConfig?: ExecuteConfig }) | null) : null)
|
|
316
317
|
const executeConfig = normalizeAgentExecuteConfig(agent?.executeConfig)
|
|
317
318
|
|
|
318
319
|
return [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
-
import type { Session } from '@/types'
|
|
3
|
+
import type { Agent, Session } from '@/types'
|
|
4
4
|
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
5
5
|
import { loadSettings, loadSession, loadAgent, loadMcpServers, patchAgent, patchSession } from '../storage'
|
|
6
6
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
@@ -221,6 +221,7 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
|
|
|
221
221
|
activeExtensions,
|
|
222
222
|
fileAccessPolicy: effectiveFileAccessPolicy,
|
|
223
223
|
sandboxConfig: agentRecord?.sandboxConfig ?? null,
|
|
224
|
+
agentRecord: agentRecord as Agent | null,
|
|
224
225
|
filesystemScope,
|
|
225
226
|
}
|
|
226
227
|
|
|
@@ -49,6 +49,39 @@ test('validateTaskCompletion still enforces stricter minimum for implementation
|
|
|
49
49
|
assert.ok(validation.reasons.some((reason) => reason.includes('Result summary is too short')))
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
+
test('validateTaskCompletion does not auto-apply implementation quality gates to scheduled tasks', () => {
|
|
53
|
+
const validation = validateTaskCompletion({
|
|
54
|
+
title: '[Sched] Daily wiki hygiene (run #1)',
|
|
55
|
+
description: 'Run scripts/wiki-hygiene.mjs and post the digest to Slack.',
|
|
56
|
+
result: 'Ran node scripts/wiki-hygiene.mjs and posted the digest to Slack with exit code 0.',
|
|
57
|
+
sourceType: 'schedule',
|
|
58
|
+
sourceScheduleId: 'schedule-wiki',
|
|
59
|
+
error: null,
|
|
60
|
+
} as Partial<BoardTask>)
|
|
61
|
+
|
|
62
|
+
assert.equal(validation.ok, true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('validateTaskCompletion still enforces explicit quality gates on scheduled tasks', () => {
|
|
66
|
+
const validation = validateTaskCompletion({
|
|
67
|
+
title: '[Sched] Daily wiki hygiene (run #1)',
|
|
68
|
+
description: 'Run scripts/wiki-hygiene.mjs and post the digest to Slack.',
|
|
69
|
+
result: 'Ran node scripts/wiki-hygiene.mjs and posted the digest to Slack with exit code 0.',
|
|
70
|
+
sourceType: 'schedule',
|
|
71
|
+
sourceScheduleId: 'schedule-wiki',
|
|
72
|
+
qualityGate: {
|
|
73
|
+
enabled: true,
|
|
74
|
+
minResultChars: 20,
|
|
75
|
+
minEvidenceItems: 2,
|
|
76
|
+
requireVerification: true,
|
|
77
|
+
},
|
|
78
|
+
error: null,
|
|
79
|
+
} as Partial<BoardTask>)
|
|
80
|
+
|
|
81
|
+
assert.equal(validation.ok, false)
|
|
82
|
+
assert.ok(validation.reasons.some((reason) => reason.includes('verification evidence is required')))
|
|
83
|
+
})
|
|
84
|
+
|
|
52
85
|
test('validateTaskCompletion fails implementation task with unfinished next-step language', () => {
|
|
53
86
|
const validation = validateTaskCompletion({
|
|
54
87
|
title: 'Build weather dashboard',
|
|
@@ -85,7 +85,8 @@ export function validateTaskCompletion(
|
|
|
85
85
|
const report = options.report || null
|
|
86
86
|
const hasExplicitQualityGate = !!task.qualityGate && typeof task.qualityGate === 'object'
|
|
87
87
|
const qualityGate = normalizeTaskQualityGate(task.qualityGate || null, options.settings || null)
|
|
88
|
-
const
|
|
88
|
+
const scheduleTask = task.sourceType === 'schedule' || typeof task.sourceScheduleId === 'string'
|
|
89
|
+
const implementationTask = !scheduleTask && (IMPLEMENTATION_HINT.test(title) || IMPLEMENTATION_HINT.test(description))
|
|
89
90
|
|
|
90
91
|
if (error) reasons.push('Task has a non-empty error field.')
|
|
91
92
|
if (/^untitled task$/i.test(title) && !description) {
|