Add AgentOS daemon control plane and maintenance tooling

This commit is contained in:
-Puter
2026-07-19 06:29:51 +05:30
parent a37e0cc3c9
commit ec84c52155
621 changed files with 97578 additions and 15 deletions

View File

@@ -0,0 +1,74 @@
import type { Id } from "@code/backend/convex/_generated/dataModel";
import { env } from "@code/env/server";
import { AgentOs, createAgentOsActorEffect } from "@code/primitives/agent-os";
import { Effect, Schema } from "effect";
import { setup } from "rivetkit";
import { createClient } from "rivetkit/client";
import type { DaemonCommand } from "./convex";
import { ConvexControlPlane } from "./convex";
export class AgentOsCommandError extends Schema.TaggedErrorClass<AgentOsCommandError>()(
"AgentOsCommandError",
{
method: Schema.String,
cause: Schema.Defect(),
}
) {}
const asArgs = (value: unknown): unknown[] => {
if (!Array.isArray(value)) {
throw new TypeError("agentOS command args must be an array");
}
return value;
};
export const makeAgentOsRuntime = Effect.gen(function* () {
const actor = yield* createAgentOsActorEffect();
const registry = setup({ use: { agentOs: actor } });
registry.start();
const client = createClient<typeof registry>(env.RIVET_ENDPOINT);
const execute = Effect.fn("AgentOsRuntime.execute")(function* (
command: DaemonCommand
) {
const controlPlane = yield* ConvexControlPlane;
const handle = client.agentOs.getOrCreate([...command.actorKey]);
return yield* Effect.tryPromise({
try: () =>
handle.action({
name: command.method,
args: asArgs(command.args),
}),
catch: (cause) =>
new AgentOsCommandError({ method: command.method, cause }),
}).pipe(
Effect.tap((result) =>
controlPlane.recordEvent({
commandId: command._id,
kind: "agentos.action.succeeded",
data: { method: command.method, result },
})
),
Effect.tapError((error) =>
controlPlane.recordEvent({
commandId: command._id,
kind: "agentos.action.failed",
data: { method: command.method, error: String(error.cause) },
})
)
);
});
const recordLifecycleEvent = (
commandId: Id<"daemonCommands"> | undefined,
kind: string,
data: unknown
) =>
Effect.gen(function* () {
const controlPlane = yield* ConvexControlPlane;
yield* controlPlane.recordEvent({ commandId, kind, data });
});
return { execute, recordLifecycleEvent } as const;
}).pipe(Effect.provide(AgentOs.layer));

174
apps/daemon/src/convex.ts Normal file
View File

@@ -0,0 +1,174 @@
import { api } from "@code/backend/convex/_generated/api";
import type { Id } from "@code/backend/convex/_generated/dataModel";
import { env } from "@code/env/server";
import { ConvexClient } from "convex/browser";
import { Cause, Context, Effect, Layer, Queue, Schema, Stream } from "effect";
export class ConvexControlPlaneError extends Schema.TaggedErrorClass<ConvexControlPlaneError>()(
"ConvexControlPlaneError",
{
operation: Schema.String,
cause: Schema.Defect(),
}
) {}
export interface DaemonCommand {
readonly _id: Id<"daemonCommands">;
readonly daemonId: string;
readonly actorKey: readonly string[];
readonly method: string;
readonly args: unknown;
}
const tryPromise = <A>(operation: string, evaluate: () => Promise<A>) =>
Effect.tryPromise({
try: evaluate,
catch: (cause) => new ConvexControlPlaneError({ operation, cause }),
});
export class ConvexControlPlane extends Context.Service<
ConvexControlPlane,
{
readonly commands: Stream.Stream<
readonly DaemonCommand[],
ConvexControlPlaneError
>;
readonly connect: (input: {
readonly sessionId: string;
readonly hostname: string;
readonly platform: string;
readonly architecture: string;
}) => Effect.Effect<void, ConvexControlPlaneError>;
readonly heartbeat: (
sessionId: string
) => Effect.Effect<void, ConvexControlPlaneError>;
readonly disconnect: (
sessionId: string
) => Effect.Effect<void, ConvexControlPlaneError>;
readonly claim: (
commandId: Id<"daemonCommands">,
sessionId: string
) => Effect.Effect<DaemonCommand | null, ConvexControlPlaneError>;
readonly start: (
commandId: Id<"daemonCommands">,
sessionId: string
) => Effect.Effect<boolean, ConvexControlPlaneError>;
readonly succeed: (
commandId: Id<"daemonCommands">,
sessionId: string,
result: unknown
) => Effect.Effect<boolean, ConvexControlPlaneError>;
readonly fail: (
commandId: Id<"daemonCommands">,
sessionId: string,
error: string
) => Effect.Effect<boolean, ConvexControlPlaneError>;
readonly recordEvent: (input: {
readonly commandId?: Id<"daemonCommands">;
readonly kind: string;
readonly data: unknown;
}) => Effect.Effect<void, ConvexControlPlaneError>;
readonly close: Effect.Effect<void, ConvexControlPlaneError>;
}
>()("@code/daemon/ConvexControlPlane") {
static readonly layer = Layer.effect(
ConvexControlPlane,
Effect.gen(function* () {
const client = yield* Effect.acquireRelease(
Effect.sync(() => new ConvexClient(env.CONVEX_URL)),
(client) => Effect.promise(() => client.close())
);
const commands = Stream.callback<
readonly DaemonCommand[],
ConvexControlPlaneError
>((queue) =>
Effect.acquireRelease(
Effect.sync(() =>
client.onUpdate(
api.daemonCommands.available,
{ daemonId: env.DAEMON_ID },
(items) => {
Queue.offerUnsafe(queue, items);
},
(cause) => {
Queue.failCauseUnsafe(
queue,
Cause.fail(
new ConvexControlPlaneError({
operation: "subscribe commands",
cause,
})
)
);
}
)
),
(unsubscribe) => Effect.sync(unsubscribe)
)
);
return ConvexControlPlane.of({
commands,
connect: (input) =>
tryPromise("connect daemon", async () => {
await client.mutation(api.daemonRuntime.connect, {
daemonId: env.DAEMON_ID,
version: env.DAEMON_VERSION,
...input,
});
}),
heartbeat: (sessionId) =>
tryPromise("heartbeat daemon", async () => {
await client.mutation(api.daemonRuntime.heartbeat, {
daemonId: env.DAEMON_ID,
sessionId,
});
}),
disconnect: (sessionId) =>
tryPromise("disconnect daemon", async () => {
await client.mutation(api.daemonRuntime.disconnect, {
daemonId: env.DAEMON_ID,
sessionId,
});
}),
claim: (commandId, sessionId) =>
tryPromise("claim command", () =>
client.mutation(api.daemonCommands.claim, {
commandId,
daemonId: env.DAEMON_ID,
sessionId,
leaseDurationMs: env.DAEMON_COMMAND_LEASE_MS,
})
),
start: (commandId, sessionId) =>
tryPromise("start command", () =>
client.mutation(api.daemonCommands.start, { commandId, sessionId })
),
succeed: (commandId, sessionId, result) =>
tryPromise("complete command", () =>
client.mutation(api.daemonCommands.succeed, {
commandId,
sessionId,
result,
})
),
fail: (commandId, sessionId, error) =>
tryPromise("fail command", () =>
client.mutation(api.daemonCommands.fail, {
commandId,
sessionId,
error,
})
),
recordEvent: (input) =>
tryPromise("record daemon event", async () => {
await client.mutation(api.daemonRuntime.recordEvent, {
daemonId: env.DAEMON_ID,
...input,
});
}),
close: tryPromise("close Convex client", () => client.close()),
});
})
);
}

7
apps/daemon/src/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import { BunRuntime } from "@effect/platform-bun";
import { Effect } from "effect";
import { ConvexControlPlane } from "./convex";
import { runDaemon } from "./runtime";
BunRuntime.runMain(runDaemon.pipe(Effect.provide(ConvexControlPlane.layer)));

View File

@@ -0,0 +1,83 @@
import { env } from "@code/env/server";
import { Console, Effect, FiberSet, Result, Stream } from "effect";
import { makeAgentOsRuntime } from "./agent-os";
import { ConvexControlPlane, type DaemonCommand } from "./convex";
const errorMessage = (cause: unknown) =>
cause instanceof Error ? cause.message : String(cause);
export const runDaemon = Effect.scoped(
Effect.gen(function* () {
const controlPlane = yield* ConvexControlPlane;
const agentOs = yield* makeAgentOsRuntime;
const sessionId = crypto.randomUUID();
const hostname = yield* Effect.promise(() => import("node:os")).pipe(
Effect.map((os) => os.hostname())
);
const fibers = yield* FiberSet.make<void>();
yield* controlPlane.connect({
sessionId,
hostname,
platform: process.platform,
architecture: process.arch,
});
yield* Console.log(`daemon ${env.DAEMON_ID} connected as ${sessionId}`);
yield* Effect.addFinalizer(() =>
controlPlane
.disconnect(sessionId)
.pipe(
Effect.catchCause((cause) =>
Console.error("failed to mark daemon offline", cause)
)
)
);
yield* Stream.tick(env.DAEMON_HEARTBEAT_MS).pipe(
Stream.runForEach(() => controlPlane.heartbeat(sessionId)),
Effect.forkScoped
);
const processCommand = Effect.fn("Daemon.processCommand")(function* (
candidate: DaemonCommand
) {
const command = yield* controlPlane.claim(candidate._id, sessionId);
if (!command) {
return;
}
const started = yield* controlPlane.start(command._id, sessionId);
if (!started) {
return;
}
yield* controlPlane.recordEvent({
commandId: command._id,
kind: "command.started",
data: { method: command.method, actorKey: command.actorKey },
});
const result = yield* agentOs.execute(command).pipe(Effect.result);
if (Result.isSuccess(result)) {
yield* controlPlane.succeed(command._id, sessionId, result.success);
return;
}
yield* controlPlane.fail(
command._id,
sessionId,
errorMessage(result.failure)
);
});
yield* controlPlane.commands.pipe(
Stream.runForEach((commands) =>
Effect.forEach(
commands,
(command) => FiberSet.run(fibers, processCommand(command)),
{
discard: true,
}
)
)
);
})
);