@swarmclawai/swarmclaw 1.1.3 → 1.1.4

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.
Files changed (41) hide show
  1. package/README.md +16 -3
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +23 -0
  4. package/src/app/api/agents/route.ts +14 -0
  5. package/src/app/autonomy/page.tsx +136 -3
  6. package/src/app/chatrooms/page.tsx +38 -16
  7. package/src/components/agents/agent-sheet.tsx +110 -0
  8. package/src/components/auth/setup-wizard/index.tsx +6 -0
  9. package/src/components/auth/setup-wizard/step-agents.tsx +35 -0
  10. package/src/components/auth/setup-wizard/types.ts +2 -0
  11. package/src/components/auth/setup-wizard/utils.ts +2 -0
  12. package/src/components/chatrooms/chatroom-list.tsx +11 -8
  13. package/src/components/tasks/task-column.tsx +1 -0
  14. package/src/components/tasks/task-list.tsx +1 -0
  15. package/src/lib/keyed-queue.test.ts +52 -0
  16. package/src/lib/keyed-queue.ts +33 -0
  17. package/src/lib/orchestrator-config.test.ts +45 -0
  18. package/src/lib/orchestrator-config.ts +72 -0
  19. package/src/lib/providers/error-classification.test.ts +35 -0
  20. package/src/lib/providers/error-classification.ts +70 -0
  21. package/src/lib/providers/index.ts +9 -18
  22. package/src/lib/server/agents/agent-runtime-config.ts +1 -0
  23. package/src/lib/server/agents/subagent-swarm.ts +59 -4
  24. package/src/lib/server/autonomy/supervisor-reflection.ts +54 -2
  25. package/src/lib/server/build-llm.ts +6 -2
  26. package/src/lib/server/chat-execution/situational-awareness.ts +85 -1
  27. package/src/lib/server/connectors/manager.ts +3 -0
  28. package/src/lib/server/provider-endpoint.ts +13 -2
  29. package/src/lib/server/provider-health.ts +30 -2
  30. package/src/lib/server/runtime/daemon-state.ts +13 -2
  31. package/src/lib/server/runtime/heartbeat-service.ts +289 -9
  32. package/src/lib/server/runtime/orchestrator-events.ts +18 -0
  33. package/src/lib/server/runtime/queue.ts +192 -33
  34. package/src/lib/server/runtime/system-events.ts +35 -0
  35. package/src/lib/server/session-tools/chatroom.ts +24 -1
  36. package/src/lib/server/storage.ts +2 -0
  37. package/src/lib/server/tool-retry.ts +2 -2
  38. package/src/lib/shared-utils.ts +10 -0
  39. package/src/lib/validation/schemas.test.ts +20 -0
  40. package/src/lib/validation/schemas.ts +6 -1
  41. package/src/types/index.ts +14 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@
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
- SwarmClaw is a self-hosted AI runtime for OpenClaw and multi-agent work. It helps you run autonomous agents with heartbeats, schedules, delegation, memory, runtime skills, and reviewed conversation-to-skill learning across OpenClaw gateways and other providers.
11
+ SwarmClaw is a self-hosted AI runtime for OpenClaw and multi-agent work. It helps you run autonomous agents and orchestrators with heartbeats, schedules, delegation, memory, runtime skills, and reviewed conversation-to-skill learning across OpenClaw gateways and other providers.
12
12
 
13
13
  GitHub: https://github.com/swarmclawai/swarmclaw
14
14
  Docs: https://swarmclaw.ai/docs
@@ -177,6 +177,19 @@ The building blocks are the same: **agents, tools, memory, delegation, schedules
177
177
 
178
178
  ## Release Notes
179
179
 
180
+ ### v1.1.4 Highlights
181
+
182
+ - **Orchestrator agents return as a first-class autonomy mode**: eligible agents can now run scheduled orchestrator wake cycles with their own mission, governance policy, wake interval, cycle cap, Autonomy-desk controls, and setup/editor support.
183
+ - **Runtime durability is much harder to knock over**: the task queue now supports parallel execution with restart-safe swarm state, orphaned running-task recovery, stuck-task idle timeout detection, and provider-health persistence across daemon restarts.
184
+ - **Recovery and safety paths are tighter**: provider errors are classified for smarter failover, unavailable agents defer work instead of burning it, supervisor blocks can create executable notifications, and agent budget limits now gate task execution before work starts.
185
+ - **Temporary session rooms are easier to inspect**: chatrooms now split persistent rooms from temporary session-style rooms so orchestrator or structured-session conversations can stay visible without polluting the normal room list.
186
+
187
+ ### v1.1.3 Highlights
188
+
189
+ - **Release integrity repair**: `build:ci` no longer trips over the langgraph checkpoint duplicate-column path, which restores clean build validation for the release line.
190
+ - **Storage writes are safer**: credential and agent saves were tightened to upsert-only behavior and bulk-delete safety guards so tests or scripts cannot accidentally wipe live state.
191
+ - **Plugin-to-extension cleanup finished**: remaining rename residue in scripts and tests was cleaned up so packaging and release tooling stay aligned with the current extensions model.
192
+
180
193
  ### v1.1.2 Highlights
181
194
 
182
195
  - **Structured Sessions expanded into richer orchestration**: ProtocolRun-based sessions now support dependency-aware step graphs, reusable step outputs, and a broader advanced execution model on the same durable runtime instead of bringing back a separate orchestrator.
@@ -207,9 +220,9 @@ The building blocks are the same: **agents, tools, memory, delegation, schedules
207
220
 
208
221
  ## What SwarmClaw Focuses On
209
222
 
210
- - **Delegation and background execution**: delegated work, subagents, durable jobs, checkpointing, and background task execution.
223
+ - **Delegation, orchestrators, and background execution**: delegated work, orchestrator agents, subagents, durable jobs, checkpointing, and background task execution.
211
224
  - **Structured Sessions and orchestration**: temporary bounded runs for one agent or many, launched from context and backed by durable templates, branching, loops, parallel joins, transcripts, outputs, operator controls, and chatroom breakout flows.
212
- - **Autonomy and memory**: heartbeats, schedules, long-running execution, durable memory, reflection memory, human-context learning, document recall, and project-aware context.
225
+ - **Autonomy and memory**: heartbeats, orchestrator wake cycles, schedules, long-running execution, durable memory, reflection memory, human-context learning, document recall, and project-aware context.
213
226
  - **OpenClaw integration**: named gateway profiles, external runtimes, deploy helpers, config sync, approval handling, and OpenClaw agent file editing.
214
227
  - **Runtime skills**: pinned skills, OpenClaw-compatible `SKILL.md` import, on-demand skill execution, and configurable keyword or embedding-based recommendation.
215
228
  - **Conversation-to-skill drafts**: draft a reusable skill from a real chat, review it, then approve it into the skill library.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -7,6 +7,7 @@ import { suspendAgentReferences } from '@/lib/server/agents/agent-cascade'
7
7
  import { notify } from '@/lib/server/ws-hub'
8
8
  import { normalizeAgentSandboxConfig } from '@/lib/agent-sandbox-defaults'
9
9
  import { normalizeCapabilitySelection } from '@/lib/capability-selection'
10
+ import { normalizeOrchestratorConfig } from '@/lib/orchestrator-config'
10
11
 
11
12
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
13
  const ops: CollectionOps<any> = { load: () => loadAgents({ includeTrashed: true }), save: saveAgents, topic: 'agents', table: 'agents' }
@@ -50,6 +51,28 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
50
51
  if (body.sandboxConfig !== undefined) {
51
52
  agent.sandboxConfig = normalizeAgentSandboxConfig(body.sandboxConfig)
52
53
  }
54
+ if (
55
+ body.provider !== undefined
56
+ || body.orchestratorEnabled !== undefined
57
+ || body.orchestratorMission !== undefined
58
+ || body.orchestratorWakeInterval !== undefined
59
+ || body.orchestratorGovernance !== undefined
60
+ || body.orchestratorMaxCyclesPerDay !== undefined
61
+ ) {
62
+ const orchestratorConfig = normalizeOrchestratorConfig({
63
+ provider: typeof body.provider === 'string' ? body.provider : agent.provider,
64
+ orchestratorEnabled: body.orchestratorEnabled ?? agent.orchestratorEnabled,
65
+ orchestratorMission: body.orchestratorMission ?? agent.orchestratorMission,
66
+ orchestratorWakeInterval: body.orchestratorWakeInterval ?? agent.orchestratorWakeInterval,
67
+ orchestratorGovernance: body.orchestratorGovernance ?? agent.orchestratorGovernance,
68
+ orchestratorMaxCyclesPerDay: body.orchestratorMaxCyclesPerDay ?? agent.orchestratorMaxCyclesPerDay,
69
+ })
70
+ agent.orchestratorEnabled = orchestratorConfig.orchestratorEnabled
71
+ agent.orchestratorMission = orchestratorConfig.orchestratorMission
72
+ agent.orchestratorWakeInterval = orchestratorConfig.orchestratorWakeInterval
73
+ agent.orchestratorGovernance = orchestratorConfig.orchestratorGovernance
74
+ agent.orchestratorMaxCyclesPerDay = orchestratorConfig.orchestratorMaxCyclesPerDay
75
+ }
53
76
  if (body.preferredGatewayTags !== undefined) {
54
77
  agent.preferredGatewayTags = Array.isArray(body.preferredGatewayTags)
55
78
  ? body.preferredGatewayTags.filter((tag: unknown): tag is string => typeof tag === 'string' && tag.trim().length > 0)
@@ -7,6 +7,7 @@ import { notify } from '@/lib/server/ws-hub'
7
7
  import { getAgentSpendWindows } from '@/lib/server/cost'
8
8
  import { resolveAgentToolSelection } from '@/lib/agent-default-tools'
9
9
  import { normalizeAgentSandboxConfig } from '@/lib/agent-sandbox-defaults'
10
+ import { normalizeOrchestratorConfig } from '@/lib/orchestrator-config'
10
11
  import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
11
12
  import { z } from 'zod'
12
13
  export const dynamic = 'force-dynamic'
@@ -63,6 +64,14 @@ export async function POST(req: Request) {
63
64
  return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
64
65
  }
65
66
  const body = parsed.data
67
+ const orchestratorConfig = normalizeOrchestratorConfig({
68
+ provider: body.provider,
69
+ orchestratorEnabled: body.orchestratorEnabled,
70
+ orchestratorMission: body.orchestratorMission,
71
+ orchestratorWakeInterval: body.orchestratorWakeInterval,
72
+ orchestratorGovernance: body.orchestratorGovernance,
73
+ orchestratorMaxCyclesPerDay: body.orchestratorMaxCyclesPerDay,
74
+ })
66
75
  const capabilitySelection = resolveAgentToolSelection({
67
76
  hasExplicitTools: Boolean(rawRecord && Object.prototype.hasOwnProperty.call(rawRecord, 'tools')),
68
77
  hasExplicitExtensions: Boolean(rawRecord && Object.prototype.hasOwnProperty.call(rawRecord, 'extensions')),
@@ -110,6 +119,11 @@ export async function POST(req: Request) {
110
119
  heartbeatIntervalSec: body.heartbeatIntervalSec,
111
120
  heartbeatModel: body.heartbeatModel,
112
121
  heartbeatPrompt: body.heartbeatPrompt,
122
+ orchestratorEnabled: orchestratorConfig.orchestratorEnabled,
123
+ orchestratorMission: orchestratorConfig.orchestratorMission,
124
+ orchestratorWakeInterval: orchestratorConfig.orchestratorWakeInterval,
125
+ orchestratorGovernance: orchestratorConfig.orchestratorGovernance,
126
+ orchestratorMaxCyclesPerDay: orchestratorConfig.orchestratorMaxCyclesPerDay,
113
127
  elevenLabsVoiceId: body.elevenLabsVoiceId,
114
128
  monthlyBudget: body.monthlyBudget ?? null,
115
129
  dailyBudget: body.dailyBudget ?? null,
@@ -4,8 +4,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
4
4
  import { api } from '@/lib/app/api-client'
5
5
  import { FilterPill } from '@/components/ui/filter-pill'
6
6
  import { StatCard } from '@/components/ui/stat-card'
7
+ import { isOrchestratorEligible } from '@/lib/orchestrator-config'
7
8
  import { timeAgo } from '@/lib/time-format'
8
- import type { ApprovalRequest, EstopState, Mission, SupervisorIncident } from '@/types'
9
+ import { useWs } from '@/hooks/use-ws'
10
+ import type { Agent, ApprovalRequest, EstopState, Mission, SupervisorIncident } from '@/types'
9
11
 
10
12
  type EstopResponse = EstopState & {
11
13
  ok?: boolean
@@ -107,11 +109,12 @@ export default function AutonomyPage() {
107
109
  const [estop, setEstop] = useState<EstopResponse | null>(null)
108
110
  const [incidents, setIncidents] = useState<SupervisorIncident[]>([])
109
111
  const [missions, setMissions] = useState<Mission[]>([])
112
+ const [agents, setAgents] = useState<Agent[]>([])
110
113
  const [loading, setLoading] = useState(true)
111
114
  const [refreshing, setRefreshing] = useState(false)
112
115
  const [error, setError] = useState<string | null>(null)
113
116
  const [actionMessage, setActionMessage] = useState<string | null>(null)
114
- const [pendingAction, setPendingAction] = useState<'autonomy' | 'all' | 'resume' | 'refresh' | 'policy' | 'approve' | 'reject' | null>(null)
117
+ const [pendingAction, setPendingAction] = useState<'autonomy' | 'all' | 'resume' | 'refresh' | 'policy' | 'approve' | 'reject' | 'orchestrator-toggle' | null>(null)
115
118
  const [incidentFilter, setIncidentFilter] = useState<IncidentFilter>('all')
116
119
  const [refreshedAt, setRefreshedAt] = useState<number | null>(null)
117
120
 
@@ -119,14 +122,16 @@ export default function AutonomyPage() {
119
122
  if (mode === 'initial') setLoading(true)
120
123
  else setRefreshing(true)
121
124
  try {
122
- const [estopState, incidentList, missionList] = await Promise.all([
125
+ const [estopState, incidentList, missionList, agentMap] = await Promise.all([
123
126
  api<EstopResponse>('GET', '/autonomy/estop'),
124
127
  api<SupervisorIncident[]>('GET', '/autonomy/incidents?limit=60'),
125
128
  api<Mission[]>('GET', '/missions?status=non_terminal&limit=20'),
129
+ api<Record<string, Agent>>('GET', '/agents'),
126
130
  ])
127
131
  setEstop(estopState)
128
132
  setIncidents(Array.isArray(incidentList) ? incidentList : [])
129
133
  setMissions(Array.isArray(missionList) ? missionList : [])
134
+ setAgents(agentMap ? Object.values(agentMap) : [])
130
135
  setRefreshedAt(Date.now())
131
136
  setError(null)
132
137
  } catch (err) {
@@ -255,6 +260,29 @@ export default function AutonomyPage() {
255
260
  }
256
261
  }, [load])
257
262
 
263
+ const loadAgents = useCallback(async () => {
264
+ try {
265
+ const agentMap = await api<Record<string, Agent>>('GET', '/agents')
266
+ setAgents(agentMap ? Object.values(agentMap) : [])
267
+ } catch { /* swallow — load() will surface errors */ }
268
+ }, [])
269
+ useWs('agents', loadAgents, 30_000)
270
+
271
+ async function toggleOrchestrator(agent: Agent) {
272
+ setPendingAction('orchestrator-toggle')
273
+ try {
274
+ const next = !agent.orchestratorEnabled
275
+ await api('PUT', `/agents/${agent.id}`, { orchestratorEnabled: next })
276
+ setActionMessage(`${agent.name} orchestrator ${next ? 'enabled' : 'disabled'}.`)
277
+ setError(null)
278
+ await loadAgents()
279
+ } catch (err) {
280
+ setError(err instanceof Error ? err.message : 'Unable to toggle orchestrator.')
281
+ } finally {
282
+ setPendingAction(null)
283
+ }
284
+ }
285
+
258
286
  const sortedIncidents = useMemo(
259
287
  () => [...incidents].sort((left, right) => right.createdAt - left.createdAt),
260
288
  [incidents],
@@ -273,6 +301,11 @@ export default function AutonomyPage() {
273
301
  return sortedIncidents
274
302
  }, [incidentFilter, sortedIncidents])
275
303
 
304
+ const orchestrators = useMemo(
305
+ () => agents.filter((a) => a.orchestratorEnabled && !a.trashedAt && isOrchestratorEligible(a)),
306
+ [agents],
307
+ )
308
+
276
309
  const latestIncident = sortedIncidents[0] || null
277
310
  const highSeverityCount = sortedIncidents.filter((incident) => incident.severity === 'high').length
278
311
  const runtimeFailureCount = sortedIncidents.filter((incident) => incident.kind === 'runtime_failure').length
@@ -502,6 +535,106 @@ export default function AutonomyPage() {
502
535
  )}
503
536
  </section>
504
537
 
538
+ <section className="rounded-[20px] border border-white/[0.06] bg-surface p-5">
539
+ <div className="mb-4 flex items-start justify-between gap-4">
540
+ <div>
541
+ <h2 className="font-display text-[18px] font-700 tracking-[-0.02em] text-text">Orchestrators</h2>
542
+ <p className="mt-1 text-[12px] leading-[1.7] text-text-3/72">
543
+ Agents running in orchestrator mode with autonomous wake cycles. Toggle individual orchestrators on or off without leaving the safety desk.
544
+ </p>
545
+ </div>
546
+ <div className="rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-[11px] text-text-3/72">
547
+ {orchestrators.length} active
548
+ </div>
549
+ </div>
550
+
551
+ {orchestrators.length === 0 ? (
552
+ <div className="flex min-h-[120px] items-center justify-center rounded-[16px] border border-dashed border-white/[0.08] bg-white/[0.02] p-6 text-center">
553
+ <div className="max-w-[320px]">
554
+ <h3 className="font-display text-[14px] font-700 tracking-[-0.02em] text-text">No orchestrators configured</h3>
555
+ <p className="mt-2 text-[12px] leading-[1.7] text-text-3/70">
556
+ Enable orchestrator mode on an agent to have it appear here with autonomous wake cycles.
557
+ </p>
558
+ </div>
559
+ </div>
560
+ ) : (
561
+ <div className="grid gap-3 lg:grid-cols-2">
562
+ {orchestrators.map((agent) => {
563
+ const governanceLabel = agent.orchestratorGovernance === 'approval-required'
564
+ ? 'Approval required'
565
+ : agent.orchestratorGovernance === 'notify-only'
566
+ ? 'Notify only'
567
+ : 'Autonomous'
568
+ const governanceTone = agent.orchestratorGovernance === 'approval-required'
569
+ ? 'bg-amber-500/12 text-amber-300'
570
+ : agent.orchestratorGovernance === 'notify-only'
571
+ ? 'bg-sky-500/12 text-sky-300'
572
+ : 'bg-emerald-500/12 text-emerald-300'
573
+ const isDisabled = agent.disabled === true
574
+ return (
575
+ <div
576
+ key={agent.id}
577
+ className={`rounded-[16px] border p-4 ${isDisabled ? 'border-white/[0.04] bg-white/[0.01] opacity-60' : 'border-white/[0.06] bg-white/[0.02]'}`}
578
+ >
579
+ <div className="mb-2 flex items-center justify-between gap-3">
580
+ <div className="flex items-center gap-2">
581
+ <span className={`rounded-full px-2.5 py-1 text-[10px] font-700 uppercase tracking-[0.08em] ${governanceTone}`}>
582
+ {governanceLabel}
583
+ </span>
584
+ <span className={`rounded-full px-2.5 py-1 text-[10px] font-700 uppercase tracking-[0.08em] ${
585
+ isDisabled
586
+ ? 'bg-white/[0.06] text-text-3/55'
587
+ : 'bg-emerald-500/12 text-emerald-300'
588
+ }`}>
589
+ {isDisabled ? 'Disabled' : 'Enabled'}
590
+ </span>
591
+ </div>
592
+ <button
593
+ type="button"
594
+ onClick={() => void toggleOrchestrator(agent)}
595
+ disabled={pendingAction !== null}
596
+ aria-pressed={agent.orchestratorEnabled}
597
+ aria-label={`${agent.orchestratorEnabled ? 'Disable' : 'Enable'} orchestrator for ${agent.name}`}
598
+ className={`flex h-6 w-11 shrink-0 items-center rounded-full border px-[3px] transition-all duration-200 cursor-pointer ${
599
+ agent.orchestratorEnabled
600
+ ? 'border-accent-bright/35 bg-accent shadow-[0_0_0_1px_rgba(89,153,255,0.12)]'
601
+ : 'border-white/[0.10] bg-white/[0.08]'
602
+ } disabled:cursor-default disabled:opacity-45`}
603
+ >
604
+ <span className={`h-4 w-4 rounded-full bg-white shadow-[0_1px_3px_rgba(0,0,0,0.35)] transition-transform duration-200 ${
605
+ agent.orchestratorEnabled ? 'translate-x-[20px]' : 'translate-x-0'
606
+ }`} />
607
+ </button>
608
+ </div>
609
+ <div className="text-[14px] font-600 text-text">{agent.name}</div>
610
+ {agent.description && (
611
+ <div className="mt-1 text-[12px] leading-[1.7] text-text-3/72">{agent.description}</div>
612
+ )}
613
+ {agent.orchestratorMission && (
614
+ <div className="mt-2 text-[12px] leading-[1.7] text-text-2/82">
615
+ {agent.orchestratorMission.length > 120
616
+ ? `${agent.orchestratorMission.slice(0, 119).trimEnd()}…`
617
+ : agent.orchestratorMission}
618
+ </div>
619
+ )}
620
+ <div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-[11px] text-text-3/58">
621
+ {agent.orchestratorLastWakeAt && (
622
+ <span>Last wake {timeAgo(agent.orchestratorLastWakeAt, now)}</span>
623
+ )}
624
+ {typeof agent.orchestratorCycleCount === 'number' && (
625
+ <span>{agent.orchestratorCycleCount} cycle{agent.orchestratorCycleCount === 1 ? '' : 's'}</span>
626
+ )}
627
+ {agent.orchestratorWakeInterval && (
628
+ <span>Every {agent.orchestratorWakeInterval}</span>
629
+ )}
630
+ </div>
631
+ </div>
632
+ )
633
+ })}
634
+ </div>
635
+ )}
636
+ </section>
637
+
505
638
  <div className="grid gap-5 xl:grid-cols-[360px_minmax(0,1fr)]">
506
639
  <section className="rounded-[20px] border border-white/[0.06] bg-surface p-5">
507
640
  <div className="mb-4 flex items-start gap-3">
@@ -1,33 +1,55 @@
1
1
  'use client'
2
2
 
3
+ import { useState } from 'react'
3
4
  import { useChatroomStore } from '@/stores/use-chatroom-store'
4
5
  import { ChatroomList } from '@/components/chatrooms/chatroom-list'
5
6
  import { ChatroomView } from '@/components/chatrooms/chatroom-view'
6
7
  import { MainContent } from '@/components/layout/main-content'
7
8
 
8
9
  export default function ChatroomsPage() {
10
+ const [viewMode, setViewMode] = useState<'chatrooms' | 'sessions'>('chatrooms')
11
+
9
12
  return (
10
13
  <MainContent>
11
14
  <div className="flex-1 flex h-full min-w-0">
12
15
  <div className="w-[280px] shrink-0 border-r border-white/[0.06] flex flex-col">
13
16
  <div className="flex items-center px-4 pt-4 pb-2 shrink-0">
14
- <h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em] flex-1">Chatrooms</h2>
15
- <button
16
- onClick={() => {
17
- useChatroomStore.getState().setEditingChatroomId(null)
18
- useChatroomStore.getState().setChatroomSheetOpen(true)
19
- }}
20
- className="flex items-center gap-1 px-2 py-1 rounded-[6px] text-[11px] font-600 text-accent-bright bg-accent-soft hover:bg-accent-bright/15 transition-all cursor-pointer"
21
- style={{ fontFamily: 'inherit' }}
22
- >
23
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
24
- <line x1="12" y1="5" x2="12" y2="19" />
25
- <line x1="5" y1="12" x2="19" y2="12" />
26
- </svg>
27
- New
28
- </button>
17
+ <h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em] flex-1">
18
+ {viewMode === 'sessions' ? 'Sessions' : 'Chatrooms'}
19
+ </h2>
20
+ {viewMode === 'chatrooms' && (
21
+ <button
22
+ onClick={() => {
23
+ useChatroomStore.getState().setEditingChatroomId(null)
24
+ useChatroomStore.getState().setChatroomSheetOpen(true)
25
+ }}
26
+ className="flex items-center gap-1 px-2 py-1 rounded-[6px] text-[11px] font-600 text-accent-bright bg-accent-soft hover:bg-accent-bright/15 transition-all cursor-pointer"
27
+ style={{ fontFamily: 'inherit' }}
28
+ >
29
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
30
+ <line x1="12" y1="5" x2="12" y2="19" />
31
+ <line x1="5" y1="12" x2="19" y2="12" />
32
+ </svg>
33
+ New
34
+ </button>
35
+ )}
36
+ </div>
37
+ <div className="flex items-center gap-1 px-3 pb-2 shrink-0">
38
+ {(['chatrooms', 'sessions'] as const).map((mode) => (
39
+ <button
40
+ key={mode}
41
+ type="button"
42
+ onClick={() => setViewMode(mode)}
43
+ data-active={viewMode === mode || undefined}
44
+ className="rounded-[8px] border-none px-3 py-1.5 text-[11px] font-600 capitalize cursor-pointer transition-all focus-visible:ring-1 focus-visible:ring-accent-bright/50
45
+ data-[active]:bg-accent-soft data-[active]:text-accent-bright
46
+ bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]"
47
+ >
48
+ {mode}
49
+ </button>
50
+ ))}
29
51
  </div>
30
- <ChatroomList />
52
+ <ChatroomList viewMode={viewMode} />
31
53
  </div>
32
54
  <ChatroomView />
33
55
  </div>
@@ -14,6 +14,7 @@ import type { ProviderType, ClaudeSkill, AgentWallet, AgentPackManifest, AgentRo
14
14
  import { WalletSection } from '@/components/wallets/wallet-section'
15
15
  import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
16
16
  import { NATIVE_CAPABILITY_PROVIDER_IDS, NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
17
+ import { isOrchestratorProviderEligible } from '@/lib/orchestrator-config'
17
18
  import { AgentAvatar } from './agent-avatar'
18
19
  import { AgentPickerList } from '@/components/shared/agent-picker-list'
19
20
  import { randomSoul } from '@/lib/soul-suggestions'
@@ -239,6 +240,11 @@ export function AgentSheet() {
239
240
  const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
240
241
  const [heartbeatModel, setHeartbeatModel] = useState('')
241
242
  const [heartbeatPrompt, setHeartbeatPrompt] = useState('')
243
+ const [orchestratorEnabled, setOrchestratorEnabled] = useState(false)
244
+ const [orchestratorMission, setOrchestratorMission] = useState('')
245
+ const [orchestratorWakeInterval, setOrchestratorWakeInterval] = useState('5m')
246
+ const [orchestratorGovernance, setOrchestratorGovernance] = useState<'autonomous' | 'approval-required' | 'notify-only'>('autonomous')
247
+ const [orchestratorMaxCyclesPerDay, setOrchestratorMaxCyclesPerDay] = useState<string>('')
242
248
  const [sessionResetMode, setSessionResetMode] = useState<'' | 'idle' | 'daily' | 'isolated'>('')
243
249
  const [sessionIdleTimeoutSec, setSessionIdleTimeoutSec] = useState('')
244
250
  const [sessionMaxAgeSec, setSessionMaxAgeSec] = useState('')
@@ -440,6 +446,11 @@ export function AgentSheet() {
440
446
  setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
441
447
  setHeartbeatModel(editing.heartbeatModel || '')
442
448
  setHeartbeatPrompt(editing.heartbeatPrompt || '')
449
+ setOrchestratorEnabled(editing.orchestratorEnabled || false)
450
+ setOrchestratorMission(editing.orchestratorMission || '')
451
+ setOrchestratorWakeInterval(typeof editing.orchestratorWakeInterval === 'string' ? editing.orchestratorWakeInterval : typeof editing.orchestratorWakeInterval === 'number' ? `${editing.orchestratorWakeInterval}s` : '5m')
452
+ setOrchestratorGovernance(editing.orchestratorGovernance || 'autonomous')
453
+ setOrchestratorMaxCyclesPerDay(editing.orchestratorMaxCyclesPerDay != null ? String(editing.orchestratorMaxCyclesPerDay) : '')
443
454
  setSessionResetMode(editing.sessionResetMode || '')
444
455
  setSessionIdleTimeoutSec(editing.sessionIdleTimeoutSec != null ? String(editing.sessionIdleTimeoutSec) : '')
445
456
  setSessionMaxAgeSec(editing.sessionMaxAgeSec != null ? String(editing.sessionMaxAgeSec) : '')
@@ -505,6 +516,11 @@ export function AgentSheet() {
505
516
  setHeartbeatIntervalSec('')
506
517
  setHeartbeatModel('')
507
518
  setHeartbeatPrompt('')
519
+ setOrchestratorEnabled(false)
520
+ setOrchestratorMission('')
521
+ setOrchestratorWakeInterval('5m')
522
+ setOrchestratorGovernance('autonomous')
523
+ setOrchestratorMaxCyclesPerDay('')
508
524
  setSessionResetMode('')
509
525
  setSessionIdleTimeoutSec('')
510
526
  setSessionMaxAgeSec('')
@@ -704,6 +720,11 @@ export function AgentSheet() {
704
720
  heartbeatIntervalSec: heartbeatIntervalSec ? Number(heartbeatIntervalSec) : null,
705
721
  heartbeatModel: heartbeatModel.trim() || null,
706
722
  heartbeatPrompt: heartbeatPrompt.trim() || null,
723
+ orchestratorEnabled,
724
+ orchestratorMission: orchestratorMission.trim() || undefined,
725
+ orchestratorWakeInterval: orchestratorWakeInterval.trim() || null,
726
+ orchestratorGovernance,
727
+ orchestratorMaxCyclesPerDay: orchestratorMaxCyclesPerDay ? Number(orchestratorMaxCyclesPerDay) : null,
707
728
  identityState,
708
729
  sessionResetMode: sessionResetMode || null,
709
730
  sessionIdleTimeoutSec: Number.isFinite(parsedSessionIdleTimeoutSec) && parsedSessionIdleTimeoutSec! >= 0 ? parsedSessionIdleTimeoutSec : null,
@@ -898,6 +919,7 @@ export function AgentSheet() {
898
919
  if (skills.length > 0 || skillIds.length > 0 || mcpServerIds.length > 0 || mcpDisabledTools.length > 0) badges.push('Skills & MCP')
899
920
  if (toolsDifferFromDefault || filesystemScope === 'machine' || delegationEnabled || delegationTargetMode === 'selected' || delegationTargetAgentIds.length > 0) badges.push('Tools')
900
921
  if (budgetEnabled) badges.push('Budget')
922
+ if (orchestratorEnabled) badges.push('Orchestrator')
901
923
  if (disabled) badges.push('Disabled')
902
924
  if (autoRecovery) badges.push('Recovery')
903
925
  if (projectId) badges.push('Project')
@@ -919,6 +941,7 @@ export function AgentSheet() {
919
941
  mcpServerIds.length,
920
942
  memoryScopeMode,
921
943
  memoryTierPreference,
944
+ orchestratorEnabled,
922
945
  proactiveMemory,
923
946
  projectId,
924
947
  routingStrategy,
@@ -1631,6 +1654,93 @@ export function AgentSheet() {
1631
1654
  </div>
1632
1655
  </SectionCard>
1633
1656
 
1657
+ {isOrchestratorProviderEligible(provider) && (
1658
+ <SectionCard
1659
+ title="Orchestrator"
1660
+ description="Turn this agent into a self-directing orchestrator that wakes on a schedule and manages the platform."
1661
+ >
1662
+ <div className="flex items-center justify-between gap-4 rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-4">
1663
+ <div className="min-w-0">
1664
+ <p className="text-[14px] font-600 text-text">Orchestrator Mode</p>
1665
+ <p className="mt-1 text-[12px] leading-[1.6] text-text-3/75">
1666
+ Enable autonomous platform management — wakes on a schedule, reviews state, and delegates work.
1667
+ </p>
1668
+ </div>
1669
+ <button
1670
+ type="button"
1671
+ onClick={() => setOrchestratorEnabled((current) => !current)}
1672
+ className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${orchestratorEnabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
1673
+ aria-pressed={orchestratorEnabled}
1674
+ >
1675
+ <span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${orchestratorEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
1676
+ </button>
1677
+ </div>
1678
+
1679
+ {orchestratorEnabled && (
1680
+ <div className="mt-4 space-y-4">
1681
+ <div>
1682
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1683
+ Mission
1684
+ </label>
1685
+ <textarea
1686
+ value={orchestratorMission}
1687
+ onChange={(e) => setOrchestratorMission(e.target.value)}
1688
+ placeholder="Describe the orchestrator's mission — what should it manage, optimize, or oversee?"
1689
+ rows={3}
1690
+ className={`${inputClass} resize-y min-h-[84px]`}
1691
+ style={{ fontFamily: 'inherit' }}
1692
+ />
1693
+ </div>
1694
+
1695
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
1696
+ <div>
1697
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1698
+ Wake Interval
1699
+ </label>
1700
+ <input
1701
+ type="text"
1702
+ value={orchestratorWakeInterval}
1703
+ onChange={(e) => setOrchestratorWakeInterval(e.target.value)}
1704
+ placeholder="5m"
1705
+ className={inputClass}
1706
+ style={{ fontFamily: 'inherit' }}
1707
+ />
1708
+ </div>
1709
+ <div>
1710
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1711
+ Governance
1712
+ </label>
1713
+ <select
1714
+ value={orchestratorGovernance}
1715
+ onChange={(e) => setOrchestratorGovernance(e.target.value as typeof orchestratorGovernance)}
1716
+ className={inputClass}
1717
+ style={{ fontFamily: 'inherit' }}
1718
+ >
1719
+ <option value="autonomous">Autonomous</option>
1720
+ <option value="approval-required">Approval Required</option>
1721
+ <option value="notify-only">Notify Only</option>
1722
+ </select>
1723
+ </div>
1724
+ <div>
1725
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1726
+ Max Cycles/Day
1727
+ </label>
1728
+ <input
1729
+ type="number"
1730
+ value={orchestratorMaxCyclesPerDay}
1731
+ onChange={(e) => setOrchestratorMaxCyclesPerDay(e.target.value)}
1732
+ placeholder="No limit"
1733
+ min={1}
1734
+ className={inputClass}
1735
+ style={{ fontFamily: 'inherit' }}
1736
+ />
1737
+ </div>
1738
+ </div>
1739
+ </div>
1740
+ )}
1741
+ </SectionCard>
1742
+ )}
1743
+
1634
1744
  <AdvancedSettingsSection
1635
1745
  open={showAdvancedSettings}
1636
1746
  onToggle={() => setShowAdvancedSettings((current) => !current)}
@@ -136,6 +136,8 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
136
136
  delegationTargetMode: 'all',
137
137
  delegationTargetAgentIds: [],
138
138
  autoDraftSkillSuggestions: true,
139
+ orchestratorEnabled: false,
140
+ orchestratorMission: '',
139
141
  avatarSeed: crypto.randomUUID().slice(0, 8),
140
142
  avatarUrl: null,
141
143
  enabled: true,
@@ -195,6 +197,8 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
195
197
  delegationTargetMode: 'all',
196
198
  delegationTargetAgentIds: [],
197
199
  autoDraftSkillSuggestions: true,
200
+ orchestratorEnabled: false,
201
+ orchestratorMission: '',
198
202
  avatarSeed: crypto.randomUUID().slice(0, 8),
199
203
  avatarUrl: null,
200
204
  enabled: true,
@@ -327,6 +331,8 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
327
331
  delegationTargetMode: draft.delegationTargetMode,
328
332
  delegationTargetAgentIds: draft.delegationTargetMode === 'selected' ? draft.delegationTargetAgentIds : [],
329
333
  autoDraftSkillSuggestions: draft.autoDraftSkillSuggestions,
334
+ orchestratorEnabled: draft.orchestratorEnabled,
335
+ orchestratorMission: draft.orchestratorMission.trim() || undefined,
330
336
  avatarSeed: draft.avatarSeed.trim() || undefined,
331
337
  avatarUrl: draft.avatarUrl || null,
332
338
  }
@@ -10,6 +10,7 @@ import type { ProviderModelDiscoveryResult } from '@/types'
10
10
  import { StepShell, SkipLink } from './shared'
11
11
  import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
12
12
  import { AgentAvatar } from '@/components/agents/agent-avatar'
13
+ import { isOrchestratorProviderEligible } from '@/lib/orchestrator-config'
13
14
 
14
15
  /* ── Model combobox: search discovered models or type a custom one ── */
15
16
 
@@ -478,6 +479,40 @@ export function StepAgents({
478
479
  </div>
479
480
  </div>
480
481
  </div>
482
+ {matchedProvider && isOrchestratorProviderEligible(matchedProvider.provider) && (
483
+ <div className="md:col-span-2">
484
+ <div className="flex items-center justify-between rounded-[12px] border border-white/[0.08] bg-bg px-4 py-3">
485
+ <div>
486
+ <div className="text-[12px] font-600 text-text">Enable Orchestrator</div>
487
+ <div className="mt-1 text-[11px] text-text-3">
488
+ Allow this agent to autonomously manage the platform
489
+ </div>
490
+ </div>
491
+ <button
492
+ type="button"
493
+ onClick={() => onUpdateDraft(draft.id, { orchestratorEnabled: !draft.orchestratorEnabled })}
494
+ className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0 ${draft.orchestratorEnabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
495
+ aria-pressed={draft.orchestratorEnabled}
496
+ >
497
+ <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all ${draft.orchestratorEnabled ? 'left-[18px]' : 'left-0.5'}`} />
498
+ </button>
499
+ </div>
500
+ {draft.orchestratorEnabled && (
501
+ <div className="mt-2">
502
+ <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Mission (optional)</label>
503
+ <textarea
504
+ value={draft.orchestratorMission}
505
+ onChange={(e) => onUpdateDraft(draft.id, { orchestratorMission: e.target.value })}
506
+ rows={2}
507
+ placeholder="e.g. Monitor system health and restart failing services"
508
+ className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
509
+ text-text text-[14px] outline-none transition-all duration-200 resize-none
510
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
511
+ />
512
+ </div>
513
+ )}
514
+ </div>
515
+ )}
481
516
  </div>
482
517
 
483
518
  <details className="mt-4 rounded-[12px] border border-white/[0.08] bg-bg px-4 py-3">
@@ -67,6 +67,8 @@ export interface StarterDraftAgent {
67
67
  delegationTargetMode: 'all' | 'selected'
68
68
  delegationTargetAgentIds: string[]
69
69
  autoDraftSkillSuggestions: boolean
70
+ orchestratorEnabled: boolean
71
+ orchestratorMission: string
70
72
  avatarSeed: string
71
73
  avatarUrl: string | null
72
74
  enabled: boolean