@swarmclawai/swarmclaw 1.9.15 → 1.9.17
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 +17 -0
- package/package.json +2 -2
- package/src/app/api/agents/agents-route.test.ts +43 -0
- package/src/app/api/config-versions/config-versions-route.test.ts +81 -0
- package/src/components/agents/agent-sheet.tsx +136 -0
- package/src/lib/agent-config-history.test.ts +50 -0
- package/src/lib/agent-config-history.ts +48 -0
- package/src/lib/agent-planning-mode.test.ts +32 -0
- package/src/lib/agent-planning-mode.ts +34 -0
- package/src/types/agent.ts +1 -0
package/README.md
CHANGED
|
@@ -149,6 +149,14 @@ clawhub install swarmclaw
|
|
|
149
149
|
|
|
150
150
|
[Browse on ClawHub](https://clawhub.ai/skills/swarmclaw)
|
|
151
151
|
|
|
152
|
+
## v1.9.17 Highlights
|
|
153
|
+
|
|
154
|
+
Agent configuration history is now visible in the agent editor, so operators can review recent saved versions and restore prior settings without leaving the agent workflow.
|
|
155
|
+
|
|
156
|
+
- **Agent sheet history.** Advanced agent settings list recent saved versions with timestamp, actor, and provider/model snapshot.
|
|
157
|
+
- **One-click restore.** Restoring a prior version uses the existing config-version restore API, refreshes agent state, and closes the sheet to avoid stale form data.
|
|
158
|
+
- **Regression coverage.** New tests cover config-version list/restore routes and UI summary formatting.
|
|
159
|
+
|
|
152
160
|
## Hosted Deploys
|
|
153
161
|
|
|
154
162
|
SwarmClaw now ships provider-ready deploy files at the repo root:
|
|
@@ -399,6 +407,15 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
407
|
|
|
400
408
|
## Releases
|
|
401
409
|
|
|
410
|
+
### v1.9.16 Highlights
|
|
411
|
+
|
|
412
|
+
Agent planning controls release: strict planning is now a first-class agent setting instead of a hidden persisted field, so operators can decide which agents must expose machine-readable plans before multi-step work.
|
|
413
|
+
|
|
414
|
+
- **Agent editor control.** Advanced agent settings now include a Standard / Strict planning selector with inline behavior guidance.
|
|
415
|
+
- **Runtime prompt wiring.** Strict planning continues to inject the existing `[MAIN_LOOP_PLAN]` contract before multi-step tool work, and the test suite now keeps that prompt section in the runtime gate.
|
|
416
|
+
- **Portable agent packs.** Agent exports preserve `planningMode`, so planning discipline follows agents across installs.
|
|
417
|
+
- **API coverage.** Agent create and update route tests verify that strict planning persists without clobbering unrelated settings.
|
|
418
|
+
|
|
402
419
|
### v1.9.15 Highlights
|
|
403
420
|
|
|
404
421
|
Run handoff release: SwarmClaw now turns completed, failed, queued, or running execution records into copyable handoff packets with outcome, evidence, artifacts, timeline, usage, resume commands, and recommended next actions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.17",
|
|
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",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
89
89
|
"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",
|
|
90
90
|
"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",
|
|
91
|
-
"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/agents/delegation-advisory.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/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.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/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.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/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.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/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.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/tasks/task-workspace-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-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
91
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.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/agents/delegation-advisory.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/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.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/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.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/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.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/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.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/tasks/task-workspace-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-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
92
92
|
"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",
|
|
93
93
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
94
94
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -101,6 +101,27 @@ test('POST /api/agents accepts a valid provider and creates the agent', async ()
|
|
|
101
101
|
saveAgents(agents)
|
|
102
102
|
})
|
|
103
103
|
|
|
104
|
+
test('POST /api/agents persists strict planning mode for created agents', async () => {
|
|
105
|
+
const response = await createAgent(new Request('http://local/api/agents', {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { 'content-type': 'application/json' },
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
name: 'Planning Agent',
|
|
110
|
+
provider: 'ollama',
|
|
111
|
+
model: 'qwen3.5',
|
|
112
|
+
planningMode: 'strict',
|
|
113
|
+
}),
|
|
114
|
+
}))
|
|
115
|
+
|
|
116
|
+
assert.equal(response.status, 200)
|
|
117
|
+
const body = await response.json()
|
|
118
|
+
assert.equal(body.planningMode, 'strict')
|
|
119
|
+
|
|
120
|
+
const agents = loadAgents()
|
|
121
|
+
delete agents[body.id]
|
|
122
|
+
saveAgents(agents)
|
|
123
|
+
})
|
|
124
|
+
|
|
104
125
|
test('POST /api/agents rejects missing required fields with a 400', async () => {
|
|
105
126
|
const response = await createAgent(new Request('http://local/api/agents', {
|
|
106
127
|
method: 'POST',
|
|
@@ -163,6 +184,28 @@ test('PUT /api/agents/:id does not clobber untouched fields with schema defaults
|
|
|
163
184
|
assert.equal(body.proactiveMemory, false)
|
|
164
185
|
})
|
|
165
186
|
|
|
187
|
+
test('PUT /api/agents/:id updates planning mode without clobbering other fields', async () => {
|
|
188
|
+
seedAgent('agent-planning-mode', {
|
|
189
|
+
name: 'Planner',
|
|
190
|
+
tools: ['memory'],
|
|
191
|
+
planningMode: 'off',
|
|
192
|
+
proactiveMemory: false,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const response = await putAgent(new Request('http://local/api/agents/agent-planning-mode', {
|
|
196
|
+
method: 'PUT',
|
|
197
|
+
headers: { 'content-type': 'application/json' },
|
|
198
|
+
body: JSON.stringify({ planningMode: 'strict' }),
|
|
199
|
+
}), routeParams('agent-planning-mode'))
|
|
200
|
+
|
|
201
|
+
assert.equal(response.status, 200)
|
|
202
|
+
const body = await response.json()
|
|
203
|
+
assert.equal(body.planningMode, 'strict')
|
|
204
|
+
assert.equal(body.name, 'Planner')
|
|
205
|
+
assert.deepEqual(body.tools, ['memory'])
|
|
206
|
+
assert.equal(body.proactiveMemory, false)
|
|
207
|
+
})
|
|
208
|
+
|
|
166
209
|
test('PUT /api/agents/:id rejects non-string name', async () => {
|
|
167
210
|
seedAgent('agent-bad-name', { name: 'Good' })
|
|
168
211
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
5
|
+
|
|
6
|
+
test('config version routes list and restore prior agent settings', () => {
|
|
7
|
+
const output = runWithTempDataDir<{
|
|
8
|
+
updateStatus: number
|
|
9
|
+
listStatus: number
|
|
10
|
+
restoreStatus: number
|
|
11
|
+
versionCount: number
|
|
12
|
+
versionName: string | null
|
|
13
|
+
restoredName: string | null
|
|
14
|
+
restoredModel: string | null
|
|
15
|
+
}>(`
|
|
16
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
17
|
+
const agentRouteMod = await import('./src/app/api/agents/[id]/route')
|
|
18
|
+
const configRouteMod = await import('./src/app/api/config-versions/route')
|
|
19
|
+
const restoreRouteMod = await import('./src/app/api/config-versions/restore/route')
|
|
20
|
+
const storage = storageMod.default || storageMod
|
|
21
|
+
const agentRoute = agentRouteMod.default || agentRouteMod
|
|
22
|
+
const configRoute = configRouteMod.default || configRouteMod
|
|
23
|
+
const restoreRoute = restoreRouteMod.default || restoreRouteMod
|
|
24
|
+
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
storage.saveAgents({
|
|
27
|
+
agent_history_1: {
|
|
28
|
+
id: 'agent_history_1',
|
|
29
|
+
name: 'Before Save',
|
|
30
|
+
description: 'Original',
|
|
31
|
+
systemPrompt: '',
|
|
32
|
+
provider: 'ollama',
|
|
33
|
+
model: 'qwen3.5',
|
|
34
|
+
credentialId: null,
|
|
35
|
+
fallbackCredentialIds: [],
|
|
36
|
+
apiEndpoint: null,
|
|
37
|
+
gatewayProfileId: null,
|
|
38
|
+
extensions: [],
|
|
39
|
+
createdAt: now,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const params = { params: Promise.resolve({ id: 'agent_history_1' }) }
|
|
45
|
+
const updateResponse = await agentRoute.PUT(new Request('http://local/api/agents/agent_history_1', {
|
|
46
|
+
method: 'PUT',
|
|
47
|
+
headers: { 'content-type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({ name: 'After Save', model: 'qwen3.6' }),
|
|
49
|
+
}), params)
|
|
50
|
+
|
|
51
|
+
const listResponse = await configRoute.GET(new Request('http://local/api/config-versions?entityKind=agent&entityId=agent_history_1'))
|
|
52
|
+
const listPayload = await listResponse.json()
|
|
53
|
+
const firstVersion = listPayload.versions?.[0] || null
|
|
54
|
+
|
|
55
|
+
const restoreResponse = await restoreRoute.POST(new Request('http://local/api/config-versions/restore', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'content-type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ versionId: firstVersion?.id }),
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
const restored = storage.loadAgents().agent_history_1
|
|
62
|
+
|
|
63
|
+
console.log(JSON.stringify({
|
|
64
|
+
updateStatus: updateResponse.status,
|
|
65
|
+
listStatus: listResponse.status,
|
|
66
|
+
restoreStatus: restoreResponse.status,
|
|
67
|
+
versionCount: Array.isArray(listPayload.versions) ? listPayload.versions.length : 0,
|
|
68
|
+
versionName: firstVersion?.snapshot?.name || null,
|
|
69
|
+
restoredName: restored?.name || null,
|
|
70
|
+
restoredModel: restored?.model || null,
|
|
71
|
+
}))
|
|
72
|
+
`, { prefix: 'swarmclaw-config-versions-route-' })
|
|
73
|
+
|
|
74
|
+
assert.equal(output.updateStatus, 200)
|
|
75
|
+
assert.equal(output.listStatus, 200)
|
|
76
|
+
assert.equal(output.restoreStatus, 200)
|
|
77
|
+
assert.equal(output.versionCount, 1)
|
|
78
|
+
assert.equal(output.versionName, 'Before Save')
|
|
79
|
+
assert.equal(output.restoredName, 'Before Save')
|
|
80
|
+
assert.equal(output.restoredModel, 'qwen3.5')
|
|
81
|
+
})
|
|
@@ -26,10 +26,13 @@ import { StatusDot } from '@/components/ui/status-dot'
|
|
|
26
26
|
import { resolveStoredOllamaMode } from '@/lib/ollama-mode'
|
|
27
27
|
import { errorMessage } from '@/lib/shared-utils'
|
|
28
28
|
import { getDefaultAgentToolIds } from '@/lib/agent-default-tools'
|
|
29
|
+
import { AGENT_PLANNING_MODE_OPTIONS, describeAgentPlanningMode, normalizeAgentPlanningMode, type AgentPlanningMode } from '@/lib/agent-planning-mode'
|
|
30
|
+
import { buildAgentConfigVersionSummary } from '@/lib/agent-config-history'
|
|
29
31
|
import { getEnabledExtensionIds, getEnabledToolIds } from '@/lib/capability-selection'
|
|
30
32
|
import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredentials } from '@/lib/agent-provider-options'
|
|
31
33
|
import { AgentSocialSettings } from '@/features/swarmfeed/agent-social-settings'
|
|
32
34
|
import { AgentMarketplaceSettings } from '@/features/swarmdock/agent-marketplace-settings'
|
|
35
|
+
import type { ConfigVersion } from '@/types/config-version'
|
|
33
36
|
|
|
34
37
|
const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
|
|
35
38
|
const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
|
|
@@ -238,6 +241,7 @@ export function AgentSheet() {
|
|
|
238
241
|
const [memoryTierPreference, setMemoryTierPreference] = useState<'working' | 'durable' | 'archive' | 'blended'>('blended')
|
|
239
242
|
const [proactiveMemory, setProactiveMemory] = useState(true)
|
|
240
243
|
const [autoDraftSkillSuggestions, setAutoDraftSkillSuggestions] = useState(true)
|
|
244
|
+
const [planningMode, setPlanningMode] = useState<AgentPlanningMode>('off')
|
|
241
245
|
const [autoRecovery, setAutoRecovery] = useState(false)
|
|
242
246
|
const [disabled, setDisabled] = useState(false)
|
|
243
247
|
const [filesystemScope, setFilesystemScope] = useState<'workspace' | 'machine'>('workspace')
|
|
@@ -290,6 +294,10 @@ export function AgentSheet() {
|
|
|
290
294
|
const lastAutoSyncedModelsKeyRef = useRef<string | null>(null)
|
|
291
295
|
const skipAutoModelRef = useRef(false)
|
|
292
296
|
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false)
|
|
297
|
+
const [configVersions, setConfigVersions] = useState<ConfigVersion[]>([])
|
|
298
|
+
const [configVersionsLoading, setConfigVersionsLoading] = useState(false)
|
|
299
|
+
const [configVersionsError, setConfigVersionsError] = useState<string | null>(null)
|
|
300
|
+
const [restoringConfigVersionId, setRestoringConfigVersionId] = useState<string | null>(null)
|
|
293
301
|
|
|
294
302
|
const handleFileUpload = (setter: (v: string) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
295
303
|
const file = e.target.files?.[0]
|
|
@@ -359,6 +367,23 @@ export function AgentSheet() {
|
|
|
359
367
|
return { synced: !sameModels, models: result.models }
|
|
360
368
|
}, [agentSelectableProviders, loadProviders, openclawEnabled])
|
|
361
369
|
|
|
370
|
+
const loadAgentConfigVersions = useCallback(async (agentId: string) => {
|
|
371
|
+
setConfigVersionsLoading(true)
|
|
372
|
+
setConfigVersionsError(null)
|
|
373
|
+
try {
|
|
374
|
+
const response = await api<{ versions: ConfigVersion[] }>(
|
|
375
|
+
'GET',
|
|
376
|
+
`/config-versions?entityKind=agent&entityId=${encodeURIComponent(agentId)}`,
|
|
377
|
+
)
|
|
378
|
+
setConfigVersions(Array.isArray(response.versions) ? response.versions : [])
|
|
379
|
+
} catch (err) {
|
|
380
|
+
setConfigVersions([])
|
|
381
|
+
setConfigVersionsError(errorMessage(err))
|
|
382
|
+
} finally {
|
|
383
|
+
setConfigVersionsLoading(false)
|
|
384
|
+
}
|
|
385
|
+
}, [])
|
|
386
|
+
|
|
362
387
|
const providerNeedsKey = !editing && (
|
|
363
388
|
(currentProvider?.requiresApiKey && providerCredentials.length === 0 && !addingKey) ||
|
|
364
389
|
(provider === 'ollama' && ollamaMode === 'cloud' && providerCredentials.length === 0 && !addingKey)
|
|
@@ -444,6 +469,7 @@ export function AgentSheet() {
|
|
|
444
469
|
setMemoryTierPreference(editing.memoryTierPreference || 'blended')
|
|
445
470
|
setProactiveMemory(editing.proactiveMemory !== false)
|
|
446
471
|
setAutoDraftSkillSuggestions(editing.autoDraftSkillSuggestions !== false)
|
|
472
|
+
setPlanningMode(normalizeAgentPlanningMode(editing.planningMode))
|
|
447
473
|
setAutoRecovery(editing.autoRecovery || false)
|
|
448
474
|
setDisabled(editing.disabled === true)
|
|
449
475
|
setFilesystemScope(editing.filesystemScope === 'machine' ? 'machine' : 'workspace')
|
|
@@ -527,6 +553,7 @@ export function AgentSheet() {
|
|
|
527
553
|
setMemoryTierPreference(src.memoryTierPreference || 'blended')
|
|
528
554
|
setProactiveMemory(src.proactiveMemory !== false)
|
|
529
555
|
setAutoDraftSkillSuggestions(src.autoDraftSkillSuggestions !== false)
|
|
556
|
+
setPlanningMode(normalizeAgentPlanningMode(src.planningMode))
|
|
530
557
|
setAutoRecovery(src.autoRecovery || false)
|
|
531
558
|
setDisabled(false)
|
|
532
559
|
setFilesystemScope(src.filesystemScope === 'machine' ? 'machine' : 'workspace')
|
|
@@ -602,6 +629,7 @@ export function AgentSheet() {
|
|
|
602
629
|
setMemoryTierPreference('blended')
|
|
603
630
|
setProactiveMemory(true)
|
|
604
631
|
setAutoDraftSkillSuggestions(true)
|
|
632
|
+
setPlanningMode('off')
|
|
605
633
|
setAutoRecovery(false)
|
|
606
634
|
setDisabled(false)
|
|
607
635
|
setVoiceId('')
|
|
@@ -635,6 +663,17 @@ export function AgentSheet() {
|
|
|
635
663
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
636
664
|
}, [open, editingId])
|
|
637
665
|
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
if (!open || !editingId) {
|
|
668
|
+
setConfigVersions([])
|
|
669
|
+
setConfigVersionsError(null)
|
|
670
|
+
setConfigVersionsLoading(false)
|
|
671
|
+
setRestoringConfigVersionId(null)
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
void loadAgentConfigVersions(editingId)
|
|
675
|
+
}, [editingId, loadAgentConfigVersions, open])
|
|
676
|
+
|
|
638
677
|
useEffect(() => {
|
|
639
678
|
if (skipAutoModelRef.current) {
|
|
640
679
|
skipAutoModelRef.current = false
|
|
@@ -809,6 +848,7 @@ export function AgentSheet() {
|
|
|
809
848
|
memoryTierPreference,
|
|
810
849
|
proactiveMemory,
|
|
811
850
|
autoDraftSkillSuggestions,
|
|
851
|
+
planningMode,
|
|
812
852
|
autoRecovery,
|
|
813
853
|
disabled,
|
|
814
854
|
filesystemScope: filesystemScope === 'machine' ? 'machine' as const : undefined,
|
|
@@ -885,6 +925,33 @@ export function AgentSheet() {
|
|
|
885
925
|
}
|
|
886
926
|
}
|
|
887
927
|
|
|
928
|
+
const handleRestoreConfigVersion = async (versionId: string) => {
|
|
929
|
+
if (!editing) return
|
|
930
|
+
const confirmed = window.confirm('Restore this saved agent configuration? Current settings will become a new history entry when restored.')
|
|
931
|
+
if (!confirmed) return
|
|
932
|
+
setRestoringConfigVersionId(versionId)
|
|
933
|
+
try {
|
|
934
|
+
await api('POST', '/config-versions/restore', { versionId })
|
|
935
|
+
await loadAgents()
|
|
936
|
+
if (
|
|
937
|
+
activeSessionId
|
|
938
|
+
&& currentSession?.agentId === editing.id
|
|
939
|
+
&& (
|
|
940
|
+
currentSession.shortcutForAgentId === editing.id
|
|
941
|
+
|| activeSessionId === editing.threadSessionId
|
|
942
|
+
)
|
|
943
|
+
) {
|
|
944
|
+
await refreshSession(activeSessionId)
|
|
945
|
+
}
|
|
946
|
+
toast.success('Agent configuration restored')
|
|
947
|
+
onClose()
|
|
948
|
+
} catch (err) {
|
|
949
|
+
toast.error(`Restore failed: ${errorMessage(err)}`)
|
|
950
|
+
} finally {
|
|
951
|
+
setRestoringConfigVersionId(null)
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
888
955
|
const handleExport = () => {
|
|
889
956
|
if (!editing) return
|
|
890
957
|
const recommendedProviders = agentSelectableProviders.some((providerOption) => (
|
|
@@ -916,6 +983,7 @@ export function AgentSheet() {
|
|
|
916
983
|
extensions: getEnabledExtensionIds(editing),
|
|
917
984
|
capabilities: editing.capabilities,
|
|
918
985
|
elevenLabsVoiceId: editing.elevenLabsVoiceId || null,
|
|
986
|
+
planningMode: normalizeAgentPlanningMode(editing.planningMode),
|
|
919
987
|
soul: editing.soul,
|
|
920
988
|
systemPrompt: editing.systemPrompt,
|
|
921
989
|
}],
|
|
@@ -1043,6 +1111,7 @@ export function AgentSheet() {
|
|
|
1043
1111
|
if (projectId) badges.push('Project')
|
|
1044
1112
|
if (thinkingLevel) badges.push('Thinking')
|
|
1045
1113
|
if (!autoDraftSkillSuggestions) badges.push('Skill drafting')
|
|
1114
|
+
if (planningMode === 'strict') badges.push('Planning')
|
|
1046
1115
|
return Array.from(new Set(badges))
|
|
1047
1116
|
}, [
|
|
1048
1117
|
autoDraftSkillSuggestions,
|
|
@@ -1061,6 +1130,7 @@ export function AgentSheet() {
|
|
|
1061
1130
|
memoryScopeMode,
|
|
1062
1131
|
memoryTierPreference,
|
|
1063
1132
|
proactiveMemory,
|
|
1133
|
+
planningMode,
|
|
1064
1134
|
projectId,
|
|
1065
1135
|
routingStrategy,
|
|
1066
1136
|
routingTargets.length,
|
|
@@ -1091,6 +1161,7 @@ export function AgentSheet() {
|
|
|
1091
1161
|
}
|
|
1092
1162
|
|
|
1093
1163
|
const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
|
|
1164
|
+
const configVersionSummaries = configVersions.map((version) => buildAgentConfigVersionSummary(version))
|
|
1094
1165
|
|
|
1095
1166
|
return (
|
|
1096
1167
|
<>
|
|
@@ -2145,7 +2216,15 @@ export function AgentSheet() {
|
|
|
2145
2216
|
<option value="durable">Durable memory</option>
|
|
2146
2217
|
<option value="archive">Archive memory</option>
|
|
2147
2218
|
</select>
|
|
2219
|
+
<select value={planningMode} onChange={(e) => setPlanningMode(normalizeAgentPlanningMode(e.target.value))} className={inputClass}>
|
|
2220
|
+
{AGENT_PLANNING_MODE_OPTIONS.map((option) => (
|
|
2221
|
+
<option key={option.value} value={option.value}>{option.label}</option>
|
|
2222
|
+
))}
|
|
2223
|
+
</select>
|
|
2148
2224
|
</div>
|
|
2225
|
+
<p className="mb-4 text-[12px] leading-[1.6] text-text-3/70">
|
|
2226
|
+
{describeAgentPlanningMode(planningMode)}
|
|
2227
|
+
</p>
|
|
2149
2228
|
<div className="space-y-3">
|
|
2150
2229
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
2151
2230
|
<div
|
|
@@ -2770,6 +2849,63 @@ export function AgentSheet() {
|
|
|
2770
2849
|
</div>
|
|
2771
2850
|
</SectionCard>
|
|
2772
2851
|
|
|
2852
|
+
{editing && (
|
|
2853
|
+
<SectionCard
|
|
2854
|
+
title="Configuration History"
|
|
2855
|
+
description="Recent saved versions for this agent."
|
|
2856
|
+
className="mb-6 border-white/[0.05] bg-white/[0.01]"
|
|
2857
|
+
action={(
|
|
2858
|
+
<button
|
|
2859
|
+
type="button"
|
|
2860
|
+
onClick={() => void loadAgentConfigVersions(editing.id)}
|
|
2861
|
+
disabled={configVersionsLoading}
|
|
2862
|
+
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-[12px] font-600 text-text-3 hover:bg-white/[0.04] hover:text-text-2 transition-all disabled:opacity-50"
|
|
2863
|
+
style={{ fontFamily: 'inherit' }}
|
|
2864
|
+
>
|
|
2865
|
+
{configVersionsLoading ? 'Refreshing' : 'Refresh'}
|
|
2866
|
+
</button>
|
|
2867
|
+
)}
|
|
2868
|
+
>
|
|
2869
|
+
{configVersionsError ? (
|
|
2870
|
+
<div className="rounded-[12px] border border-red-500/20 bg-red-500/[0.06] p-3 text-[13px] text-red-300">
|
|
2871
|
+
{configVersionsError}
|
|
2872
|
+
</div>
|
|
2873
|
+
) : configVersionsLoading && configVersionSummaries.length === 0 ? (
|
|
2874
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-white/[0.02] p-3 text-[13px] text-text-3">
|
|
2875
|
+
Loading saved versions...
|
|
2876
|
+
</div>
|
|
2877
|
+
) : configVersionSummaries.length === 0 ? (
|
|
2878
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-white/[0.02] p-3 text-[13px] text-text-3">
|
|
2879
|
+
No saved versions yet.
|
|
2880
|
+
</div>
|
|
2881
|
+
) : (
|
|
2882
|
+
<div className="space-y-3">
|
|
2883
|
+
{configVersionSummaries.slice(0, 8).map((summary) => (
|
|
2884
|
+
<div
|
|
2885
|
+
key={summary.id}
|
|
2886
|
+
className="flex flex-col gap-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] p-3 sm:flex-row sm:items-center sm:justify-between"
|
|
2887
|
+
>
|
|
2888
|
+
<div className="min-w-0">
|
|
2889
|
+
<div className="truncate text-[14px] font-700 text-text">{summary.title}</div>
|
|
2890
|
+
<div className="mt-1 truncate text-[12px] text-text-3">{summary.subtitle}</div>
|
|
2891
|
+
<div className="mt-1 text-[11px] text-text-3/70">{summary.meta}</div>
|
|
2892
|
+
</div>
|
|
2893
|
+
<button
|
|
2894
|
+
type="button"
|
|
2895
|
+
onClick={() => void handleRestoreConfigVersion(summary.id)}
|
|
2896
|
+
disabled={Boolean(restoringConfigVersionId)}
|
|
2897
|
+
className="shrink-0 rounded-[10px] border border-accent-bright/20 bg-accent-soft/30 px-3 py-2 text-[12px] font-700 text-accent-bright transition-all hover:bg-accent-soft disabled:opacity-50"
|
|
2898
|
+
style={{ fontFamily: 'inherit' }}
|
|
2899
|
+
>
|
|
2900
|
+
{restoringConfigVersionId === summary.id ? 'Restoring' : 'Restore'}
|
|
2901
|
+
</button>
|
|
2902
|
+
</div>
|
|
2903
|
+
))}
|
|
2904
|
+
</div>
|
|
2905
|
+
)}
|
|
2906
|
+
</SectionCard>
|
|
2907
|
+
)}
|
|
2908
|
+
|
|
2773
2909
|
<SectionCard
|
|
2774
2910
|
title="Utilities"
|
|
2775
2911
|
description="Import and export agents."
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { buildAgentConfigVersionSummary, formatAgentConfigVersionAge } from './agent-config-history'
|
|
5
|
+
import type { ConfigVersion } from '@/types/config-version'
|
|
6
|
+
|
|
7
|
+
function makeVersion(overrides: Partial<ConfigVersion> = {}): ConfigVersion {
|
|
8
|
+
return {
|
|
9
|
+
id: 'version_1',
|
|
10
|
+
entityKind: 'agent',
|
|
11
|
+
entityId: 'agent_1',
|
|
12
|
+
actor: 'user',
|
|
13
|
+
approvalId: null,
|
|
14
|
+
note: null,
|
|
15
|
+
createdAt: 1_700_000_000_000,
|
|
16
|
+
snapshot: {
|
|
17
|
+
name: 'Release Agent',
|
|
18
|
+
provider: 'openai',
|
|
19
|
+
model: 'gpt-5.2',
|
|
20
|
+
},
|
|
21
|
+
...overrides,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('formatAgentConfigVersionAge formats compact relative ages', () => {
|
|
26
|
+
assert.equal(formatAgentConfigVersionAge(1_700_000_000_000, 1_700_000_030_000), 'just now')
|
|
27
|
+
assert.equal(formatAgentConfigVersionAge(1_700_000_000_000, 1_700_000_300_000), '5m ago')
|
|
28
|
+
assert.equal(formatAgentConfigVersionAge(1_700_000_000_000, 1_700_010_800_000), '3h ago')
|
|
29
|
+
assert.equal(formatAgentConfigVersionAge(1_700_000_000_000, 1_700_259_200_000), '3d ago')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('buildAgentConfigVersionSummary surfaces useful agent snapshot labels', () => {
|
|
33
|
+
const summary = buildAgentConfigVersionSummary(makeVersion(), 1_700_000_300_000)
|
|
34
|
+
|
|
35
|
+
assert.equal(summary.id, 'version_1')
|
|
36
|
+
assert.equal(summary.title, 'Release Agent')
|
|
37
|
+
assert.equal(summary.subtitle, 'openai / gpt-5.2')
|
|
38
|
+
assert.equal(summary.meta, '5m ago by user')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('buildAgentConfigVersionSummary handles sparse snapshots', () => {
|
|
42
|
+
const summary = buildAgentConfigVersionSummary(makeVersion({
|
|
43
|
+
actor: 'system',
|
|
44
|
+
snapshot: {},
|
|
45
|
+
}), 1_700_000_300_000)
|
|
46
|
+
|
|
47
|
+
assert.equal(summary.title, 'Previous agent settings')
|
|
48
|
+
assert.equal(summary.subtitle, 'No provider snapshot')
|
|
49
|
+
assert.equal(summary.meta, '5m ago by system')
|
|
50
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ConfigVersion } from '@/types/config-version'
|
|
2
|
+
|
|
3
|
+
export interface AgentConfigVersionSummary {
|
|
4
|
+
id: string
|
|
5
|
+
title: string
|
|
6
|
+
subtitle: string
|
|
7
|
+
meta: string
|
|
8
|
+
createdAt: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function snapshotString(snapshot: Record<string, unknown>, key: string): string {
|
|
12
|
+
const value = snapshot[key]
|
|
13
|
+
return typeof value === 'string' && value.trim() ? value.trim() : ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatAgentConfigVersionAge(createdAt: number, now = Date.now()): string {
|
|
17
|
+
const ageMs = Math.max(0, now - createdAt)
|
|
18
|
+
const seconds = Math.floor(ageMs / 1000)
|
|
19
|
+
if (seconds < 60) return 'just now'
|
|
20
|
+
const minutes = Math.floor(seconds / 60)
|
|
21
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
22
|
+
const hours = Math.floor(minutes / 60)
|
|
23
|
+
if (hours < 24) return `${hours}h ago`
|
|
24
|
+
const days = Math.floor(hours / 24)
|
|
25
|
+
return `${days}d ago`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildAgentConfigVersionSummary(
|
|
29
|
+
version: ConfigVersion,
|
|
30
|
+
now = Date.now(),
|
|
31
|
+
): AgentConfigVersionSummary {
|
|
32
|
+
const snapshot = version.snapshot || {}
|
|
33
|
+
const name = snapshotString(snapshot, 'name')
|
|
34
|
+
const provider = snapshotString(snapshot, 'provider')
|
|
35
|
+
const model = snapshotString(snapshot, 'model')
|
|
36
|
+
const title = name || 'Previous agent settings'
|
|
37
|
+
const subtitle = provider && model
|
|
38
|
+
? `${provider} / ${model}`
|
|
39
|
+
: provider || model || 'No provider snapshot'
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
id: version.id,
|
|
43
|
+
title,
|
|
44
|
+
subtitle,
|
|
45
|
+
meta: `${formatAgentConfigVersionAge(version.createdAt, now)} by ${version.actor || 'user'}`,
|
|
46
|
+
createdAt: version.createdAt,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AGENT_PLANNING_MODE_OPTIONS,
|
|
6
|
+
describeAgentPlanningMode,
|
|
7
|
+
isAgentPlanningModeEnabled,
|
|
8
|
+
normalizeAgentPlanningMode,
|
|
9
|
+
} from './agent-planning-mode'
|
|
10
|
+
|
|
11
|
+
test('normalizeAgentPlanningMode accepts only supported persisted values', () => {
|
|
12
|
+
assert.equal(normalizeAgentPlanningMode('strict'), 'strict')
|
|
13
|
+
assert.equal(normalizeAgentPlanningMode('off'), 'off')
|
|
14
|
+
assert.equal(normalizeAgentPlanningMode(null), 'off')
|
|
15
|
+
assert.equal(normalizeAgentPlanningMode(undefined), 'off')
|
|
16
|
+
assert.equal(normalizeAgentPlanningMode('unexpected'), 'off')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('planning mode options include a safe default and strict mode', () => {
|
|
20
|
+
assert.deepEqual(
|
|
21
|
+
AGENT_PLANNING_MODE_OPTIONS.map((option) => option.value),
|
|
22
|
+
['off', 'strict'],
|
|
23
|
+
)
|
|
24
|
+
assert.equal(isAgentPlanningModeEnabled('strict'), true)
|
|
25
|
+
assert.equal(isAgentPlanningModeEnabled('off'), false)
|
|
26
|
+
assert.equal(isAgentPlanningModeEnabled(null), false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('describeAgentPlanningMode returns operator-facing copy for each mode', () => {
|
|
30
|
+
assert.match(describeAgentPlanningMode('off'), /No extra plan contract/)
|
|
31
|
+
assert.match(describeAgentPlanningMode('strict'), /machine-readable plan block/)
|
|
32
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Agent } from '@/types'
|
|
2
|
+
|
|
3
|
+
export type AgentPlanningMode = NonNullable<Agent['planningMode']>
|
|
4
|
+
|
|
5
|
+
export const AGENT_PLANNING_MODE_OPTIONS: ReadonlyArray<{
|
|
6
|
+
value: AgentPlanningMode
|
|
7
|
+
label: string
|
|
8
|
+
description: string
|
|
9
|
+
}> = [
|
|
10
|
+
{
|
|
11
|
+
value: 'off',
|
|
12
|
+
label: 'Standard',
|
|
13
|
+
description: 'No extra plan contract. The agent can answer, plan, or act normally based on the task.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
value: 'strict',
|
|
17
|
+
label: 'Strict planning',
|
|
18
|
+
description: 'Require a machine-readable plan block before multi-step tool work so progress can be tracked.',
|
|
19
|
+
},
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export function normalizeAgentPlanningMode(value: unknown): AgentPlanningMode {
|
|
23
|
+
return value === 'strict' ? 'strict' : 'off'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isAgentPlanningModeEnabled(value: Agent['planningMode'] | undefined): boolean {
|
|
27
|
+
return normalizeAgentPlanningMode(value) === 'strict'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function describeAgentPlanningMode(value: Agent['planningMode'] | undefined): string {
|
|
31
|
+
const mode = normalizeAgentPlanningMode(value)
|
|
32
|
+
return AGENT_PLANNING_MODE_OPTIONS.find((option) => option.value === mode)?.description
|
|
33
|
+
|| AGENT_PLANNING_MODE_OPTIONS[0].description
|
|
34
|
+
}
|