Archived
make webhook
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
import { mkdir, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { AgentStore, Job } from "../../adapters/database/store.js";
|
||||
import { checkoutTrustedRevision } from "../../adapters/git/repository/checkout.js";
|
||||
import { GiteaClient } from "../../adapters/gitea/client/client.js";
|
||||
import { findAcceptedPlan } from "../../adapters/gitea/issues.js";
|
||||
import {
|
||||
formatError,
|
||||
protocolVersion,
|
||||
type Result,
|
||||
} from "../../core/contracts.js";
|
||||
import { runImplementation } from "./orchestration/implementation.js";
|
||||
import { runPlan } from "./orchestration/plan.js";
|
||||
|
||||
const leaseMs = 30_000;
|
||||
|
||||
export async function executeJob(input: {
|
||||
store: AgentStore;
|
||||
job: Job;
|
||||
worker: string;
|
||||
serverUrl: string;
|
||||
repository: { owner: string; repo: string };
|
||||
readToken: string;
|
||||
botLogin: string;
|
||||
workspaceRoot: string;
|
||||
shutdown: AbortSignal;
|
||||
}): Promise<void> {
|
||||
const controller = new AbortController();
|
||||
const signal = AbortSignal.any([input.shutdown, controller.signal]);
|
||||
const monitor = setInterval(() => {
|
||||
if (input.store.isCancelRequested(input.job.id))
|
||||
controller.abort(new Error("Agent job was cancelled"));
|
||||
else if (!input.store.heartbeat(input.job.id, input.worker, leaseMs))
|
||||
controller.abort(new Error("Agent job lease was lost"));
|
||||
}, 5_000);
|
||||
monitor.unref();
|
||||
try {
|
||||
const stableWorkspace = join(
|
||||
input.workspaceRoot,
|
||||
String(input.job.repositoryId),
|
||||
String(input.job.issueNumber),
|
||||
);
|
||||
const workspace =
|
||||
input.job.attempts === 1
|
||||
? stableWorkspace
|
||||
: `${stableWorkspace}-recovery-${input.job.id}-${input.job.attempts}`;
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
await mkdir(workspace, { recursive: true, mode: 0o700 });
|
||||
input.store.setWorkspace(input.job.id, input.worker, workspace);
|
||||
const client = new GiteaClient(
|
||||
input.serverUrl,
|
||||
input.readToken,
|
||||
input.repository.owner,
|
||||
input.repository.repo,
|
||||
signal,
|
||||
);
|
||||
const repository = await client.getRepository();
|
||||
if (repository.id !== input.job.repositoryId)
|
||||
throw new Error("Configured repository identity changed");
|
||||
const base = await client.getBranch(repository.default_branch);
|
||||
if (!base)
|
||||
throw new Error(
|
||||
`Default branch ${repository.default_branch} was not found`,
|
||||
);
|
||||
await checkoutTrustedRevision({
|
||||
workspace,
|
||||
serverUrl: input.serverUrl,
|
||||
repository: `${input.repository.owner}/${input.repository.repo}`,
|
||||
sha: base.commit.id,
|
||||
readToken: input.readToken,
|
||||
signal,
|
||||
});
|
||||
if (input.job.mode === "plan") {
|
||||
await executePlan(input, client, workspace, signal);
|
||||
return;
|
||||
}
|
||||
await executeImplementation(input, client, workspace, signal);
|
||||
} catch (error) {
|
||||
await handleFailure(input, signal, error);
|
||||
} finally {
|
||||
clearInterval(monitor);
|
||||
}
|
||||
}
|
||||
|
||||
async function executePlan(
|
||||
input: Parameters<typeof executeJob>[0],
|
||||
client: GiteaClient,
|
||||
workspace: string,
|
||||
signal: AbortSignal,
|
||||
): Promise<void> {
|
||||
const conversation =
|
||||
input.job.attempts === 1
|
||||
? input.store.getConversation(
|
||||
input.job.repositoryId,
|
||||
input.job.issueNumber,
|
||||
"planner",
|
||||
"issue",
|
||||
)
|
||||
: undefined;
|
||||
const output = await runPlan({
|
||||
issueNumber: input.job.issueNumber,
|
||||
botLogin: input.botLogin,
|
||||
client,
|
||||
workspace,
|
||||
instruction: input.job.instruction,
|
||||
signal,
|
||||
...(conversation ? { existingSessionId: conversation.sessionId } : {}),
|
||||
onSession: (sessionId) => {
|
||||
if (input.job.attempts === 1)
|
||||
input.store.saveConversation({
|
||||
repositoryId: input.job.repositoryId,
|
||||
issueNumber: input.job.issueNumber,
|
||||
role: "planner",
|
||||
scope: "issue",
|
||||
sessionId,
|
||||
});
|
||||
},
|
||||
});
|
||||
input.store.finishExecution(input.job.id, input.worker, output.result);
|
||||
}
|
||||
|
||||
async function executeImplementation(
|
||||
input: Parameters<typeof executeJob>[0],
|
||||
client: GiteaClient,
|
||||
workspace: string,
|
||||
signal: AbortSignal,
|
||||
): Promise<void> {
|
||||
const accepted = findAcceptedPlan(
|
||||
await client.getComments(input.job.issueNumber),
|
||||
input.botLogin,
|
||||
input.job.issueNumber,
|
||||
);
|
||||
const scope = accepted?.marker.planDigest || "missing-plan";
|
||||
const conversation =
|
||||
input.job.attempts === 1
|
||||
? input.store.getConversation(
|
||||
input.job.repositoryId,
|
||||
input.job.issueNumber,
|
||||
"implementer",
|
||||
scope,
|
||||
)
|
||||
: undefined;
|
||||
const output = await runImplementation({
|
||||
issueNumber: input.job.issueNumber,
|
||||
botLogin: input.botLogin,
|
||||
client,
|
||||
workspace,
|
||||
readToken: input.readToken,
|
||||
expectedPlanDigest: scope,
|
||||
instruction: input.job.instruction,
|
||||
signal,
|
||||
...(conversation ? { existingSessionId: conversation.sessionId } : {}),
|
||||
onSession: (sessionId) => {
|
||||
if (input.job.attempts === 1)
|
||||
input.store.saveConversation({
|
||||
repositoryId: input.job.repositoryId,
|
||||
issueNumber: input.job.issueNumber,
|
||||
role: "implementer",
|
||||
scope,
|
||||
sessionId,
|
||||
});
|
||||
},
|
||||
});
|
||||
input.store.finishExecution(input.job.id, input.worker, output.result);
|
||||
}
|
||||
|
||||
async function handleFailure(
|
||||
input: Parameters<typeof executeJob>[0],
|
||||
signal: AbortSignal,
|
||||
error: unknown,
|
||||
): Promise<void> {
|
||||
if (
|
||||
input.shutdown.aborted ||
|
||||
!input.store.ownsLease(input.job.id, input.worker)
|
||||
) {
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
level: "info",
|
||||
jobId: input.job.id,
|
||||
message: "Execution interrupted; lease will be recovered",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result: Result = {
|
||||
version: protocolVersion,
|
||||
mode: input.job.mode,
|
||||
status: "failed",
|
||||
message: signal.aborted
|
||||
? "Agent job was cancelled or interrupted"
|
||||
: formatError(error),
|
||||
};
|
||||
try {
|
||||
input.store.finishExecution(input.job.id, input.worker, result);
|
||||
} catch (finishError) {
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
level: "error",
|
||||
jobId: input.job.id,
|
||||
message: formatError(finishError),
|
||||
}),
|
||||
);
|
||||
}
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
level: "error",
|
||||
jobId: input.job.id,
|
||||
message: formatError(error),
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user