feat(agents): restore agent-worker container architecture + fix chat scroll and parallel chats
- Restore agent-worker from commit 153399f: autonomous HTTP server per agent
(main.go 597 lines, main_test.go 438 lines, Dockerfile.agent-worker)
- Add container fields to agents table (serviceName, servicePort, containerImage, containerStatus)
- Update executor.go: real delegateToAgent() with HTTP POST to agent containers
- Update db.go: GetAgentByID, UpdateContainerStatus, GetAgentHistory, SaveHistory
- Update orchestrator.go: inject DB into executor for container address resolution
- Add tRPC endpoints: agents.deployContainer, agents.stopContainer, agents.containerStatus
- Add Docker Swarm deploy/stop logic in server/agents.ts
- Add Start/Stop container buttons to Agents.tsx with status badges
- Fix chat auto-scroll: replace ScrollArea with overflow-y-auto for direct scrollTop control
- Fix parallel chats: make isThinking per-conversation (thinkingConvId) instead of global
so switching between chats works while one is processing
This commit is contained in:
320
server/agents.ts
320
server/agents.ts
@@ -1,12 +1,28 @@
|
||||
import { eq, and, desc, gte } from "drizzle-orm";
|
||||
import { agents, agentMetrics, agentHistory, agentAccessControl, type Agent, type InsertAgent, type AgentMetric, type InsertAgentMetric } from "../drizzle/schema";
|
||||
import {
|
||||
agents,
|
||||
agentMetrics,
|
||||
agentHistory,
|
||||
agentAccessControl,
|
||||
type Agent,
|
||||
type InsertAgent,
|
||||
type AgentMetric,
|
||||
type InsertAgentMetric,
|
||||
} from "../drizzle/schema";
|
||||
import { getDb } from "./db";
|
||||
import { nanoid } from "nanoid";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* Создать нового агента
|
||||
*/
|
||||
export async function createAgent(userId: number, data: InsertAgent): Promise<Agent | null> {
|
||||
export async function createAgent(
|
||||
userId: number,
|
||||
data: InsertAgent
|
||||
): Promise<Agent | null> {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
|
||||
@@ -17,7 +33,11 @@ export async function createAgent(userId: number, data: InsertAgent): Promise<Ag
|
||||
});
|
||||
|
||||
const agentId = result[0].insertId;
|
||||
const created = await db.select().from(agents).where(eq(agents.id, Number(agentId))).limit(1);
|
||||
const created = await db
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(eq(agents.id, Number(agentId)))
|
||||
.limit(1);
|
||||
return created[0] || null;
|
||||
} catch (error) {
|
||||
console.error("[DB] Failed to create agent:", error);
|
||||
@@ -33,7 +53,11 @@ export async function getAgentById(agentId: number): Promise<Agent | null> {
|
||||
if (!db) return null;
|
||||
|
||||
try {
|
||||
const result = await db.select().from(agents).where(eq(agents.id, agentId)).limit(1);
|
||||
const result = await db
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(eq(agents.id, agentId))
|
||||
.limit(1);
|
||||
return result[0] || null;
|
||||
} catch (error) {
|
||||
console.error("[DB] Failed to get agent:", error);
|
||||
@@ -80,7 +104,11 @@ export async function getSystemAgents(): Promise<Agent[]> {
|
||||
if (!db) return [];
|
||||
|
||||
try {
|
||||
return await db.select().from(agents).where(eq(agents.isSystem, true)).orderBy(agents.id);
|
||||
return await db
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(eq(agents.isSystem, true))
|
||||
.orderBy(agents.id);
|
||||
} catch (error) {
|
||||
console.error("[DB] Failed to get system agents:", error);
|
||||
return [];
|
||||
@@ -90,7 +118,10 @@ export async function getSystemAgents(): Promise<Agent[]> {
|
||||
/**
|
||||
* Обновить конфигурацию агента
|
||||
*/
|
||||
export async function updateAgent(agentId: number, updates: Partial<InsertAgent>): Promise<Agent | null> {
|
||||
export async function updateAgent(
|
||||
agentId: number,
|
||||
updates: Partial<InsertAgent>
|
||||
): Promise<Agent | null> {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
|
||||
@@ -122,7 +153,10 @@ export async function deleteAgent(agentId: number): Promise<boolean> {
|
||||
/**
|
||||
* Сохранить метрику запроса
|
||||
*/
|
||||
export async function saveMetric(agentId: number, data: Omit<InsertAgentMetric, "agentId">): Promise<AgentMetric | null> {
|
||||
export async function saveMetric(
|
||||
agentId: number,
|
||||
data: Omit<InsertAgentMetric, "agentId">
|
||||
): Promise<AgentMetric | null> {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
|
||||
@@ -135,7 +169,11 @@ export async function saveMetric(agentId: number, data: Omit<InsertAgentMetric,
|
||||
});
|
||||
|
||||
const metricId = result[0].insertId;
|
||||
const created = await db.select().from(agentMetrics).where(eq(agentMetrics.id, Number(metricId))).limit(1);
|
||||
const created = await db
|
||||
.select()
|
||||
.from(agentMetrics)
|
||||
.where(eq(agentMetrics.id, Number(metricId)))
|
||||
.limit(1);
|
||||
return created[0] || null;
|
||||
} catch (error) {
|
||||
console.error("[DB] Failed to save metric:", error);
|
||||
@@ -146,7 +184,10 @@ export async function saveMetric(agentId: number, data: Omit<InsertAgentMetric,
|
||||
/**
|
||||
* Получить метрики агента за последние N часов
|
||||
*/
|
||||
export async function getAgentMetrics(agentId: number, hoursBack: number = 24): Promise<AgentMetric[]> {
|
||||
export async function getAgentMetrics(
|
||||
agentId: number,
|
||||
hoursBack: number = 24
|
||||
): Promise<AgentMetric[]> {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
@@ -155,7 +196,12 @@ export async function getAgentMetrics(agentId: number, hoursBack: number = 24):
|
||||
return await db
|
||||
.select()
|
||||
.from(agentMetrics)
|
||||
.where(and(eq(agentMetrics.agentId, agentId), gte(agentMetrics.createdAt, since)))
|
||||
.where(
|
||||
and(
|
||||
eq(agentMetrics.agentId, agentId),
|
||||
gte(agentMetrics.createdAt, since)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(agentMetrics.createdAt));
|
||||
} catch (error) {
|
||||
console.error("[DB] Failed to get agent metrics:", error);
|
||||
@@ -174,17 +220,26 @@ export async function getAgentStats(agentId: number, hoursBack: number = 24) {
|
||||
const metrics = await getAgentMetrics(agentId, hoursBack);
|
||||
|
||||
const totalRequests = metrics.length;
|
||||
const successRequests = metrics.filter((m) => m.status === "success").length;
|
||||
const errorRequests = metrics.filter((m) => m.status === "error").length;
|
||||
const avgProcessingTime = metrics.length > 0 ? metrics.reduce((sum, m) => sum + m.processingTimeMs, 0) / metrics.length : 0;
|
||||
const totalTokens = metrics.reduce((sum, m) => sum + (m.totalTokens || 0), 0);
|
||||
const avgTokensPerRequest = metrics.length > 0 ? totalTokens / metrics.length : 0;
|
||||
const successRequests = metrics.filter(m => m.status === "success").length;
|
||||
const errorRequests = metrics.filter(m => m.status === "error").length;
|
||||
const avgProcessingTime =
|
||||
metrics.length > 0
|
||||
? metrics.reduce((sum, m) => sum + m.processingTimeMs, 0) /
|
||||
metrics.length
|
||||
: 0;
|
||||
const totalTokens = metrics.reduce(
|
||||
(sum, m) => sum + (m.totalTokens || 0),
|
||||
0
|
||||
);
|
||||
const avgTokensPerRequest =
|
||||
metrics.length > 0 ? totalTokens / metrics.length : 0;
|
||||
|
||||
return {
|
||||
totalRequests,
|
||||
successRequests,
|
||||
errorRequests,
|
||||
successRate: totalRequests > 0 ? (successRequests / totalRequests) * 100 : 0,
|
||||
successRate:
|
||||
totalRequests > 0 ? (successRequests / totalRequests) * 100 : 0,
|
||||
avgProcessingTime: Math.round(avgProcessingTime),
|
||||
totalTokens,
|
||||
avgTokensPerRequest: Math.round(avgTokensPerRequest),
|
||||
@@ -224,7 +279,10 @@ export async function getAgentAccessControl(agentId: number) {
|
||||
if (!db) return [];
|
||||
|
||||
try {
|
||||
return await db.select().from(agentAccessControl).where(eq(agentAccessControl.agentId, agentId));
|
||||
return await db
|
||||
.select()
|
||||
.from(agentAccessControl)
|
||||
.where(eq(agentAccessControl.agentId, agentId));
|
||||
} catch (error) {
|
||||
console.error("[DB] Failed to get agent access control:", error);
|
||||
return [];
|
||||
@@ -234,7 +292,11 @@ export async function getAgentAccessControl(agentId: number) {
|
||||
/**
|
||||
* Обновить управление доступами для инструмента
|
||||
*/
|
||||
export async function updateToolAccess(agentId: number, tool: string, updates: Partial<typeof agentAccessControl.$inferInsert>) {
|
||||
export async function updateToolAccess(
|
||||
agentId: number,
|
||||
tool: string,
|
||||
updates: Partial<typeof agentAccessControl.$inferInsert>
|
||||
) {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
|
||||
@@ -242,14 +304,24 @@ export async function updateToolAccess(agentId: number, tool: string, updates: P
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(agentAccessControl)
|
||||
.where(and(eq(agentAccessControl.agentId, agentId), eq(agentAccessControl.tool, tool)))
|
||||
.where(
|
||||
and(
|
||||
eq(agentAccessControl.agentId, agentId),
|
||||
eq(agentAccessControl.tool, tool)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
await db
|
||||
.update(agentAccessControl)
|
||||
.set(updates)
|
||||
.where(and(eq(agentAccessControl.agentId, agentId), eq(agentAccessControl.tool, tool)));
|
||||
.where(
|
||||
and(
|
||||
eq(agentAccessControl.agentId, agentId),
|
||||
eq(agentAccessControl.tool, tool)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
await db.insert(agentAccessControl).values({
|
||||
agentId,
|
||||
@@ -261,7 +333,12 @@ export async function updateToolAccess(agentId: number, tool: string, updates: P
|
||||
const result = await db
|
||||
.select()
|
||||
.from(agentAccessControl)
|
||||
.where(and(eq(agentAccessControl.agentId, agentId), eq(agentAccessControl.tool, tool)))
|
||||
.where(
|
||||
and(
|
||||
eq(agentAccessControl.agentId, agentId),
|
||||
eq(agentAccessControl.tool, tool)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return result[0] || null;
|
||||
@@ -300,3 +377,204 @@ export async function saveHistory(
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Container Management (Docker Swarm) ────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Deploy an agent as a Docker Swarm service.
|
||||
* Each agent runs in its own container with the agent-worker binary.
|
||||
*/
|
||||
export async function deployAgentContainer(
|
||||
agentId: number
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
serviceName: string;
|
||||
servicePort: number;
|
||||
error?: string;
|
||||
}> {
|
||||
const db = await getDb();
|
||||
if (!db)
|
||||
return {
|
||||
success: false,
|
||||
serviceName: "",
|
||||
servicePort: 0,
|
||||
error: "DB not available",
|
||||
};
|
||||
|
||||
const agent = await getAgentById(agentId);
|
||||
if (!agent)
|
||||
return {
|
||||
success: false,
|
||||
serviceName: "",
|
||||
servicePort: 0,
|
||||
error: "Agent not found",
|
||||
};
|
||||
|
||||
const serviceName = `goclaw-agent-${agentId}`;
|
||||
const servicePort = 8001 + ((agentId - 1) % 999); // Ports 8001-8999
|
||||
const containerImage =
|
||||
(agent as any).containerImage || "goclaw-agent-worker:latest";
|
||||
|
||||
try {
|
||||
// Update status to deploying
|
||||
await db
|
||||
.update(agents)
|
||||
.set({
|
||||
containerStatus: "deploying",
|
||||
serviceName,
|
||||
servicePort,
|
||||
} as any)
|
||||
.where(eq(agents.id, agentId));
|
||||
|
||||
// Build Docker Swarm service create command
|
||||
const dbUrl =
|
||||
process.env.DATABASE_URL || "mysql://goclaw:goclaw123@db:3306/goclaw";
|
||||
const llmBaseUrl = process.env.LLM_BASE_URL || "https://api.openai.com/v1";
|
||||
const llmApiKey = process.env.LLM_API_KEY || "";
|
||||
|
||||
const cmd = [
|
||||
"docker service create",
|
||||
`--name ${serviceName}`,
|
||||
`--env AGENT_ID=${agentId}`,
|
||||
`--env AGENT_PORT=${servicePort}`,
|
||||
`--env DATABASE_URL="${dbUrl}"`,
|
||||
`--env LLM_BASE_URL="${llmBaseUrl}"`,
|
||||
`--env LLM_API_KEY="${llmApiKey}"`,
|
||||
`--network goclaw-overlay`,
|
||||
`--replicas 1`,
|
||||
containerImage,
|
||||
].join(" \\\n ");
|
||||
|
||||
console.log(
|
||||
`[Container] Deploying agent ${agentId}: ${serviceName} on port ${servicePort}`
|
||||
);
|
||||
|
||||
const { stdout, stderr } = await execAsync(cmd, { timeout: 30000 });
|
||||
|
||||
// Update status to running
|
||||
await db
|
||||
.update(agents)
|
||||
.set({
|
||||
containerStatus: "running",
|
||||
serviceName,
|
||||
servicePort,
|
||||
} as any)
|
||||
.where(eq(agents.id, agentId));
|
||||
|
||||
console.log(`[Container] Agent ${agentId} deployed: ${serviceName}`);
|
||||
return { success: true, serviceName, servicePort };
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`[Container] Failed to deploy agent ${agentId}:`,
|
||||
error.message
|
||||
);
|
||||
|
||||
// Update status to error
|
||||
await db
|
||||
.update(agents)
|
||||
.set({
|
||||
containerStatus: "error",
|
||||
} as any)
|
||||
.where(eq(agents.id, agentId));
|
||||
|
||||
return {
|
||||
success: false,
|
||||
serviceName,
|
||||
servicePort: 0,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop and remove an agent's Docker Swarm service.
|
||||
*/
|
||||
export async function stopAgentContainer(
|
||||
agentId: number
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const db = await getDb();
|
||||
if (!db) return { success: false, error: "DB not available" };
|
||||
|
||||
const agent = await getAgentById(agentId);
|
||||
if (!agent) return { success: false, error: "Agent not found" };
|
||||
|
||||
const serviceName = (agent as any).serviceName || `goclaw-agent-${agentId}`;
|
||||
|
||||
try {
|
||||
// Remove the Docker Swarm service
|
||||
console.log(`[Container] Stopping agent ${agentId}: ${serviceName}`);
|
||||
await execAsync(`docker service rm ${serviceName}`, {
|
||||
timeout: 15000,
|
||||
}).catch(() => {
|
||||
// Service may not exist — that's OK
|
||||
console.log(
|
||||
`[Container] Service ${serviceName} not found (already removed)`
|
||||
);
|
||||
});
|
||||
|
||||
// Update status to stopped
|
||||
await db
|
||||
.update(agents)
|
||||
.set({
|
||||
containerStatus: "stopped",
|
||||
serviceName: null,
|
||||
servicePort: null,
|
||||
} as any)
|
||||
.where(eq(agents.id, agentId));
|
||||
|
||||
console.log(`[Container] Agent ${agentId} stopped: ${serviceName}`);
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`[Container] Failed to stop agent ${agentId}:`,
|
||||
error.message
|
||||
);
|
||||
|
||||
// Still mark as stopped in DB even if Docker failed
|
||||
await db
|
||||
.update(agents)
|
||||
.set({
|
||||
containerStatus: "stopped",
|
||||
} as any)
|
||||
.where(eq(agents.id, agentId));
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of an agent's container.
|
||||
* Checks Docker Swarm service and returns current state.
|
||||
*/
|
||||
export async function getAgentContainerStatus(
|
||||
agentId: number
|
||||
): Promise<{ status: string; serviceName?: string; servicePort?: number }> {
|
||||
const db = await getDb();
|
||||
if (!db) return { status: "unknown" };
|
||||
|
||||
const agent = await getAgentById(agentId);
|
||||
if (!agent) return { status: "unknown" };
|
||||
|
||||
const containerStatus = (agent as any).containerStatus || "stopped";
|
||||
const serviceName = (agent as any).serviceName || "";
|
||||
const servicePort = (agent as any).servicePort || 0;
|
||||
|
||||
// Verify actual Docker Swarm state
|
||||
if (serviceName) {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`docker service inspect ${serviceName} --format '{{.Spec.Mode.Replicated.Replicas}}'`,
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
const replicas = parseInt(stdout.trim());
|
||||
if (replicas > 0) {
|
||||
return { status: "running", serviceName, servicePort };
|
||||
}
|
||||
} catch {
|
||||
// Service doesn't exist
|
||||
return { status: "stopped" };
|
||||
}
|
||||
}
|
||||
|
||||
return { status: containerStatus, serviceName, servicePort };
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@ import { getDb } from "./db";
|
||||
import { getSessionCookieOptions } from "./_core/cookies";
|
||||
import { systemRouter } from "./_core/systemRouter";
|
||||
import { publicProcedure, router, protectedProcedure } from "./_core/trpc";
|
||||
import { retryWithBackoff, isRetryableError, logRetryAttempt, DEFAULT_RETRY_CONFIG } from "./chat-resilience";
|
||||
import {
|
||||
retryWithBackoff,
|
||||
isRetryableError,
|
||||
logRetryAttempt,
|
||||
DEFAULT_RETRY_CONFIG,
|
||||
} from "./chat-resilience";
|
||||
import { checkOllamaHealth, listModels, chatCompletion } from "./ollama";
|
||||
import {
|
||||
checkGatewayHealth,
|
||||
@@ -127,10 +132,12 @@ export const appRouter = router({
|
||||
return getAllAgents();
|
||||
}),
|
||||
|
||||
get: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
||||
const { getAgentById } = await import("./agents");
|
||||
return getAgentById(input.id);
|
||||
}),
|
||||
get: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const { getAgentById } = await import("./agents");
|
||||
return getAgentById(input.id);
|
||||
}),
|
||||
|
||||
create: publicProcedure
|
||||
.input(
|
||||
@@ -184,39 +191,59 @@ export const appRouter = router({
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { updateAgent } = await import("./agents");
|
||||
const { id, temperature, topP, frequencyPenalty, presencePenalty, ...rest } = input;
|
||||
const {
|
||||
id,
|
||||
temperature,
|
||||
topP,
|
||||
frequencyPenalty,
|
||||
presencePenalty,
|
||||
...rest
|
||||
} = input;
|
||||
const updates: Record<string, any> = { ...rest };
|
||||
if (temperature !== undefined) updates.temperature = temperature.toString();
|
||||
if (temperature !== undefined)
|
||||
updates.temperature = temperature.toString();
|
||||
if (topP !== undefined) updates.topP = topP.toString();
|
||||
if (frequencyPenalty !== undefined) updates.frequencyPenalty = frequencyPenalty.toString();
|
||||
if (presencePenalty !== undefined) updates.presencePenalty = presencePenalty.toString();
|
||||
if (frequencyPenalty !== undefined)
|
||||
updates.frequencyPenalty = frequencyPenalty.toString();
|
||||
if (presencePenalty !== undefined)
|
||||
updates.presencePenalty = presencePenalty.toString();
|
||||
return updateAgent(id, updates as any);
|
||||
}),
|
||||
|
||||
delete: publicProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
|
||||
const { deleteAgent } = await import("./agents");
|
||||
return deleteAgent(input.id);
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const { deleteAgent } = await import("./agents");
|
||||
return deleteAgent(input.id);
|
||||
}),
|
||||
|
||||
stats: publicProcedure.input(z.object({ id: z.number(), hoursBack: z.number().default(24) })).query(async ({ input }) => {
|
||||
const { getAgentStats } = await import("./agents");
|
||||
return getAgentStats(input.id, input.hoursBack);
|
||||
}),
|
||||
stats: publicProcedure
|
||||
.input(z.object({ id: z.number(), hoursBack: z.number().default(24) }))
|
||||
.query(async ({ input }) => {
|
||||
const { getAgentStats } = await import("./agents");
|
||||
return getAgentStats(input.id, input.hoursBack);
|
||||
}),
|
||||
|
||||
metrics: publicProcedure.input(z.object({ id: z.number(), hoursBack: z.number().default(24) })).query(async ({ input }) => {
|
||||
const { getAgentMetrics } = await import("./agents");
|
||||
return getAgentMetrics(input.id, input.hoursBack);
|
||||
}),
|
||||
metrics: publicProcedure
|
||||
.input(z.object({ id: z.number(), hoursBack: z.number().default(24) }))
|
||||
.query(async ({ input }) => {
|
||||
const { getAgentMetrics } = await import("./agents");
|
||||
return getAgentMetrics(input.id, input.hoursBack);
|
||||
}),
|
||||
|
||||
history: publicProcedure.input(z.object({ id: z.number(), limit: z.number().default(50) })).query(async ({ input }) => {
|
||||
const { getAgentHistory } = await import("./agents");
|
||||
return getAgentHistory(input.id, input.limit);
|
||||
}),
|
||||
history: publicProcedure
|
||||
.input(z.object({ id: z.number(), limit: z.number().default(50) }))
|
||||
.query(async ({ input }) => {
|
||||
const { getAgentHistory } = await import("./agents");
|
||||
return getAgentHistory(input.id, input.limit);
|
||||
}),
|
||||
|
||||
accessControl: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
||||
const { getAgentAccessControl } = await import("./agents");
|
||||
return getAgentAccessControl(input.id);
|
||||
}),
|
||||
accessControl: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const { getAgentAccessControl } = await import("./agents");
|
||||
return getAgentAccessControl(input.id);
|
||||
}),
|
||||
|
||||
updateToolAccess: publicProcedure
|
||||
.input(
|
||||
@@ -236,6 +263,37 @@ export const appRouter = router({
|
||||
return updateToolAccess(agentId, input.tool, updates);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Deploy an agent as a Docker Swarm container.
|
||||
* Creates a Swarm service with the agent-worker binary.
|
||||
*/
|
||||
deployContainer: publicProcedure
|
||||
.input(z.object({ agentId: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const { deployAgentContainer } = await import("./agents");
|
||||
return deployAgentContainer(input.agentId);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Stop and remove an agent's Docker Swarm container.
|
||||
*/
|
||||
stopContainer: publicProcedure
|
||||
.input(z.object({ agentId: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const { stopAgentContainer } = await import("./agents");
|
||||
return stopAgentContainer(input.agentId);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get container status for an agent (checks Docker Swarm).
|
||||
*/
|
||||
containerStatus: publicProcedure
|
||||
.input(z.object({ agentId: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const { getAgentContainerStatus } = await import("./agents");
|
||||
return getAgentContainerStatus(input.agentId);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Chat with a specific agent using its configuration
|
||||
*/
|
||||
@@ -251,10 +309,17 @@ export const appRouter = router({
|
||||
const { getAgentById, saveHistory } = await import("./agents");
|
||||
const agent = await getAgentById(input.agentId);
|
||||
if (!agent) {
|
||||
return { success: false as const, response: "", error: "Agent not found" };
|
||||
return {
|
||||
success: false as const,
|
||||
response: "",
|
||||
error: "Agent not found",
|
||||
};
|
||||
}
|
||||
|
||||
const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [];
|
||||
const messages: Array<{
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
}> = [];
|
||||
if (agent.systemPrompt) {
|
||||
messages.push({ role: "system", content: agent.systemPrompt });
|
||||
}
|
||||
@@ -262,14 +327,12 @@ export const appRouter = router({
|
||||
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const result = await chatCompletion(
|
||||
agent.model,
|
||||
messages,
|
||||
{
|
||||
temperature: agent.temperature ? parseFloat(agent.temperature as string) : 0.7,
|
||||
max_tokens: agent.maxTokens ?? 2048,
|
||||
}
|
||||
);
|
||||
const result = await chatCompletion(agent.model, messages, {
|
||||
temperature: agent.temperature
|
||||
? parseFloat(agent.temperature as string)
|
||||
: 0.7,
|
||||
max_tokens: agent.maxTokens ?? 2048,
|
||||
});
|
||||
const processingTimeMs = Date.now() - startTime;
|
||||
const response = result.choices[0]?.message?.content ?? "";
|
||||
|
||||
@@ -304,7 +367,7 @@ export const appRouter = router({
|
||||
}),
|
||||
}),
|
||||
|
||||
/**
|
||||
/**
|
||||
* Tools — управление инструментами агентов
|
||||
*/
|
||||
tools: router({
|
||||
@@ -342,7 +405,17 @@ export const appRouter = router({
|
||||
z.object({
|
||||
sessionId: z.string(),
|
||||
action: z.object({
|
||||
type: z.enum(["navigate", "click", "type", "extract", "screenshot", "scroll", "wait", "evaluate", "close"]),
|
||||
type: z.enum([
|
||||
"navigate",
|
||||
"click",
|
||||
"type",
|
||||
"extract",
|
||||
"screenshot",
|
||||
"scroll",
|
||||
"wait",
|
||||
"evaluate",
|
||||
"close",
|
||||
]),
|
||||
params: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
})
|
||||
@@ -363,7 +436,10 @@ export const appRouter = router({
|
||||
.input(z.object({ sessionId: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const { executeBrowserAction } = await import("./browser-agent");
|
||||
return executeBrowserAction(input.sessionId, { type: "close", params: {} });
|
||||
return executeBrowserAction(input.sessionId, {
|
||||
type: "close",
|
||||
params: {},
|
||||
});
|
||||
}),
|
||||
|
||||
closeAllSessions: publicProcedure
|
||||
@@ -403,11 +479,14 @@ export const appRouter = router({
|
||||
description: z.string(),
|
||||
category: z.string(),
|
||||
dangerous: z.boolean(),
|
||||
parameters: z.record(z.string(), z.object({
|
||||
type: z.string(),
|
||||
description: z.string(),
|
||||
required: z.boolean().optional(),
|
||||
})),
|
||||
parameters: z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
type: z.string(),
|
||||
description: z.string(),
|
||||
required: z.boolean().optional(),
|
||||
})
|
||||
),
|
||||
implementation: z.string(),
|
||||
})
|
||||
)
|
||||
@@ -557,7 +636,9 @@ export const appRouter = router({
|
||||
DEFAULT_RETRY_CONFIG,
|
||||
(attempt, error) => {
|
||||
if (isRetryableError(error)) {
|
||||
logRetryAttempt(attempt, error, { messageCount: input.messages.length });
|
||||
logRetryAttempt(attempt, error, {
|
||||
messageCount: input.messages.length,
|
||||
});
|
||||
} else {
|
||||
// Non-retryable error, throw immediately
|
||||
throw error;
|
||||
@@ -570,7 +651,7 @@ export const appRouter = router({
|
||||
tools: publicProcedure.query(async () => {
|
||||
const gwTools = await getGatewayTools();
|
||||
if (gwTools) {
|
||||
return gwTools.map((t) => ({
|
||||
return gwTools.map(t => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: t.parameters,
|
||||
@@ -579,7 +660,7 @@ export const appRouter = router({
|
||||
}
|
||||
// Fallback: Node.js tool definitions
|
||||
const { ORCHESTRATOR_TOOLS } = await import("./orchestrator");
|
||||
return ORCHESTRATOR_TOOLS.map((t) => ({
|
||||
return ORCHESTRATOR_TOOLS.map(t => ({
|
||||
name: t.function.name,
|
||||
description: t.function.description,
|
||||
parameters: t.function.parameters,
|
||||
@@ -623,11 +704,12 @@ export const appRouter = router({
|
||||
const days = Math.floor(uptimeSec / 86400);
|
||||
const hours = Math.floor((uptimeSec % 86400) / 3600);
|
||||
const mins = Math.floor((uptimeSec % 3600) / 60);
|
||||
const uptime = days > 0
|
||||
? `${days}d ${hours}h ${mins}m`
|
||||
: hours > 0
|
||||
? `${hours}h ${mins}m`
|
||||
: `${mins}m`;
|
||||
const uptime =
|
||||
days > 0
|
||||
? `${days}d ${hours}h ${mins}m`
|
||||
: hours > 0
|
||||
? `${hours}h ${mins}m`
|
||||
: `${mins}m`;
|
||||
|
||||
// 2. Container / node stats from Go Gateway
|
||||
const [nodesResult, statsResult] = await Promise.allSettled([
|
||||
@@ -635,19 +717,22 @@ export const appRouter = router({
|
||||
getGatewayNodeStats(),
|
||||
]);
|
||||
|
||||
const nodes = nodesResult.status === "fulfilled" && nodesResult.value
|
||||
? nodesResult.value
|
||||
: null;
|
||||
const statsData = statsResult.status === "fulfilled" && statsResult.value
|
||||
? statsResult.value
|
||||
: null;
|
||||
const nodes =
|
||||
nodesResult.status === "fulfilled" && nodesResult.value
|
||||
? nodesResult.value
|
||||
: null;
|
||||
const statsData =
|
||||
statsResult.status === "fulfilled" && statsResult.value
|
||||
? statsResult.value
|
||||
: null;
|
||||
|
||||
const containerCount = nodes?.containers?.length ?? nodes?.count ?? 0;
|
||||
const totalContainers = nodes?.containers?.length ?? 0;
|
||||
|
||||
// CPU: average across all containers
|
||||
const cpuPct = statsData?.stats?.length
|
||||
? statsData.stats.reduce((sum, s) => sum + s.cpuPct, 0) / statsData.stats.length
|
||||
? statsData.stats.reduce((sum, s) => sum + s.cpuPct, 0) /
|
||||
statsData.stats.length
|
||||
: 0;
|
||||
|
||||
// MEM: total used MB
|
||||
@@ -779,7 +864,9 @@ export const appRouter = router({
|
||||
taskId: z.number(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
status: z.enum(["pending", "in_progress", "completed", "failed", "blocked"]).optional(),
|
||||
status: z
|
||||
.enum(["pending", "in_progress", "completed", "failed", "blocked"])
|
||||
.optional(),
|
||||
priority: z.enum(["low", "medium", "high", "critical"]).optional(),
|
||||
result: z.string().optional(),
|
||||
errorMessage: z.string().optional(),
|
||||
@@ -857,7 +944,11 @@ export const appRouter = router({
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { createResearchTasks } = await import("./web-research");
|
||||
return createResearchTasks(input.agentId, input.conversationId, input.queries);
|
||||
return createResearchTasks(
|
||||
input.agentId,
|
||||
input.conversationId,
|
||||
input.queries
|
||||
);
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user