From ce4536d1faf70c672ccfb58bd5c5e668098d30a9 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 26 Mar 2026 20:06:29 -0500 Subject: [PATCH] Add agent Mine inbox API surface Co-Authored-By: Paperclip --- packages/shared/src/constants.ts | 10 +++++ packages/shared/src/index.ts | 4 ++ packages/shared/src/validators/agent.ts | 8 ++++ packages/shared/src/validators/index.ts | 2 + .../agent-permissions-routes.test.ts | 39 +++++++++++++++++++ server/src/routes/agents.ts | 18 +++++++++ skills/paperclip/SKILL.md | 1 + skills/paperclip/references/api-reference.md | 29 ++++++++++++++ ui/src/pages/Inbox.tsx | 7 ++-- 9 files changed, 114 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 0c5aa424..6ace12a4 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -119,6 +119,16 @@ export const ISSUE_STATUSES = [ ] as const; export type IssueStatus = (typeof ISSUE_STATUSES)[number]; +export const INBOX_MINE_ISSUE_STATUSES = [ + "backlog", + "todo", + "in_progress", + "in_review", + "blocked", + "done", +] as const; +export const INBOX_MINE_ISSUE_STATUS_FILTER = INBOX_MINE_ISSUE_STATUSES.join(","); + export const ISSUE_PRIORITIES = ["critical", "high", "medium", "low"] as const; export type IssuePriority = (typeof ISSUE_PRIORITIES)[number]; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 891011f7..982a825c 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -9,6 +9,8 @@ export { AGENT_ROLE_LABELS, AGENT_ICON_NAMES, ISSUE_STATUSES, + INBOX_MINE_ISSUE_STATUSES, + INBOX_MINE_ISSUE_STATUS_FILTER, ISSUE_PRIORITIES, ISSUE_ORIGIN_KINDS, GOAL_LEVELS, @@ -344,6 +346,7 @@ export { upsertAgentInstructionsFileSchema, updateAgentInstructionsPathSchema, createAgentKeySchema, + agentMineInboxQuerySchema, wakeAgentSchema, resetAgentSessionSchema, testAdapterEnvironmentSchema, @@ -356,6 +359,7 @@ export { type UpsertAgentInstructionsFile, type UpdateAgentInstructionsPath, type CreateAgentKey, + type AgentMineInboxQuery, type WakeAgent, type ResetAgentSession, type TestAdapterEnvironment, diff --git a/packages/shared/src/validators/agent.ts b/packages/shared/src/validators/agent.ts index 8c29150b..288ae683 100644 --- a/packages/shared/src/validators/agent.ts +++ b/packages/shared/src/validators/agent.ts @@ -4,6 +4,7 @@ import { AGENT_ICON_NAMES, AGENT_ROLES, AGENT_STATUSES, + INBOX_MINE_ISSUE_STATUS_FILTER, } from "../constants.js"; import { envConfigSchema } from "./secret.js"; @@ -93,6 +94,13 @@ export const createAgentKeySchema = z.object({ export type CreateAgentKey = z.infer; +export const agentMineInboxQuerySchema = z.object({ + userId: z.string().trim().min(1), + status: z.string().trim().min(1).optional().default(INBOX_MINE_ISSUE_STATUS_FILTER), +}); + +export type AgentMineInboxQuery = z.infer; + export const wakeAgentSchema = z.object({ source: z.enum(["timer", "assignment", "on_demand", "automation"]).optional().default("on_demand"), triggerDetail: z.enum(["manual", "ping", "callback", "system"]).optional(), diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index 3f33bceb..1ab21793 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -85,6 +85,7 @@ export { upsertAgentInstructionsFileSchema, updateAgentInstructionsPathSchema, createAgentKeySchema, + agentMineInboxQuerySchema, wakeAgentSchema, resetAgentSessionSchema, testAdapterEnvironmentSchema, @@ -97,6 +98,7 @@ export { type UpsertAgentInstructionsFile, type UpdateAgentInstructionsPath, type CreateAgentKey, + type AgentMineInboxQuery, type WakeAgent, type ResetAgentSession, type TestAdapterEnvironment, diff --git a/server/src/__tests__/agent-permissions-routes.test.ts b/server/src/__tests__/agent-permissions-routes.test.ts index 08941f77..7bd79f76 100644 --- a/server/src/__tests__/agent-permissions-routes.test.ts +++ b/server/src/__tests__/agent-permissions-routes.test.ts @@ -1,6 +1,7 @@ import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { INBOX_MINE_ISSUE_STATUS_FILTER } from "@paperclipai/shared"; import { agentRoutes } from "../routes/agents.js"; import { errorHandler } from "../middleware/index.js"; @@ -272,4 +273,42 @@ describe("agent permission routes", () => { expect(res.body.access.canAssignTasks).toBe(true); expect(res.body.access.taskAssignSource).toBe("agent_creator"); }); + + it("exposes a dedicated agent route for the inbox mine view", async () => { + mockIssueService.list.mockResolvedValue([ + { + id: "issue-1", + identifier: "PAP-910", + title: "Inbox follow-up", + status: "todo", + }, + ]); + + const app = createApp({ + type: "agent", + agentId, + companyId, + runId: "run-1", + source: "agent_key", + }); + + const res = await request(app) + .get("/api/agents/me/inbox/mine") + .query({ userId: "board-user" }); + + expect(res.status).toBe(200); + expect(mockIssueService.list).toHaveBeenCalledWith(companyId, { + touchedByUserId: "board-user", + inboxArchivedByUserId: "board-user", + status: INBOX_MINE_ISSUE_STATUS_FILTER, + }); + expect(res.body).toEqual([ + { + id: "issue-1", + identifier: "PAP-910", + title: "Inbox follow-up", + status: "todo", + }, + ]); + }); }); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index b4964578..2ad85e63 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -6,6 +6,7 @@ import { agents as agentsTable, companies, heartbeatRuns } from "@paperclipai/db import { and, desc, eq, inArray, not, sql } from "drizzle-orm"; import { agentSkillSyncSchema, + agentMineInboxQuerySchema, createAgentKeySchema, createAgentHireSchema, createAgentSchema, @@ -1006,6 +1007,23 @@ export function agentRoutes(db: Db) { ); }); + router.get("/agents/me/inbox/mine", async (req, res) => { + if (req.actor.type !== "agent" || !req.actor.agentId || !req.actor.companyId) { + res.status(401).json({ error: "Agent authentication required" }); + return; + } + + const query = agentMineInboxQuerySchema.parse(req.query); + const issuesSvc = issueService(db); + const rows = await issuesSvc.list(req.actor.companyId, { + touchedByUserId: query.userId, + inboxArchivedByUserId: query.userId, + status: query.status, + }); + + res.json(rows); + }); + router.get("/agents/:id", async (req, res) => { const id = req.params.id as string; const agent = await svc.getById(id); diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index 407f08da..142ee63a 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -255,6 +255,7 @@ PATCH /api/agents/{agentId}/instructions-path | ----------------------------------------- | ------------------------------------------------------------------------------------------ | | My identity | `GET /api/agents/me` | | My compact inbox | `GET /api/agents/me/inbox-lite` | +| Report a user's Mine inbox view | `GET /api/agents/me/inbox/mine?userId=:userId` | | My assignments | `GET /api/companies/:companyId/issues?assigneeAgentId=:id&status=todo,in_progress,blocked` | | Checkout task | `POST /api/issues/:issueId/checkout` | | Get task + ancestors | `GET /api/issues/:issueId` | diff --git a/skills/paperclip/references/api-reference.md b/skills/paperclip/references/api-reference.md index 63293725..aea4250c 100644 --- a/skills/paperclip/references/api-reference.md +++ b/skills/paperclip/references/api-reference.md @@ -226,6 +226,34 @@ PATCH /api/issues/issue-99 { "comment": "JWT signing done. Still need token refresh logic. Will continue next heartbeat." } ``` +### Worked Example: Report A Board User's Mine Inbox + +When a board user asks "what's in my inbox?", an agent can derive that user's id from the triggering issue or comment metadata and fetch the same Mine-tab issue set the UI uses. + +``` +# Board user created the requesting issue. +GET /api/issues/issue-200 +-> { id: "issue-200", createdByUserId: "user-7", ... } + +# Fetch the board user's Mine inbox issues. +GET /api/agents/me/inbox/mine?userId=user-7 +-> [ + { + id: "issue-310", + identifier: "PAP-310", + title: "Review CEO strategy revision", + status: "in_review", + myLastTouchAt: "2026-03-26T18:00:00.000Z", + lastExternalCommentAt: "2026-03-26T19:10:00.000Z", + isUnreadForMe: true + } + ] + +# Summarize it back to the board in a comment or document. +PATCH /api/issues/issue-200 +{ "comment": "Your Mine inbox has 1 unread issue: [PAP-310](/PAP/issues/PAP-310)." } +``` + --- ## Worked Example: Manager Heartbeat @@ -566,6 +594,7 @@ Terminal states: `done`, `cancelled` | Method | Path | Description | | ------ | ---------------------------------- | ------------------------------------ | | GET | `/api/agents/me` | Your agent record + chain of command | +| GET | `/api/agents/me/inbox/mine?userId=:userId` | Mine-tab issue list for a specific board user | | GET | `/api/agents/:agentId` | Agent details + chain of command | | GET | `/api/companies/:companyId/agents` | List all agents in company | | GET | `/api/companies/:companyId/org` | Org chart tree | diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index d2fe26a6..8e429dca 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -1,6 +1,7 @@ import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Link, useLocation, useNavigate } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { INBOX_MINE_ISSUE_STATUS_FILTER } from "@paperclipai/shared"; import { approvalsApi } from "../api/approvals"; import { accessApi } from "../api/access"; import { ApiError } from "../api/client"; @@ -68,8 +69,6 @@ type SectionKey = | "work_items" | "alerts"; -const INBOX_ISSUE_STATUSES = "backlog,todo,in_progress,in_review,blocked,done"; - function firstNonEmptyLine(value: string | null | undefined): string | null { if (!value) return null; const line = value.split("\n").map((chunk) => chunk.trim()).find(Boolean); @@ -595,7 +594,7 @@ export function Inbox() { issuesApi.list(selectedCompanyId!, { touchedByUserId: "me", inboxArchivedByUserId: "me", - status: INBOX_ISSUE_STATUSES, + status: INBOX_MINE_ISSUE_STATUS_FILTER, }), enabled: !!selectedCompanyId, }); @@ -607,7 +606,7 @@ export function Inbox() { queryFn: () => issuesApi.list(selectedCompanyId!, { touchedByUserId: "me", - status: INBOX_ISSUE_STATUSES, + status: INBOX_MINE_ISSUE_STATUS_FILTER, }), enabled: !!selectedCompanyId, });