@desplega.ai/agent-swarm 1.0.1 → 1.2.0
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/.claude/settings.local.json +2 -1
- package/.dockerignore +58 -0
- package/.env.docker.example +12 -0
- package/Dockerfile +16 -0
- package/Dockerfile.worker +112 -0
- package/README.md +117 -0
- package/cc-plugin/.claude-plugin/plugin.json +13 -0
- package/cc-plugin/README.md +49 -0
- package/cc-plugin/commands/setup-leader.md +73 -0
- package/cc-plugin/commands/start-worker.md +64 -0
- package/cc-plugin/hooks/hooks.json +71 -0
- package/deploy/DEPLOY.md +60 -0
- package/deploy/agent-swarm.service +17 -0
- package/deploy/install.ts +85 -0
- package/deploy/prod-db.ts +42 -0
- package/deploy/uninstall.ts +12 -0
- package/deploy/update.ts +21 -0
- package/docker-compose.worker.yml +35 -0
- package/docker-entrypoint.sh +62 -0
- package/package.json +9 -2
- package/src/be/db.ts +204 -2
- package/src/cli.tsx +96 -8
- package/src/commands/hook.ts +2 -2
- package/src/commands/setup.tsx +579 -548
- package/src/commands/worker.ts +225 -0
- package/src/hooks/hook.ts +180 -175
- package/src/http.ts +12 -4
- package/src/server.ts +1 -2
- package/src/tools/get-task-details.ts +7 -3
- package/src/tools/join-swarm.ts +23 -11
- package/src/tools/poll-task.ts +34 -2
- package/src/tools/send-task.ts +40 -2
- package/src/tools/store-progress.ts +29 -0
- package/src/types.ts +24 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Agent Swarm MCP Server
|
|
3
|
+
After=network.target
|
|
4
|
+
|
|
5
|
+
[Service]
|
|
6
|
+
Type=simple
|
|
7
|
+
User=root
|
|
8
|
+
Group=root
|
|
9
|
+
WorkingDirectory=/opt/agent-swarm
|
|
10
|
+
ExecStart=/usr/local/bin/bun run start:http
|
|
11
|
+
ExecStartPost=/bin/sh -c 'sleep 2 && curl -sf http://localhost:3013/health || exit 1'
|
|
12
|
+
Restart=always
|
|
13
|
+
RestartSec=5
|
|
14
|
+
EnvironmentFile=/opt/agent-swarm/.env
|
|
15
|
+
|
|
16
|
+
[Install]
|
|
17
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
|
|
5
|
+
const APP_DIR = "/opt/agent-swarm";
|
|
6
|
+
const SERVICE_FILE = "/etc/systemd/system/agent-swarm.service";
|
|
7
|
+
const SCRIPT_DIR = import.meta.dir;
|
|
8
|
+
const PROJECT_DIR = `${SCRIPT_DIR}/..`;
|
|
9
|
+
|
|
10
|
+
// Detect bun path
|
|
11
|
+
const bunPath = (await $`which bun`.text()).trim();
|
|
12
|
+
console.log(`Using bun at: ${bunPath}`);
|
|
13
|
+
|
|
14
|
+
// Copy project files
|
|
15
|
+
await $`mkdir -p ${APP_DIR}`;
|
|
16
|
+
await $`cp -r ${PROJECT_DIR}/src ${APP_DIR}/`;
|
|
17
|
+
await $`cp ${PROJECT_DIR}/package.json ${PROJECT_DIR}/bun.lock ${PROJECT_DIR}/tsconfig.json ${APP_DIR}/`;
|
|
18
|
+
|
|
19
|
+
// Install dependencies
|
|
20
|
+
await $`cd ${APP_DIR} && bun install --frozen-lockfile --production`;
|
|
21
|
+
|
|
22
|
+
// Create .env if not exists
|
|
23
|
+
const envFile = Bun.file(`${APP_DIR}/.env`);
|
|
24
|
+
if (!(await envFile.exists())) {
|
|
25
|
+
await Bun.write(envFile, `PORT=3013
|
|
26
|
+
API_KEY=
|
|
27
|
+
`);
|
|
28
|
+
console.log("Created .env - set API_KEY for authentication");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Set ownership
|
|
32
|
+
await $`chown -R root:root ${APP_DIR}`;
|
|
33
|
+
|
|
34
|
+
// Install systemd service with detected bun path
|
|
35
|
+
const serviceContent = `[Unit]
|
|
36
|
+
Description=Agent Swarm MCP Server
|
|
37
|
+
After=network.target
|
|
38
|
+
|
|
39
|
+
[Service]
|
|
40
|
+
Type=simple
|
|
41
|
+
User=root
|
|
42
|
+
Group=root
|
|
43
|
+
WorkingDirectory=${APP_DIR}
|
|
44
|
+
ExecStart=${bunPath} run start:http
|
|
45
|
+
ExecStartPost=/bin/sh -c 'sleep 2 && curl -sf http://localhost:3013/health || exit 1'
|
|
46
|
+
Restart=always
|
|
47
|
+
RestartSec=5
|
|
48
|
+
EnvironmentFile=${APP_DIR}/.env
|
|
49
|
+
|
|
50
|
+
[Install]
|
|
51
|
+
WantedBy=multi-user.target
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
await Bun.write(SERVICE_FILE, serviceContent);
|
|
55
|
+
|
|
56
|
+
// Healthcheck service (runs curl, restarts main service on failure)
|
|
57
|
+
const healthcheckService = `[Unit]
|
|
58
|
+
Description=Agent Swarm Health Check
|
|
59
|
+
|
|
60
|
+
[Service]
|
|
61
|
+
Type=oneshot
|
|
62
|
+
ExecStart=/bin/sh -c 'curl -sf http://localhost:3013/health || systemctl restart agent-swarm'
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
// Timer to run healthcheck every 30 seconds
|
|
66
|
+
const healthcheckTimer = `[Unit]
|
|
67
|
+
Description=Agent Swarm Health Check Timer
|
|
68
|
+
|
|
69
|
+
[Timer]
|
|
70
|
+
OnBootSec=30s
|
|
71
|
+
OnUnitActiveSec=30s
|
|
72
|
+
|
|
73
|
+
[Install]
|
|
74
|
+
WantedBy=timers.target
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
await Bun.write("/etc/systemd/system/agent-swarm-healthcheck.service", healthcheckService);
|
|
78
|
+
await Bun.write("/etc/systemd/system/agent-swarm-healthcheck.timer", healthcheckTimer);
|
|
79
|
+
|
|
80
|
+
await $`systemctl daemon-reload`;
|
|
81
|
+
await $`systemctl enable agent-swarm agent-swarm-healthcheck.timer`;
|
|
82
|
+
await $`systemctl restart agent-swarm`;
|
|
83
|
+
await $`systemctl start agent-swarm-healthcheck.timer`;
|
|
84
|
+
|
|
85
|
+
console.log("Installed and running with health checks every 30s.");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
import * as readline from "node:readline";
|
|
5
|
+
|
|
6
|
+
const DB_PATH = "/opt/agent-swarm/agent-swarm-db.sqlite";
|
|
7
|
+
const SSH_HOST = process.argv[2] || "hetzner";
|
|
8
|
+
|
|
9
|
+
console.log(`Connected to ${SSH_HOST}:${DB_PATH}`);
|
|
10
|
+
console.log("Type SQL queries or .tables, .schema, etc. Ctrl+C to exit.\n");
|
|
11
|
+
|
|
12
|
+
const rl = readline.createInterface({
|
|
13
|
+
input: process.stdin,
|
|
14
|
+
output: process.stdout,
|
|
15
|
+
prompt: "sqlite> ",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
rl.prompt();
|
|
19
|
+
|
|
20
|
+
rl.on("line", async (line) => {
|
|
21
|
+
const query = line.trim();
|
|
22
|
+
if (!query) {
|
|
23
|
+
rl.prompt();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Escape single quotes in query and wrap in single quotes for remote shell
|
|
29
|
+
const escaped = query.replace(/'/g, "'\\''");
|
|
30
|
+
const result = await $`ssh ${SSH_HOST} sqlite3 -header -column ${DB_PATH} ${"'" + escaped + "'"}`.text();
|
|
31
|
+
if (result) console.log(result);
|
|
32
|
+
} catch (e: any) {
|
|
33
|
+
console.error(e.stderr?.toString() || e.message);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
rl.prompt();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
rl.on("close", () => {
|
|
40
|
+
console.log("\nBye!");
|
|
41
|
+
process.exit(0);
|
|
42
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
|
|
5
|
+
await $`systemctl stop agent-swarm agent-swarm-healthcheck.timer`.nothrow();
|
|
6
|
+
await $`systemctl disable agent-swarm agent-swarm-healthcheck.timer`.nothrow();
|
|
7
|
+
await $`rm -f /etc/systemd/system/agent-swarm.service`;
|
|
8
|
+
await $`rm -f /etc/systemd/system/agent-swarm-healthcheck.service`;
|
|
9
|
+
await $`rm -f /etc/systemd/system/agent-swarm-healthcheck.timer`;
|
|
10
|
+
await $`systemctl daemon-reload`;
|
|
11
|
+
|
|
12
|
+
console.log("Service removed. Data remains at /opt/agent-swarm");
|
package/deploy/update.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
|
|
5
|
+
const APP_DIR = "/opt/agent-swarm";
|
|
6
|
+
const SCRIPT_DIR = import.meta.dir;
|
|
7
|
+
const PROJECT_DIR = `${SCRIPT_DIR}/..`;
|
|
8
|
+
|
|
9
|
+
console.log("Updating agent-swarm...");
|
|
10
|
+
|
|
11
|
+
// Copy project files
|
|
12
|
+
await $`cp -r ${PROJECT_DIR}/src ${APP_DIR}/`;
|
|
13
|
+
await $`cp ${PROJECT_DIR}/package.json ${PROJECT_DIR}/bun.lock ${PROJECT_DIR}/tsconfig.json ${APP_DIR}/`;
|
|
14
|
+
|
|
15
|
+
// Install dependencies
|
|
16
|
+
await $`cd ${APP_DIR} && bun install --frozen-lockfile --production`;
|
|
17
|
+
|
|
18
|
+
// Restart service
|
|
19
|
+
await $`systemctl restart agent-swarm`;
|
|
20
|
+
|
|
21
|
+
console.log("Updated and restarted.");
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Docker Compose for Agent Swarm Worker
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# docker-compose -f docker-compose.worker.yml up --build
|
|
5
|
+
#
|
|
6
|
+
# Required environment variables (in .env.docker or passed directly):
|
|
7
|
+
# - CLAUDE_CODE_OAUTH_TOKEN: OAuth token for Claude CLI
|
|
8
|
+
# - API_KEY: API key for MCP server authentication
|
|
9
|
+
#
|
|
10
|
+
# Optional environment variables:
|
|
11
|
+
# - AGENT_ID: UUID for agent identification (assigned on join if not set)
|
|
12
|
+
# - MCP_BASE_URL: MCP server URL (default: http://host.docker.internal:3013)
|
|
13
|
+
# - SESSION_ID: Folder name for logs (auto-generated if not provided)
|
|
14
|
+
# - WORKER_YOLO: If "true", continue on errors (default: false)
|
|
15
|
+
|
|
16
|
+
services:
|
|
17
|
+
worker:
|
|
18
|
+
build:
|
|
19
|
+
context: .
|
|
20
|
+
dockerfile: Dockerfile.worker
|
|
21
|
+
environment:
|
|
22
|
+
- CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN}
|
|
23
|
+
- API_KEY=${API_KEY}
|
|
24
|
+
- AGENT_ID=${AGENT_ID:-}
|
|
25
|
+
# Use host.docker.internal to reach host's localhost on Mac/Windows
|
|
26
|
+
# On Linux, use --add-host=host.docker.internal:host-gateway or --network host
|
|
27
|
+
- MCP_BASE_URL=${MCP_BASE_URL:-http://host.docker.internal:3013}
|
|
28
|
+
- SESSION_ID=${SESSION_ID:-}
|
|
29
|
+
- WORKER_YOLO=${WORKER_YOLO:-false}
|
|
30
|
+
volumes:
|
|
31
|
+
- ./logs:/logs
|
|
32
|
+
# Optional: mount a workspace directory for persistent work
|
|
33
|
+
# - ./workspace:/workspace
|
|
34
|
+
restart: unless-stopped
|
|
35
|
+
# No port mappings needed - worker only makes outbound connections
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Validate required environment variables
|
|
5
|
+
if [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
|
|
6
|
+
echo "Error: CLAUDE_CODE_OAUTH_TOKEN environment variable is required"
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
if [ -z "$API_KEY" ]; then
|
|
11
|
+
echo "Error: API_KEY environment variable is required"
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
MCP_URL="${MCP_BASE_URL:-http://host.docker.internal:3013}"
|
|
16
|
+
|
|
17
|
+
echo "=== Agent Swarm Worker ==="
|
|
18
|
+
echo "Agent ID: ${AGENT_ID:-<not set>}"
|
|
19
|
+
echo "MCP Base URL: $MCP_URL"
|
|
20
|
+
echo "YOLO Mode: ${WORKER_YOLO:-false}"
|
|
21
|
+
echo "Session ID: ${SESSION_ID:-<auto-generated>}"
|
|
22
|
+
echo "Working Directory: /workspace"
|
|
23
|
+
echo "=========================="
|
|
24
|
+
|
|
25
|
+
# Create .mcp.json in /workspace (project-level config)
|
|
26
|
+
echo "Creating MCP config in /workspace..."
|
|
27
|
+
if [ -n "$AGENT_ID" ]; then
|
|
28
|
+
# With AGENT_ID header
|
|
29
|
+
cat > /workspace/.mcp.json << EOF
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"agent-swarm": {
|
|
33
|
+
"type": "http",
|
|
34
|
+
"url": "${MCP_URL}/mcp",
|
|
35
|
+
"headers": {
|
|
36
|
+
"Authorization": "Bearer ${API_KEY}",
|
|
37
|
+
"X-Agent-ID": "${AGENT_ID}"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
EOF
|
|
43
|
+
else
|
|
44
|
+
# Without AGENT_ID header
|
|
45
|
+
cat > /workspace/.mcp.json << EOF
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"agent-swarm": {
|
|
49
|
+
"type": "http",
|
|
50
|
+
"url": "${MCP_URL}/mcp",
|
|
51
|
+
"headers": {
|
|
52
|
+
"Authorization": "Bearer ${API_KEY}"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
EOF
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Run the worker using compiled binary
|
|
61
|
+
echo "Starting worker..."
|
|
62
|
+
exec /usr/local/bin/agent-swarm worker "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Agent orchestration layer MCP for Claude Code, Codex, Gemini CLI, and more!",
|
|
5
5
|
"module": "src/stdio.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"hook": "bun src/hooks/hook.ts",
|
|
18
18
|
"claude": "bun src/cli.tsx claude",
|
|
19
19
|
"claude:headless": "bun src/cli.tsx claude --headless",
|
|
20
|
+
"worker": "bun src/cli.tsx worker",
|
|
21
|
+
"worker:yolo": "bun src/cli.tsx worker --yolo",
|
|
20
22
|
"dev": "bun --hot src/stdio.ts",
|
|
21
23
|
"dev:http": "bun --hot src/http.ts",
|
|
22
24
|
"start": "bun src/stdio.ts",
|
|
@@ -25,7 +27,11 @@
|
|
|
25
27
|
"inspector:http": "bunx @modelcontextprotocol/inspector --transport http http://localhost:3013/mcp",
|
|
26
28
|
"lint": "biome check src",
|
|
27
29
|
"lint:fix": "biome check --write src",
|
|
28
|
-
"format": "biome format --write src"
|
|
30
|
+
"format": "biome format --write src",
|
|
31
|
+
"build:binary": "bun build ./src/cli.tsx --compile --target=bun-linux-x64 --outfile ./dist/agent-swarm",
|
|
32
|
+
"build:binary:arm64": "bun build ./src/cli.tsx --compile --target=bun-linux-arm64 --outfile ./dist/agent-swarm",
|
|
33
|
+
"docker:build:worker": "docker build -f Dockerfile.worker -t agent-swarm-worker .",
|
|
34
|
+
"docker:run:worker": "docker run --rm -it --env-file .env.docker -v ./logs:/logs agent-swarm-worker"
|
|
29
35
|
},
|
|
30
36
|
"devDependencies": {
|
|
31
37
|
"@biomejs/biome": "^2.3.9",
|
|
@@ -41,6 +47,7 @@
|
|
|
41
47
|
"date-fns": "^4.1.0",
|
|
42
48
|
"ink": "^6.5.1",
|
|
43
49
|
"react": "^19.2.3",
|
|
50
|
+
"react-devtools-core": "^7.0.1",
|
|
44
51
|
"zod": "^4.2.1"
|
|
45
52
|
}
|
|
46
53
|
}
|
package/src/be/db.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Agent,
|
|
4
|
+
AgentLog,
|
|
5
|
+
AgentLogEventType,
|
|
6
|
+
AgentStatus,
|
|
7
|
+
AgentTask,
|
|
8
|
+
AgentTaskStatus,
|
|
9
|
+
AgentWithTasks,
|
|
10
|
+
} from "../types";
|
|
3
11
|
|
|
4
12
|
let db: Database | null = null;
|
|
5
13
|
|
|
6
|
-
export function initDb(dbPath = "./
|
|
14
|
+
export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
7
15
|
if (db) {
|
|
8
16
|
return db;
|
|
9
17
|
}
|
|
@@ -38,6 +46,22 @@ export function initDb(dbPath = "./cc-orch.sqlite"): Database {
|
|
|
38
46
|
|
|
39
47
|
CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentId ON agent_tasks(agentId);
|
|
40
48
|
CREATE INDEX IF NOT EXISTS idx_agent_tasks_status ON agent_tasks(status);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS agent_log (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
eventType TEXT NOT NULL,
|
|
53
|
+
agentId TEXT,
|
|
54
|
+
taskId TEXT,
|
|
55
|
+
oldValue TEXT,
|
|
56
|
+
newValue TEXT,
|
|
57
|
+
metadata TEXT,
|
|
58
|
+
createdAt TEXT NOT NULL
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_agent_log_agentId ON agent_log(agentId);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_agent_log_taskId ON agent_log(taskId);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_agent_log_eventType ON agent_log(eventType);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_agent_log_createdAt ON agent_log(createdAt);
|
|
41
65
|
`);
|
|
42
66
|
|
|
43
67
|
return db;
|
|
@@ -105,6 +129,9 @@ export function createAgent(
|
|
|
105
129
|
const id = agent.id ?? crypto.randomUUID();
|
|
106
130
|
const row = agentQueries.insert().get(id, agent.name, agent.isLead ? 1 : 0, agent.status);
|
|
107
131
|
if (!row) throw new Error("Failed to create agent");
|
|
132
|
+
try {
|
|
133
|
+
createLogEntry({ eventType: "agent_joined", agentId: id, newValue: agent.status });
|
|
134
|
+
} catch {}
|
|
108
135
|
return rowToAgent(row);
|
|
109
136
|
}
|
|
110
137
|
|
|
@@ -118,11 +145,28 @@ export function getAllAgents(): Agent[] {
|
|
|
118
145
|
}
|
|
119
146
|
|
|
120
147
|
export function updateAgentStatus(id: string, status: AgentStatus): Agent | null {
|
|
148
|
+
const oldAgent = getAgentById(id);
|
|
121
149
|
const row = agentQueries.updateStatus().get(status, id);
|
|
150
|
+
if (row && oldAgent) {
|
|
151
|
+
try {
|
|
152
|
+
createLogEntry({
|
|
153
|
+
eventType: "agent_status_change",
|
|
154
|
+
agentId: id,
|
|
155
|
+
oldValue: oldAgent.status,
|
|
156
|
+
newValue: status,
|
|
157
|
+
});
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
122
160
|
return row ? rowToAgent(row) : null;
|
|
123
161
|
}
|
|
124
162
|
|
|
125
163
|
export function deleteAgent(id: string): boolean {
|
|
164
|
+
const agent = getAgentById(id);
|
|
165
|
+
if (agent) {
|
|
166
|
+
try {
|
|
167
|
+
createLogEntry({ eventType: "agent_left", agentId: id, oldValue: agent.status });
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
126
170
|
const result = getDb().run("DELETE FROM agents WHERE id = ?", [id]);
|
|
127
171
|
return result.changes > 0;
|
|
128
172
|
}
|
|
@@ -206,6 +250,9 @@ export function createTask(agentId: string, task: string): AgentTask {
|
|
|
206
250
|
const id = crypto.randomUUID();
|
|
207
251
|
const row = taskQueries.insert().get(id, agentId, task, "pending");
|
|
208
252
|
if (!row) throw new Error("Failed to create task");
|
|
253
|
+
try {
|
|
254
|
+
createLogEntry({ eventType: "task_created", agentId, taskId: id, newValue: "pending" });
|
|
255
|
+
} catch {}
|
|
209
256
|
return rowToAgentTask(row);
|
|
210
257
|
}
|
|
211
258
|
|
|
@@ -219,12 +266,24 @@ export function getPendingTaskForAgent(agentId: string): AgentTask | null {
|
|
|
219
266
|
}
|
|
220
267
|
|
|
221
268
|
export function startTask(taskId: string): AgentTask | null {
|
|
269
|
+
const oldTask = getTaskById(taskId);
|
|
222
270
|
const row = getDb()
|
|
223
271
|
.prepare<AgentTaskRow, [string]>(
|
|
224
272
|
`UPDATE agent_tasks SET status = 'in_progress', lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
225
273
|
WHERE id = ? RETURNING *`,
|
|
226
274
|
)
|
|
227
275
|
.get(taskId);
|
|
276
|
+
if (row && oldTask) {
|
|
277
|
+
try {
|
|
278
|
+
createLogEntry({
|
|
279
|
+
eventType: "task_status_change",
|
|
280
|
+
taskId,
|
|
281
|
+
agentId: row.agentId,
|
|
282
|
+
oldValue: oldTask.status,
|
|
283
|
+
newValue: "in_progress",
|
|
284
|
+
});
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
228
287
|
return row ? rowToAgentTask(row) : null;
|
|
229
288
|
}
|
|
230
289
|
|
|
@@ -257,6 +316,7 @@ export function getAllTasks(status?: AgentTaskStatus): AgentTask[] {
|
|
|
257
316
|
}
|
|
258
317
|
|
|
259
318
|
export function completeTask(id: string, output?: string): AgentTask | null {
|
|
319
|
+
const oldTask = getTaskById(id);
|
|
260
320
|
const finishedAt = new Date().toISOString();
|
|
261
321
|
let row = taskQueries.updateStatus().get("completed", finishedAt, id);
|
|
262
322
|
if (!row) return null;
|
|
@@ -265,12 +325,37 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
265
325
|
row = taskQueries.setOutput().get(output, id);
|
|
266
326
|
}
|
|
267
327
|
|
|
328
|
+
if (row && oldTask) {
|
|
329
|
+
try {
|
|
330
|
+
createLogEntry({
|
|
331
|
+
eventType: "task_status_change",
|
|
332
|
+
taskId: id,
|
|
333
|
+
agentId: row.agentId,
|
|
334
|
+
oldValue: oldTask.status,
|
|
335
|
+
newValue: "completed",
|
|
336
|
+
});
|
|
337
|
+
} catch {}
|
|
338
|
+
}
|
|
339
|
+
|
|
268
340
|
return row ? rowToAgentTask(row) : null;
|
|
269
341
|
}
|
|
270
342
|
|
|
271
343
|
export function failTask(id: string, reason: string): AgentTask | null {
|
|
344
|
+
const oldTask = getTaskById(id);
|
|
272
345
|
const finishedAt = new Date().toISOString();
|
|
273
346
|
const row = taskQueries.setFailure().get(reason, finishedAt, id);
|
|
347
|
+
if (row && oldTask) {
|
|
348
|
+
try {
|
|
349
|
+
createLogEntry({
|
|
350
|
+
eventType: "task_status_change",
|
|
351
|
+
taskId: id,
|
|
352
|
+
agentId: row.agentId,
|
|
353
|
+
oldValue: oldTask.status,
|
|
354
|
+
newValue: "failed",
|
|
355
|
+
metadata: { reason },
|
|
356
|
+
});
|
|
357
|
+
} catch {}
|
|
358
|
+
}
|
|
274
359
|
return row ? rowToAgentTask(row) : null;
|
|
275
360
|
}
|
|
276
361
|
|
|
@@ -281,6 +366,16 @@ export function deleteTask(id: string): boolean {
|
|
|
281
366
|
|
|
282
367
|
export function updateTaskProgress(id: string, progress: string): AgentTask | null {
|
|
283
368
|
const row = taskQueries.setProgress().get(progress, id);
|
|
369
|
+
if (row) {
|
|
370
|
+
try {
|
|
371
|
+
createLogEntry({
|
|
372
|
+
eventType: "task_progress",
|
|
373
|
+
taskId: id,
|
|
374
|
+
agentId: row.agentId,
|
|
375
|
+
newValue: progress,
|
|
376
|
+
});
|
|
377
|
+
} catch {}
|
|
378
|
+
}
|
|
284
379
|
return row ? rowToAgentTask(row) : null;
|
|
285
380
|
}
|
|
286
381
|
|
|
@@ -311,3 +406,110 @@ export function getAllAgentsWithTasks(): AgentWithTasks[] {
|
|
|
311
406
|
|
|
312
407
|
return txn();
|
|
313
408
|
}
|
|
409
|
+
|
|
410
|
+
// ============================================================================
|
|
411
|
+
// Agent Log Queries
|
|
412
|
+
// ============================================================================
|
|
413
|
+
|
|
414
|
+
type AgentLogRow = {
|
|
415
|
+
id: string;
|
|
416
|
+
eventType: AgentLogEventType;
|
|
417
|
+
agentId: string | null;
|
|
418
|
+
taskId: string | null;
|
|
419
|
+
oldValue: string | null;
|
|
420
|
+
newValue: string | null;
|
|
421
|
+
metadata: string | null;
|
|
422
|
+
createdAt: string;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
function rowToAgentLog(row: AgentLogRow): AgentLog {
|
|
426
|
+
return {
|
|
427
|
+
id: row.id,
|
|
428
|
+
eventType: row.eventType,
|
|
429
|
+
agentId: row.agentId ?? undefined,
|
|
430
|
+
taskId: row.taskId ?? undefined,
|
|
431
|
+
oldValue: row.oldValue ?? undefined,
|
|
432
|
+
newValue: row.newValue ?? undefined,
|
|
433
|
+
metadata: row.metadata ?? undefined,
|
|
434
|
+
createdAt: row.createdAt,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export const logQueries = {
|
|
439
|
+
insert: () =>
|
|
440
|
+
getDb().prepare<
|
|
441
|
+
AgentLogRow,
|
|
442
|
+
[string, string, string | null, string | null, string | null, string | null, string | null]
|
|
443
|
+
>(
|
|
444
|
+
`INSERT INTO agent_log (id, eventType, agentId, taskId, oldValue, newValue, metadata, createdAt)
|
|
445
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *`,
|
|
446
|
+
),
|
|
447
|
+
|
|
448
|
+
getByAgentId: () =>
|
|
449
|
+
getDb().prepare<AgentLogRow, [string]>(
|
|
450
|
+
"SELECT * FROM agent_log WHERE agentId = ? ORDER BY createdAt DESC",
|
|
451
|
+
),
|
|
452
|
+
|
|
453
|
+
getByTaskId: () =>
|
|
454
|
+
getDb().prepare<AgentLogRow, [string]>(
|
|
455
|
+
"SELECT * FROM agent_log WHERE taskId = ? ORDER BY createdAt DESC",
|
|
456
|
+
),
|
|
457
|
+
|
|
458
|
+
getByEventType: () =>
|
|
459
|
+
getDb().prepare<AgentLogRow, [string]>(
|
|
460
|
+
"SELECT * FROM agent_log WHERE eventType = ? ORDER BY createdAt DESC",
|
|
461
|
+
),
|
|
462
|
+
|
|
463
|
+
getAll: () => getDb().prepare<AgentLogRow, []>("SELECT * FROM agent_log ORDER BY createdAt DESC"),
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
export function createLogEntry(entry: {
|
|
467
|
+
eventType: AgentLogEventType;
|
|
468
|
+
agentId?: string;
|
|
469
|
+
taskId?: string;
|
|
470
|
+
oldValue?: string;
|
|
471
|
+
newValue?: string;
|
|
472
|
+
metadata?: Record<string, unknown>;
|
|
473
|
+
}): AgentLog {
|
|
474
|
+
const id = crypto.randomUUID();
|
|
475
|
+
const row = logQueries
|
|
476
|
+
.insert()
|
|
477
|
+
.get(
|
|
478
|
+
id,
|
|
479
|
+
entry.eventType,
|
|
480
|
+
entry.agentId ?? null,
|
|
481
|
+
entry.taskId ?? null,
|
|
482
|
+
entry.oldValue ?? null,
|
|
483
|
+
entry.newValue ?? null,
|
|
484
|
+
entry.metadata ? JSON.stringify(entry.metadata) : null,
|
|
485
|
+
);
|
|
486
|
+
if (!row) throw new Error("Failed to create log entry");
|
|
487
|
+
return rowToAgentLog(row);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function getLogsByAgentId(agentId: string): AgentLog[] {
|
|
491
|
+
return logQueries.getByAgentId().all(agentId).map(rowToAgentLog);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function getLogsByTaskId(taskId: string): AgentLog[] {
|
|
495
|
+
return logQueries.getByTaskId().all(taskId).map(rowToAgentLog);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function getLogsByTaskIdChronological(taskId: string): AgentLog[] {
|
|
499
|
+
return getDb()
|
|
500
|
+
.prepare<AgentLogRow, [string]>(
|
|
501
|
+
"SELECT * FROM agent_log WHERE taskId = ? ORDER BY createdAt ASC",
|
|
502
|
+
)
|
|
503
|
+
.all(taskId)
|
|
504
|
+
.map(rowToAgentLog);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function getAllLogs(limit?: number): AgentLog[] {
|
|
508
|
+
if (limit) {
|
|
509
|
+
return getDb()
|
|
510
|
+
.prepare<AgentLogRow, [number]>("SELECT * FROM agent_log ORDER BY createdAt DESC LIMIT ?")
|
|
511
|
+
.all(limit)
|
|
512
|
+
.map(rowToAgentLog);
|
|
513
|
+
}
|
|
514
|
+
return logQueries.getAll().all().map(rowToAgentLog);
|
|
515
|
+
}
|