diff --git a/apps/web/components/common/mention-suggestion.tsx b/apps/web/components/common/mention-suggestion.tsx
index 74806f83..67e65346 100644
--- a/apps/web/components/common/mention-suggestion.tsx
+++ b/apps/web/components/common/mention-suggestion.tsx
@@ -235,7 +235,7 @@ export function createMentionSuggestion(): Omit<
}));
const agentItems: MentionItem[] = agents
- .filter((a) => a.name.toLowerCase().includes(q))
+ .filter((a) => !a.archived_at && a.name.toLowerCase().includes(q))
.map((a) => ({ id: a.id, label: a.name, type: "agent" as const }));
const issueItems: MentionItem[] = issues
diff --git a/apps/web/features/issues/components/pickers/assignee-picker.tsx b/apps/web/features/issues/components/pickers/assignee-picker.tsx
index c75589ba..3bc3a70f 100644
--- a/apps/web/features/issues/components/pickers/assignee-picker.tsx
+++ b/apps/web/features/issues/components/pickers/assignee-picker.tsx
@@ -56,7 +56,7 @@ export function AssigneePicker({
m.name.toLowerCase().includes(query),
);
const filteredAgents = agents.filter((a) =>
- a.name.toLowerCase().includes(query),
+ !a.archived_at && a.name.toLowerCase().includes(query),
);
const isSelected = (type: string, id: string) =>
diff --git a/apps/web/features/workspace/store.ts b/apps/web/features/workspace/store.ts
index 591cc7b9..ea18ed07 100644
--- a/apps/web/features/workspace/store.ts
+++ b/apps/web/features/workspace/store.ts
@@ -82,7 +82,7 @@ export const useWorkspaceStore = create
((set, get) => ({
toast.error("Failed to load members");
return [] as MemberWithUser[];
}),
- api.listAgents({ workspace_id: nextWorkspace.id }).catch((e) => {
+ api.listAgents({ workspace_id: nextWorkspace.id, include_archived: true }).catch((e) => {
logger.error("failed to load agents", e);
toast.error("Failed to load agents");
return [] as Agent[];
@@ -154,7 +154,7 @@ export const useWorkspaceStore = create((set, get) => ({
const { workspace } = get();
if (!workspace) return;
try {
- const agents = await api.listAgents({ workspace_id: workspace.id });
+ const agents = await api.listAgents({ workspace_id: workspace.id, include_archived: true });
set({ agents });
} catch (e) {
logger.error("failed to refresh agents", e);
diff --git a/apps/web/shared/api/client.ts b/apps/web/shared/api/client.ts
index 5314e978..4721ccf3 100644
--- a/apps/web/shared/api/client.ts
+++ b/apps/web/shared/api/client.ts
@@ -291,10 +291,11 @@ export class ApiClient {
}
// Agents
- async listAgents(params?: { workspace_id?: string }): Promise {
+ async listAgents(params?: { workspace_id?: string; include_archived?: boolean }): Promise {
const search = new URLSearchParams();
const wsId = params?.workspace_id ?? this.workspaceId;
if (wsId) search.set("workspace_id", wsId);
+ if (params?.include_archived) search.set("include_archived", "true");
return this.fetch(`/api/agents?${search}`);
}
@@ -316,8 +317,12 @@ export class ApiClient {
});
}
- async deleteAgent(id: string): Promise {
- await this.fetch(`/api/agents/${id}`, { method: "DELETE" });
+ async archiveAgent(id: string): Promise {
+ return this.fetch(`/api/agents/${id}/archive`, { method: "POST" });
+ }
+
+ async restoreAgent(id: string): Promise {
+ return this.fetch(`/api/agents/${id}/restore`, { method: "POST" });
}
async listRuntimes(params?: { workspace_id?: string }): Promise {
diff --git a/apps/web/shared/types/agent.ts b/apps/web/shared/types/agent.ts
index 232b4da4..9f596ab4 100644
--- a/apps/web/shared/types/agent.ts
+++ b/apps/web/shared/types/agent.ts
@@ -73,6 +73,8 @@ export interface Agent {
triggers: AgentTrigger[];
created_at: string;
updated_at: string;
+ archived_at: string | null;
+ archived_by: string | null;
}
export interface CreateAgentRequest {
diff --git a/apps/web/shared/types/events.ts b/apps/web/shared/types/events.ts
index 8b3a5fc6..3116f03d 100644
--- a/apps/web/shared/types/events.ts
+++ b/apps/web/shared/types/events.ts
@@ -15,7 +15,8 @@ export type WSEventType =
| "comment:deleted"
| "agent:status"
| "agent:created"
- | "agent:deleted"
+ | "agent:archived"
+ | "agent:restored"
| "task:dispatch"
| "task:progress"
| "task:completed"
@@ -71,9 +72,12 @@ export interface AgentCreatedPayload {
agent: Agent;
}
-export interface AgentDeletedPayload {
- agent_id: string;
- workspace_id: string;
+export interface AgentArchivedPayload {
+ agent: Agent;
+}
+
+export interface AgentRestoredPayload {
+ agent: Agent;
}
export interface InboxNewPayload {
diff --git a/apps/web/test/helpers.tsx b/apps/web/test/helpers.tsx
index 6a01ae7a..94988fe1 100644
--- a/apps/web/test/helpers.tsx
+++ b/apps/web/test/helpers.tsx
@@ -62,6 +62,8 @@ export const mockAgents: Agent[] = [
triggers: [],
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
+ archived_at: null,
+ archived_by: null,
},
];
diff --git a/server/cmd/server/router.go b/server/cmd/server/router.go
index 509826e1..acb6583d 100644
--- a/server/cmd/server/router.go
+++ b/server/cmd/server/router.go
@@ -196,7 +196,8 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route
r.Route("/{id}", func(r chi.Router) {
r.Get("/", h.GetAgent)
r.Put("/", h.UpdateAgent)
- r.Delete("/", h.DeleteAgent)
+ r.Post("/archive", h.ArchiveAgent)
+ r.Post("/restore", h.RestoreAgent)
r.Get("/tasks", h.ListAgentTasks)
r.Get("/skills", h.ListAgentSkills)
r.Put("/skills", h.SetAgentSkills)
diff --git a/server/internal/handler/agent.go b/server/internal/handler/agent.go
index d81c47e9..bcfb3a6d 100644
--- a/server/internal/handler/agent.go
+++ b/server/internal/handler/agent.go
@@ -32,6 +32,8 @@ type AgentResponse struct {
Triggers any `json:"triggers"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
+ ArchivedAt *string `json:"archived_at"`
+ ArchivedBy *string `json:"archived_by"`
}
func agentToResponse(a db.Agent) AgentResponse {
@@ -78,6 +80,8 @@ func agentToResponse(a db.Agent) AgentResponse {
Triggers: triggers,
CreatedAt: timestampToString(a.CreatedAt),
UpdatedAt: timestampToString(a.UpdatedAt),
+ ArchivedAt: timestampToPtr(a.ArchivedAt),
+ ArchivedBy: uuidToPtr(a.ArchivedBy),
}
}
@@ -146,7 +150,13 @@ func (h *Handler) ListAgents(w http.ResponseWriter, r *http.Request) {
return
}
- agents, err := h.Queries.ListAgents(r.Context(), parseUUID(workspaceID))
+ var agents []db.Agent
+ var err error
+ if r.URL.Query().Get("include_archived") == "true" {
+ agents, err = h.Queries.ListAllAgents(r.Context(), parseUUID(workspaceID))
+ } else {
+ agents, err = h.Queries.ListAgents(r.Context(), parseUUID(workspaceID))
+ }
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list agents")
return
@@ -317,7 +327,7 @@ type UpdateAgentRequest struct {
Triggers any `json:"triggers"`
}
-// canManageAgent checks whether the current user can update or delete an agent.
+// canManageAgent checks whether the current user can update or archive an agent.
// Only the agent owner or workspace owner/admin can manage any agent,
// regardless of whether it is public or private.
func (h *Handler) canManageAgent(w http.ResponseWriter, r *http.Request, agent db.Agent) bool {
@@ -415,30 +425,72 @@ func (h *Handler) UpdateAgent(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, resp)
}
-func (h *Handler) DeleteAgent(w http.ResponseWriter, r *http.Request) {
+func (h *Handler) ArchiveAgent(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
agent, ok := h.loadAgentForUser(w, r, id)
if !ok {
return
}
- wsID := uuidToString(agent.WorkspaceID)
-
if !h.canManageAgent(w, r, agent) {
return
}
-
- err := h.Queries.DeleteAgent(r.Context(), parseUUID(id))
- if err != nil {
- slog.Warn("delete agent failed", append(logger.RequestAttrs(r), "error", err, "agent_id", id)...)
- writeError(w, http.StatusInternalServerError, "failed to delete agent")
+ if agent.ArchivedAt.Valid {
+ writeError(w, http.StatusConflict, "agent is already archived")
return
}
- slog.Info("agent deleted", append(logger.RequestAttrs(r), "agent_id", id, "workspace_id", wsID)...)
+ userID := requestUserID(r)
+ archived, err := h.Queries.ArchiveAgent(r.Context(), db.ArchiveAgentParams{
+ ID: parseUUID(id),
+ ArchivedBy: parseUUID(userID),
+ })
+ if err != nil {
+ slog.Warn("archive agent failed", append(logger.RequestAttrs(r), "error", err, "agent_id", id)...)
+ writeError(w, http.StatusInternalServerError, "failed to archive agent")
+ return
+ }
+
+ // Cancel all pending/active tasks for this agent.
+ if err := h.Queries.CancelAgentTasksByAgent(r.Context(), parseUUID(id)); err != nil {
+ slog.Warn("cancel agent tasks on archive failed", append(logger.RequestAttrs(r), "error", err, "agent_id", id)...)
+ }
+
+ wsID := uuidToString(archived.WorkspaceID)
+ slog.Info("agent archived", append(logger.RequestAttrs(r), "agent_id", id, "workspace_id", wsID)...)
+ resp := agentToResponse(archived)
+ actorType, actorID := h.resolveActor(r, userID, wsID)
+ h.publish(protocol.EventAgentArchived, wsID, actorType, actorID, map[string]any{"agent": resp})
+ writeJSON(w, http.StatusOK, resp)
+}
+
+func (h *Handler) RestoreAgent(w http.ResponseWriter, r *http.Request) {
+ id := chi.URLParam(r, "id")
+ agent, ok := h.loadAgentForUser(w, r, id)
+ if !ok {
+ return
+ }
+ if !h.canManageAgent(w, r, agent) {
+ return
+ }
+ if !agent.ArchivedAt.Valid {
+ writeError(w, http.StatusConflict, "agent is not archived")
+ return
+ }
+
+ restored, err := h.Queries.RestoreAgent(r.Context(), parseUUID(id))
+ if err != nil {
+ slog.Warn("restore agent failed", append(logger.RequestAttrs(r), "error", err, "agent_id", id)...)
+ writeError(w, http.StatusInternalServerError, "failed to restore agent")
+ return
+ }
+
+ wsID := uuidToString(restored.WorkspaceID)
+ slog.Info("agent restored", append(logger.RequestAttrs(r), "agent_id", id, "workspace_id", wsID)...)
+ resp := agentToResponse(restored)
userID := requestUserID(r)
actorType, actorID := h.resolveActor(r, userID, wsID)
- h.publish(protocol.EventAgentDeleted, wsID, actorType, actorID, map[string]any{"agent_id": id, "workspace_id": wsID})
- w.WriteHeader(http.StatusNoContent)
+ h.publish(protocol.EventAgentRestored, wsID, actorType, actorID, map[string]any{"agent": resp})
+ writeJSON(w, http.StatusOK, resp)
}
func (h *Handler) ListAgentTasks(w http.ResponseWriter, r *http.Request) {
diff --git a/server/internal/handler/comment.go b/server/internal/handler/comment.go
index 5e0e4c59..a40847fa 100644
--- a/server/internal/handler/comment.go
+++ b/server/internal/handler/comment.go
@@ -273,9 +273,9 @@ func (h *Handler) enqueueMentionedAgentTasks(ctx context.Context, issue db.Issue
issue.AssigneeID.Valid && uuidToString(issue.AssigneeID) == m.ID {
continue
}
- // Load the agent to check visibility and trigger config.
+ // Load the agent to check visibility, archive status, and trigger config.
agent, err := h.Queries.GetAgent(ctx, agentUUID)
- if err != nil || !agent.RuntimeID.Valid {
+ if err != nil || !agent.RuntimeID.Valid || agent.ArchivedAt.Valid {
continue
}
// Private agents can only be mentioned by the agent owner or workspace admin/owner.
diff --git a/server/internal/handler/issue.go b/server/internal/handler/issue.go
index 3a74d830..418252e2 100644
--- a/server/internal/handler/issue.go
+++ b/server/internal/handler/issue.go
@@ -466,6 +466,9 @@ func (h *Handler) canAssignAgent(ctx context.Context, r *http.Request, agentID,
if err != nil {
return false, "agent not found"
}
+ if agent.ArchivedAt.Valid {
+ return false, "cannot assign to archived agent"
+ }
if agent.Visibility != "private" {
return true, ""
}
@@ -521,7 +524,7 @@ func (h *Handler) isAgentTriggerEnabled(ctx context.Context, issue db.Issue, tri
}
agent, err := h.Queries.GetAgent(ctx, issue.AssigneeID)
- if err != nil || !agent.RuntimeID.Valid {
+ if err != nil || !agent.RuntimeID.Valid || agent.ArchivedAt.Valid {
return false
}
diff --git a/server/internal/service/task.go b/server/internal/service/task.go
index d4026ff6..92d602cf 100644
--- a/server/internal/service/task.go
+++ b/server/internal/service/task.go
@@ -44,6 +44,10 @@ func (s *TaskService) EnqueueTaskForIssue(ctx context.Context, issue db.Issue, t
slog.Error("task enqueue failed", "issue_id", util.UUIDToString(issue.ID), "error", err)
return db.AgentTaskQueue{}, fmt.Errorf("load agent: %w", err)
}
+ if agent.ArchivedAt.Valid {
+ slog.Debug("task enqueue skipped: agent is archived", "issue_id", util.UUIDToString(issue.ID), "agent_id", util.UUIDToString(agent.ID))
+ return db.AgentTaskQueue{}, fmt.Errorf("agent is archived")
+ }
if !agent.RuntimeID.Valid {
slog.Error("task enqueue failed", "issue_id", util.UUIDToString(issue.ID), "error", "agent has no runtime")
return db.AgentTaskQueue{}, fmt.Errorf("agent has no runtime")
@@ -79,6 +83,10 @@ func (s *TaskService) EnqueueTaskForMention(ctx context.Context, issue db.Issue,
slog.Error("mention task enqueue failed: agent not found", "issue_id", util.UUIDToString(issue.ID), "agent_id", util.UUIDToString(agentID), "error", err)
return db.AgentTaskQueue{}, fmt.Errorf("load agent: %w", err)
}
+ if agent.ArchivedAt.Valid {
+ slog.Debug("mention task enqueue skipped: agent is archived", "issue_id", util.UUIDToString(issue.ID), "agent_id", util.UUIDToString(agentID))
+ return db.AgentTaskQueue{}, fmt.Errorf("agent is archived")
+ }
if !agent.RuntimeID.Valid {
slog.Error("mention task enqueue failed: agent has no runtime", "issue_id", util.UUIDToString(issue.ID), "agent_id", util.UUIDToString(agentID))
return db.AgentTaskQueue{}, fmt.Errorf("agent has no runtime")
@@ -549,5 +557,7 @@ func agentToMap(a db.Agent) map[string]any {
"triggers": triggers,
"created_at": util.TimestampToString(a.CreatedAt),
"updated_at": util.TimestampToString(a.UpdatedAt),
+ "archived_at": util.TimestampToPtr(a.ArchivedAt),
+ "archived_by": util.UUIDToPtr(a.ArchivedBy),
}
}
diff --git a/server/migrations/031_agent_archive.down.sql b/server/migrations/031_agent_archive.down.sql
new file mode 100644
index 00000000..5f547c22
--- /dev/null
+++ b/server/migrations/031_agent_archive.down.sql
@@ -0,0 +1,2 @@
+ALTER TABLE agent DROP COLUMN IF EXISTS archived_by;
+ALTER TABLE agent DROP COLUMN IF EXISTS archived_at;
diff --git a/server/migrations/031_agent_archive.up.sql b/server/migrations/031_agent_archive.up.sql
new file mode 100644
index 00000000..49fc0660
--- /dev/null
+++ b/server/migrations/031_agent_archive.up.sql
@@ -0,0 +1,4 @@
+-- Add archive support to agents (soft-delete replacement).
+-- archived_at IS NOT NULL means the agent is archived.
+ALTER TABLE agent ADD COLUMN archived_at TIMESTAMPTZ DEFAULT NULL;
+ALTER TABLE agent ADD COLUMN archived_by UUID DEFAULT NULL REFERENCES "user"(id);
diff --git a/server/pkg/db/generated/agent.sql.go b/server/pkg/db/generated/agent.sql.go
index 06984472..bc785374 100644
--- a/server/pkg/db/generated/agent.sql.go
+++ b/server/pkg/db/generated/agent.sql.go
@@ -11,6 +11,44 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
+const archiveAgent = `-- name: ArchiveAgent :one
+UPDATE agent SET archived_at = now(), archived_by = $2, updated_at = now()
+WHERE id = $1
+RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions, archived_at, archived_by
+`
+
+type ArchiveAgentParams struct {
+ ID pgtype.UUID `json:"id"`
+ ArchivedBy pgtype.UUID `json:"archived_by"`
+}
+
+func (q *Queries) ArchiveAgent(ctx context.Context, arg ArchiveAgentParams) (Agent, error) {
+ row := q.db.QueryRow(ctx, archiveAgent, arg.ID, arg.ArchivedBy)
+ var i Agent
+ err := row.Scan(
+ &i.ID,
+ &i.WorkspaceID,
+ &i.Name,
+ &i.AvatarUrl,
+ &i.RuntimeMode,
+ &i.RuntimeConfig,
+ &i.Visibility,
+ &i.Status,
+ &i.MaxConcurrentTasks,
+ &i.OwnerID,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ &i.Description,
+ &i.Tools,
+ &i.Triggers,
+ &i.RuntimeID,
+ &i.Instructions,
+ &i.ArchivedAt,
+ &i.ArchivedBy,
+ )
+ return i, err
+}
+
const cancelAgentTask = `-- name: CancelAgentTask :one
UPDATE agent_task_queue
SET status = 'cancelled', completed_at = now()
@@ -42,6 +80,17 @@ func (q *Queries) CancelAgentTask(ctx context.Context, id pgtype.UUID) (AgentTas
return i, err
}
+const cancelAgentTasksByAgent = `-- name: CancelAgentTasksByAgent :exec
+UPDATE agent_task_queue
+SET status = 'cancelled'
+WHERE agent_id = $1 AND status IN ('queued', 'dispatched', 'running')
+`
+
+func (q *Queries) CancelAgentTasksByAgent(ctx context.Context, agentID pgtype.UUID) error {
+ _, err := q.db.Exec(ctx, cancelAgentTasksByAgent, agentID)
+ return err
+}
+
const cancelAgentTasksByIssue = `-- name: CancelAgentTasksByIssue :exec
UPDATE agent_task_queue
SET status = 'cancelled'
@@ -160,7 +209,7 @@ INSERT INTO agent (
runtime_config, runtime_id, visibility, max_concurrent_tasks, owner_id,
tools, triggers, instructions
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
-RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions
+RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions, archived_at, archived_by
`
type CreateAgentParams struct {
@@ -214,6 +263,8 @@ func (q *Queries) CreateAgent(ctx context.Context, arg CreateAgentParams) (Agent
&i.Triggers,
&i.RuntimeID,
&i.Instructions,
+ &i.ArchivedAt,
+ &i.ArchivedBy,
)
return i, err
}
@@ -262,15 +313,6 @@ func (q *Queries) CreateAgentTask(ctx context.Context, arg CreateAgentTaskParams
return i, err
}
-const deleteAgent = `-- name: DeleteAgent :exec
-DELETE FROM agent WHERE id = $1
-`
-
-func (q *Queries) DeleteAgent(ctx context.Context, id pgtype.UUID) error {
- _, err := q.db.Exec(ctx, deleteAgent, id)
- return err
-}
-
const failAgentTask = `-- name: FailAgentTask :one
UPDATE agent_task_queue
SET status = 'failed', completed_at = now(), error = $2
@@ -350,7 +392,7 @@ func (q *Queries) FailStaleTasks(ctx context.Context, arg FailStaleTasksParams)
}
const getAgent = `-- name: GetAgent :one
-SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions FROM agent
+SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions, archived_at, archived_by FROM agent
WHERE id = $1
`
@@ -375,12 +417,14 @@ func (q *Queries) GetAgent(ctx context.Context, id pgtype.UUID) (Agent, error) {
&i.Triggers,
&i.RuntimeID,
&i.Instructions,
+ &i.ArchivedAt,
+ &i.ArchivedBy,
)
return i, err
}
const getAgentInWorkspace = `-- name: GetAgentInWorkspace :one
-SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions FROM agent
+SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions, archived_at, archived_by FROM agent
WHERE id = $1 AND workspace_id = $2
`
@@ -410,6 +454,8 @@ func (q *Queries) GetAgentInWorkspace(ctx context.Context, arg GetAgentInWorkspa
&i.Triggers,
&i.RuntimeID,
&i.Instructions,
+ &i.ArchivedAt,
+ &i.ArchivedBy,
)
return i, err
}
@@ -604,8 +650,8 @@ func (q *Queries) ListAgentTasks(ctx context.Context, agentID pgtype.UUID) ([]Ag
}
const listAgents = `-- name: ListAgents :many
-SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions FROM agent
-WHERE workspace_id = $1
+SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions, archived_at, archived_by FROM agent
+WHERE workspace_id = $1 AND archived_at IS NULL
ORDER BY created_at ASC
`
@@ -636,6 +682,54 @@ func (q *Queries) ListAgents(ctx context.Context, workspaceID pgtype.UUID) ([]Ag
&i.Triggers,
&i.RuntimeID,
&i.Instructions,
+ &i.ArchivedAt,
+ &i.ArchivedBy,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listAllAgents = `-- name: ListAllAgents :many
+SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions, archived_at, archived_by FROM agent
+WHERE workspace_id = $1
+ORDER BY created_at ASC
+`
+
+func (q *Queries) ListAllAgents(ctx context.Context, workspaceID pgtype.UUID) ([]Agent, error) {
+ rows, err := q.db.Query(ctx, listAllAgents, workspaceID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []Agent{}
+ for rows.Next() {
+ var i Agent
+ if err := rows.Scan(
+ &i.ID,
+ &i.WorkspaceID,
+ &i.Name,
+ &i.AvatarUrl,
+ &i.RuntimeMode,
+ &i.RuntimeConfig,
+ &i.Visibility,
+ &i.Status,
+ &i.MaxConcurrentTasks,
+ &i.OwnerID,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ &i.Description,
+ &i.Tools,
+ &i.Triggers,
+ &i.RuntimeID,
+ &i.Instructions,
+ &i.ArchivedAt,
+ &i.ArchivedBy,
); err != nil {
return nil, err
}
@@ -733,6 +827,39 @@ func (q *Queries) ListTasksByIssue(ctx context.Context, issueID pgtype.UUID) ([]
return items, nil
}
+const restoreAgent = `-- name: RestoreAgent :one
+UPDATE agent SET archived_at = NULL, archived_by = NULL, updated_at = now()
+WHERE id = $1
+RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions, archived_at, archived_by
+`
+
+func (q *Queries) RestoreAgent(ctx context.Context, id pgtype.UUID) (Agent, error) {
+ row := q.db.QueryRow(ctx, restoreAgent, id)
+ var i Agent
+ err := row.Scan(
+ &i.ID,
+ &i.WorkspaceID,
+ &i.Name,
+ &i.AvatarUrl,
+ &i.RuntimeMode,
+ &i.RuntimeConfig,
+ &i.Visibility,
+ &i.Status,
+ &i.MaxConcurrentTasks,
+ &i.OwnerID,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ &i.Description,
+ &i.Tools,
+ &i.Triggers,
+ &i.RuntimeID,
+ &i.Instructions,
+ &i.ArchivedAt,
+ &i.ArchivedBy,
+ )
+ return i, err
+}
+
const startAgentTask = `-- name: StartAgentTask :one
UPDATE agent_task_queue
SET status = 'running', started_at = now()
@@ -780,7 +907,7 @@ UPDATE agent SET
instructions = COALESCE($13, instructions),
updated_at = now()
WHERE id = $1
-RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions
+RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions, archived_at, archived_by
`
type UpdateAgentParams struct {
@@ -834,6 +961,8 @@ func (q *Queries) UpdateAgent(ctx context.Context, arg UpdateAgentParams) (Agent
&i.Triggers,
&i.RuntimeID,
&i.Instructions,
+ &i.ArchivedAt,
+ &i.ArchivedBy,
)
return i, err
}
@@ -841,7 +970,7 @@ func (q *Queries) UpdateAgent(ctx context.Context, arg UpdateAgentParams) (Agent
const updateAgentStatus = `-- name: UpdateAgentStatus :one
UPDATE agent SET status = $2, updated_at = now()
WHERE id = $1
-RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions
+RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, tools, triggers, runtime_id, instructions, archived_at, archived_by
`
type UpdateAgentStatusParams struct {
@@ -870,6 +999,8 @@ func (q *Queries) UpdateAgentStatus(ctx context.Context, arg UpdateAgentStatusPa
&i.Triggers,
&i.RuntimeID,
&i.Instructions,
+ &i.ArchivedAt,
+ &i.ArchivedBy,
)
return i, err
}
diff --git a/server/pkg/db/generated/models.go b/server/pkg/db/generated/models.go
index 7c812e50..1aa34fe7 100644
--- a/server/pkg/db/generated/models.go
+++ b/server/pkg/db/generated/models.go
@@ -37,6 +37,8 @@ type Agent struct {
Triggers []byte `json:"triggers"`
RuntimeID pgtype.UUID `json:"runtime_id"`
Instructions string `json:"instructions"`
+ ArchivedAt pgtype.Timestamptz `json:"archived_at"`
+ ArchivedBy pgtype.UUID `json:"archived_by"`
}
type AgentRuntime struct {
diff --git a/server/pkg/db/queries/agent.sql b/server/pkg/db/queries/agent.sql
index b55476b8..95239b2f 100644
--- a/server/pkg/db/queries/agent.sql
+++ b/server/pkg/db/queries/agent.sql
@@ -1,5 +1,10 @@
-- name: ListAgents :many
SELECT * FROM agent
+WHERE workspace_id = $1 AND archived_at IS NULL
+ORDER BY created_at ASC;
+
+-- name: ListAllAgents :many
+SELECT * FROM agent
WHERE workspace_id = $1
ORDER BY created_at ASC;
@@ -37,8 +42,15 @@ UPDATE agent SET
WHERE id = $1
RETURNING *;
--- name: DeleteAgent :exec
-DELETE FROM agent WHERE id = $1;
+-- name: ArchiveAgent :one
+UPDATE agent SET archived_at = now(), archived_by = $2, updated_at = now()
+WHERE id = $1
+RETURNING *;
+
+-- name: RestoreAgent :one
+UPDATE agent SET archived_at = NULL, archived_by = NULL, updated_at = now()
+WHERE id = $1
+RETURNING *;
-- name: ListAgentTasks :many
SELECT * FROM agent_task_queue
@@ -55,6 +67,11 @@ UPDATE agent_task_queue
SET status = 'cancelled'
WHERE issue_id = $1 AND status IN ('queued', 'dispatched', 'running');
+-- name: CancelAgentTasksByAgent :exec
+UPDATE agent_task_queue
+SET status = 'cancelled'
+WHERE agent_id = $1 AND status IN ('queued', 'dispatched', 'running');
+
-- name: GetAgentTask :one
SELECT * FROM agent_task_queue
WHERE id = $1;
diff --git a/server/pkg/protocol/events.go b/server/pkg/protocol/events.go
index 7a8636ec..d48cd6f5 100644
--- a/server/pkg/protocol/events.go
+++ b/server/pkg/protocol/events.go
@@ -17,9 +17,10 @@ const (
EventIssueReactionRemoved = "issue_reaction:removed"
// Agent events
- EventAgentStatus = "agent:status"
- EventAgentCreated = "agent:created"
- EventAgentDeleted = "agent:deleted"
+ EventAgentStatus = "agent:status"
+ EventAgentCreated = "agent:created"
+ EventAgentArchived = "agent:archived"
+ EventAgentRestored = "agent:restored"
// Task events (server <-> daemon)
EventTaskDispatch = "task:dispatch"