mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
Compare commits
1 Commits
agent/lamb
...
agent/cc-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68c3d312bd |
@@ -1 +1,2 @@
|
||||
export { runtimeKeys, runtimeListOptions } from "./queries";
|
||||
export { useDeleteRuntime } from "./mutations";
|
||||
|
||||
13
apps/web/core/runtimes/mutations.ts
Normal file
13
apps/web/core/runtimes/mutations.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { api } from "@/shared/api";
|
||||
import { runtimeKeys } from "./queries";
|
||||
|
||||
export function useDeleteRuntime(wsId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (runtimeId: string) => api.deleteRuntime(runtimeId),
|
||||
onSettled: () => {
|
||||
qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4,11 +4,12 @@ import { api } from "@/shared/api";
|
||||
export const runtimeKeys = {
|
||||
all: (wsId: string) => ["runtimes", wsId] as const,
|
||||
list: (wsId: string) => [...runtimeKeys.all(wsId), "list"] as const,
|
||||
listMine: (wsId: string) => [...runtimeKeys.all(wsId), "list", "mine"] as const,
|
||||
};
|
||||
|
||||
export function runtimeListOptions(wsId: string) {
|
||||
export function runtimeListOptions(wsId: string, owner?: "me") {
|
||||
return queryOptions({
|
||||
queryKey: runtimeKeys.list(wsId),
|
||||
queryFn: () => api.listRuntimes({ workspace_id: wsId }),
|
||||
queryKey: owner === "me" ? runtimeKeys.listMine(wsId) : runtimeKeys.list(wsId),
|
||||
queryFn: () => api.listRuntimes({ workspace_id: wsId, owner }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { AgentRuntime } from "@/shared/types";
|
||||
import { useAuthStore } from "@/features/auth";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { memberListOptions } from "@core/workspace/queries";
|
||||
import { useDeleteRuntime } from "@core/runtimes/mutations";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { formatLastSeen } from "../utils";
|
||||
import { RuntimeModeIcon, StatusBadge, InfoField } from "./shared";
|
||||
import { PingSection } from "./ping-section";
|
||||
@@ -20,6 +41,41 @@ export function RuntimeDetail({ runtime }: { runtime: AgentRuntime }) {
|
||||
const cliVersion =
|
||||
runtime.runtime_mode === "local" ? getCliVersion(runtime.metadata) : null;
|
||||
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
const deleteMutation = useDeleteRuntime(wsId);
|
||||
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
|
||||
// Resolve owner info
|
||||
const ownerMember = runtime.owner_id
|
||||
? members.find((m) => m.user_id === runtime.owner_id)
|
||||
: null;
|
||||
const ownerName = ownerMember?.name ?? null;
|
||||
|
||||
// Permission check for delete
|
||||
const currentMember = user
|
||||
? members.find((m) => m.user_id === user.id)
|
||||
: null;
|
||||
const isAdmin = currentMember
|
||||
? currentMember.role === "owner" || currentMember.role === "admin"
|
||||
: false;
|
||||
const isRuntimeOwner = user && runtime.owner_id === user.id;
|
||||
const canDelete = isAdmin || isRuntimeOwner;
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteMutation.mutate(runtime.id, {
|
||||
onSuccess: () => {
|
||||
toast.success("Runtime deleted");
|
||||
setDeleteOpen(false);
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to delete runtime");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
@@ -36,7 +92,19 @@ export function RuntimeDetail({ runtime }: { runtime: AgentRuntime }) {
|
||||
<h2 className="text-sm font-semibold truncate">{runtime.name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={runtime.status} />
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusBadge status={runtime.status} />
|
||||
{canDelete && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
@@ -50,6 +118,7 @@ export function RuntimeDetail({ runtime }: { runtime: AgentRuntime }) {
|
||||
label="Last Seen"
|
||||
value={formatLastSeen(runtime.last_seen_at)}
|
||||
/>
|
||||
{ownerName && <InfoField label="Owner" value={ownerName} />}
|
||||
{runtime.device_info && (
|
||||
<InfoField label="Device" value={runtime.device_info} />
|
||||
)}
|
||||
@@ -114,6 +183,28 @@ export function RuntimeDetail({ runtime }: { runtime: AgentRuntime }) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete confirmation */}
|
||||
<AlertDialog open={deleteOpen} onOpenChange={(v) => { if (!v) setDeleteOpen(false); }}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Runtime</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete “{runtime.name}”? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? "Deleting..." : "Delete"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { Server } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { AgentRuntime } from "@/shared/types";
|
||||
import { useWorkspaceId } from "@core/hooks";
|
||||
import { memberListOptions } from "@core/workspace/queries";
|
||||
import { RuntimeModeIcon } from "./shared";
|
||||
|
||||
type RuntimeFilter = "mine" | "all";
|
||||
|
||||
function RuntimeListItem({
|
||||
runtime,
|
||||
isSelected,
|
||||
ownerName,
|
||||
onClick,
|
||||
}: {
|
||||
runtime: AgentRuntime;
|
||||
isSelected: boolean;
|
||||
ownerName: string | null;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
@@ -29,6 +36,7 @@ function RuntimeListItem({
|
||||
<div className="truncate text-sm font-medium">{runtime.name}</div>
|
||||
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{runtime.provider} · {runtime.runtime_mode}
|
||||
{ownerName && <> · {ownerName}</>}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -44,11 +52,23 @@ export function RuntimeList({
|
||||
runtimes,
|
||||
selectedId,
|
||||
onSelect,
|
||||
filter,
|
||||
onFilterChange,
|
||||
}: {
|
||||
runtimes: AgentRuntime[];
|
||||
selectedId: string;
|
||||
onSelect: (id: string) => void;
|
||||
filter: RuntimeFilter;
|
||||
onFilterChange: (filter: RuntimeFilter) => void;
|
||||
}) {
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
|
||||
const getOwnerName = (ownerId: string | null) => {
|
||||
if (!ownerId) return null;
|
||||
return members.find((m) => m.user_id === ownerId)?.name ?? null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full border-r">
|
||||
<div className="flex h-12 items-center justify-between border-b px-4">
|
||||
@@ -58,11 +78,36 @@ export function RuntimeList({
|
||||
{runtimes.length} online
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Filter toggle */}
|
||||
<div className="flex border-b px-4 py-2 gap-1">
|
||||
<button
|
||||
onClick={() => onFilterChange("mine")}
|
||||
className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
|
||||
filter === "mine"
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Mine
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onFilterChange("all")}
|
||||
className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
|
||||
filter === "all"
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{runtimes.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center px-4 py-12">
|
||||
<Server className="h-8 w-8 text-muted-foreground/40" />
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
No runtimes registered
|
||||
{filter === "mine" ? "No runtimes owned by you" : "No runtimes registered"}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground text-center">
|
||||
Run{" "}
|
||||
@@ -79,6 +124,7 @@ export function RuntimeList({
|
||||
key={runtime.id}
|
||||
runtime={runtime}
|
||||
isSelected={runtime.id === selectedId}
|
||||
ownerName={filter === "all" ? getOwnerName(runtime.owner_id) : null}
|
||||
onClick={() => onSelect(runtime.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -17,20 +17,25 @@ import { useWSEvent } from "@/features/realtime";
|
||||
import { RuntimeList } from "./runtime-list";
|
||||
import { RuntimeDetail } from "./runtime-detail";
|
||||
|
||||
type RuntimeFilter = "mine" | "all";
|
||||
|
||||
export default function RuntimesPage() {
|
||||
const isLoading = useAuthStore((s) => s.isLoading);
|
||||
const wsId = useWorkspaceId();
|
||||
const qc = useQueryClient();
|
||||
const { data: runtimes = [], isLoading: fetching } = useQuery(runtimeListOptions(wsId));
|
||||
const [filter, setFilter] = useState<RuntimeFilter>("mine");
|
||||
const [selectedId, setSelectedId] = useState("");
|
||||
|
||||
const ownerParam = filter === "mine" ? "me" as const : undefined;
|
||||
const { data: runtimes = [], isLoading: fetching } = useQuery(runtimeListOptions(wsId, ownerParam));
|
||||
|
||||
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
||||
id: "multica_runtimes_layout",
|
||||
});
|
||||
|
||||
// Re-fetch on daemon register/deregister events.
|
||||
const handleDaemonEvent = useCallback(() => {
|
||||
qc.invalidateQueries({ queryKey: runtimeKeys.list(wsId) });
|
||||
qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) });
|
||||
}, [qc, wsId]);
|
||||
|
||||
useWSEvent("daemon:register", handleDaemonEvent);
|
||||
@@ -95,6 +100,8 @@ export default function RuntimesPage() {
|
||||
runtimes={runtimes}
|
||||
selectedId={effectiveSelectedId}
|
||||
onSelect={setSelectedId}
|
||||
filter={filter}
|
||||
onFilterChange={setFilter}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
|
||||
|
||||
@@ -338,13 +338,18 @@ export class ApiClient {
|
||||
return this.fetch(`/api/agents/${id}/restore`, { method: "POST" });
|
||||
}
|
||||
|
||||
async listRuntimes(params?: { workspace_id?: string }): Promise<AgentRuntime[]> {
|
||||
async listRuntimes(params?: { workspace_id?: string; owner?: "me" }): Promise<AgentRuntime[]> {
|
||||
const search = new URLSearchParams();
|
||||
const wsId = params?.workspace_id ?? this.workspaceId;
|
||||
if (wsId) search.set("workspace_id", wsId);
|
||||
if (params?.owner) search.set("owner", params.owner);
|
||||
return this.fetch(`/api/runtimes?${search}`);
|
||||
}
|
||||
|
||||
async deleteRuntime(runtimeId: string): Promise<void> {
|
||||
await this.fetch(`/api/runtimes/${runtimeId}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
async getRuntimeUsage(runtimeId: string, params?: { days?: number }): Promise<RuntimeUsage[]> {
|
||||
const search = new URLSearchParams();
|
||||
if (params?.days) search.set("days", String(params.days));
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface RuntimeDevice {
|
||||
status: "online" | "offline";
|
||||
device_info: string;
|
||||
metadata: Record<string, unknown>;
|
||||
owner_id: string | null;
|
||||
last_seen_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
@@ -232,12 +232,15 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route
|
||||
// Runtimes
|
||||
r.Route("/api/runtimes", func(r chi.Router) {
|
||||
r.Get("/", h.ListAgentRuntimes)
|
||||
r.Get("/{runtimeId}/usage", h.GetRuntimeUsage)
|
||||
r.Get("/{runtimeId}/activity", h.GetRuntimeTaskActivity)
|
||||
r.Post("/{runtimeId}/ping", h.InitiatePing)
|
||||
r.Get("/{runtimeId}/ping/{pingId}", h.GetPing)
|
||||
r.Post("/{runtimeId}/update", h.InitiateUpdate)
|
||||
r.Get("/{runtimeId}/update/{updateId}", h.GetUpdate)
|
||||
r.Route("/{runtimeId}", func(r chi.Router) {
|
||||
r.Get("/usage", h.GetRuntimeUsage)
|
||||
r.Get("/activity", h.GetRuntimeTaskActivity)
|
||||
r.Post("/ping", h.InitiatePing)
|
||||
r.Get("/ping/{pingId}", h.GetPing)
|
||||
r.Post("/update", h.InitiateUpdate)
|
||||
r.Get("/update/{updateId}", h.GetUpdate)
|
||||
r.Delete("/", h.DeleteAgentRuntime)
|
||||
})
|
||||
})
|
||||
|
||||
// Inbox
|
||||
|
||||
@@ -57,7 +57,8 @@ func (h *Handler) DaemonRegister(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Verify the caller is a member of the target workspace.
|
||||
if _, ok := h.requireWorkspaceMember(w, r, req.WorkspaceID, "workspace not found"); !ok {
|
||||
member, ok := h.requireWorkspaceMember(w, r, req.WorkspaceID, "workspace not found")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -104,6 +105,7 @@ func (h *Handler) DaemonRegister(w http.ResponseWriter, r *http.Request) {
|
||||
Status: status,
|
||||
DeviceInfo: deviceInfo,
|
||||
Metadata: metadata,
|
||||
OwnerID: member.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to register runtime: "+err.Error())
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
||||
"github.com/multica-ai/multica/server/pkg/protocol"
|
||||
)
|
||||
|
||||
type AgentRuntimeResponse struct {
|
||||
@@ -21,6 +23,7 @@ type AgentRuntimeResponse struct {
|
||||
Status string `json:"status"`
|
||||
DeviceInfo string `json:"device_info"`
|
||||
Metadata any `json:"metadata"`
|
||||
OwnerID *string `json:"owner_id"`
|
||||
LastSeenAt *string `json:"last_seen_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
@@ -45,6 +48,7 @@ func runtimeToResponse(rt db.AgentRuntime) AgentRuntimeResponse {
|
||||
Status: rt.Status,
|
||||
DeviceInfo: rt.DeviceInfo,
|
||||
Metadata: metadata,
|
||||
OwnerID: uuidToPtr(rt.OwnerID),
|
||||
LastSeenAt: timestampToPtr(rt.LastSeenAt),
|
||||
CreatedAt: timestampToString(rt.CreatedAt),
|
||||
UpdatedAt: timestampToString(rt.UpdatedAt),
|
||||
@@ -285,7 +289,22 @@ func parseSinceParam(r *http.Request, defaultDays int) pgtype.Timestamptz {
|
||||
func (h *Handler) ListAgentRuntimes(w http.ResponseWriter, r *http.Request) {
|
||||
workspaceID := resolveWorkspaceID(r)
|
||||
|
||||
runtimes, err := h.Queries.ListAgentRuntimes(r.Context(), parseUUID(workspaceID))
|
||||
var runtimes []db.AgentRuntime
|
||||
var err error
|
||||
|
||||
if ownerFilter := r.URL.Query().Get("owner"); ownerFilter == "me" {
|
||||
userID, ok := requireUserID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
runtimes, err = h.Queries.ListAgentRuntimesByOwner(r.Context(), db.ListAgentRuntimesByOwnerParams{
|
||||
WorkspaceID: parseUUID(workspaceID),
|
||||
OwnerID: parseUUID(userID),
|
||||
})
|
||||
} else {
|
||||
runtimes, err = h.Queries.ListAgentRuntimes(r.Context(), parseUUID(workspaceID))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to list runtimes")
|
||||
return
|
||||
@@ -298,3 +317,54 @@ func (h *Handler) ListAgentRuntimes(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// DeleteAgentRuntime deletes a runtime after permission and dependency checks.
|
||||
func (h *Handler) DeleteAgentRuntime(w http.ResponseWriter, r *http.Request) {
|
||||
runtimeID := chi.URLParam(r, "runtimeId")
|
||||
|
||||
rt, err := h.Queries.GetAgentRuntime(r.Context(), parseUUID(runtimeID))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "runtime not found")
|
||||
return
|
||||
}
|
||||
|
||||
wsID := uuidToString(rt.WorkspaceID)
|
||||
member, ok := h.requireWorkspaceMember(w, r, wsID, "runtime not found")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Permission: owner/admin can delete any runtime; members can only delete their own.
|
||||
userID := uuidToString(member.UserID)
|
||||
isAdmin := roleAllowed(member.Role, "owner", "admin")
|
||||
isOwner := rt.OwnerID.Valid && uuidToString(rt.OwnerID) == userID
|
||||
if !isAdmin && !isOwner {
|
||||
writeError(w, http.StatusForbidden, "you can only delete your own runtimes")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if any agents are bound to this runtime (ON DELETE RESTRICT).
|
||||
agentCount, err := h.Queries.CountAgentsByRuntime(r.Context(), rt.ID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to check runtime dependencies")
|
||||
return
|
||||
}
|
||||
if agentCount > 0 {
|
||||
writeError(w, http.StatusConflict, "cannot delete runtime: it has agents bound to it. Reassign or remove the agents first.")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.Queries.DeleteAgentRuntime(r.Context(), rt.ID); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to delete runtime")
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("runtime deleted", "runtime_id", runtimeID, "deleted_by", userID)
|
||||
|
||||
// Notify frontend to refresh runtime list.
|
||||
h.publish(protocol.EventDaemonRegister, wsID, "member", userID, map[string]any{
|
||||
"action": "delete",
|
||||
})
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
1
server/migrations/032_runtime_owner.down.sql
Normal file
1
server/migrations/032_runtime_owner.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE agent_runtime DROP COLUMN IF EXISTS owner_id;
|
||||
9
server/migrations/032_runtime_owner.up.sql
Normal file
9
server/migrations/032_runtime_owner.up.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE agent_runtime ADD COLUMN owner_id UUID REFERENCES "user"(id);
|
||||
|
||||
-- Backfill: set existing runtimes' owner to the workspace owner
|
||||
UPDATE agent_runtime ar
|
||||
SET owner_id = (
|
||||
SELECT m.user_id FROM member m
|
||||
WHERE m.workspace_id = ar.workspace_id AND m.role = 'owner'
|
||||
LIMIT 1
|
||||
);
|
||||
@@ -52,6 +52,7 @@ type AgentRuntime struct {
|
||||
LastSeenAt pgtype.Timestamptz `json:"last_seen_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
OwnerID pgtype.UUID `json:"owner_id"`
|
||||
}
|
||||
|
||||
type AgentSkill struct {
|
||||
|
||||
@@ -11,6 +11,26 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const countAgentsByRuntime = `-- name: CountAgentsByRuntime :one
|
||||
SELECT count(*) FROM agent WHERE runtime_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) CountAgentsByRuntime(ctx context.Context, runtimeID pgtype.UUID) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countAgentsByRuntime, runtimeID)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const deleteAgentRuntime = `-- name: DeleteAgentRuntime :exec
|
||||
DELETE FROM agent_runtime WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteAgentRuntime(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteAgentRuntime, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const failTasksForOfflineRuntimes = `-- name: FailTasksForOfflineRuntimes :many
|
||||
UPDATE agent_task_queue
|
||||
SET status = 'failed', completed_at = now(), error = 'runtime went offline'
|
||||
@@ -50,7 +70,7 @@ func (q *Queries) FailTasksForOfflineRuntimes(ctx context.Context) ([]FailTasksF
|
||||
}
|
||||
|
||||
const getAgentRuntime = `-- name: GetAgentRuntime :one
|
||||
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at FROM agent_runtime
|
||||
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id FROM agent_runtime
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
@@ -70,12 +90,13 @@ func (q *Queries) GetAgentRuntime(ctx context.Context, id pgtype.UUID) (AgentRun
|
||||
&i.LastSeenAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAgentRuntimeForWorkspace = `-- name: GetAgentRuntimeForWorkspace :one
|
||||
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at FROM agent_runtime
|
||||
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id FROM agent_runtime
|
||||
WHERE id = $1 AND workspace_id = $2
|
||||
`
|
||||
|
||||
@@ -100,12 +121,13 @@ func (q *Queries) GetAgentRuntimeForWorkspace(ctx context.Context, arg GetAgentR
|
||||
&i.LastSeenAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listAgentRuntimes = `-- name: ListAgentRuntimes :many
|
||||
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at FROM agent_runtime
|
||||
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id FROM agent_runtime
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY created_at ASC
|
||||
`
|
||||
@@ -132,6 +154,52 @@ func (q *Queries) ListAgentRuntimes(ctx context.Context, workspaceID pgtype.UUID
|
||||
&i.LastSeenAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listAgentRuntimesByOwner = `-- name: ListAgentRuntimesByOwner :many
|
||||
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id FROM agent_runtime
|
||||
WHERE workspace_id = $1 AND owner_id = $2
|
||||
ORDER BY created_at ASC
|
||||
`
|
||||
|
||||
type ListAgentRuntimesByOwnerParams struct {
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
OwnerID pgtype.UUID `json:"owner_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListAgentRuntimesByOwner(ctx context.Context, arg ListAgentRuntimesByOwnerParams) ([]AgentRuntime, error) {
|
||||
rows, err := q.db.Query(ctx, listAgentRuntimesByOwner, arg.WorkspaceID, arg.OwnerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []AgentRuntime{}
|
||||
for rows.Next() {
|
||||
var i AgentRuntime
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.WorkspaceID,
|
||||
&i.DaemonID,
|
||||
&i.Name,
|
||||
&i.RuntimeMode,
|
||||
&i.Provider,
|
||||
&i.Status,
|
||||
&i.DeviceInfo,
|
||||
&i.Metadata,
|
||||
&i.LastSeenAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -191,7 +259,7 @@ const updateAgentRuntimeHeartbeat = `-- name: UpdateAgentRuntimeHeartbeat :one
|
||||
UPDATE agent_runtime
|
||||
SET status = 'online', last_seen_at = now(), updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at
|
||||
RETURNING id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id
|
||||
`
|
||||
|
||||
func (q *Queries) UpdateAgentRuntimeHeartbeat(ctx context.Context, id pgtype.UUID) (AgentRuntime, error) {
|
||||
@@ -210,6 +278,7 @@ func (q *Queries) UpdateAgentRuntimeHeartbeat(ctx context.Context, id pgtype.UUI
|
||||
&i.LastSeenAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -224,8 +293,9 @@ INSERT INTO agent_runtime (
|
||||
status,
|
||||
device_info,
|
||||
metadata,
|
||||
owner_id,
|
||||
last_seen_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now())
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now())
|
||||
ON CONFLICT (workspace_id, daemon_id, provider)
|
||||
DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
@@ -233,9 +303,10 @@ DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
device_info = EXCLUDED.device_info,
|
||||
metadata = EXCLUDED.metadata,
|
||||
owner_id = COALESCE(EXCLUDED.owner_id, agent_runtime.owner_id),
|
||||
last_seen_at = now(),
|
||||
updated_at = now()
|
||||
RETURNING id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at
|
||||
RETURNING id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id
|
||||
`
|
||||
|
||||
type UpsertAgentRuntimeParams struct {
|
||||
@@ -247,6 +318,7 @@ type UpsertAgentRuntimeParams struct {
|
||||
Status string `json:"status"`
|
||||
DeviceInfo string `json:"device_info"`
|
||||
Metadata []byte `json:"metadata"`
|
||||
OwnerID pgtype.UUID `json:"owner_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertAgentRuntime(ctx context.Context, arg UpsertAgentRuntimeParams) (AgentRuntime, error) {
|
||||
@@ -259,6 +331,7 @@ func (q *Queries) UpsertAgentRuntime(ctx context.Context, arg UpsertAgentRuntime
|
||||
arg.Status,
|
||||
arg.DeviceInfo,
|
||||
arg.Metadata,
|
||||
arg.OwnerID,
|
||||
)
|
||||
var i AgentRuntime
|
||||
err := row.Scan(
|
||||
@@ -274,6 +347,7 @@ func (q *Queries) UpsertAgentRuntime(ctx context.Context, arg UpsertAgentRuntime
|
||||
&i.LastSeenAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ INSERT INTO agent_runtime (
|
||||
status,
|
||||
device_info,
|
||||
metadata,
|
||||
owner_id,
|
||||
last_seen_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now())
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now())
|
||||
ON CONFLICT (workspace_id, daemon_id, provider)
|
||||
DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
@@ -30,6 +31,7 @@ DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
device_info = EXCLUDED.device_info,
|
||||
metadata = EXCLUDED.metadata,
|
||||
owner_id = COALESCE(EXCLUDED.owner_id, agent_runtime.owner_id),
|
||||
last_seen_at = now(),
|
||||
updated_at = now()
|
||||
RETURNING *;
|
||||
@@ -62,3 +64,14 @@ WHERE status IN ('dispatched', 'running')
|
||||
SELECT id FROM agent_runtime WHERE status = 'offline'
|
||||
)
|
||||
RETURNING id, agent_id, issue_id;
|
||||
|
||||
-- name: ListAgentRuntimesByOwner :many
|
||||
SELECT * FROM agent_runtime
|
||||
WHERE workspace_id = $1 AND owner_id = $2
|
||||
ORDER BY created_at ASC;
|
||||
|
||||
-- name: DeleteAgentRuntime :exec
|
||||
DELETE FROM agent_runtime WHERE id = $1;
|
||||
|
||||
-- name: CountAgentsByRuntime :one
|
||||
SELECT count(*) FROM agent WHERE runtime_id = $1;
|
||||
|
||||
Reference in New Issue
Block a user