make webhook

This commit is contained in:
2026-07-13 18:50:38 +02:00
parent 2f128550fc
commit 40c822d3bf
67 changed files with 6286 additions and 2053 deletions
+211
View File
@@ -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),
}),
);
}