@swarmclawai/swarmclaw 1.5.60 → 1.5.61

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 CHANGED
@@ -399,6 +399,14 @@ Operational docs: https://swarmclaw.ai/docs/observability
399
399
 
400
400
  ## Releases
401
401
 
402
+ ### v1.5.61 Highlights
403
+
404
+ Adds an opt-in per-agent planning mode that rides on the existing `[MAIN_LOOP_PLAN]` token machinery.
405
+
406
+ - **`Agent.planningMode: 'off' | 'strict' | null`** — new optional field on the Agent type. Defaults to `null` (off) so existing agents are unaffected. Validated by `AgentCreateSchema` / `AgentUpdateSchema` and surfaced through `createAgent` in `agent-service.ts`.
407
+ - **Strict planning prompt section.** New `buildPlanningModeSection` in `prompt-sections.ts` injects a short contract into the system prompt when `planningMode === 'strict'`: before any multi-step work, emit a single-line `[MAIN_LOOP_PLAN]{"steps":...}` block. The existing parser in `main-agent-loop.ts` reads these blocks into `MainLoopState.planSteps` / `currentPlanStep` / `completedPlanSteps` with no additional wiring. Skipped in minimal prompt mode and for heartbeat turns.
408
+ - **Test coverage.** `prompt-sections.planning-mode.test.ts` covers the null / off / strict / minimal / missing-agent paths (6 cases).
409
+
402
410
  ### v1.5.60 Highlights
403
411
 
404
412
  Adds a turn-snapshot primitive for external replay and comparison tooling, without touching the execution flow.
@@ -439,11 +447,6 @@ This release closes the org-orchestration feature gap with Paperclip while keepi
439
447
  - **Multi-workspace scaffolding.** New `Workspace` registry with `GET|POST|PATCH|DELETE /api/workspaces` and `GET|POST /api/workspaces/active`. The default workspace seeds itself on first read; switching the active workspace persists to `workspace-registry.json`. **Note:** this is metadata only in v1.5.57 — actual data-dir forking per workspace is intentionally deferred (low-risk shipping).
440
448
  - **CLI manifest expanded.** New top-level groups: `workspaces`, `workflow-states`, `config-versions`, `cost-attribution`, `chatroom-policy`. Run `swarmclaw workspaces list`, `swarmclaw cost-attribution by-code --query codes=client-a,range=30d`, `swarmclaw config-versions list --query entityKind=agent,entityId=...`, etc. CLI route-coverage test passes.
441
449
 
442
- ### v1.5.56 Highlights
443
-
444
- - **Fix: TTS error responses are now proper JSON instead of a raw Buffer blob.** `POST /api/tts` and `POST /api/tts/stream` previously returned `500` with the error message wrapped in a `new NextResponse(string, ...)` that the CLI JSON-decoded into `{"type":"Buffer","data":[78,111,...]}`. Both routes now return `NextResponse.json({error}, {status: 500})`. Regression test added.
445
- - **Zod-validated PUT/PATCH endpoints — hardening sweep.** Extends the v1.5.55 work (agents, tasks, webhooks) to close the same silent-corruption bug class on the remaining vulnerable routes: `PUT /api/secrets/:id`, `POST /api/secrets`, `PATCH /api/goals/:id`, `PUT /api/providers/:id`, `PUT /api/documents/:id`, `PUT /api/external-agents/:id`, and `PUT /api/chatrooms/:id`. Each route validates against a dedicated schema (`SecretUpdateSchema`, `SecretCreateSchema`, `GoalUpdateSchema`, `ProviderUpdateSchema`, `DocumentUpdateSchema`, `ExternalAgentUpdateSchema`, `ChatroomUpdateSchema`) in `src/lib/validation/schemas.ts`, then filters parsed data to the keys actually present in the raw body so Zod defaults can't overwrite untouched stored fields. Endpoints already doing per-field `typeof` guards (knowledge, gateways, projects) were left as-is.
446
-
447
450
  Older releases: https://swarmclaw.ai/docs/release-notes
448
451
 
449
452
  - GitHub releases: https://github.com/swarmclawai/swarmclaw/releases
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.5.60",
3
+ "version": "1.5.61",
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",
@@ -178,6 +178,7 @@ export function createAgent(input: {
178
178
  memoryTierPreference: (body.memoryTierPreference as Agent['memoryTierPreference']) || undefined,
179
179
  proactiveMemory: body.proactiveMemory !== false,
180
180
  autoDraftSkillSuggestions: body.autoDraftSkillSuggestions as Agent['autoDraftSkillSuggestions'],
181
+ planningMode: (body.planningMode as Agent['planningMode']) ?? null,
181
182
  projectId: typeof body.projectId === 'string' && body.projectId.trim() ? body.projectId.trim() : undefined,
182
183
  avatarSeed: typeof body.avatarSeed === 'string' ? body.avatarSeed : undefined,
183
184
  avatarUrl: typeof body.avatarUrl === 'string' ? body.avatarUrl : undefined,
@@ -0,0 +1,63 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import { buildPlanningModeSection } from './prompt-sections'
4
+ import type { Agent } from '@/types'
5
+
6
+ function agentWith(partial: Partial<Agent>): Agent {
7
+ return {
8
+ id: 'test',
9
+ name: 'Test',
10
+ provider: 'anthropic',
11
+ model: 'claude-sonnet-4-5',
12
+ credentialId: null,
13
+ apiEndpoint: null,
14
+ soul: null,
15
+ systemPrompt: null,
16
+ description: null,
17
+ tools: [],
18
+ extensions: [],
19
+ heartbeatEnabled: false,
20
+ delegationEnabled: false,
21
+ delegationTargetMode: 'all',
22
+ delegationTargetAgentIds: [],
23
+ skillIds: [],
24
+ createdAt: 0,
25
+ updatedAt: 0,
26
+ ...partial,
27
+ } as unknown as Agent
28
+ }
29
+
30
+ test('buildPlanningModeSection returns null when planningMode is undefined', () => {
31
+ const out = buildPlanningModeSection(agentWith({}), false)
32
+ assert.equal(out, null)
33
+ })
34
+
35
+ test('buildPlanningModeSection returns null when planningMode is "off"', () => {
36
+ const out = buildPlanningModeSection(agentWith({ planningMode: 'off' }), false)
37
+ assert.equal(out, null)
38
+ })
39
+
40
+ test('buildPlanningModeSection returns null when planningMode is null', () => {
41
+ const out = buildPlanningModeSection(agentWith({ planningMode: null }), false)
42
+ assert.equal(out, null)
43
+ })
44
+
45
+ test('buildPlanningModeSection returns null in minimal prompt mode', () => {
46
+ const out = buildPlanningModeSection(agentWith({ planningMode: 'strict' }), true)
47
+ assert.equal(out, null)
48
+ })
49
+
50
+ test('buildPlanningModeSection returns null when agent is missing', () => {
51
+ assert.equal(buildPlanningModeSection(null, false), null)
52
+ assert.equal(buildPlanningModeSection(undefined, false), null)
53
+ })
54
+
55
+ test('buildPlanningModeSection emits plan block guidance when strict', () => {
56
+ const out = buildPlanningModeSection(agentWith({ planningMode: 'strict' }), false)
57
+ assert.ok(out, 'should return a non-empty block')
58
+ assert.match(out!, /## Planning Mode: Strict/)
59
+ assert.match(out!, /\[MAIN_LOOP_PLAN\]/)
60
+ assert.match(out!, /"steps":/)
61
+ assert.match(out!, /current_step/)
62
+ assert.match(out!, /completed_steps/)
63
+ })
@@ -81,6 +81,42 @@ export function buildIdentitySection(
81
81
  return parts
82
82
  }
83
83
 
84
+ // ---------------------------------------------------------------------------
85
+ // Planning Mode — opt-in, per-agent
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /**
89
+ * When `agent.planningMode === 'strict'`, inject a plan-enforcement section
90
+ * that tells the model to emit a [MAIN_LOOP_PLAN]{...} block before tool use
91
+ * on any multi-step turn. The existing main-agent-loop parser in
92
+ * `parseMainLoopPlan()` consumes these tokens and populates planSteps /
93
+ * currentPlanStep / completedPlanSteps in MainLoopState.
94
+ *
95
+ * Returns null when planning mode is off or minimal prompt mode is active.
96
+ */
97
+ export function buildPlanningModeSection(
98
+ agent: Agent | null | undefined,
99
+ isMinimalPrompt: boolean,
100
+ ): string | null {
101
+ if (!agent || isMinimalPrompt) return null
102
+ if (agent.planningMode !== 'strict') return null
103
+ return [
104
+ '## Planning Mode: Strict',
105
+ '',
106
+ 'Before any multi-step work (two or more tool calls or file edits), emit a single machine-readable plan block on its own line:',
107
+ '',
108
+ '```',
109
+ '[MAIN_LOOP_PLAN]{"steps":["step 1","step 2","step 3"],"current_step":"step 1","completed_steps":[]}',
110
+ '```',
111
+ '',
112
+ 'Rules:',
113
+ '- Each step should be a short imperative phrase (≤80 chars).',
114
+ '- Update `current_step` as you advance, and append finished steps to `completed_steps`.',
115
+ '- Skip the block for trivial single-tool responses or pure Q&A.',
116
+ '- The block is parsed by the runtime; do not wrap it in prose, code fences, or extra punctuation.',
117
+ ].join('\n')
118
+ }
119
+
84
120
  // ---------------------------------------------------------------------------
85
121
  // Thinking Level Guidance
86
122
  // ---------------------------------------------------------------------------
@@ -17,6 +17,7 @@ import { loadRuntimeSettings, getAgentLoopRecursionLimit } from '@/lib/server/ru
17
17
  import { truncateToolResultText } from '@/lib/server/chat-execution/tool-result-guard'
18
18
  import {
19
19
  buildIdentitySection,
20
+ buildPlanningModeSection,
20
21
  buildThinkingSection,
21
22
  buildRuntimeOrientationSection,
22
23
  buildWorkspaceSection,
@@ -384,6 +385,8 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
384
385
  }
385
386
 
386
387
  // Composable prompt sections — each builder returns string | null (or string[])
388
+ const planningBlock = buildPlanningModeSection(sessionAgent, isMinimalPrompt)
389
+ if (planningBlock) promptParts.push(planningBlock)
387
390
  const thinkingBlock = buildThinkingSection(agentThinkingLevel, isMinimalPrompt)
388
391
  if (thinkingBlock) promptParts.push(thinkingBlock)
389
392
  const { rootSessionId } = resolveSessionLineageIds(session)
@@ -122,6 +122,7 @@ export const AgentCreateSchema = z.object({
122
122
  memoryTierPreference: z.enum(['working', 'durable', 'archive', 'blended']).nullable().optional().default(null),
123
123
  proactiveMemory: z.boolean().optional().default(true),
124
124
  autoDraftSkillSuggestions: z.boolean().optional().default(true),
125
+ planningMode: z.enum(['off', 'strict']).nullable().optional().default(null),
125
126
  projectId: z.string().optional(),
126
127
  avatarSeed: z.string().optional(),
127
128
  avatarUrl: z.string().nullable().optional().default(null),
@@ -125,6 +125,14 @@ export interface Agent {
125
125
  proactiveMemory?: boolean
126
126
  /** Auto-refresh a reviewed skill draft from meaningful chat turns for this agent. */
127
127
  autoDraftSkillSuggestions?: boolean
128
+ /**
129
+ * Planning enforcement mode.
130
+ * - 'off' (default): no extra planning guidance
131
+ * - 'strict': instruct the model to emit a [MAIN_LOOP_PLAN] block before any
132
+ * tool call on multi-step turns. The existing main-agent-loop plan parser
133
+ * reads these blocks into MainLoopState.planSteps.
134
+ */
135
+ planningMode?: 'off' | 'strict' | null
128
136
  /** Controls whether file operations are confined to the workspace or allowed anywhere on the host. Default: 'workspace'. */
129
137
  filesystemScope?: 'workspace' | 'machine' | null
130
138
  /** Per-agent filesystem restrictions. Globs matched against resolved paths. */