g1t/apps/web/app/routes/repo/agents-live.ts

65 lines3,009 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents and memory, checks and conflicts, profiles, slug renames, custom domains1/**
2 * A project's agent runs as JSON, for the parts of other pages that show
3 * them live (a pull request's Agent panel, the pull request list), and
4 * the one place runs are stopped and messaged from.
5 */
6import { env } from "cloudflare:workers";
7import { data } from "react-router";
8
9import { RUN_KINDS, type RunKind, takesMessages } from "@g1t/contracts";
10
11import type { Route } from "./+types/agents-live";
12import { agents, work } from "../../lib/services.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look13import { assertSameOrigin, getViewer } from "../../lib/session.server";
14import { accessTo, refusal } from "../../lib/access.server";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains15
16export async function loader({ params, context, request }: Route.LoaderArgs) {
17 const viewer = getViewer(context);
18 const query = new URL(request.url).searchParams;
19 const number = Number(query.get("number"));
20 const kind = query.get("kind");
21 const found = await agents.listRuns(viewer, {
22 repo: { namespace: params.owner, name: params.repo },
23 active: query.get("active") === "1",
24 number: number > 0 ? number : undefined,
25 kind: kind && RUN_KINDS.includes(kind as RunKind) ? (kind as RunKind) : undefined,
26 limit: Math.min(Number(query.get("limit")) || 50, 200),
27 });
28 if (!found.ok) throw data({ error: found.error.message }, { status: 404 });
29 return Response.json(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30 { runs: found.value, member: (await accessTo(context, params)).can.run },
Agents and memory, checks and conflicts, profiles, slug renames, custom domains31 { headers: { "cache-control": "no-store" } },
32 );
33}
34
35export async function action({ params, context, request }: Route.ActionArgs) {
36 assertSameOrigin(request);
37 const viewer = getViewer(context);
38 if (!viewer) return { ok: false, error: "Sign in first." };
39 const path = { namespace: params.owner, name: params.repo };
40 const form = await request.formData();
41 const intent = String(form.get("intent") ?? "");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look42 // Stopping or messaging an agent needs Write: Read cannot spend compute.
43 const refused = await refusal(context, params, "run");
44 if (refused) return { ok: false, error: refused };
Agents and memory, checks and conflicts, profiles, slug renames, custom domains45 if (intent === "stop") {
46 const stopped = await env.RUNNER.stopRun(viewer, path, String(form.get("run") ?? ""));
47 return stopped.ok ? { ok: true, notice: "Stopped." } : { ok: false, error: stopped.error.message };
48 }
49 if (intent === "message") {
50 const number = Number(form.get("number"));
51 const sent = await work.messageAgent(viewer, path, number, String(form.get("body") ?? ""));
52 if (!sent.ok) return { ok: false, error: sent.error.message };
53 // Whether the agent reads it now, or its next run on the pull request does.
54 const active = await agents.listRuns(viewer, { repo: path, number, active: true, limit: 1 });
55 const run = active.ok ? active.value[0] : undefined;
56 return {
57 ok: true,
58 notice:
59 run && takesMessages(run.kind)
60 ? "Sent. The agent reads it at its next step."
61 : "Sent. This run does not read messages while it works, so the agent's next run on this pull request gets it.",
62 };
63 }
64 return { ok: false, error: "Unknown action." };
65}