Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@
"@modelcontextprotocol/sdk": "^1.30.0",
"@prisma/client": "^6.2.1",
"@tanstack/react-query": "^5.64.2",
"@types/js-yaml": "^4.0.9",
"@xyflow/react": "^12.11.3",
"clsx": "^2.1.1",
"html-to-image": "^1.11.13",
"js-yaml": "^5.3.0",
"jspdf": "^4.2.1",
"lucide-react": "^0.474.0",
"next": "^15.1.6",
Expand Down
10 changes: 2 additions & 8 deletions src/app/api/approvals/[id]/cancel/route.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { z } from "zod";
import { ApprovalRepository } from "@/repositories/ApprovalRepository";
import { ApprovalHistoryRepository } from "@/repositories/ApprovalHistoryRepository";
import { ExecutionRepository } from "@/repositories/ExecutionRepository";
import { ApprovalEngine } from "@/modules/approval";
import { unauthorized, forbidden, badRequest, serverError, notFound } from "@/lib/api/handlers";
import { apiServices } from "@/lib/api/services";

const cancelSchema = z.object({
idempotencyKey: z.string().min(1, "Idempotency key is required"),
});

const approvalRepo = new ApprovalRepository();
const historyRepo = new ApprovalHistoryRepository();
const executionRepo = new ExecutionRepository();
const approvalEngine = new ApprovalEngine(approvalRepo, historyRepo, executionRepo);
const { approvalEngine } = apiServices();

export async function POST(
request: Request,
Expand Down
16 changes: 6 additions & 10 deletions src/app/api/approvals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,21 @@ import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { z } from "zod";
import { respondApprovalSchema } from "@/validators/approvalSchema";
import { ApprovalRepository } from "@/repositories/ApprovalRepository";
import { ApprovalHistoryRepository } from "@/repositories/ApprovalHistoryRepository";
import { AuditLogRepository } from "@/repositories/AuditLogRepository";
import { ExecutionRepository } from "@/repositories/ExecutionRepository";
import { ApprovalEngine } from "@/modules/approval";
import { unauthorized, forbidden, notFound, badRequest, serverError } from "@/lib/api/handlers";
import { rateLimit } from "@/lib/api/rateLimit";
import { apiServices } from "@/lib/api/services";

const approvalRepo = new ApprovalRepository();
const _auditRepo = new AuditLogRepository();
const historyRepo = new ApprovalHistoryRepository();
const executionRepo = new ExecutionRepository();
const approvalEngine = new ApprovalEngine(approvalRepo, historyRepo, executionRepo);
const { approvalRepo, approvalEngine } = apiServices();

export async function GET(_request: Request) {
const { userId } = await auth();
if (!userId) return unauthorized();

try {
// Eventually-consistent escalation sweep: expire stale PENDING requests
// whose in-process escalateAfterMin timer was lost to a deploy/freeze.
await approvalRepo.expireStaleForUser(userId).catch(() => 0);

// Return ALL approvals for the user (not just pending) — the review UI needs
// both the pending queue and history. Single query, ownership-scoped.
const requests = await approvalRepo.findByUserId(userId);
Expand Down
24 changes: 13 additions & 11 deletions src/app/api/canvas/copilot/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { getLLMProvider } from "@/providers/llm";
import { auth } from "@clerk/nextjs/server";
import { getProviderForModel } from "@/providers/llm";
import type { AgentGraphDefinition } from "@/types/graph";
import { unauthorized, serverError } from "@/lib/api/handlers";
import { rateLimit } from "@/lib/api/rateLimit";

const COPILOT_SYSTEM_PROMPT = `You are a graph architect for Agent Studio. Given a natural language description of a multi-agent system, generate a complete AgentGraphDefinition JSON.

Expand Down Expand Up @@ -55,9 +58,15 @@ Make sure:
5. Return ONLY valid JSON, no markdown fences, no explanation`;

export async function POST(request: NextRequest) {
const { userId } = await auth();
if (!userId) return unauthorized();

const limited = rateLimit(`canvas:copilot:${userId}`);
if (limited) return limited;

try {
const body = await request.json();
const { prompt } = body;
const { prompt, model } = body;

if (!prompt || typeof prompt !== "string" || prompt.trim().length === 0) {
return NextResponse.json(
Expand All @@ -66,7 +75,7 @@ export async function POST(request: NextRequest) {
);
}

const llm = getLLMProvider();
const llm = getProviderForModel(model);
const llmResponse = await llm.complete(
[
{ role: "system", content: COPILOT_SYSTEM_PROMPT },
Expand Down Expand Up @@ -123,13 +132,6 @@ export async function POST(request: NextRequest) {

return NextResponse.json({ success: true, graph });
} catch (error) {
console.error("[Canvas Copilot]", error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "Graph generation failed",
},
{ status: 500 }
);
return serverError(error);
}
}
18 changes: 10 additions & 8 deletions src/app/api/canvas/optimize-prompt/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { getLLMProvider } from "@/providers/llm";
import { unauthorized, serverError } from "@/lib/api/handlers";
import { rateLimit } from "@/lib/api/rateLimit";

const OPTIMIZER_SYSTEM_PROMPT = `You are a prompt engineering expert. Given an agent's current system prompt and its role context, produce an optimized version that:

Expand All @@ -18,6 +21,12 @@ Rules:
- Return ONLY the optimized prompt text, no explanations or meta-commentary`;

export async function POST(request: NextRequest) {
const { userId } = await auth();
if (!userId) return unauthorized();

const limited = rateLimit(`canvas:optimize:${userId}`);
if (limited) return limited;

try {
const body = await request.json();
const { prompt, nodeType, label, condition, toolName } = body;
Expand Down Expand Up @@ -65,13 +74,6 @@ export async function POST(request: NextRequest) {
},
});
} catch (error) {
console.error("[Prompt Optimizer]", error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "Optimization failed",
},
{ status: 500 }
);
return serverError(error);
}
}
24 changes: 8 additions & 16 deletions src/app/api/canvas/preview/[id]/stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,15 @@ import { auth } from "@clerk/nextjs/server";
import { executionEventBus, ExecutionEvent } from "@/modules/graph/eventBus";
import { GraphInterpreter } from "@/modules/graph/graphInterpreter";
import { previewStore } from "@/modules/graph/previewStore";
import { SkillRepository } from "@/repositories/SkillRepository";
import { ExecutionRepository } from "@/repositories/ExecutionRepository";
import { ApprovalRepository } from "@/repositories/ApprovalRepository";
import { ExecutionLogRepository } from "@/repositories/ExecutionLogRepository";
import { createToolRegistry } from "@/modules/tools";
import { PermissionChecker } from "@/modules/execution/tool-registry/permissionChecker";
import { getLLMProvider } from "@/providers/llm";
import { AgentGraphDefinition } from "@/types/graph";
import { logger } from "@/lib/logger";
import { apiServices } from "@/lib/api/services";

export const dynamic = "force-dynamic";

const skillRepo = new SkillRepository();

const SSE_HEADERS = {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Expand Down Expand Up @@ -112,11 +107,6 @@ export async function GET(
return new Response(stream, { headers: SSE_HEADERS });
}

import { McpClientService } from "@/services/McpClientService";
import { McpServerRepository } from "@/repositories/McpServerRepository";
import { OpenApiService } from "@/services/OpenApiService";
import { OpenApiRepository } from "@/repositories/OpenApiRepository";

/** Run the graph interpreter in dry-run mode against the preview session. */
async function runPreview(
previewId: string,
Expand All @@ -125,14 +115,16 @@ async function runPreview(
graph: AgentGraphDefinition,
inputData: Record<string, unknown>
): Promise<void> {
const { skillRepo, mcpService, openApiService, executionRepo, approvalRepo, logRepo } = apiServices();

const version = await skillRepo.findVersionById(skillVersionId);
if (!version) throw new Error("Skill version not found");
const skill = await skillRepo.findByIdForUser(version.skillId, userId);
if (!skill) throw new Error("Skill not found");

// Ghost previews run on an ISOLATED registry (never the shared execution
// registries) so a preview can never mutate live tool state mid-run.
const toolRegistry = createToolRegistry();
const mcpService = new McpClientService(new McpServerRepository());
const openApiService = new OpenApiService(new OpenApiRepository());

await Promise.all([
mcpService.registerUserMcpTools(userId, toolRegistry).catch((err) => {
Expand All @@ -147,9 +139,9 @@ async function runPreview(
llm: getLLMProvider(),
toolRegistry,
permissionChecker: new PermissionChecker(),
executionRepo: new ExecutionRepository(),
approvalRepo: new ApprovalRepository(),
logRepo: new ExecutionLogRepository(),
executionRepo,
approvalRepo,
logRepo,
});

await interpreter.run({
Expand Down
10 changes: 2 additions & 8 deletions src/app/api/executions/[id]/cancel/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { ExecutionService } from "@/services/ExecutionService";
import { ExecutionRepository } from "@/repositories/ExecutionRepository";
import { SkillRepository } from "@/repositories/SkillRepository";
import { AuditLogRepository } from "@/repositories/AuditLogRepository";
import { unauthorized, serverError, notFound } from "@/lib/api/handlers";
import { apiServices } from "@/lib/api/services";

const executionRepo = new ExecutionRepository();
const skillRepo = new SkillRepository();
const auditRepo = new AuditLogRepository();
const executionService = new ExecutionService(executionRepo, skillRepo, auditRepo);
const { executionService } = apiServices();

export async function POST(
_request: Request,
Expand Down
23 changes: 2 additions & 21 deletions src/app/api/executions/[id]/detail/route.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,9 @@
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { ExecutionService } from "@/services/ExecutionService";
import { ExecutionHistoryService } from "@/modules/history";
import { ExecutionRepository } from "@/repositories/ExecutionRepository";
import { SkillRepository } from "@/repositories/SkillRepository";
import { AuditLogRepository } from "@/repositories/AuditLogRepository";
import { ExecutionLogRepository } from "@/repositories/ExecutionLogRepository";
import { ApprovalRepository } from "@/repositories/ApprovalRepository";
import { ApprovalHistoryRepository } from "@/repositories/ApprovalHistoryRepository";
import { unauthorized, serverError, notFound } from "@/lib/api/handlers";
import { apiServices } from "@/lib/api/services";

const executionRepo = new ExecutionRepository();
const skillRepo = new SkillRepository();
const auditRepo = new AuditLogRepository();
const executionService = new ExecutionService(executionRepo, skillRepo, auditRepo);
const historyService = new ExecutionHistoryService(
executionRepo,
skillRepo,
auditRepo,
executionService,
new ExecutionLogRepository(),
new ApprovalRepository(),
new ApprovalHistoryRepository()
);
const { historyService } = apiServices();

/** Full execution detail: trace data, structured logs, timeline, and approval events. */
export async function GET(
Expand Down
23 changes: 2 additions & 21 deletions src/app/api/executions/[id]/export/route.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,9 @@
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { ExecutionService } from "@/services/ExecutionService";
import { ExecutionHistoryService } from "@/modules/history";
import { ExecutionRepository } from "@/repositories/ExecutionRepository";
import { SkillRepository } from "@/repositories/SkillRepository";
import { AuditLogRepository } from "@/repositories/AuditLogRepository";
import { ExecutionLogRepository } from "@/repositories/ExecutionLogRepository";
import { ApprovalRepository } from "@/repositories/ApprovalRepository";
import { ApprovalHistoryRepository } from "@/repositories/ApprovalHistoryRepository";
import { unauthorized, serverError, notFound } from "@/lib/api/handlers";
import { apiServices } from "@/lib/api/services";

const executionRepo = new ExecutionRepository();
const skillRepo = new SkillRepository();
const auditRepo = new AuditLogRepository();
const executionService = new ExecutionService(executionRepo, skillRepo, auditRepo);
const historyService = new ExecutionHistoryService(
executionRepo,
skillRepo,
auditRepo,
executionService,
new ExecutionLogRepository(),
new ApprovalRepository(),
new ApprovalHistoryRepository()
);
const { historyService } = apiServices();

/**
* JSON export of a full execution report: trace data, structured logs,
Expand Down
Loading