feat: add durable AgentOS slice execution
This commit is contained in:
@@ -9,15 +9,21 @@
|
||||
"dev": "bun --env-file=../../.env flue dev",
|
||||
"dev:tailscale": "bun --env-file=../../.env flue dev",
|
||||
"run": "bun --env-file=../../.env flue run",
|
||||
"runner": "bun --env-file=../../.env src/runner.ts",
|
||||
"run:zopu": "bun --env-file=../../.env flue run zopu",
|
||||
"run:work-planner": "bun --env-file=../../.env flue run work-planner"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentos-software/codex-cli": "0.3.4",
|
||||
"@agentos-software/git": "0.3.3",
|
||||
"@code/backend": "workspace:*",
|
||||
"@code/env": "workspace:*",
|
||||
"@code/primitives": "workspace:*",
|
||||
"@flue/runtime": "latest",
|
||||
"@rivet-dev/agentos": "0.2.10",
|
||||
"convex": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"rivetkit": "2.3.9",
|
||||
"valibot": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,6 +2,13 @@ import { parseAgentEnv } from "@code/env/agent";
|
||||
import { registerProvider } from "@flue/runtime";
|
||||
import type { Fetchable } from "@flue/runtime/routing";
|
||||
import { flue } from "@flue/runtime/routing";
|
||||
import { Hono } from "hono";
|
||||
|
||||
import {
|
||||
cancelAgentOsAttempt,
|
||||
executeAgentOsAttempt,
|
||||
runtimeRegistry,
|
||||
} from "./runtime/agent-os";
|
||||
|
||||
const agentEnv = parseAgentEnv(process.env);
|
||||
|
||||
@@ -24,5 +31,35 @@ registerProvider(agentEnv.AGENT_MODEL_PROVIDER, {
|
||||
},
|
||||
});
|
||||
|
||||
// flue() returns a complete Hono app; the Fetchable contract accepts it.
|
||||
export default flue() satisfies Fetchable;
|
||||
const app = new Hono();
|
||||
|
||||
app.post("/internal/work-attempts/execute", async (context) => {
|
||||
if (
|
||||
context.req.header("authorization") !== `Bearer ${agentEnv.FLUE_DB_TOKEN}`
|
||||
) {
|
||||
return context.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
try {
|
||||
return context.json(await executeAgentOsAttempt(await context.req.json()));
|
||||
} catch (error) {
|
||||
return context.json(
|
||||
{ error: error instanceof Error ? error.message : "Execution failed" },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/internal/work-attempts/:workspaceKey/cancel", async (context) => {
|
||||
if (
|
||||
context.req.header("authorization") !== `Bearer ${agentEnv.FLUE_DB_TOKEN}`
|
||||
) {
|
||||
return context.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
await cancelAgentOsAttempt(context.req.param("workspaceKey"));
|
||||
return context.json({ cancelled: true });
|
||||
});
|
||||
|
||||
app.all("/api/rivet/*", (context) => runtimeRegistry.handler(context.req.raw));
|
||||
app.route("/", flue());
|
||||
|
||||
export default app satisfies Fetchable;
|
||||
|
||||
3
packages/agents/src/runner.ts
Normal file
3
packages/agents/src/runner.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { runtimeRegistry } from "./runtime/agent-os";
|
||||
|
||||
await runtimeRegistry.startEnvoy();
|
||||
213
packages/agents/src/runtime/agent-os.ts
Normal file
213
packages/agents/src/runtime/agent-os.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { parseAgentEnv } from "@code/env/agent";
|
||||
import { agentOS, setup } from "@rivet-dev/agentos";
|
||||
import { createClient } from "@rivet-dev/agentos/client";
|
||||
import { Effect } from "effect";
|
||||
|
||||
import {
|
||||
codexSessionEnv,
|
||||
makeCodexAgentOsConfig,
|
||||
} from "../../../primitives/src/agent-os";
|
||||
import { decodeWorkAttemptExecutionInput } from "../../../primitives/src/execution-runtime";
|
||||
import type {
|
||||
ExecutionEvent,
|
||||
WorkAttemptExecutionResult,
|
||||
} from "../../../primitives/src/execution-runtime";
|
||||
|
||||
const workspace = agentOS(makeCodexAgentOsConfig() as never);
|
||||
|
||||
export const runtimeRegistry = setup({ use: { workspace } });
|
||||
|
||||
const shellQuote = (value: string): string =>
|
||||
`'${value.replaceAll("'", `'"'"'`)}'`;
|
||||
|
||||
const event = (
|
||||
sequence: number,
|
||||
kind: ExecutionEvent["kind"],
|
||||
message: string,
|
||||
metadata: Record<string, string> = {}
|
||||
): ExecutionEvent => ({
|
||||
kind,
|
||||
message,
|
||||
metadata,
|
||||
occurredAt: Date.now(),
|
||||
sequence,
|
||||
});
|
||||
|
||||
const requireSuccess = (
|
||||
result: { exitCode: number; stderr: string; stdout: string },
|
||||
operation: string
|
||||
) => {
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`${operation} failed: ${result.stderr || result.stdout}`);
|
||||
}
|
||||
return result.stdout.trim();
|
||||
};
|
||||
|
||||
const gitAuthEnv = (username: string, credential: string) => ({
|
||||
GIT_CONFIG_COUNT: "1",
|
||||
GIT_CONFIG_KEY_0: "http.extraHeader",
|
||||
GIT_CONFIG_VALUE_0: `Authorization: Basic ${Buffer.from(`${username}:${credential}`).toString("base64")}`,
|
||||
});
|
||||
|
||||
export const executeAgentOsAttempt = async (
|
||||
rawInput: unknown
|
||||
): Promise<WorkAttemptExecutionResult> => {
|
||||
const input = await Effect.runPromise(
|
||||
decodeWorkAttemptExecutionInput(rawInput)
|
||||
);
|
||||
const env = parseAgentEnv(process.env);
|
||||
const client = createClient<typeof runtimeRegistry>({
|
||||
endpoint: env.RIVET_PUBLIC_ENDPOINT ?? env.RIVET_ENDPOINT,
|
||||
});
|
||||
const vm = client.workspace.getOrCreate([input.workspaceKey]);
|
||||
const events: ExecutionEvent[] = [
|
||||
event(0, "runtime.preparing", "AgentOS workspace selected", {
|
||||
workspaceKey: input.workspaceKey,
|
||||
}),
|
||||
];
|
||||
const authEnv = gitAuthEnv(
|
||||
input.auth.username ??
|
||||
(input.auth.provider === "github" ? "x-access-token" : "git"),
|
||||
input.auth.credential
|
||||
);
|
||||
|
||||
await vm.mkdir("/workspace", { recursive: true });
|
||||
if (!(await vm.exists("/workspace/repository/.git"))) {
|
||||
events.push(event(1, "repository.cloning", "Cloning project repository"));
|
||||
const clone = await vm.execArgv(
|
||||
"git",
|
||||
["clone", input.repositoryUrl, "/workspace/repository"],
|
||||
{
|
||||
cwd: "/workspace",
|
||||
env: authEnv,
|
||||
}
|
||||
);
|
||||
requireSuccess(clone, "Repository clone");
|
||||
}
|
||||
|
||||
const checkout = await vm.exec(
|
||||
`git fetch origin ${shellQuote(input.baseBranch)} && git checkout -B ${shellQuote(`zopu/${input.runId}`)} ${shellQuote(`origin/${input.baseBranch}`)}`,
|
||||
{ cwd: "/workspace/repository", env: authEnv }
|
||||
);
|
||||
requireSuccess(checkout, "Repository checkout");
|
||||
const baseRevision = requireSuccess(
|
||||
await vm.execArgv("git", ["rev-parse", "HEAD"], {
|
||||
cwd: "/workspace/repository",
|
||||
}),
|
||||
"Base revision lookup"
|
||||
);
|
||||
events.push(
|
||||
event(2, "repository.ready", "Repository checkout is ready", {
|
||||
baseRevision,
|
||||
})
|
||||
);
|
||||
|
||||
const sessionId = `codex-${input.attemptId}`;
|
||||
await vm.openSession({
|
||||
additionalInstructions:
|
||||
"Work only inside /workspace/repository. Do not reveal credentials. Make the requested change and run focused verification. Do not push or open a pull request.",
|
||||
agent: "codex",
|
||||
cwd: "/workspace/repository",
|
||||
env: codexSessionEnv({
|
||||
apiKey: env.AGENT_MODEL_API_KEY,
|
||||
baseUrl: env.AGENT_MODEL_BASE_URL,
|
||||
model: env.AGENT_MODEL_NAME,
|
||||
}),
|
||||
permissionPolicy: "allow_all",
|
||||
sessionId,
|
||||
});
|
||||
events.push(
|
||||
event(3, "harness.started", "Codex implementation session started")
|
||||
);
|
||||
const promptResult = await vm.prompt({
|
||||
content: [{ text: input.prompt, type: "text" }],
|
||||
idempotencyKey: input.attemptId,
|
||||
sessionId,
|
||||
});
|
||||
events.push(
|
||||
event(4, "harness.progress", "Codex implementation turn completed", {
|
||||
stopReason: String(promptResult.stopReason),
|
||||
})
|
||||
);
|
||||
|
||||
const status = requireSuccess(
|
||||
await vm.execArgv("git", ["status", "--porcelain"], {
|
||||
cwd: "/workspace/repository",
|
||||
}),
|
||||
"Changed file lookup"
|
||||
);
|
||||
const diff = requireSuccess(
|
||||
await vm.execArgv("git", ["diff", "--binary", "HEAD"], {
|
||||
cwd: "/workspace/repository",
|
||||
}),
|
||||
"Diff collection"
|
||||
);
|
||||
const changedFiles = status
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => line.slice(3).trim());
|
||||
let candidateRevision = baseRevision;
|
||||
if (changedFiles.length > 0) {
|
||||
requireSuccess(
|
||||
await vm.execArgv("git", ["add", "-A"], { cwd: "/workspace/repository" }),
|
||||
"Candidate staging"
|
||||
);
|
||||
const tree = requireSuccess(
|
||||
await vm.execArgv("git", ["write-tree"], {
|
||||
cwd: "/workspace/repository",
|
||||
}),
|
||||
"Candidate tree creation"
|
||||
);
|
||||
candidateRevision = requireSuccess(
|
||||
await vm.exec(
|
||||
`printf %s ${shellQuote(`Zopu candidate for ${input.attemptId}`)} | git commit-tree ${shellQuote(tree)} -p ${shellQuote(baseRevision)}`,
|
||||
{
|
||||
cwd: "/workspace/repository",
|
||||
env: {
|
||||
GIT_AUTHOR_EMAIL: "agent@zopu.dev",
|
||||
GIT_AUTHOR_NAME: "Zopu Agent",
|
||||
GIT_COMMITTER_EMAIL: "agent@zopu.dev",
|
||||
GIT_COMMITTER_NAME: "Zopu Agent",
|
||||
},
|
||||
}
|
||||
),
|
||||
"Candidate revision creation"
|
||||
);
|
||||
requireSuccess(
|
||||
await vm.execArgv("git", ["reset"], { cwd: "/workspace/repository" }),
|
||||
"Candidate index reset"
|
||||
);
|
||||
}
|
||||
events.push(
|
||||
event(
|
||||
5,
|
||||
"repository.changed",
|
||||
`${changedFiles.length} changed file(s) collected`,
|
||||
{
|
||||
candidateRevision,
|
||||
}
|
||||
),
|
||||
event(6, "runtime.completed", "AgentOS execution completed")
|
||||
);
|
||||
|
||||
return {
|
||||
baseRevision,
|
||||
candidateRevision,
|
||||
changedFiles,
|
||||
diff,
|
||||
environmentId: input.workspaceKey,
|
||||
events,
|
||||
summary:
|
||||
changedFiles.length > 0
|
||||
? `Codex changed ${changedFiles.length} file(s)`
|
||||
: "Codex completed without repository changes",
|
||||
};
|
||||
};
|
||||
|
||||
export const cancelAgentOsAttempt = async (workspaceKey: string) => {
|
||||
const env = parseAgentEnv(process.env);
|
||||
const client = createClient<typeof runtimeRegistry>({
|
||||
endpoint: env.RIVET_PUBLIC_ENDPOINT ?? env.RIVET_ENDPOINT,
|
||||
});
|
||||
await client.workspace.getOrCreate([workspaceKey]).cancelPrompt({});
|
||||
};
|
||||
Reference in New Issue
Block a user