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

@@ -7,7 +7,8 @@
"build": "flue build",
"check-types": "tsc --noEmit",
"dev": "flue dev",
"run": "flue run"
"run": "flue run",
"run:zopu": "flue run zopu"
},
"dependencies": {
"@flue/runtime": "latest"

View File

@@ -1,6 +0,0 @@
import { defineAgent } from '@flue/runtime';
export default defineAgent(() => ({
model: 'anthropic/claude-sonnet-4-6',
instructions: 'Give a concise, helpful hello-world response for this project.',
}));

View File

@@ -0,0 +1,11 @@
import { defineAgent } from "@flue/runtime";
export default defineAgent(() => ({
description: "Zopu is the primary coding agent users collaborate with.",
model: "anthropic/claude-sonnet-4-6",
instructions: `You are Zopu, the primary agent users collaborate with.
Work as a pragmatic senior coding agent. Understand the user's goal, inspect the current workspace, make the necessary changes, and verify that the result works. Prefer direct, maintainable solutions over unnecessary abstraction.
Your workspace is an isolated agentOS VM dedicated to this agent. Treat its filesystem, processes, and session state as the complete working environment. Use the capabilities available in the workspace, and do not assume access to resources outside it.`,
}));

View File

@@ -9,6 +9,9 @@
*/
import type * as auth from "../auth.js";
import type * as daemonCommands from "../daemonCommands.js";
import type * as daemonRuntime from "../daemonRuntime.js";
import type * as daemons from "../daemons.js";
import type * as healthCheck from "../healthCheck.js";
import type * as http from "../http.js";
import type * as privateData from "../privateData.js";
@@ -22,6 +25,9 @@ import type {
declare const fullApi: ApiFromModules<{
auth: typeof auth;
daemonCommands: typeof daemonCommands;
daemonRuntime: typeof daemonRuntime;
daemons: typeof daemons;
healthCheck: typeof healthCheck;
http: typeof http;
privateData: typeof privateData;

View File

@@ -0,0 +1,199 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
const commandStatus = v.union(
v.literal("queued"),
v.literal("claimed"),
v.literal("running"),
v.literal("succeeded"),
v.literal("failed"),
v.literal("cancelled")
);
export const enqueue = mutation({
args: {
daemonId: v.string(),
method: v.string(),
args: v.any(),
actorKey: v.array(v.string()),
},
handler: async (ctx, args) => {
const timestamp = Date.now();
return await ctx.db.insert("daemonCommands", {
...args,
status: "queued",
attempts: 0,
createdAt: timestamp,
updatedAt: timestamp,
});
},
});
export const list = query({
args: {
daemonId: v.string(),
status: v.optional(commandStatus),
},
handler: async (ctx, args) => {
if (args.status) {
return await ctx.db
.query("daemonCommands")
.withIndex("by_daemonId_and_status", (q) =>
q.eq("daemonId", args.daemonId).eq("status", args.status!)
)
.order("desc")
.take(100);
}
return await ctx.db
.query("daemonCommands")
.filter((q) => q.eq(q.field("daemonId"), args.daemonId))
.order("desc")
.take(100);
},
});
export const available = query({
args: { daemonId: v.string() },
handler: async (ctx, args) => {
const queued = await ctx.db
.query("daemonCommands")
.withIndex("by_daemonId_and_status", (q) =>
q.eq("daemonId", args.daemonId).eq("status", "queued")
)
.order("asc")
.take(25);
const expiredClaims = await ctx.db
.query("daemonCommands")
.withIndex("by_daemonId_and_status", (q) =>
q.eq("daemonId", args.daemonId).eq("status", "claimed")
)
.filter((q) => q.lt(q.field("leaseExpiresAt"), Date.now()))
.order("asc")
.take(25);
return [...expiredClaims, ...queued].slice(0, 25);
},
});
export const claim = mutation({
args: {
commandId: v.id("daemonCommands"),
daemonId: v.string(),
sessionId: v.string(),
leaseDurationMs: v.number(),
},
handler: async (ctx, args) => {
const command = await ctx.db.get("daemonCommands", args.commandId);
if (!command || command.daemonId !== args.daemonId) {
return null;
}
const timestamp = Date.now();
const canClaim =
command.status === "queued" ||
(command.status === "claimed" &&
command.leaseExpiresAt !== undefined &&
command.leaseExpiresAt <= timestamp);
if (!canClaim) {
return null;
}
await ctx.db.patch("daemonCommands", command._id, {
status: "claimed",
claimedBySessionId: args.sessionId,
leaseExpiresAt: timestamp + args.leaseDurationMs,
attempts: command.attempts + 1,
updatedAt: timestamp,
});
return { ...command, status: "claimed" as const };
},
});
export const start = mutation({
args: {
commandId: v.id("daemonCommands"),
sessionId: v.string(),
},
handler: async (ctx, args) => {
const command = await ctx.db.get("daemonCommands", args.commandId);
if (
!command ||
command.status !== "claimed" ||
command.claimedBySessionId !== args.sessionId
) {
return false;
}
const timestamp = Date.now();
await ctx.db.patch("daemonCommands", command._id, {
status: "running",
startedAt: timestamp,
updatedAt: timestamp,
});
return true;
},
});
export const succeed = mutation({
args: {
commandId: v.id("daemonCommands"),
sessionId: v.string(),
result: v.any(),
},
handler: async (ctx, args) => {
const command = await ctx.db.get("daemonCommands", args.commandId);
if (!command || command.claimedBySessionId !== args.sessionId) {
return false;
}
const timestamp = Date.now();
await ctx.db.patch("daemonCommands", command._id, {
status: "succeeded",
result: args.result,
completedAt: timestamp,
updatedAt: timestamp,
leaseExpiresAt: undefined,
});
return true;
},
});
export const fail = mutation({
args: {
commandId: v.id("daemonCommands"),
sessionId: v.string(),
error: v.string(),
},
handler: async (ctx, args) => {
const command = await ctx.db.get("daemonCommands", args.commandId);
if (!command || command.claimedBySessionId !== args.sessionId) {
return false;
}
const timestamp = Date.now();
await ctx.db.patch("daemonCommands", command._id, {
status: "failed",
error: args.error,
completedAt: timestamp,
updatedAt: timestamp,
leaseExpiresAt: undefined,
});
return true;
},
});
export const cancel = mutation({
args: { commandId: v.id("daemonCommands") },
handler: async (ctx, args) => {
const command = await ctx.db.get("daemonCommands", args.commandId);
if (
!command ||
["succeeded", "failed", "cancelled"].includes(command.status)
) {
return false;
}
const timestamp = Date.now();
await ctx.db.patch("daemonCommands", command._id, {
status: "cancelled",
completedAt: timestamp,
updatedAt: timestamp,
leaseExpiresAt: undefined,
});
return true;
},
});

View File

@@ -0,0 +1,107 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
export const connect = mutation({
args: {
daemonId: v.string(),
sessionId: v.string(),
hostname: v.string(),
platform: v.string(),
architecture: v.string(),
version: v.string(),
},
handler: async (ctx, args) => {
const existing = await ctx.db
.query("daemonPresence")
.withIndex("by_sessionId", (q) => q.eq("sessionId", args.sessionId))
.unique();
const timestamp = Date.now();
if (existing) {
await ctx.db.patch("daemonPresence", existing._id, {
status: "online",
lastHeartbeatAt: timestamp,
});
return existing._id;
}
return await ctx.db.insert("daemonPresence", {
...args,
status: "online",
startedAt: timestamp,
lastHeartbeatAt: timestamp,
});
},
});
export const heartbeat = mutation({
args: {
daemonId: v.string(),
sessionId: v.string(),
},
handler: async (ctx, args) => {
const presence = await ctx.db
.query("daemonPresence")
.withIndex("by_sessionId", (q) => q.eq("sessionId", args.sessionId))
.unique();
if (!presence || presence.daemonId !== args.daemonId) {
return false;
}
await ctx.db.patch("daemonPresence", presence._id, {
status: "online",
lastHeartbeatAt: Date.now(),
});
return true;
},
});
export const disconnect = mutation({
args: {
daemonId: v.string(),
sessionId: v.string(),
},
handler: async (ctx, args) => {
const presence = await ctx.db
.query("daemonPresence")
.withIndex("by_sessionId", (q) => q.eq("sessionId", args.sessionId))
.unique();
if (!presence || presence.daemonId !== args.daemonId) {
return false;
}
await ctx.db.patch("daemonPresence", presence._id, {
status: "offline",
lastHeartbeatAt: Date.now(),
});
return true;
},
});
export const recordEvent = mutation({
args: {
daemonId: v.string(),
commandId: v.optional(v.id("daemonCommands")),
kind: v.string(),
data: v.any(),
},
handler: async (ctx, args) => {
return await ctx.db.insert("daemonCommandEvents", {
...args,
createdAt: Date.now(),
});
},
});
export const listEvents = query({
args: {
daemonId: v.string(),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
return await ctx.db
.query("daemonCommandEvents")
.withIndex("by_daemonId_and_createdAt", (q) =>
q.eq("daemonId", args.daemonId)
)
.order("desc")
.take(Math.min(args.limit ?? 100, 500));
},
});

View File

@@ -0,0 +1,60 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
const now = () => Date.now();
export const upsert = mutation({
args: {
daemonId: v.string(),
name: v.string(),
desiredState: v.union(v.literal("online"), v.literal("offline")),
agentOsConfig: v.any(),
},
handler: async (ctx, args) => {
const existing = await ctx.db
.query("daemonDefinitions")
.withIndex("by_daemonId", (q) => q.eq("daemonId", args.daemonId))
.unique();
const timestamp = now();
if (existing) {
await ctx.db.patch("daemonDefinitions", existing._id, {
name: args.name,
desiredState: args.desiredState,
agentOsConfig: args.agentOsConfig,
configVersion: existing.configVersion + 1,
updatedAt: timestamp,
});
return existing._id;
}
return await ctx.db.insert("daemonDefinitions", {
...args,
configVersion: 1,
createdAt: timestamp,
updatedAt: timestamp,
});
},
});
export const get = query({
args: { daemonId: v.string() },
handler: async (ctx, args) => {
const definition = await ctx.db
.query("daemonDefinitions")
.withIndex("by_daemonId", (q) => q.eq("daemonId", args.daemonId))
.unique();
const presence = await ctx.db
.query("daemonPresence")
.withIndex("by_daemonId", (q) => q.eq("daemonId", args.daemonId))
.order("desc")
.first();
return { definition, presence };
},
});
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("daemonDefinitions").order("desc").collect();
},
});

View File

@@ -2,6 +2,66 @@ import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
daemonDefinitions: defineTable({
daemonId: v.string(),
name: v.string(),
desiredState: v.union(v.literal("online"), v.literal("offline")),
agentOsConfig: v.any(),
configVersion: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
}).index("by_daemonId", ["daemonId"]),
daemonPresence: defineTable({
daemonId: v.string(),
sessionId: v.string(),
status: v.union(
v.literal("online"),
v.literal("draining"),
v.literal("offline")
),
hostname: v.string(),
platform: v.string(),
architecture: v.string(),
version: v.string(),
startedAt: v.number(),
lastHeartbeatAt: v.number(),
})
.index("by_daemonId", ["daemonId"])
.index("by_sessionId", ["sessionId"]),
daemonCommands: defineTable({
daemonId: v.string(),
method: v.string(),
args: v.any(),
actorKey: v.array(v.string()),
status: v.union(
v.literal("queued"),
v.literal("claimed"),
v.literal("running"),
v.literal("succeeded"),
v.literal("failed"),
v.literal("cancelled")
),
attempts: v.number(),
claimedBySessionId: v.optional(v.string()),
leaseExpiresAt: v.optional(v.number()),
createdAt: v.number(),
updatedAt: v.number(),
startedAt: v.optional(v.number()),
completedAt: v.optional(v.number()),
result: v.optional(v.any()),
error: v.optional(v.string()),
})
.index("by_daemonId_and_status", ["daemonId", "status"])
.index("by_status_and_leaseExpiresAt", ["status", "leaseExpiresAt"]),
daemonCommandEvents: defineTable({
daemonId: v.string(),
commandId: v.optional(v.id("daemonCommands")),
kind: v.string(),
data: v.any(),
createdAt: v.number(),
})
.index("by_daemonId_and_createdAt", ["daemonId", "createdAt"])
.index("by_commandId_and_createdAt", ["commandId", "createdAt"]),
todos: defineTable({
text: v.string(),
completed: v.boolean(),

View File

@@ -5,7 +5,8 @@
"type": "module",
"exports": {
"./web": "./src/web.ts",
"./native": "./src/native.ts"
"./native": "./src/native.ts",
"./server": "./src/server.ts"
},
"dependencies": {
"@t3-oss/env-core": "^0.13.11",

22
packages/env/src/server.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";
const convexUrlSchema = (exampleHost: string) =>
z.url().refine((url) => new URL(url).hostname !== exampleHost, {
message: `Replace the ${exampleHost} placeholder before running the app`,
});
export const env = createEnv({
server: {
CONVEX_URL: convexUrlSchema("example.convex.cloud"),
DAEMON_ID: z.string().min(1),
DAEMON_NAME: z.string().min(1).optional(),
DAEMON_VERSION: z.string().default("0.0.0"),
DAEMON_HEARTBEAT_MS: z.coerce.number().int().positive().default(15_000),
DAEMON_COMMAND_LEASE_MS: z.coerce.number().int().positive().default(60_000),
RIVET_ENDPOINT: z.url().optional(),
},
runtimeEnv: process.env,
skipValidation: process.env.SKIP_ENV_VALIDATION === "true",
emptyStringAsUndefined: true,
});

View File

@@ -0,0 +1,22 @@
{
"name": "@code/primitives",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts",
"./agent-os": "./src/agent-os.ts"
},
"scripts": {
"check-types": "tsc --noEmit"
},
"dependencies": {
"@rivet-dev/agentos": "^0.2.7",
"effect": "4.0.0-beta.99"
},
"devDependencies": {
"@code/config": "workspace:*",
"@types/node": "^22.13.14",
"typescript": "^6"
}
}

View File

@@ -0,0 +1,64 @@
import {
agentOS as createAgentOsActor,
type AgentOSConfigInput,
} from "@rivet-dev/agentos";
import { Context, Effect, Layer, Schema } from "effect";
export class AgentOsError extends Schema.TaggedErrorClass<AgentOsError>()(
"AgentOsError",
{
cause: Schema.Defect(),
}
) {}
export interface AgentOsOptions {
readonly config?: AgentOSConfigInput<undefined>;
}
export class AgentOs extends Context.Service<
AgentOs,
{
readonly createActor: (
config?: AgentOSConfigInput<undefined>
) => Effect.Effect<
ReturnType<typeof createAgentOsActor<undefined>>,
AgentOsError
>;
}
>()("@code/primitives/agent-os/AgentOs") {
static readonly layer = Layer.succeed(
AgentOs,
AgentOs.of({
createActor: Effect.fn("AgentOs.createActor")(function* (
config: AgentOSConfigInput<undefined> = {}
) {
return yield* Effect.try({
try: () => createAgentOsActor<undefined>(config),
catch: (cause) => new AgentOsError({ cause }),
});
}),
})
);
}
export const makeAgentOsLayer = (options: AgentOsOptions = {}) =>
Layer.succeed(
AgentOs,
AgentOs.of({
createActor: Effect.fn("AgentOs.createActor")(function* (
config: AgentOSConfigInput<undefined> = options.config ?? {}
) {
return yield* Effect.try({
try: () => createAgentOsActor<undefined>(config),
catch: (cause) => new AgentOsError({ cause }),
});
}),
})
);
export const createAgentOsActorEffect = Effect.fn("createAgentOsActorEffect")(
function* (config?: AgentOSConfigInput<undefined>) {
const agentOs = yield* AgentOs;
return yield* agentOs.createActor(config);
}
);

View File

@@ -0,0 +1 @@
export * from "./agent-os";

View File

@@ -0,0 +1,5 @@
{
"extends": "@code/config/tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["dist"]
}