import { mkdir } from "node:fs/promises"; import { join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { FileAttachmentStore, HarnessSession, InMemoryApprovalBroker, InMemoryEventStore, InMemoryModelConnectionStore, InMemorySessionRegistry, SafeWorkspace, SkillCatalog, SkillEventInference, createMockAdsMetricsTool, createPiAgentSessionPortFactory, createSaveReportDraftTool, validateBaseUrl, } from "@agent-studio/harness"; import type { ModelConnectionInput } from "@agent-studio/shared"; import { buildApp } from "./app.js"; import { loadServerConfig } from "./config.js"; export async function buildDefaultApp(environment: NodeJS.ProcessEnv = process.env) { const config = loadServerConfig(environment); const projectRoot = process.cwd(); const sessionsRoot = resolve(projectRoot, config.sessionsRoot); const agentDir = resolve(projectRoot, config.agentDir); await mkdir(sessionsRoot, { recursive: true, mode: 0o700 }); await mkdir(agentDir, { recursive: true, mode: 0o700 }); const eventStore = new InMemoryEventStore(); const skillCatalog = new SkillCatalog({ cwd: projectRoot, agentDir, ...(environment.AGENT_SKILLS_DIRS === undefined ? {} : { extraDirsValue: environment.AGENT_SKILLS_DIRS }), }); await skillCatalog.reload(); const environmentConnection = await readEnvironmentConnection(environment, config.allowPrivateLlmBaseUrls); const modelStore = new InMemoryModelConnectionStore({ ...(environmentConnection === undefined ? {} : { environment: environmentConnection }), validateUrl: (url) => validateBaseUrl(url, { allowPrivateNetwork: config.allowPrivateLlmBaseUrls, allowLocalhostHttp: environment.NODE_ENV !== "production", }), }); const approvalBroker = new InMemoryApprovalBroker({ eventStore, timeoutMs: config.approvalTimeoutMs, }); const attachmentStore = new FileAttachmentStore({ sessionsRoot, eventStore, maxBytes: config.maxImageBytes, maxAttachmentsPerMessage: config.maxImagesPerMessage, }); const registry = new InMemorySessionRegistry({ sessionsRoot, createSession: ({ sessionId, workspaceRoot }) => { eventStore.append(sessionId, { type: "session.started", payload: { cwd: "." } }); return new HarnessSession({ sessionId, workspaceRoot, eventStore, modelStore, attachmentStore, approvalBroker, runTimeoutMs: config.runTimeoutMs, createSkillInference: (runId) => new SkillEventInference({ sessionId, runId, eventStore, skillIndex: skillCatalog.index(), }), createAgentPort: async (input) => { const workspace = await SafeWorkspace.create(workspaceRoot); return createPiAgentSessionPortFactory({ agentDir, approvalBroker, customTools: [ createMockAdsMetricsTool(join(projectRoot, "fixtures", "ads-account.json")), createSaveReportDraftTool(workspace), ], })(input); }, }); }, }); return buildApp({ sessionRegistry: registry, skillCatalog, modelStore, attachmentStore, approvalBroker, }); } async function readEnvironmentConnection( environment: NodeJS.ProcessEnv, allowPrivateNetwork: boolean, ): Promise { const providerId = environment.AGENT_PROVIDER?.trim(); const modelId = environment.AGENT_MODEL?.trim(); if (providerId === undefined || providerId === "" || modelId === undefined || modelId === "") return undefined; const api = environment.LLM_API ?? "openai-responses"; if (api !== "openai-responses" && api !== "openai-completions" && api !== "anthropic-messages") return undefined; const rawBaseUrl = environment.LLM_BASE_URL?.trim(); const baseUrl = rawBaseUrl === undefined || rawBaseUrl === "" ? undefined : (await validateBaseUrl(rawBaseUrl, { allowPrivateNetwork, allowLocalhostHttp: environment.NODE_ENV !== "production", })).normalizedUrl; const apiKey = providerId === "anthropic" ? environment.ANTHROPIC_API_KEY?.trim() : environment.OPENAI_API_KEY?.trim(); return { providerId, modelId, api, supportsImages: environment.AGENT_MODEL_SUPPORTS_IMAGES !== "false", ...(baseUrl === undefined ? {} : { baseUrl }), ...(apiKey === undefined || apiKey === "" ? {} : { apiKey }), }; } const entryPoint = process.argv[1]; if (entryPoint !== undefined && import.meta.url === pathToFileURL(entryPoint).href) { const config = loadServerConfig(process.env); const app = await buildDefaultApp(); await app.listen({ host: config.host, port: config.port }); }