170 lines
5.3 KiB
TypeScript
170 lines
5.3 KiB
TypeScript
import path from "node:path";
|
|
|
|
import { CopilotClient } from "@github/copilot-sdk";
|
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
|
|
|
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
|
|
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
|
import type { ClawdbotConfig } from "../config/config.js";
|
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
import { resolveUserPath } from "../utils.js";
|
|
import { resolveSessionAgentIds } from "./agent-scope.js";
|
|
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "./bootstrap-files.js";
|
|
import { writeCliImages } from "./cli-runner/helpers.js";
|
|
import { buildSystemPrompt } from "./cli-runner/helpers.js";
|
|
import { resolveClawdbotDocsPath } from "./docs-path.js";
|
|
import { FailoverError, resolveFailoverStatus } from "./failover-error.js";
|
|
import { classifyFailoverReason, isFailoverErrorMessage } from "./pi-embedded-helpers.js";
|
|
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
|
|
|
const log = createSubsystemLogger("agent/copilot-sdk");
|
|
|
|
export async function runCopilotSdkAgent(params: {
|
|
sessionId: string;
|
|
sessionKey?: string;
|
|
sessionFile: string;
|
|
workspaceDir: string;
|
|
config?: ClawdbotConfig;
|
|
prompt: string;
|
|
provider: string;
|
|
model?: string;
|
|
thinkLevel?: ThinkLevel;
|
|
timeoutMs: number;
|
|
runId: string;
|
|
extraSystemPrompt?: string;
|
|
ownerNumbers?: string[];
|
|
cliSessionId?: string;
|
|
images?: ImageContent[];
|
|
}): Promise<EmbeddedPiRunResult> {
|
|
const started = Date.now();
|
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
|
const workspaceDir = resolvedWorkspace;
|
|
|
|
const modelId = (params.model ?? "gpt-4.1").trim() || "gpt-4.1";
|
|
const modelDisplay = `${params.provider}/${modelId}`;
|
|
|
|
const extraSystemPrompt = [
|
|
params.extraSystemPrompt?.trim(),
|
|
"Tools are disabled in this session. Do not call tools.",
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
|
|
const sessionLabel = params.sessionKey ?? params.sessionId;
|
|
const { contextFiles } = await resolveBootstrapContextForRun({
|
|
workspaceDir,
|
|
config: params.config,
|
|
sessionKey: params.sessionKey,
|
|
sessionId: params.sessionId,
|
|
warn: makeBootstrapWarn({ sessionLabel, warn: (message) => log.warn(message) }),
|
|
});
|
|
const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
|
|
sessionKey: params.sessionKey,
|
|
config: params.config,
|
|
});
|
|
const heartbeatPrompt =
|
|
sessionAgentId === defaultAgentId
|
|
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
|
: undefined;
|
|
const docsPath = await resolveClawdbotDocsPath({
|
|
workspaceDir,
|
|
argv1: process.argv[1],
|
|
cwd: process.cwd(),
|
|
moduleUrl: import.meta.url,
|
|
});
|
|
const systemPrompt = buildSystemPrompt({
|
|
workspaceDir,
|
|
config: params.config,
|
|
defaultThinkLevel: params.thinkLevel,
|
|
extraSystemPrompt,
|
|
ownerNumbers: params.ownerNumbers,
|
|
heartbeatPrompt,
|
|
docsPath: docsPath ?? undefined,
|
|
tools: [],
|
|
contextFiles,
|
|
modelDisplay,
|
|
agentId: sessionAgentId,
|
|
});
|
|
|
|
let cleanupImages: (() => Promise<void>) | undefined;
|
|
let attachments: Array<{ type: "file"; path: string; displayName?: string }> | undefined;
|
|
if (params.images && params.images.length > 0) {
|
|
const imagePayload = await writeCliImages(params.images);
|
|
cleanupImages = imagePayload.cleanup;
|
|
attachments = imagePayload.paths.map((imagePath) => ({
|
|
type: "file",
|
|
path: imagePath,
|
|
displayName: path.basename(imagePath),
|
|
}));
|
|
}
|
|
|
|
const client = new CopilotClient();
|
|
|
|
try {
|
|
await client.start();
|
|
|
|
let session: Awaited<ReturnType<CopilotClient["createSession"]>>;
|
|
|
|
if (params.cliSessionId) {
|
|
try {
|
|
session = await client.resumeSession(params.cliSessionId);
|
|
} catch (err) {
|
|
log.warn(`copilot-sdk resume failed, starting new session: ${String(err)}`);
|
|
session = await client.createSession({
|
|
model: modelId,
|
|
systemMessage: { content: systemPrompt },
|
|
});
|
|
}
|
|
} else {
|
|
session = await client.createSession({
|
|
model: modelId,
|
|
systemMessage: { content: systemPrompt },
|
|
});
|
|
}
|
|
|
|
const messageOptions: { prompt: string; attachments?: typeof attachments } = {
|
|
prompt: params.prompt,
|
|
};
|
|
if (attachments && attachments.length > 0) {
|
|
messageOptions.attachments = attachments;
|
|
}
|
|
|
|
const response = await session.sendAndWait(messageOptions, params.timeoutMs);
|
|
const text = response?.data?.content?.trim();
|
|
if (!text) {
|
|
throw new Error("Copilot SDK returned no response.");
|
|
}
|
|
|
|
return {
|
|
payloads: [{ text }],
|
|
meta: {
|
|
durationMs: Date.now() - started,
|
|
agentMeta: {
|
|
sessionId: session.sessionId,
|
|
provider: params.provider,
|
|
model: modelId,
|
|
},
|
|
},
|
|
} satisfies EmbeddedPiRunResult;
|
|
} catch (err) {
|
|
if (err instanceof FailoverError) throw err;
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
if (isFailoverErrorMessage(message)) {
|
|
const reason = classifyFailoverReason(message) ?? "unknown";
|
|
const status = resolveFailoverStatus(reason);
|
|
throw new FailoverError(message, {
|
|
reason,
|
|
provider: params.provider,
|
|
model: modelId,
|
|
status,
|
|
});
|
|
}
|
|
throw err;
|
|
} finally {
|
|
if (cleanupImages) {
|
|
await cleanupImages();
|
|
}
|
|
await client.stop();
|
|
}
|
|
}
|