@swarmclawai/swarmclaw 1.5.62 → 1.5.63

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.63 Highlights
403
+
404
+ Chatroom fix from @borislavnnikolov: CLI-backed agents (codex-cli, copilot-cli, gemini-cli, and the rest of the `NON_LANGGRAPH_PROVIDER_IDS` set) now work correctly as chatroom members instead of falling through a LangGraph path they cannot run. With the execution path fixed, the worker-only membership blocks are lifted too, so any non-trashed agent can be added to a room.
405
+
406
+ - **Direct provider runtime for CLI chatroom turns.** `src/app/api/chatrooms/[id]/chat/route.ts` now branches on `NON_LANGGRAPH_PROVIDER_IDS` and calls `provider.handler.streamChat()` directly for CLI-backed agents while keeping the LangGraph `streamAgentChat` path for everything else. Streaming, tool events, and persisted messages all flow through unchanged.
407
+ - **Full member selection.** The create, update, members, session-tool, and UI layers (`src/app/api/chatrooms/route.ts`, `src/app/api/chatrooms/[id]/route.ts`, `src/app/api/chatrooms/[id]/members/route.ts`, `src/lib/server/session-tools/chatroom.ts`, `src/components/chatrooms/chatroom-sheet.tsx`) no longer reject or hide worker-only agents. Any non-trashed agent is eligible.
408
+ - **Regression test.** `src/app/api/chatrooms/[id]/chat/route.test.ts` proves a `codex-cli`-backed chatroom turn bypasses `streamAgentChat`, streams a response through the provider handler, and persists one assistant reply.
409
+
402
410
  ### v1.5.62 Highlights
403
411
 
404
412
  Hardens parallel sub-agent dispatch with a concurrency cap, a quorum join policy, and a cycle check — so a fan-out can't accidentally saturate providers, melt a mission budget, or wedge the runtime on a delegation loop.
@@ -432,16 +440,6 @@ Viral-loop release. Adds public share links for missions, skills, and sessions,
432
440
  - **Share-link-based skill install.** `POST /api/skills/import` already accepts an http(s) URL; pointing it at `https://<your-host>/api/s/<token>/raw` now installs a shared skill from another SwarmClaw instance without auth handshakes. Pairs naturally with existing `swarmclaw skills import` CLI.
433
441
  - **Share-link repository tests.** `share-link-repository.test.ts` covers mint / list / revoke / lookup-by-token round-trip plus expiry handling against a temporary data dir.
434
442
 
435
- ### v1.5.58 Highlights
436
-
437
- This release broadens the built-in evaluation harness so SwarmClaw runs can be benchmarked against named suites, adds two targeted starter kits, exposes live per-session cost data, tightens auto-skill drafting, and ships a zero-setup demo mission template.
438
-
439
- - **Benchmark-style eval suites.** New `SWEBENCH_LITE_SCENARIOS` and `GAIA_L1_SCENARIOS` in `src/lib/server/eval/scenarios-swebench.ts` and `scenarios-gaia.ts` — curated parallels (not the upstream datasets) sized for a single-agent harness run. The shared `EvalScenario` type now carries an optional `suite: 'core' | 'swe-bench-lite' | 'gaia-l1' | 'tool-use' | 'code-action'` tag. `POST /api/eval/suite` accepts `{ suite: "swe-bench-lite" }` to scope a run. New `GET /api/eval/suites` lists every suite with scenario count, max score, and categories. CLI commands: `swarmclaw eval suites`, and `swarmclaw eval suite` still takes a JSON body now including `suite`. Useful for advertising verifiable numbers against a named benchmark instead of a bespoke scoring rubric.
440
- - **Two additional starter kits.** `inbox_triage` (single Triager agent over email + memory + documents) and `data_analyst` (single Analyst agent over shell + files + web + documents) join the existing seven kits in `src/lib/setup-defaults.ts`. Both are surfaced on the intent-driven setup path alongside Personal Assistant, Research Copilot, Builder Studio, and Delegate Team.
441
- - **Live per-session usage API.** New `GET /api/usage/live?sessionId=...` returns a lightweight snapshot — records, tokens in/out, estimated cost, firstAt/lastAt, wallclockMs, turns — so frontends can surface a live cost meter without pulling the full aggregated `/api/usage` payload. Without a `sessionId` the route returns the ten most recently active sessions. Registered in the CLI as `swarmclaw usage live`.
442
- - **Auto-skill drafting is stricter and rate-limited.** `shouldAutoDraftSkillSuggestion` in `chat-turn-finalization.ts` now requires at least 3 tool events in the completed turn (was 1), and a new per-agent daily cap limits automatic drafts to 3 per day per agent to prevent suggestion-inbox spam. Both thresholds are named constants (`AUTO_DRAFT_MIN_TOOL_EVENTS`, `AUTO_DRAFT_DAILY_LIMIT`). Agents with `autoDraftSkillSuggestions = false` are unaffected (auto-drafting remains opt-in per agent).
443
- - **Hello World demo mission template.** New `hello-world-demo` entry in `BUILT_IN_MISSION_TEMPLATES` — a bounded, zero-setup mission that reads three files in the working directory and writes a one-paragraph markdown summary to `hello-world-report.md`. Budgets (USD 0.25, 20k tokens, 30 turns, 15 min) are small enough to run on a local Ollama model without cost. Intended as the first thing a new user watches an agent complete end to end.
444
-
445
443
  Older releases: https://swarmclaw.ai/docs/release-notes
446
444
 
447
445
  - 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.62",
3
+ "version": "1.5.63",
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",
@@ -297,3 +297,114 @@ test('chatroom route forwards tool activity and records one reply per participat
297
297
  assert.deepEqual(output.assistantCounts, { alpha: 1, beta: 1 })
298
298
  assert.deepEqual([...new Set(output.agentOrder)].sort(), ['alpha', 'beta'])
299
299
  })
300
+
301
+ test('chatroom route uses direct provider runtime for CLI providers', () => {
302
+ const output = runWithTempDataDir<{
303
+ errors: string[]
304
+ streamedText: string
305
+ assistantTexts: string[]
306
+ }>(`
307
+ const storageMod = await import('./src/lib/server/storage')
308
+ const providersMod = await import('@/lib/providers')
309
+ const routeMod = await import('./src/app/api/chatrooms/[id]/chat/route')
310
+ const streamMod = await import('@/lib/server/chat-execution/stream-agent-chat')
311
+ const storage = storageMod.default || storageMod
312
+ const providers = providersMod.default || providersMod
313
+ const route = routeMod.default || routeMod
314
+ const stream = streamMod.default || streamMod
315
+
316
+ const originalHandler = providers.PROVIDERS['codex-cli'].handler
317
+
318
+ const now = Date.now()
319
+ storage.saveAgents({
320
+ alpha: {
321
+ id: 'alpha',
322
+ name: 'Alpha',
323
+ provider: 'codex-cli',
324
+ model: 'gpt-5.3-codex',
325
+ extensions: [],
326
+ createdAt: now,
327
+ updatedAt: now,
328
+ },
329
+ })
330
+ storage.saveChatrooms({
331
+ room_1: {
332
+ id: 'room_1',
333
+ name: 'CLI Room',
334
+ agentIds: ['alpha'],
335
+ messages: [],
336
+ createdAt: now,
337
+ updatedAt: now,
338
+ chatMode: 'sequential',
339
+ autoAddress: true,
340
+ },
341
+ })
342
+
343
+ async function readSse(response) {
344
+ const reader = response.body.getReader()
345
+ const decoder = new TextDecoder()
346
+ let buffer = ''
347
+ const events = []
348
+ while (true) {
349
+ const { done, value } = await reader.read()
350
+ if (done) break
351
+ buffer += decoder.decode(value, { stream: true })
352
+ let idx = buffer.indexOf('\\n\\n')
353
+ while (idx !== -1) {
354
+ const chunk = buffer.slice(0, idx)
355
+ buffer = buffer.slice(idx + 2)
356
+ const line = chunk
357
+ .split('\\n')
358
+ .map((entry) => entry.trim())
359
+ .find((entry) => entry.startsWith('data: '))
360
+ if (line) {
361
+ events.push(JSON.parse(line.slice(6)))
362
+ }
363
+ idx = buffer.indexOf('\\n\\n')
364
+ }
365
+ }
366
+ return events
367
+ }
368
+
369
+ stream.setStreamAgentChatForTest(async () => {
370
+ throw new Error('streamAgentChat should not be called for codex-cli chatroom turns')
371
+ })
372
+ providers.PROVIDERS['codex-cli'].handler = {
373
+ streamChat: async (opts) => {
374
+ const reply = 'Codex CLI answered from direct provider runtime.'
375
+ opts.write('data: ' + JSON.stringify({ t: 'd', text: reply }) + '\\n')
376
+ return reply
377
+ },
378
+ }
379
+
380
+ try {
381
+ const response = await route.POST(
382
+ new Request('http://local/api/chatrooms/room_1/chat', {
383
+ method: 'POST',
384
+ headers: { 'content-type': 'application/json' },
385
+ body: JSON.stringify({ senderId: 'user', text: 'Say hello to the room.' }),
386
+ }),
387
+ { params: Promise.resolve({ id: 'room_1' }) },
388
+ )
389
+
390
+ const events = await readSse(response)
391
+ const chatroom = storage.loadChatrooms().room_1
392
+ const assistantTexts = chatroom.messages
393
+ .filter((entry) => entry.role === 'assistant')
394
+ .map((entry) => entry.text)
395
+
396
+ console.log(JSON.stringify({
397
+ errors: events.filter((entry) => entry.t === 'err').map((entry) => entry.text),
398
+ streamedText: events.filter((entry) => entry.t === 'd').map((entry) => entry.text).join(''),
399
+ assistantTexts,
400
+ }))
401
+ } finally {
402
+ providers.PROVIDERS['codex-cli'].handler = originalHandler
403
+ stream.setStreamAgentChatForTest(null)
404
+ }
405
+ `, { prefix: 'swarmclaw-chatroom-route-cli-provider-' })
406
+
407
+ assert.equal(output.errors.some((text) => /streamAgentChat should not be called/i.test(text)), false)
408
+ assert.equal(output.streamedText.includes('Codex CLI answered from direct provider runtime.'), true)
409
+ assert.equal(output.assistantTexts.some((text) => text.includes('Codex CLI answered from direct provider runtime.')), true)
410
+ })
@@ -6,6 +6,7 @@ import { notFound } from '@/lib/server/collection-helpers'
6
6
  import { safeParseBody } from '@/lib/server/safe-parse-body'
7
7
  import { streamAgentChat } from '@/lib/server/chat-execution/stream-agent-chat'
8
8
  import { getProvider } from '@/lib/providers'
9
+ import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
9
10
  import {
10
11
  resolveApiKey,
11
12
  parseMentions,
@@ -231,37 +232,55 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
231
232
 
232
233
  let fullText = ''
233
234
  let agentError = ''
234
- const result = await streamAgentChat({
235
- session: syntheticSession,
236
- message: messageForAgent,
237
- imagePath,
238
- attachedFiles,
239
- apiKey,
240
- systemPrompt: fullSystemPrompt,
241
- write: (raw: string) => {
242
- const lines = raw.split('\n').filter(Boolean)
243
- for (const line of lines) {
244
- if (!line.startsWith('data: ')) continue
245
- try {
246
- const parsed = JSON.parse(line.slice(6).trim())
247
- if (parsed.t === 'd' && parsed.text) {
248
- fullText += parsed.text
249
- writeEvent({ t: 'd', text: parsed.text, agentId: agent.id, agentName: agent.name })
250
- } else if (parsed.t === 'tool_call' || parsed.t === 'tool_result') {
251
- writeEvent({ ...parsed, agentId: agent.id, agentName: agent.name })
252
- } else if (parsed.t === 'err' && parsed.text) {
253
- agentError = parsed.text
254
- writeEvent({ t: 'err', text: parsed.text, agentId: agent.id, agentName: agent.name })
255
- }
256
- } catch {
257
- // skip malformed lines
235
+ const forwardProviderEvents = (raw: string) => {
236
+ const lines = raw.split('\n').filter(Boolean)
237
+ for (const line of lines) {
238
+ if (!line.startsWith('data: ')) continue
239
+ try {
240
+ const parsed = JSON.parse(line.slice(6).trim())
241
+ if (parsed.t === 'd' && parsed.text) {
242
+ fullText += parsed.text
243
+ writeEvent({ t: 'd', text: parsed.text, agentId: agent.id, agentName: agent.name })
244
+ } else if (parsed.t === 'tool_call' || parsed.t === 'tool_result') {
245
+ writeEvent({ ...parsed, agentId: agent.id, agentName: agent.name })
246
+ } else if (parsed.t === 'err' && parsed.text) {
247
+ agentError = parsed.text
248
+ writeEvent({ t: 'err', text: parsed.text, agentId: agent.id, agentName: agent.name })
258
249
  }
250
+ } catch {
251
+ // skip malformed lines
259
252
  }
260
- },
261
- history,
262
- })
253
+ }
254
+ }
263
255
 
264
- const rawResponseText = result.finalResponse || result.fullText || fullText
256
+ let rawResponseText = ''
257
+ if (NON_LANGGRAPH_PROVIDER_IDS.has(syntheticSession.provider)) {
258
+ const provider = getProvider(syntheticSession.provider)
259
+ if (!provider) throw new Error(`Unknown provider: ${syntheticSession.provider}`)
260
+ rawResponseText = await provider.handler.streamChat({
261
+ session: syntheticSession,
262
+ message: messageForAgent,
263
+ imagePath,
264
+ apiKey,
265
+ systemPrompt: fullSystemPrompt,
266
+ write: forwardProviderEvents,
267
+ active: new Map<string, unknown>(),
268
+ loadHistory: () => history,
269
+ })
270
+ if (!rawResponseText) rawResponseText = fullText
271
+ } else {
272
+ const result = await streamAgentChat({
273
+ session: syntheticSession,
274
+ message: messageForAgent,
275
+ imagePath,
276
+ attachedFiles,
277
+ apiKey,
278
+ systemPrompt: fullSystemPrompt,
279
+ write: forwardProviderEvents,
280
+ history,
281
+ })
282
+ rawResponseText = result.finalResponse || result.fullText || fullText
283
+ }
265
284
  const responseText = stripAgentReactionTokens(stripHiddenControlTokens(rawResponseText))
266
285
 
267
286
  // Don't persist empty or error-only messages — they pollute chat history
@@ -4,7 +4,6 @@ import { notify } from '@/lib/server/ws-hub'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
  import { safeParseBody } from '@/lib/server/safe-parse-body'
6
6
  import { genId } from '@/lib/id'
7
- import { isWorkerOnlyAgent, buildWorkerOnlyAgentMessage } from '@/lib/server/agents/agent-availability'
8
7
 
9
8
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
9
  const { id } = await params
@@ -18,13 +17,6 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
18
17
  if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
19
18
 
20
19
  const agents = loadAgents()
21
- if (isWorkerOnlyAgent(agents[agentId])) {
22
- return NextResponse.json(
23
- { error: buildWorkerOnlyAgentMessage(agents[agentId], 'join chatrooms') },
24
- { status: 400 },
25
- )
26
- }
27
-
28
20
  if (!chatroom.agentIds.includes(agentId)) {
29
21
  chatroom.agentIds.push(agentId)
30
22
 
@@ -4,7 +4,6 @@ import { notify } from '@/lib/server/ws-hub'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
  import { safeParseBody } from '@/lib/server/safe-parse-body'
6
6
  import { genId } from '@/lib/id'
7
- import { isWorkerOnlyAgent } from '@/lib/server/agents/agent-availability'
8
7
  import {
9
8
  ensureChatroomRoutingGuidance,
10
9
  synthesizeRoutingGuidanceFromRules,
@@ -78,16 +77,6 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
78
77
  { status: 400 },
79
78
  )
80
79
  }
81
- const cliAgentNames = agentIds
82
- .filter((agentId) => isWorkerOnlyAgent(agents[agentId]))
83
- .map((agentId) => agents[agentId]?.name || agentId)
84
- if (cliAgentNames.length > 0) {
85
- return NextResponse.json(
86
- { error: `CLI-based agents cannot join chatrooms: ${cliAgentNames.join(', ')}. They can only be used for direct chats and delegation.` },
87
- { status: 400 },
88
- )
89
- }
90
-
91
80
  const oldIds = new Set(chatroom.agentIds)
92
81
  const newIds = new Set(agentIds)
93
82
  const added = agentIds.filter((aid: string) => !oldIds.has(aid))
@@ -6,7 +6,6 @@ import { ChatroomCreateSchema, formatZodError } from '@/lib/validation/schemas'
6
6
  import { safeParseBody } from '@/lib/server/safe-parse-body'
7
7
  import { z } from 'zod'
8
8
  import type { Chatroom, ChatroomMessage } from '@/types'
9
- import { isWorkerOnlyAgent } from '@/lib/server/agents/agent-availability'
10
9
  import {
11
10
  ensureChatroomRoutingGuidance,
12
11
  synthesizeRoutingGuidanceFromRules,
@@ -61,15 +60,6 @@ export async function POST(req: Request) {
61
60
  { status: 400 },
62
61
  )
63
62
  }
64
- const cliAgentNames = requestedAgentIds
65
- .filter((agentId) => isWorkerOnlyAgent(knownAgents[agentId]))
66
- .map((agentId) => knownAgents[agentId]?.name || agentId)
67
- if (cliAgentNames.length > 0) {
68
- return NextResponse.json(
69
- { error: `CLI-based agents cannot join chatrooms: ${cliAgentNames.join(', ')}. They can only be used for direct chats and delegation.` },
70
- { status: 400 },
71
- )
72
- }
73
63
  const agentIds: string[] = requestedAgentIds
74
64
  const chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
75
65
  const autoAddress = Boolean(body.autoAddress)
@@ -9,7 +9,6 @@ import { toast } from 'sonner'
9
9
  import { AgentAvatar } from '@/components/agents/agent-avatar'
10
10
  import type { Agent } from '@/types'
11
11
  import { CheckIcon } from '@/components/shared/check-icon'
12
- import { WORKER_ONLY_PROVIDER_IDS } from '@/lib/provider-sets'
13
12
 
14
13
  export function ChatroomSheet() {
15
14
  const open = useChatroomStore((s) => s.chatroomSheetOpen)
@@ -105,9 +104,7 @@ export function ChatroomSheet() {
105
104
  )
106
105
  }
107
106
 
108
- const agentList = Object.values(agents).filter(
109
- (a: Agent) => !a.trashedAt && !WORKER_ONLY_PROVIDER_IDS.has(a.provider)
110
- ) as Agent[]
107
+ const agentList = Object.values(agents).filter((a: Agent) => !a.trashedAt) as Agent[]
111
108
 
112
109
  return (
113
110
  <BottomSheet open={open} onClose={() => setChatroomSheetOpen(false)}>
@@ -12,7 +12,6 @@ import { log } from '../logger'
12
12
  import { debug } from '../debug'
13
13
  import { logExecution } from '../execution-log'
14
14
  import { logActivity } from '../storage'
15
- import { WORKER_ONLY_PROVIDER_IDS } from '@/lib/provider-sets'
16
15
 
17
16
  /**
18
17
  * Core Chatroom Execution Logic
@@ -78,12 +77,6 @@ async function executeChatroomAction(args: Record<string, unknown>, context: { a
78
77
  const agents = loadAgents()
79
78
  const requestedAgentIds = agentIds || []
80
79
  const validAgentIds = requestedAgentIds.filter((aid: string) => !!agents[aid])
81
- const cliAgentNames = validAgentIds
82
- .filter((aid: string) => WORKER_ONLY_PROVIDER_IDS.has(agents[aid]?.provider))
83
- .map((aid: string) => agents[aid]?.name || aid)
84
- if (cliAgentNames.length > 0) {
85
- return `Error: CLI-based agents cannot join chatrooms: ${cliAgentNames.join(', ')}. They can only be used for direct chats and delegation.`
86
- }
87
80
 
88
81
  const chatroom: Chatroom = {
89
82
  id,
@@ -208,10 +201,6 @@ async function executeChatroomAction(args: Record<string, unknown>, context: { a
208
201
 
209
202
  if (action === 'add_agent') {
210
203
  if (!agentId) return 'Error: agentId required.'
211
- const agents = loadAgents()
212
- if (WORKER_ONLY_PROVIDER_IDS.has(agents[agentId]?.provider)) {
213
- return `Error: ${agents[agentId]?.name || agentId} is a CLI-based agent and cannot join chatrooms. CLI agents can only be used for direct chats and delegation.`
214
- }
215
204
  if (!chatroom.agentIds.includes(agentId)) {
216
205
  chatroom.agentIds.push(agentId)
217
206
  chatroom.updatedAt = Date.now()