135 lines
4.7 KiB
TypeScript
135 lines
4.7 KiB
TypeScript
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<HarnessSession>({
|
|
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<ModelConnectionInput | undefined> {
|
|
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 });
|
|
}
|