Checkpoint: Phase 13: Seed data for agents and orchestrator

- server/seed.ts: 6 default system agents (Orchestrator, Browser, Tool Builder, Agent Compiler, Coder, Researcher)
- Idempotent: runs only when agents table is empty
- Integrated into server/_core/index.ts startup
- server/seed.test.ts: 18 vitest tests, all pass
- Total: 69 tests pass (7 test files)
This commit is contained in:
Manus
2026-03-20 20:39:08 -04:00
parent b1a3a994bc
commit 73a26d8a8a
4 changed files with 583 additions and 1 deletions

View File

@@ -7,6 +7,7 @@ import { registerOAuthRoutes } from "./oauth";
import { appRouter } from "../routers";
import { createContext } from "./context";
import { serveStatic, setupVite } from "./vite";
import { seedDefaults } from "../seed";
function isPortAvailable(port: number): Promise<boolean> {
return new Promise(resolve => {
@@ -57,6 +58,9 @@ async function startServer() {
console.log(`Port ${preferredPort} is busy, using port ${port} instead`);
}
// Run idempotent seed on every startup (no-op if data already exists)
await seedDefaults();
server.listen(port, () => {
console.log(`Server running on http://localhost:${port}/`);
});

194
server/seed.test.ts Normal file
View File

@@ -0,0 +1,194 @@
/**
* Tests for server/seed.ts
*
* Strategy: mock `./db` so getDb() returns a fake Drizzle-like object,
* then assert that seedDefaults() inserts exactly the right agents
* (or skips when agents already exist).
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { DEFAULT_AGENTS, seedDefaults } from "./seed";
// ─── Mock getDb ───────────────────────────────────────────────────────────────
const mockInsert = vi.fn();
const mockSelect = vi.fn();
vi.mock("./db", () => ({
getDb: vi.fn(),
}));
import { getDb } from "./db";
// Helper: build a minimal fake db that tracks inserts and returns a given count
function makeFakeDb(existingAgentCount: number) {
const insertValues = vi.fn().mockResolvedValue([{ insertId: 1 }]);
const insertInto = vi.fn().mockReturnValue({ values: insertValues });
// select().from(). → [{ value: count }]
const fromFn = vi.fn().mockResolvedValue([{ value: existingAgentCount }]);
const selectFn = vi.fn().mockReturnValue({ from: fromFn });
return {
insert: insertInto,
select: selectFn,
_insertValues: insertValues,
_fromFn: fromFn,
};
}
// ─── Tests ────────────────────────────────────────────────────────────────────
describe("DEFAULT_AGENTS", () => {
it("should contain exactly 6 default agents", () => {
expect(DEFAULT_AGENTS).toHaveLength(6);
});
it("should have exactly one orchestrator agent", () => {
const orchestrators = DEFAULT_AGENTS.filter((a) => a.isOrchestrator);
expect(orchestrators).toHaveLength(1);
expect(orchestrators[0].name).toBe("GoClaw Orchestrator");
});
it("should mark all agents as system agents", () => {
const nonSystem = DEFAULT_AGENTS.filter((a) => !a.isSystem);
expect(nonSystem).toHaveLength(0);
});
it("should have unique roles", () => {
const roles = DEFAULT_AGENTS.map((a) => a.role);
const uniqueRoles = new Set(roles);
expect(uniqueRoles.size).toBe(roles.length);
});
it("each agent should have allowedTools array", () => {
for (const agent of DEFAULT_AGENTS) {
expect(Array.isArray(agent.allowedTools)).toBe(true);
expect(agent.allowedTools.length).toBeGreaterThan(0);
}
});
it("orchestrator should have the most tools", () => {
const orch = DEFAULT_AGENTS.find((a) => a.isOrchestrator)!;
const maxTools = Math.max(...DEFAULT_AGENTS.map((a) => a.allowedTools.length));
expect(orch.allowedTools.length).toBe(maxTools);
});
it("each agent should have a non-empty systemPrompt", () => {
for (const agent of DEFAULT_AGENTS) {
expect(agent.systemPrompt.trim().length).toBeGreaterThan(50);
}
});
it("each agent should have valid temperature (0-1 as string)", () => {
for (const agent of DEFAULT_AGENTS) {
const temp = parseFloat(agent.temperature);
expect(temp).toBeGreaterThanOrEqual(0);
expect(temp).toBeLessThanOrEqual(1);
}
});
it("each agent should have maxTokens > 0", () => {
for (const agent of DEFAULT_AGENTS) {
expect(agent.maxTokens).toBeGreaterThan(0);
}
});
it("orchestrator should have the highest maxTokens", () => {
const orch = DEFAULT_AGENTS.find((a) => a.isOrchestrator)!;
const maxTok = Math.max(...DEFAULT_AGENTS.map((a) => a.maxTokens));
expect(orch.maxTokens).toBe(maxTok);
});
it("each agent should have seeded metadata flag", () => {
for (const agent of DEFAULT_AGENTS) {
expect(agent.metadata.seeded).toBe(true);
}
});
});
describe("seedDefaults()", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should skip seeding when DB is unavailable", async () => {
vi.mocked(getDb).mockResolvedValue(null as any);
await expect(seedDefaults()).resolves.toBeUndefined();
// No inserts should happen
expect(mockInsert).not.toHaveBeenCalled();
});
it("should skip seeding when agents already exist", async () => {
const fakeDb = makeFakeDb(3); // 3 agents already exist
vi.mocked(getDb).mockResolvedValue(fakeDb as any);
await seedDefaults();
// select was called to check count
expect(fakeDb.select).toHaveBeenCalled();
// insert was NOT called
expect(fakeDb.insert).not.toHaveBeenCalled();
});
it("should insert all default agents when table is empty", async () => {
const fakeDb = makeFakeDb(0); // empty table
vi.mocked(getDb).mockResolvedValue(fakeDb as any);
await seedDefaults();
// insert should be called once per agent
expect(fakeDb.insert).toHaveBeenCalledTimes(DEFAULT_AGENTS.length);
expect(fakeDb._insertValues).toHaveBeenCalledTimes(DEFAULT_AGENTS.length);
});
it("should insert orchestrator agent with isOrchestrator=true", async () => {
const fakeDb = makeFakeDb(0);
vi.mocked(getDb).mockResolvedValue(fakeDb as any);
await seedDefaults();
// Find the call that inserted the orchestrator
const calls = fakeDb._insertValues.mock.calls;
const orchCall = calls.find((call: any[]) => call[0]?.isOrchestrator === true);
expect(orchCall).toBeDefined();
expect(orchCall![0].isSystem).toBe(true);
expect(orchCall![0].name).toBe("GoClaw Orchestrator");
});
it("should insert all agents with isSystem=true", async () => {
const fakeDb = makeFakeDb(0);
vi.mocked(getDb).mockResolvedValue(fakeDb as any);
await seedDefaults();
const calls = fakeDb._insertValues.mock.calls;
for (const call of calls) {
expect(call[0].isSystem).toBe(true);
}
});
it("should be idempotent — second call with existing data does nothing", async () => {
// First call: empty DB → seeds
const fakeDb1 = makeFakeDb(0);
vi.mocked(getDb).mockResolvedValue(fakeDb1 as any);
await seedDefaults();
expect(fakeDb1.insert).toHaveBeenCalledTimes(DEFAULT_AGENTS.length);
// Second call: DB now has agents → skips
const fakeDb2 = makeFakeDb(DEFAULT_AGENTS.length);
vi.mocked(getDb).mockResolvedValue(fakeDb2 as any);
await seedDefaults();
expect(fakeDb2.insert).not.toHaveBeenCalled();
});
it("should not throw when DB insert fails — just log error", async () => {
const fakeDb = makeFakeDb(0);
// Make insert throw
fakeDb._insertValues.mockRejectedValue(new Error("DB error"));
vi.mocked(getDb).mockResolvedValue(fakeDb as any);
// Should not throw
await expect(seedDefaults()).resolves.toBeUndefined();
});
});

372
server/seed.ts Normal file
View File

@@ -0,0 +1,372 @@
/**
* GoClaw — Default Seed Data
*
* Idempotent: runs only when the agents table is empty.
* Seeds the following system agents:
* 1. Orchestrator — main chat agent (isOrchestrator=true, isSystem=true)
* 2. Browser Agent — web browsing & scraping
* 3. Tool Builder — LLM-powered tool generator
* 4. Agent Compiler — LLM-powered agent factory
* 5. Coder Agent — code writing & review
* 6. Researcher — deep research & summarisation
*
* All system agents use userId=0 (reserved system owner).
*/
import { eq, count } from "drizzle-orm";
import { getDb } from "./db";
import { agents } from "../drizzle/schema";
// ─── Seed Definitions ────────────────────────────────────────────────────────
const SYSTEM_USER_ID = 0; // Reserved: system-owned agents
export interface SeedAgent {
name: string;
description: string;
role: string;
model: string;
provider: string;
temperature: string;
maxTokens: number;
topP: string;
frequencyPenalty: string;
presencePenalty: string;
systemPrompt: string;
allowedTools: string[];
allowedDomains: string[];
maxRequestsPerHour: number;
isActive: boolean;
isPublic: boolean;
isSystem: boolean;
isOrchestrator: boolean;
tags: string[];
metadata: Record<string, any>;
}
export const DEFAULT_AGENTS: SeedAgent[] = [
// ── 1. Orchestrator ────────────────────────────────────────────────────────
{
name: "GoClaw Orchestrator",
description:
"Главный AI-агент системы. Управляет всеми специализированными агентами, выполняет системные задачи, отвечает на вопросы пользователей.",
role: "orchestrator",
model: "qwen2.5:7b",
provider: "ollama",
temperature: "0.50",
maxTokens: 8192,
topP: "1.00",
frequencyPenalty: "0.00",
presencePenalty: "0.00",
systemPrompt: `You are GoClaw Orchestrator — the main AI agent managing the GoClaw distributed AI system.
You have full access to:
1. **Specialized Agents**: Browser Agent (web browsing), Tool Builder (create tools), Agent Compiler (create agents)
2. **System Tools**: shell_exec (run commands), file_read/write (manage files), http_request (web requests), docker_exec (Docker management)
3. **Skills Registry**: list_skills (see capabilities), install_skill (add new capabilities)
Your responsibilities:
- Answer user questions directly when possible
- Delegate complex web tasks to Browser Agent
- Delegate tool creation to Tool Builder agent
- Delegate agent creation to Agent Compiler
- Execute shell commands to manage the system, install packages, run scripts
- Read and write files to modify the codebase
- Monitor Docker containers and services
Decision making:
- For simple questions: answer directly without tools
- For web research: use delegate_to_agent with Browser Agent
- For creating tools: use delegate_to_agent with Tool Builder
- For creating agents: use delegate_to_agent with Agent Compiler
- For system tasks: use shell_exec, file_read/write
- For Docker: use docker_exec
- Always use list_agents first if you're unsure which agent to delegate to
Response style:
- Be concise and actionable
- Show what tools you used and their results
- If a task requires multiple steps, execute them in sequence
- Respond in the same language as the user
You are running on a Linux server with Node.js, Docker, and full internet access.`,
allowedTools: [
"shell_exec",
"file_read",
"file_write",
"http_get",
"http_post",
"docker_list",
"docker_exec",
"docker_logs",
"browser_navigate",
"browser_screenshot",
],
allowedDomains: [],
maxRequestsPerHour: 500,
isActive: true,
isPublic: false,
isSystem: true,
isOrchestrator: true,
tags: ["system", "orchestrator", "main"],
metadata: { seeded: true, version: "1.0" },
},
// ── 2. Browser Agent ───────────────────────────────────────────────────────
{
name: "Browser Agent",
description:
"Специализированный агент для веб-навигации, парсинга сайтов и сбора информации из интернета.",
role: "browser",
model: "qwen2.5:7b",
provider: "ollama",
temperature: "0.30",
maxTokens: 4096,
topP: "1.00",
frequencyPenalty: "0.00",
presencePenalty: "0.00",
systemPrompt: `You are the Browser Agent for GoClaw. Your specialty is web browsing, scraping, and information gathering.
Your capabilities:
- Navigate to any URL and extract content
- Take screenshots of web pages
- Search for information online
- Parse and summarise web content
Guidelines:
- Always verify information from multiple sources when possible
- Return structured, clean data
- Report errors clearly if a page is inaccessible
- Respect robots.txt and rate limits
- Respond in the same language as the user`,
allowedTools: ["browser_navigate", "browser_screenshot", "http_get"],
allowedDomains: [],
maxRequestsPerHour: 200,
isActive: true,
isPublic: false,
isSystem: true,
isOrchestrator: false,
tags: ["system", "browser", "web"],
metadata: { seeded: true, version: "1.0" },
},
// ── 3. Tool Builder ────────────────────────────────────────────────────────
{
name: "Tool Builder",
description:
"Создаёт новые инструменты для агентов с помощью LLM. Генерирует код, тестирует и регистрирует инструменты в реестре.",
role: "tool_builder",
model: "qwen2.5:7b",
provider: "ollama",
temperature: "0.20",
maxTokens: 4096,
topP: "1.00",
frequencyPenalty: "0.00",
presencePenalty: "0.00",
systemPrompt: `You are the Tool Builder Agent for GoClaw. You create new tools that extend the capabilities of other agents.
Your workflow:
1. Understand the tool requirement from the user description
2. Generate a clean JavaScript/TypeScript function implementation
3. Define proper parameter schemas (JSON Schema format)
4. Validate the implementation for safety and correctness
5. Register the tool in the tool registry
Guidelines:
- Write clean, well-commented code
- Always validate input parameters
- Handle errors gracefully with descriptive messages
- Mark dangerous tools explicitly (file system writes, shell execution, network calls)
- Test with edge cases before finalising
- Respond in the same language as the user`,
allowedTools: ["file_read", "file_write", "shell_exec"],
allowedDomains: [],
maxRequestsPerHour: 100,
isActive: true,
isPublic: false,
isSystem: true,
isOrchestrator: false,
tags: ["system", "tool-builder", "meta"],
metadata: { seeded: true, version: "1.0" },
},
// ── 4. Agent Compiler ──────────────────────────────────────────────────────
{
name: "Agent Compiler",
description:
"Создаёт новых специализированных агентов по техническому заданию. Настраивает модель, системный промпт, инструменты и параметры.",
role: "agent_compiler",
model: "qwen2.5:7b",
provider: "ollama",
temperature: "0.30",
maxTokens: 4096,
topP: "1.00",
frequencyPenalty: "0.00",
presencePenalty: "0.00",
systemPrompt: `You are the Agent Compiler for GoClaw. You create new specialised AI agents from natural language specifications.
Your workflow:
1. Parse the user's technical specification (ТЗ)
2. Determine the optimal LLM model and parameters
3. Write a focused system prompt for the new agent
4. Select appropriate tools from the tool registry
5. Configure access controls and rate limits
6. Deploy the agent to the system
Guidelines:
- Keep system prompts focused and specific
- Choose the minimum set of tools needed for the agent's purpose
- Set conservative rate limits for new agents
- Validate the agent configuration before deployment
- Respond in the same language as the user`,
allowedTools: ["file_read", "file_write"],
allowedDomains: [],
maxRequestsPerHour: 50,
isActive: true,
isPublic: false,
isSystem: true,
isOrchestrator: false,
tags: ["system", "agent-compiler", "meta"],
metadata: { seeded: true, version: "1.0" },
},
// ── 5. Coder Agent ─────────────────────────────────────────────────────────
{
name: "Coder Agent",
description:
"Специализированный агент для написания, проверки и рефакторинга кода. Поддерживает TypeScript, Python, Go, Bash и другие языки.",
role: "coder",
model: "qwen2.5:7b",
provider: "ollama",
temperature: "0.10",
maxTokens: 8192,
topP: "1.00",
frequencyPenalty: "0.00",
presencePenalty: "0.00",
systemPrompt: `You are the Coder Agent for GoClaw. You specialise in writing, reviewing, and refactoring code.
Your capabilities:
- Write clean, production-ready code in TypeScript, JavaScript, Python, Go, Bash, SQL
- Review code for bugs, security issues, and performance problems
- Refactor existing code for clarity and maintainability
- Write unit tests and documentation
- Execute code and verify output
Guidelines:
- Always write type-safe code with proper error handling
- Follow language-specific best practices and conventions
- Add meaningful comments for complex logic
- Prefer readable code over clever one-liners
- Test your code before presenting it as final
- Respond in the same language as the user`,
allowedTools: ["file_read", "file_write", "shell_exec"],
allowedDomains: [],
maxRequestsPerHour: 300,
isActive: true,
isPublic: false,
isSystem: true,
isOrchestrator: false,
tags: ["system", "coder", "development"],
metadata: { seeded: true, version: "1.0" },
},
// ── 6. Researcher ──────────────────────────────────────────────────────────
{
name: "Researcher",
description:
"Агент для глубокого исследования тем. Собирает информацию из множества источников, анализирует и создаёт структурированные отчёты.",
role: "researcher",
model: "qwen2.5:7b",
provider: "ollama",
temperature: "0.40",
maxTokens: 8192,
topP: "1.00",
frequencyPenalty: "0.00",
presencePenalty: "0.00",
systemPrompt: `You are the Researcher Agent for GoClaw. You specialise in deep research and knowledge synthesis.
Your capabilities:
- Search and browse multiple web sources
- Analyse and cross-reference information
- Create structured research reports
- Summarise complex topics clearly
- Identify key facts, trends, and insights
Guidelines:
- Always cite your sources
- Distinguish between facts and opinions
- Present information in a structured, readable format
- Acknowledge uncertainty when sources conflict
- Provide actionable insights when possible
- Respond in the same language as the user`,
allowedTools: ["browser_navigate", "browser_screenshot", "http_get"],
allowedDomains: [],
maxRequestsPerHour: 200,
isActive: true,
isPublic: false,
isSystem: true,
isOrchestrator: false,
tags: ["system", "researcher", "analysis"],
metadata: { seeded: true, version: "1.0" },
},
];
// ─── Seed Function ────────────────────────────────────────────────────────────
/**
* Seed default agents if the agents table is empty.
* Idempotent: safe to call on every server startup.
*/
export async function seedDefaults(): Promise<void> {
const db = await getDb();
if (!db) {
console.warn("[Seed] Database not available — skipping seed");
return;
}
try {
// Check if any agents already exist (including system agents)
const [{ value: existingCount }] = await db
.select({ value: count() })
.from(agents);
if (Number(existingCount) > 0) {
console.log(`[Seed] Skipping — ${existingCount} agent(s) already exist`);
return;
}
console.log("[Seed] No agents found — seeding default agents...");
for (const agentDef of DEFAULT_AGENTS) {
await db.insert(agents).values({
userId: SYSTEM_USER_ID,
name: agentDef.name,
description: agentDef.description,
role: agentDef.role,
model: agentDef.model,
provider: agentDef.provider,
temperature: agentDef.temperature,
maxTokens: agentDef.maxTokens,
topP: agentDef.topP,
frequencyPenalty: agentDef.frequencyPenalty,
presencePenalty: agentDef.presencePenalty,
systemPrompt: agentDef.systemPrompt,
allowedTools: agentDef.allowedTools,
allowedDomains: agentDef.allowedDomains,
maxRequestsPerHour: agentDef.maxRequestsPerHour,
isActive: agentDef.isActive,
isPublic: agentDef.isPublic,
isSystem: agentDef.isSystem,
isOrchestrator: agentDef.isOrchestrator,
tags: agentDef.tags,
metadata: agentDef.metadata,
});
console.log(`[Seed] ✓ Created agent: ${agentDef.name}`);
}
console.log(`[Seed] Done — ${DEFAULT_AGENTS.length} default agents created`);
} catch (error) {
console.error("[Seed] Failed to seed default agents:", error);
// Non-fatal: server continues even if seed fails
}
}

14
todo.md
View File

@@ -173,4 +173,16 @@
- [ ] Update Nodes.tsx: real data from tRPC + auto-refresh every 5 seconds
- [ ] Show: node ID, hostname, status, role (manager/worker), availability, CPU, RAM, Docker version, IP
- [ ] Show live indicator (green pulse) when data is fresh
- [ ] Deploy to server 2.59.219.61
- [x] Deploy to server 2.59.219.61
- [x] Docker API client: /api/nodes, /api/nodes/stats
- [x] tRPC nodes.list, nodes.stats procedures
- [x] Nodes.tsx rewritten with real data + auto-refresh 10s/15s
- [x] 14 vitest tests for nodes procedures
## Phase 13: Seed Data for Agents & Orchestrator
- [x] Create server/seed.ts with default agents (orchestrator, coder, browser, researcher)
- [x] Create default orchestrator config seed
- [x] Integrate seed into server startup (idempotent — runs only when tables are empty)
- [x] Write vitest tests for seed logic (18 tests, all pass)
- [ ] Commit to Gitea and deploy to production server
- [ ] Verify seed data on production DB