@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.
- package/README.md +24 -6
- package/package.json +1 -1
- package/src/app/api/agents/route.ts +1 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/route.ts +5 -0
- package/src/app/api/tasks/route.ts +2 -0
- package/src/app/api/usage/route.ts +9 -2
- package/src/cli/index.js +24 -0
- package/src/components/agents/agent-sheet.tsx +27 -6
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/message-list.tsx +19 -3
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/connectors/connector-sheet.tsx +8 -1
- package/src/components/home/home-view.tsx +39 -15
- package/src/components/layout/app-layout.tsx +18 -2
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +9 -2
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -4
- package/src/components/tasks/approvals-panel.tsx +120 -0
- package/src/components/usage/metrics-dashboard.tsx +25 -3
- package/src/lib/server/chat-execution.ts +96 -12
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/daemon-state.ts +70 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/main-agent-loop.ts +114 -15
- package/src/lib/server/memory-db.ts +18 -7
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +3 -0
- package/src/lib/server/plugins.ts +44 -22
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +27 -0
- package/src/lib/server/session-run-manager.ts +21 -1
- package/src/lib/server/session-tools/http.ts +19 -9
- package/src/lib/server/session-tools/index.ts +34 -0
- package/src/lib/server/session-tools/memory.ts +39 -11
- package/src/lib/server/session-tools/schedule.ts +43 -0
- package/src/lib/server/session-tools/web.ts +35 -11
- package/src/lib/server/storage.ts +12 -0
- package/src/lib/server/stream-agent-chat.ts +57 -8
- package/src/lib/server/tool-capability-policy.ts +1 -0
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/view-routes.ts +1 -0
- 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,
|
|
182
|
-
- **
|
|
183
|
-
- **
|
|
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.
|
|
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
|
|
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',
|