@swarmclawai/swarmclaw 0.6.7 → 0.6.8

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 (73) hide show
  1. package/README.md +24 -6
  2. package/package.json +1 -1
  3. package/src/app/api/agents/route.ts +1 -0
  4. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  5. package/src/app/api/eval/run/route.ts +37 -0
  6. package/src/app/api/eval/scenarios/route.ts +24 -0
  7. package/src/app/api/eval/suite/route.ts +29 -0
  8. package/src/app/api/memory/graph/route.ts +46 -0
  9. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  10. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  11. package/src/app/api/souls/[id]/route.ts +65 -0
  12. package/src/app/api/souls/route.ts +70 -0
  13. package/src/app/api/tasks/[id]/route.ts +5 -0
  14. package/src/app/api/tasks/route.ts +2 -0
  15. package/src/app/api/usage/route.ts +9 -2
  16. package/src/cli/index.js +24 -0
  17. package/src/components/agents/agent-sheet.tsx +27 -6
  18. package/src/components/agents/soul-library-picker.tsx +84 -13
  19. package/src/components/chat/activity-moment.tsx +2 -0
  20. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  21. package/src/components/chat/message-list.tsx +19 -3
  22. package/src/components/chat/session-debug-panel.tsx +106 -84
  23. package/src/components/chat/task-approval-card.tsx +78 -0
  24. package/src/components/chat/tool-call-bubble.tsx +3 -0
  25. package/src/components/connectors/connector-sheet.tsx +8 -1
  26. package/src/components/home/home-view.tsx +39 -15
  27. package/src/components/layout/app-layout.tsx +18 -2
  28. package/src/components/memory/memory-browser.tsx +73 -45
  29. package/src/components/memory/memory-graph-view.tsx +203 -0
  30. package/src/components/plugins/plugin-list.tsx +1 -1
  31. package/src/components/schedules/schedule-sheet.tsx +9 -2
  32. package/src/components/shared/hint-tip.tsx +31 -0
  33. package/src/components/shared/settings/section-runtime-loop.tsx +5 -4
  34. package/src/components/tasks/approvals-panel.tsx +120 -0
  35. package/src/components/usage/metrics-dashboard.tsx +25 -3
  36. package/src/lib/server/chat-execution.ts +96 -12
  37. package/src/lib/server/chatroom-helpers.ts +63 -5
  38. package/src/lib/server/chatroom-orchestration.ts +74 -0
  39. package/src/lib/server/context-manager.ts +132 -50
  40. package/src/lib/server/daemon-state.ts +70 -1
  41. package/src/lib/server/eval/runner.ts +126 -0
  42. package/src/lib/server/eval/scenarios.ts +218 -0
  43. package/src/lib/server/eval/scorer.ts +96 -0
  44. package/src/lib/server/eval/store.ts +37 -0
  45. package/src/lib/server/eval/types.ts +48 -0
  46. package/src/lib/server/execution-log.ts +12 -8
  47. package/src/lib/server/guardian.ts +34 -0
  48. package/src/lib/server/heartbeat-service.ts +53 -1
  49. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  50. package/src/lib/server/link-understanding.ts +55 -0
  51. package/src/lib/server/main-agent-loop.ts +114 -15
  52. package/src/lib/server/memory-db.ts +18 -7
  53. package/src/lib/server/mmr.ts +73 -0
  54. package/src/lib/server/orchestrator-lg.ts +3 -0
  55. package/src/lib/server/plugins.ts +44 -22
  56. package/src/lib/server/query-expansion.ts +57 -0
  57. package/src/lib/server/queue.ts +27 -0
  58. package/src/lib/server/session-run-manager.ts +21 -1
  59. package/src/lib/server/session-tools/http.ts +19 -9
  60. package/src/lib/server/session-tools/index.ts +34 -0
  61. package/src/lib/server/session-tools/memory.ts +39 -11
  62. package/src/lib/server/session-tools/schedule.ts +43 -0
  63. package/src/lib/server/session-tools/web.ts +35 -11
  64. package/src/lib/server/storage.ts +12 -0
  65. package/src/lib/server/stream-agent-chat.ts +57 -8
  66. package/src/lib/server/tool-capability-policy.ts +1 -0
  67. package/src/lib/server/tool-retry.ts +62 -0
  68. package/src/lib/server/transcript-repair.ts +72 -0
  69. package/src/lib/setup-defaults.ts +1 -0
  70. package/src/lib/tool-definitions.ts +1 -0
  71. package/src/lib/validation/schemas.ts +1 -0
  72. package/src/lib/view-routes.ts +1 -0
  73. package/src/types/index.ts +34 -3
package/README.md CHANGED
@@ -158,7 +158,7 @@ Notes:
158
158
  - **Agent Fleet Management** — Avatar seeds with generated avatars, running/approval fleet filters, soft-delete agent trash with restore/permanent delete, and approval counters in agent cards
159
159
  - **Agent Tools** — Shell, process control for long-running commands, files, edit file, send file, web search, web fetch, CLI delegation (Claude/Codex/OpenCode), Playwright browser automation, sub-agent spawning, canvas presentation, direct HTTP requests, git operations, persistent memory, and sandboxed code execution (JS/TS via Deno, Python)
160
160
  - **Platform Tools** — Agents can manage other agents, tasks, schedules, skills, connectors, sessions, and encrypted secrets via built-in platform tools
161
- - **Orchestration** — Multi-agent workflows powered by LangGraph with automatic sub-agent routing, checkpointed execution, and rich delegation cards that link to sub-agent chat threads
161
+ - **Orchestration** — Multi-agent workflows powered by LangGraph with automatic sub-agent routing, checkpointed execution, checkpoint timeline with time-travel restore, and rich delegation cards that link to sub-agent chat threads
162
162
  - **Agentic Execution Policy** — Tool-first autonomous action loop with progress updates, evidence-driven answers, and better use of platform tools for long-lived work
163
163
  - **Runtime Date/Time Grounding** — Session, orchestrator, chatroom, and connector prompts include authoritative current timestamp context to reduce stale-date behavior
164
164
  - **Task Board** — Queue and track agent tasks with status, comments, structured result artifacts (`outputFiles`, uploads), completion reports, and archiving. Strict capability policy pauses tasks for human approval before tool execution
@@ -178,15 +178,21 @@ Notes:
178
178
  - **Skills System** — Discover local skills, import skills from URL, and load OpenClaw `SKILL.md` files (frontmatter-compatible)
179
179
  - **Execution Logging** — Structured audit trail for triggers, tool calls, file ops, commits, and errors in a dedicated `logs.db`
180
180
  - **Context Management** — Auto-compaction of conversation history when approaching context limits, with manual `context_status` and `context_summarize` tools for agents
181
- - **Memory** — Per-agent and per-session memory with hybrid FTS5 + vector embeddings search, relevance-based memory recall injected into runs, and periodic auto-journaling for durable execution context
182
- - **Cost Tracking** — Per-message token counting and cost estimation displayed in the chat header
183
- - **Provider Health Metrics** — Usage dashboard surfaces provider request volume, success rates, models used, and last-used timestamps
181
+ - **Memory** — Per-agent and per-session memory with hybrid FTS5 + vector embeddings search, query expansion (LLM-generated semantic variants), MMR diversity ranking, cross-agent search (`scope: all`), pinned memories (always preloaded), memory sharing between agents, linked memory graph with interactive visualization, image attachments, and periodic auto-journaling for durable execution context
182
+ - **Knowledge Base** — Shared knowledge store (`knowledge_store` / `knowledge_search` actions) with tags, source tracking, and provenance URLs separate from per-agent memories
183
+ - **Memory Graph Visualization** — Interactive force-directed graph view of linked memories with node details and relationship exploration
184
+ - **Cost Tracking** — Per-message token counting and cost estimation displayed in the chat header, with per-agent monthly budget caps (`warn` or `block` enforcement)
185
+ - **Provider Health Metrics** — Usage dashboard surfaces provider request volume, success rates, average latency, models used, and last-used timestamps
184
186
  - **Model Failover** — Automatic key rotation on rate limits and auth errors with configurable fallback credentials
185
- - **Plugin System** — Extend agent behavior with JS plugins (hooks: beforeAgentStart, afterAgentComplete, beforeToolExec, afterToolExec, onMessage)
187
+ - **Plugin System** — Extend agent behavior with JS plugins (hooks: beforeAgentStart, afterAgentComplete, beforeToolExec, afterToolExec, onMessage, onTaskComplete, onAgentDelegation). Plugins can also define custom tools that agents can use
186
188
  - **Secrets Vault** — Encrypted storage for API keys and service tokens
187
189
  - **Custom Providers** — Add any OpenAI-compatible API as a provider
188
190
  - **MCP Servers** — Connect agents to any Model Context Protocol server. Per-agent server selection with tool discovery and per-tool disable toggles
189
191
  - **Sandboxed Code Execution** — Agents can write and run JS/TS (Deno) or Python scripts in an isolated sandbox with network access, scoped filesystem, and artifact output
192
+ - **Eval Framework** — Built-in agent evaluation with scenario-based testing across categories (coding, research, companionship, multi-step, memory, planning, tool-usage), weighted scoring criteria, and LLM judge support
193
+ - **Guardian Auto-Recovery** — Automatic workspace recovery when agents fail critically, rolling back to the last known good state via git reset
194
+ - **Soul Library** — Browse and apply pre-built personality templates (archetypes) to agents, or create custom souls for reuse across your swarm
195
+ - **Context Degradation Warnings** — Proactive alerts when context usage exceeds 85%, with strategy recommendations (save to memory, summarize, checkpoint)
190
196
  - **Real-Time Sync** — WebSocket push notifications for instant UI updates across tabs and devices (fallback to polling when WS is unavailable)
191
197
  - **Mobile-First UI** — Responsive glass-themed dark interface, works on phone and desktop
192
198
 
@@ -329,6 +335,7 @@ Agents with platform tools enabled can manage the SwarmClaw instance:
329
335
  | Manage Agents | List, create, update, delete agents |
330
336
  | Manage Tasks | Create and manage task board items with agent assignment |
331
337
  | Manage Schedules | Create cron, interval, or one-time scheduled jobs |
338
+ | Reminders | Schedule a conversational wake event in the current chat (`schedule_wake`) |
332
339
  | Manage Skills | List, create, update reusable skill definitions |
333
340
  | Manage Documents | Upload/search/get/delete indexed docs for lightweight RAG workflows |
334
341
  | Manage Webhooks | Register external webhook endpoints that trigger agent sessions |
@@ -459,10 +466,21 @@ module.exports = {
459
466
  hooks: {
460
467
  beforeAgentStart: async ({ session, message }) => { /* ... */ },
461
468
  afterAgentComplete: async ({ session, response }) => { /* ... */ },
462
- beforeToolExec: async ({ toolName, input }) => { /* ... */ },
469
+ beforeToolExec: async ({ toolName, input }) => { /* return { abort: true } to cancel */ },
463
470
  afterToolExec: async ({ toolName, input, output }) => { /* ... */ },
464
471
  onMessage: async ({ session, message }) => { /* ... */ },
472
+ onTaskComplete: async ({ taskId, result }) => { /* ... */ },
473
+ onAgentDelegation: async ({ sourceAgentId, targetAgentId, task }) => { /* ... */ },
465
474
  },
475
+ // Plugins can also define custom tools that agents can use
476
+ tools: [
477
+ {
478
+ name: 'my_custom_tool',
479
+ description: 'Does something amazing',
480
+ parameters: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
481
+ execute: async (args, ctx) => 'Result: ' + args.query,
482
+ },
483
+ ],
466
484
  }
467
485
  ```
468
486
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,6 +53,7 @@ export async function POST(req: Request) {
53
53
  tools: body.tools,
54
54
  capabilities: body.capabilities,
55
55
  thinkingLevel: body.thinkingLevel || undefined,
56
+ autoRecovery: body.autoRecovery || false,
56
57
  soul: body.soul || undefined,
57
58
  createdAt: now,
58
59
  updatedAt: now,
@@ -18,6 +18,7 @@ import {
18
18
  import { filterHealthyChatroomAgents } from '@/lib/server/chatroom-health'
19
19
  import { evaluateRoutingRules } from '@/lib/server/chatroom-routing'
20
20
  import { markProviderFailure, markProviderSuccess } from '@/lib/server/provider-health'
21
+ import { applyAgentReactionsFromText } from '@/lib/server/chatroom-orchestration'
21
22
  import type { Chatroom, ChatroomMessage, Agent } from '@/types'
22
23
 
23
24
  export const dynamic = 'force-dynamic'
@@ -250,6 +251,9 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
250
251
  saveChatrooms(latestChatrooms)
251
252
  notify(`chatroom:${id}`)
252
253
 
254
+ // Extract and apply reactions (e.g. [REACTION]{"emoji":"👍","to":"..."})
255
+ applyAgentReactionsFromText(responseText, id, agent.id)
256
+
253
257
  markProviderSuccess(agent.provider)
254
258
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
255
259
 
@@ -0,0 +1,37 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { runEvalScenario } from '@/lib/server/eval/runner'
4
+ import { listEvalRuns } from '@/lib/server/eval/store'
5
+
6
+ const RunSchema = z.object({
7
+ scenarioId: z.string().min(1),
8
+ agentId: z.string().min(1),
9
+ })
10
+
11
+ export async function POST(req: Request) {
12
+ try {
13
+ const body: unknown = await req.json()
14
+ const parsed = RunSchema.safeParse(body)
15
+ if (!parsed.success) {
16
+ return NextResponse.json(
17
+ { error: parsed.error.issues.map((i) => i.message).join(', ') },
18
+ { status: 400 },
19
+ )
20
+ }
21
+
22
+ const result = await runEvalScenario(parsed.data.scenarioId, parsed.data.agentId)
23
+ return NextResponse.json(result)
24
+ } catch (err: unknown) {
25
+ return NextResponse.json(
26
+ { error: err instanceof Error ? err.message : String(err) },
27
+ { status: 500 },
28
+ )
29
+ }
30
+ }
31
+
32
+ export async function GET(req: Request) {
33
+ const { searchParams } = new URL(req.url)
34
+ const limit = Math.min(parseInt(searchParams.get('limit') || '50', 10), 200)
35
+ const runs = listEvalRuns(limit)
36
+ return NextResponse.json(runs)
37
+ }
@@ -0,0 +1,24 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { EVAL_SCENARIOS } from '@/lib/server/eval/scenarios'
3
+
4
+ export async function GET(req: Request) {
5
+ const { searchParams } = new URL(req.url)
6
+ const category = searchParams.get('category')
7
+
8
+ const scenarios = category
9
+ ? EVAL_SCENARIOS.filter((s) => s.category === category)
10
+ : EVAL_SCENARIOS
11
+
12
+ return NextResponse.json(
13
+ scenarios.map((s) => ({
14
+ id: s.id,
15
+ name: s.name,
16
+ category: s.category,
17
+ description: s.description,
18
+ tools: s.tools,
19
+ timeoutMs: s.timeoutMs,
20
+ criteriaCount: s.scoringCriteria.length,
21
+ maxScore: s.scoringCriteria.reduce((sum, c) => sum + c.weight, 0),
22
+ })),
23
+ )
24
+ }
@@ -0,0 +1,29 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { runEvalSuite } from '@/lib/server/eval/runner'
4
+
5
+ const SuiteSchema = z.object({
6
+ agentId: z.string().min(1),
7
+ categories: z.array(z.string()).optional(),
8
+ })
9
+
10
+ export async function POST(req: Request) {
11
+ try {
12
+ const body: unknown = await req.json()
13
+ const parsed = SuiteSchema.safeParse(body)
14
+ if (!parsed.success) {
15
+ return NextResponse.json(
16
+ { error: parsed.error.issues.map((i) => i.message).join(', ') },
17
+ { status: 400 },
18
+ )
19
+ }
20
+
21
+ const result = await runEvalSuite(parsed.data.agentId, parsed.data.categories)
22
+ return NextResponse.json(result)
23
+ } catch (err: unknown) {
24
+ return NextResponse.json(
25
+ { error: err instanceof Error ? err.message : String(err) },
26
+ { status: 500 },
27
+ )
28
+ }
29
+ }
@@ -0,0 +1,46 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getMemoryDb } from '@/lib/server/memory-db'
3
+ import type { MemoryEntry } from '@/types'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ /** GET /api/memory/graph — returns a node-link structure of the memory graph */
8
+ export async function GET(req: Request) {
9
+ const { searchParams } = new URL(req.url)
10
+ const agentId = searchParams.get('agentId')
11
+ const limit = Math.min(1000, Math.max(1, Number(searchParams.get('limit')) || 200))
12
+
13
+ const db = getMemoryDb()
14
+ const entries: MemoryEntry[] = db.list(agentId || undefined, limit)
15
+
16
+ const nodes = entries.map(e => ({
17
+ id: e.id,
18
+ title: e.title,
19
+ category: e.category,
20
+ agentId: e.agentId,
21
+ contentPreview: e.content.slice(0, 100) + (e.content.length > 100 ? '...' : ''),
22
+ createdAt: e.createdAt,
23
+ updatedAt: e.updatedAt,
24
+ pinned: e.pinned
25
+ }))
26
+
27
+ const links: Array<{ source: string; target: string; type: string }> = []
28
+ const entryIds = new Set(entries.map(e => e.id))
29
+
30
+ for (const entry of entries) {
31
+ if (entry.linkedMemoryIds && Array.isArray(entry.linkedMemoryIds)) {
32
+ for (const targetId of entry.linkedMemoryIds) {
33
+ // Only include links where both nodes are in the current set (or could fetch more if needed)
34
+ if (entryIds.has(targetId)) {
35
+ links.push({
36
+ source: entry.id,
37
+ target: targetId,
38
+ type: 'linked'
39
+ })
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ return NextResponse.json({ nodes, links })
46
+ }
@@ -0,0 +1,31 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ /** GET /api/sessions/[id]/checkpoints — returns checkpoint history for a thread */
7
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id: threadId } = await params
9
+ if (!threadId) return NextResponse.json({ error: 'Thread ID is required' }, { status: 400 })
10
+
11
+ const saver = getCheckpointSaver()
12
+ const checkpoints = []
13
+
14
+ // LangGraph's list() is an async generator
15
+ const iterator = saver.list({ configurable: { thread_id: threadId } })
16
+
17
+ for await (const tuple of iterator) {
18
+ checkpoints.push({
19
+ checkpointId: tuple.config.configurable?.checkpoint_id,
20
+ parentCheckpointId: tuple.parentConfig?.configurable?.checkpoint_id,
21
+ metadata: tuple.metadata,
22
+ createdAt: new Date(tuple.checkpoint.ts).getTime(),
23
+ values: tuple.checkpoint.channel_values,
24
+ })
25
+ }
26
+
27
+ // Sort by created_at descending (saver.list usually does this but we want to be sure)
28
+ checkpoints.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))
29
+
30
+ return NextResponse.json(checkpoints)
31
+ }
@@ -0,0 +1,36 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
3
+ import { loadSessions, saveSessions } from '@/lib/server/storage'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ /** POST /api/sessions/[id]/restore — restores thread to a specific checkpoint */
9
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id: sessionId } = await params
11
+ const { checkpointId, timestamp } = await req.json()
12
+
13
+ if (!checkpointId || !timestamp) {
14
+ return NextResponse.json({ error: 'checkpointId and timestamp are required' }, { status: 400 })
15
+ }
16
+
17
+ const saver = getCheckpointSaver()
18
+
19
+ // 1. Delete all checkpoints after the target one
20
+ await saver.deleteCheckpointsAfter(sessionId, timestamp)
21
+
22
+ // 2. Truncate messages in the session to match the timestamp
23
+ // Both timestamp (from checkpoint.ts → getTime()) and Message.time use epoch milliseconds
24
+ const sessions = loadSessions()
25
+ const session = sessions[sessionId]
26
+ if (session) {
27
+ session.messages = session.messages.filter((m: { time: number }) => m.time <= timestamp)
28
+ session.lastActiveAt = Date.now()
29
+ saveSessions(sessions)
30
+ }
31
+
32
+ notify(`messages:${sessionId}`)
33
+ notify('sessions')
34
+
35
+ return NextResponse.json({ ok: true, restoredTo: checkpointId })
36
+ }
@@ -0,0 +1,65 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSouls, saveSouls, deleteSoul, logActivity } from '@/lib/server/storage'
3
+ import { SOUL_LIBRARY } from '@/lib/soul-library'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ /** GET /api/souls/[id] */
9
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params
11
+
12
+ // Check static library first
13
+ const staticSoul = SOUL_LIBRARY.find(s => s.id === id)
14
+ if (staticSoul) return NextResponse.json(staticSoul)
15
+
16
+ const souls = loadSouls()
17
+ if (!souls[id]) return NextResponse.json({ error: 'Soul not found' }, { status: 404 })
18
+ return NextResponse.json(souls[id])
19
+ }
20
+
21
+ /** PUT /api/souls/[id] — update custom soul */
22
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
23
+ const { id } = await params
24
+ const body = await req.json()
25
+
26
+ // Can only update custom souls
27
+ const souls = loadSouls()
28
+ if (!souls[id]) {
29
+ return NextResponse.json({ error: 'Only custom souls can be modified via this endpoint' }, { status: 403 })
30
+ }
31
+
32
+ const updated = { ...souls[id], ...body, id, updatedAt: Date.now() }
33
+ souls[id] = updated
34
+ saveSouls(souls)
35
+
36
+ notify('souls')
37
+ return NextResponse.json(updated)
38
+ }
39
+
40
+ /** DELETE /api/souls/[id] — delete custom soul */
41
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
42
+ const { id } = await params
43
+
44
+ // Only allow deleting custom ones
45
+ const souls = loadSouls()
46
+ if (!souls[id]) {
47
+ const isStatic = SOUL_LIBRARY.some(s => s.id === id)
48
+ if (isStatic) return NextResponse.json({ error: 'Cannot delete static library souls' }, { status: 403 })
49
+ return NextResponse.json({ error: 'Soul not found' }, { status: 404 })
50
+ }
51
+
52
+ const name = souls[id].name
53
+ deleteSoul(id)
54
+
55
+ logActivity({
56
+ entityType: 'soul',
57
+ entityId: id,
58
+ action: 'deleted',
59
+ actor: 'user',
60
+ summary: `Custom soul deleted: "${name}"`
61
+ })
62
+
63
+ notify('souls')
64
+ return NextResponse.json({ deleted: id })
65
+ }
@@ -0,0 +1,70 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { SOUL_LIBRARY, type SoulTemplate } from '@/lib/soul-library'
3
+ import { loadSouls, saveSouls, logActivity } from '@/lib/server/storage'
4
+ import { genId } from '@/lib/id'
5
+ import { notify } from '@/lib/server/ws-hub'
6
+
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ /** GET /api/souls — returns merged list of static library and custom user souls */
10
+ export async function GET(req: Request) {
11
+ const customSouls = loadSouls()
12
+ const { searchParams } = new URL(req.url)
13
+ const query = searchParams.get('q')?.toLowerCase() || ''
14
+ const archetype = searchParams.get('archetype')
15
+
16
+ const merged: SoulTemplate[] = [
17
+ ...SOUL_LIBRARY,
18
+ ...Object.values(customSouls) as SoulTemplate[],
19
+ ]
20
+
21
+ let filtered = merged
22
+ if (archetype && archetype !== 'All') {
23
+ filtered = filtered.filter((s) => s.archetype === archetype)
24
+ }
25
+ if (query) {
26
+ filtered = filtered.filter(
27
+ (s) =>
28
+ s.name.toLowerCase().includes(query) ||
29
+ s.description.toLowerCase().includes(query) ||
30
+ s.tags.some((t) => t.toLowerCase().includes(query)) ||
31
+ s.soul.toLowerCase().includes(query),
32
+ )
33
+ }
34
+
35
+ return NextResponse.json(filtered)
36
+ }
37
+
38
+ /** POST /api/souls — create a custom soul */
39
+ export async function POST(req: Request) {
40
+ const body = await req.json()
41
+ if (!body.name || !body.soul) {
42
+ return NextResponse.json({ error: 'Name and soul content are required' }, { status: 400 })
43
+ }
44
+
45
+ const id = body.id || `custom-${genId()}`
46
+ const souls = loadSouls()
47
+
48
+ const newSoul: SoulTemplate = {
49
+ id,
50
+ name: body.name,
51
+ description: body.description || '',
52
+ soul: body.soul,
53
+ tags: Array.isArray(body.tags) ? body.tags : [],
54
+ archetype: body.archetype || 'Custom',
55
+ }
56
+
57
+ souls[id] = newSoul
58
+ saveSouls(souls)
59
+
60
+ logActivity({
61
+ entityType: 'soul',
62
+ entityId: id,
63
+ action: 'created',
64
+ actor: 'user',
65
+ summary: `Custom soul created: "${newSoul.name}"`
66
+ })
67
+
68
+ notify('souls')
69
+ return NextResponse.json(newSoul)
70
+ }
@@ -11,6 +11,7 @@ import { createNotification } from '@/lib/server/create-notification'
11
11
  import { enqueueSystemEvent } from '@/lib/server/system-events'
12
12
  import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
13
13
  import { validateDag, cascadeUnblock } from '@/lib/server/dag-validation'
14
+ import { getPluginManager } from '@/lib/server/plugins'
14
15
 
15
16
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
16
17
  // Keep completed queue integrity even if daemon is not running.
@@ -100,6 +101,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
100
101
  entityType: 'task',
101
102
  entityId: id,
102
103
  })
104
+
105
+ if (tasks[id].status === 'completed') {
106
+ getPluginManager().runHook('onTaskComplete', { taskId: id, result: tasks[id].result })
107
+ }
103
108
 
104
109
  // Enqueue system event + heartbeat wake
105
110
  if (tasks[id].sessionId) {
@@ -11,6 +11,7 @@ import { notify } from '@/lib/server/ws-hub'
11
11
  import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
12
12
  import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
13
13
  import { validateDag } from '@/lib/server/dag-validation'
14
+ import { getPluginManager } from '@/lib/server/plugins'
14
15
 
15
16
  export async function GET(req: Request) {
16
17
  // Keep completed queue integrity even if daemon is not running.
@@ -162,6 +163,7 @@ export async function POST(req: Request) {
162
163
  if (validation.ok) {
163
164
  tasks[id].completedAt = Date.now()
164
165
  tasks[id].error = null
166
+ getPluginManager().runHook('onTaskComplete', { taskId: id, result: tasks[id].result })
165
167
  } else {
166
168
  tasks[id].status = 'failed'
167
169
  tasks[id].completedAt = null
@@ -94,12 +94,14 @@ export async function GET(req: Request) {
94
94
  errorCount: number
95
95
  lastUsed: number
96
96
  models: Set<string>
97
+ totalDurationMs: number
98
+ latencyCount: number
97
99
  }> = {}
98
100
 
99
101
  for (const r of records) {
100
102
  const prov = r.provider || 'unknown'
101
103
  if (!healthAccum[prov]) {
102
- healthAccum[prov] = { totalRequests: 0, successCount: 0, errorCount: 0, lastUsed: 0, models: new Set() }
104
+ healthAccum[prov] = { totalRequests: 0, successCount: 0, errorCount: 0, lastUsed: 0, models: new Set(), totalDurationMs: 0, latencyCount: 0 }
103
105
  }
104
106
  const h = healthAccum[prov]
105
107
  h.totalRequests += 1
@@ -107,6 +109,11 @@ export async function GET(req: Request) {
107
109
  h.successCount += 1
108
110
  if ((r.timestamp || 0) > h.lastUsed) h.lastUsed = r.timestamp || 0
109
111
  if (r.model) h.models.add(r.model)
112
+
113
+ if (typeof r.durationMs === 'number' && r.durationMs > 0) {
114
+ h.totalDurationMs += r.durationMs
115
+ h.latencyCount += 1
116
+ }
110
117
  }
111
118
 
112
119
  const providerHealth: Record<string, {
@@ -125,7 +132,7 @@ export async function GET(req: Request) {
125
132
  successCount: h.successCount,
126
133
  errorCount: h.errorCount,
127
134
  errorRate: h.totalRequests > 0 ? h.errorCount / h.totalRequests : 0,
128
- avgLatencyMs: 0, // UsageRecord does not track latency
135
+ avgLatencyMs: h.latencyCount > 0 ? h.totalDurationMs / h.latencyCount : 0,
129
136
  lastUsed: h.lastUsed,
130
137
  models: Array.from(h.models),
131
138
  }
package/src/cli/index.js CHANGED
@@ -160,6 +160,16 @@ const COMMAND_GROUPS = [
160
160
  cmd('delete', 'DELETE', '/documents/:id', 'Delete document'),
161
161
  ],
162
162
  },
163
+ {
164
+ name: 'eval',
165
+ description: 'Run agent evaluation scenarios',
166
+ commands: [
167
+ cmd('scenarios', 'GET', '/eval/scenarios', 'List available eval scenarios'),
168
+ cmd('status', 'GET', '/eval/run', 'Get eval run status'),
169
+ cmd('run', 'POST', '/eval/run', 'Run an eval scenario against an agent', { expectsJsonBody: true }),
170
+ cmd('suite', 'POST', '/eval/suite', 'Run a full eval suite against an agent', { expectsJsonBody: true }),
171
+ ],
172
+ },
163
173
  {
164
174
  name: 'files',
165
175
  description: 'Serve and manage local files',
@@ -209,6 +219,7 @@ const COMMAND_GROUPS = [
209
219
  cmd('delete', 'DELETE', '/memory/:id', 'Delete memory entry'),
210
220
  cmd('maintenance', 'GET', '/memory/maintenance', 'Analyze memory dedupe/prune candidates'),
211
221
  cmd('maintenance-run', 'POST', '/memory/maintenance', 'Run memory dedupe/prune maintenance', { expectsJsonBody: true }),
222
+ cmd('graph', 'GET', '/memory/graph', 'Get memory graph (nodes and links) for visualization'),
212
223
  ],
213
224
  },
214
225
  {
@@ -428,6 +439,8 @@ const COMMAND_GROUPS = [
428
439
  expectsJsonBody: true,
429
440
  defaultBody: { action: 'status' },
430
441
  }),
442
+ cmd('checkpoints', 'GET', '/sessions/:id/checkpoints', 'List checkpoint history for a session'),
443
+ cmd('restore', 'POST', '/sessions/:id/restore', 'Restore session to a previous checkpoint', { expectsJsonBody: true }),
431
444
  ],
432
445
  },
433
446
  {
@@ -459,6 +472,17 @@ const COMMAND_GROUPS = [
459
472
  cmd('import', 'POST', '/skills/import', 'Import skill from URL', { expectsJsonBody: true }),
460
473
  ],
461
474
  },
475
+ {
476
+ name: 'souls',
477
+ description: 'Browse and manage soul library templates',
478
+ commands: [
479
+ cmd('list', 'GET', '/souls', 'List soul templates'),
480
+ cmd('get', 'GET', '/souls/:id', 'Get soul template by id'),
481
+ cmd('create', 'POST', '/souls', 'Create custom soul template', { expectsJsonBody: true }),
482
+ cmd('update', 'PUT', '/souls/:id', 'Update soul template', { expectsJsonBody: true }),
483
+ cmd('delete', 'DELETE', '/souls/:id', 'Delete soul template'),
484
+ ],
485
+ },
462
486
  {
463
487
  name: 'tasks',
464
488
  description: 'Manage task board items',