agent-relay-server 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +85 -1
- package/package.json +2 -2
- package/public/assets/{activity-G6l1nOLj.js → activity--LYUyJtT.js} +2 -2
- package/public/assets/{activity-G6l1nOLj.js.map → activity--LYUyJtT.js.map} +1 -1
- package/public/assets/{agents-DBuCRS5A.js → agents-6mhIWvF-.js} +2 -2
- package/public/assets/{agents-DBuCRS5A.js.map → agents-6mhIWvF-.js.map} +1 -1
- package/public/assets/{analytics-BhwMtd0_.js → analytics-W-oZ8bsY.js} +2 -2
- package/public/assets/{analytics-BhwMtd0_.js.map → analytics-W-oZ8bsY.js.map} +1 -1
- package/public/assets/{automation-B7jnsYhH.js → automation-DouRxEN7.js} +2 -2
- package/public/assets/{automation-B7jnsYhH.js.map → automation-DouRxEN7.js.map} +1 -1
- package/public/assets/chat-DVu6bXmE.js +2 -0
- package/public/assets/chat-DVu6bXmE.js.map +1 -0
- package/public/assets/display-ConJ9cJB.js.map +1 -1
- package/public/assets/{formatted-body-impl-BXuR2gfL.js → formatted-body-impl-CthzMcxz.js} +2 -2
- package/public/assets/{formatted-body-impl-BXuR2gfL.js.map → formatted-body-impl-CthzMcxz.js.map} +1 -1
- package/public/assets/{index-CAnJGSBl.js → index-B1s-hGRR.js} +5 -5
- package/public/assets/{index-CAnJGSBl.js.map → index-B1s-hGRR.js.map} +1 -1
- package/public/assets/index-Ryvn1efN.css +2 -0
- package/public/assets/{maintenance-B1bX9ety.js → maintenance-CdDtYHE9.js} +2 -2
- package/public/assets/{maintenance-B1bX9ety.js.map → maintenance-CdDtYHE9.js.map} +1 -1
- package/public/assets/{managed-agents-D12-nGKd.js → managed-agents-DXigb0pj.js} +2 -2
- package/public/assets/{managed-agents-D12-nGKd.js.map → managed-agents-DXigb0pj.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BWd3swt8.js → markdown-preview-impl-BVES_oc1.js} +2 -2
- package/public/assets/{markdown-preview-impl-BWd3swt8.js.map → markdown-preview-impl-BVES_oc1.js.map} +1 -1
- package/public/assets/{memory-4ERBFJBZ.js → memory-DWAF794H.js} +2 -2
- package/public/assets/{memory-4ERBFJBZ.js.map → memory-DWAF794H.js.map} +1 -1
- package/public/assets/{messages-B3ZES72c.js → messages-D63r1IRE.js} +2 -2
- package/public/assets/{messages-B3ZES72c.js.map → messages-D63r1IRE.js.map} +1 -1
- package/public/assets/{orchestrators-qKPntM-a.js → orchestrators-rbEOs7pu.js} +2 -2
- package/public/assets/{orchestrators-qKPntM-a.js.map → orchestrators-rbEOs7pu.js.map} +1 -1
- package/public/assets/{overview-ekmeyPaO.js → overview-CeUZzyTa.js} +2 -2
- package/public/assets/{overview-ekmeyPaO.js.map → overview-CeUZzyTa.js.map} +1 -1
- package/public/assets/{pairs-B2QdTF0M.js → pairs-DJ_e1R5S.js} +2 -2
- package/public/assets/{pairs-B2QdTF0M.js.map → pairs-DJ_e1R5S.js.map} +1 -1
- package/public/assets/{security-WkMkEhb7.js → security-DyeypCyX.js} +2 -2
- package/public/assets/{security-WkMkEhb7.js.map → security-DyeypCyX.js.map} +1 -1
- package/public/assets/{settings-C5aXX3od.js → settings-CVW329oA.js} +2 -2
- package/public/assets/{settings-C5aXX3od.js.map → settings-CVW329oA.js.map} +1 -1
- package/public/assets/{tasks-ppuVeXkO.js → tasks-CR8BxsPc.js} +2 -2
- package/public/assets/{tasks-ppuVeXkO.js.map → tasks-CR8BxsPc.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DXycIJKe.js → terminal-viewer-impl-CISUjIl9.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DXycIJKe.js.map → terminal-viewer-impl-CISUjIl9.js.map} +1 -1
- package/public/assets/{work-queue-C8UzeekT.js → work-queue-ipT9bYqc.js} +2 -2
- package/public/assets/{work-queue-C8UzeekT.js.map → work-queue-ipT9bYqc.js.map} +1 -1
- package/public/index.html +2 -2
- package/src/agent-ref.ts +7 -4
- package/src/channel-target.ts +1 -0
- package/src/db/agent-search.ts +6 -0
- package/src/db/agents.ts +11 -2
- package/src/db/channels.ts +2 -1
- package/src/db/delivery.ts +4 -1
- package/src/db/index.ts +1 -0
- package/src/db/mappers.ts +1 -1
- package/src/db/message-reads.ts +10 -1
- package/src/db/migrations.ts +34 -1
- package/src/db/plans.ts +17 -2
- package/src/db/schema.ts +23 -2
- package/src/db/teams.ts +151 -0
- package/src/mcp-plan-tools.ts +1 -0
- package/src/mcp-team-tools.ts +87 -0
- package/src/mcp.ts +6 -1
- package/src/routes/index.ts +4 -0
- package/src/routes/integrations.ts +2 -1
- package/src/routes/messages.ts +1 -1
- package/src/routes/teams.ts +60 -0
- package/src/runtime-tokens.ts +9 -3
- package/src/security.ts +7 -1
- package/src/services/plan-graph.ts +2 -0
- package/src/services/register-agent.ts +3 -1
- package/src/services/spawn-agent.ts +4 -1
- package/src/services/team.ts +69 -0
- package/src/token-db.ts +3 -3
- package/src/validation.ts +2 -0
- package/public/assets/chat-KaK5_x65.js +0 -2
- package/public/assets/chat-KaK5_x65.js.map +0 -1
- package/public/assets/index-DzRKGPXS.css +0 -2
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{Qt as e,at as t,i as n,jn as r,ln as i}from"./lucide-react-ChOGD4vc.js";import{a,t as o}from"./store-CtKmqMfx.js";import{H as s,ct as c,d as l}from"./display-ConJ9cJB.js";import{t as u}from"./badge-CFVr4fG9.js";import{t as d}from"./button-BuTu0MHx.js";import{L as f,U as p,s as m}from"./index-
|
|
2
|
-
//# sourceMappingURL=work-queue-
|
|
1
|
+
import{Qt as e,at as t,i as n,jn as r,ln as i}from"./lucide-react-ChOGD4vc.js";import{a,t as o}from"./store-CtKmqMfx.js";import{H as s,ct as c,d as l}from"./display-ConJ9cJB.js";import{t as u}from"./badge-CFVr4fG9.js";import{t as d}from"./button-BuTu0MHx.js";import{L as f,U as p,s as m}from"./index-B1s-hGRR.js";import{n as h,t as g}from"./card-C3ErQ372.js";var _=r(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),n=o(e=>e.agentsById),r=o(e=>e.compose),i=o(e=>e.set),s=o(e=>e.doClaimTask),c=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!r.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&c(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(t,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:r.from,onChange:e=>i({compose:{...r,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:l(e)},e.id))]})]})]}),(0,_.jsx)(m,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(t=>(0,_.jsx)(b,{item:t,now:e,agentName:e=>n[e]?l(n[e]):e.slice(-10),onClaim:()=>w(t),onCancel:()=>T(t),onEvents:t.task?()=>E(t):void 0},t.id))]})})]})}function b({item:t,now:r,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[t.severity]||c.info,m=v[t.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:t.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:t.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:t.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:t.status}),t.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),t.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:t.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[t.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(e,{className:`w-3 h-3 inline mr-1`}),a(t.owner)]}),t.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:t.source}),(0,_.jsxs)(`span`,{title:String(t.updatedAt),children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),s(r,t.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[t.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(e,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:l,children:[(0,_.jsx)(n,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
|
|
2
|
+
//# sourceMappingURL=work-queue-ipT9bYqc.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work-queue-C8UzeekT.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `Task #${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"work-queue-ipT9bYqc.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `Task #${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"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"}
|
package/public/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%2309090b'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='%2358a6ff'/%3E%3Ccircle cx='6' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='6' cy='24' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='24' r='2.5' fill='%233fb950'/%3E%3Cline x1='8' y1='9.5' x2='13' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='9.5' x2='19' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='8' y1='22.5' x2='13' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='22.5' x2='19' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3C/svg%3E">
|
|
13
13
|
<link rel="manifest" href="manifest.webmanifest">
|
|
14
14
|
<link rel="apple-touch-icon" href="icons/agent-relay-192.png">
|
|
15
|
-
<script type="module" crossorigin src="./assets/index-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index-B1s-hGRR.js"></script>
|
|
16
16
|
<link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="./assets/preload-helper-CDM1EFcn.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="./assets/lucide-react-ChOGD4vc.js">
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<link rel="modulepreload" crossorigin href="./assets/badge-CFVr4fG9.js">
|
|
38
38
|
<link rel="modulepreload" crossorigin href="./assets/input-CawU8wfc.js">
|
|
39
39
|
<link rel="modulepreload" crossorigin href="./assets/themes-mW9zZP4O.js">
|
|
40
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
40
|
+
<link rel="stylesheet" crossorigin href="./assets/index-Ryvn1efN.css">
|
|
41
41
|
</head>
|
|
42
42
|
<body class="bg-background text-foreground antialiased">
|
|
43
43
|
<div id="root"></div>
|
package/src/agent-ref.ts
CHANGED
|
@@ -32,13 +32,14 @@ export function isAgentOnline(agent: AgentCard, now: number = Date.now()): boole
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
interface Selector {
|
|
35
|
-
field: "id" | "label" | "name" | "tag" | "capability" | "machine" | "rig";
|
|
35
|
+
field: "id" | "label" | "name" | "tag" | "capability" | "machine" | "rig" | "team";
|
|
36
36
|
value: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const SELECTOR_PREFIXES: Array<[string, Selector["field"]]> = [
|
|
40
40
|
["id:", "id"],
|
|
41
41
|
["label:", "label"],
|
|
42
|
+
["team:", "team"],
|
|
42
43
|
["name:", "name"],
|
|
43
44
|
["tag:", "tag"],
|
|
44
45
|
["capability:", "capability"],
|
|
@@ -63,6 +64,7 @@ function matchesSelector(agent: AgentCard, selector: Selector): boolean {
|
|
|
63
64
|
case "capability": return agent.capabilities.includes(selector.value);
|
|
64
65
|
case "machine": return agent.machine === selector.value;
|
|
65
66
|
case "rig": return agent.rig === selector.value;
|
|
67
|
+
case "team": return agent.teamId === selector.value;
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -152,6 +154,7 @@ export function targetMatchesAgent(target: string, agentId: string, agent?: Agen
|
|
|
152
154
|
if (target.startsWith("tag:") && agent.tags.includes(target.slice(4))) return true;
|
|
153
155
|
if (target.startsWith("cap:") && agent.capabilities.includes(target.slice(4))) return true;
|
|
154
156
|
if (target.startsWith("label:") && agent.label === target.slice(6)) return true;
|
|
157
|
+
if (target.startsWith("team:") && agent.teamId === target.slice(5)) return true;
|
|
155
158
|
return false;
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -184,7 +187,7 @@ type SendPlan =
|
|
|
184
187
|
| { kind: "not_found"; message: string }
|
|
185
188
|
| { kind: "ambiguous"; message: string; candidates: AgentCard[] };
|
|
186
189
|
|
|
187
|
-
const FANOUT_PREFIXES = ["tag:", "cap:", "capability:", "label:"];
|
|
190
|
+
const FANOUT_PREFIXES = ["tag:", "cap:", "capability:", "label:", "team:"];
|
|
188
191
|
const PASSTHROUGH_PREFIXES = ["policy:", "orchestrator:", "pool:"];
|
|
189
192
|
const RESERVED_TARGETS = new Set(["user", "system"]);
|
|
190
193
|
|
|
@@ -199,7 +202,7 @@ function describeAgent(agent: AgentCard): string {
|
|
|
199
202
|
function ambiguousMessage(ref: string, candidates: AgentCard[]): string {
|
|
200
203
|
const list = candidates.slice(0, 8).map(describeAgent).join(", ");
|
|
201
204
|
const more = candidates.length > 8 ? `, +${candidates.length - 8} more` : "";
|
|
202
|
-
return `agent reference "${ref}" is ambiguous — matches ${candidates.length}: ${list}${more}. Use a full id, label, or a fan-out selector (tag:/cap:/label:).`;
|
|
205
|
+
return `agent reference "${ref}" is ambiguous — matches ${candidates.length}: ${list}${more}. Use a full id, label, or a fan-out selector (tag:/cap:/label:/team:).`;
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
function notFoundMessage(ref: string, agents: AgentCard[]): string {
|
|
@@ -227,7 +230,7 @@ export function planSend(to: string, agents: AgentCard[], opts: ResolveOptions =
|
|
|
227
230
|
return { kind: "fanout", to: target, receipt: fanoutReceipt(recipients) };
|
|
228
231
|
}
|
|
229
232
|
if (FANOUT_PREFIXES.some((p) => target.startsWith(p))) {
|
|
230
|
-
const recipients = matchAgents(target.replace(/^cap:/, "capability:"), agents)
|
|
233
|
+
const recipients = matchAgents(target.replace(/^cap:/, "capability:"), agents, opts)
|
|
231
234
|
.filter((a) => isAgentOnline(a, now)).map((a) => a.id);
|
|
232
235
|
return { kind: "fanout", to: target, receipt: fanoutReceipt(recipients) };
|
|
233
236
|
}
|
package/src/channel-target.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { ChannelRouteTarget } from "./types";
|
|
|
15
15
|
export function parseChannelRouteTarget(target: string): ChannelRouteTarget {
|
|
16
16
|
if (target.startsWith("pool:")) return { type: "pool", id: target.slice("pool:".length) };
|
|
17
17
|
if (target.startsWith("policy:")) return { type: "policy", id: target.slice("policy:".length) };
|
|
18
|
+
if (target.startsWith("team:")) return { type: "team", id: target.slice("team:".length) };
|
|
18
19
|
if (target.startsWith("label:")) return { type: "label", id: target.slice("label:".length) };
|
|
19
20
|
if (target.startsWith("tag:")) return { type: "tag", id: target.slice("tag:".length) };
|
|
20
21
|
if (target.startsWith("cap:")) return { type: "capability", id: target.slice("cap:".length) };
|
package/src/db/agent-search.ts
CHANGED
|
@@ -95,6 +95,8 @@ export interface AgentSearchFilter {
|
|
|
95
95
|
status?: string | string[];
|
|
96
96
|
/** Exact parent agent id — backs the `spawnedBy:` / `spawnedBy:me` fleet filter (#221). */
|
|
97
97
|
spawnedBy?: string;
|
|
98
|
+
/** Exact durable team id — backs roster lookups for first-class teams (#356). */
|
|
99
|
+
teamId?: string;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
export interface AgentSearchSort {
|
|
@@ -149,6 +151,10 @@ export function searchAgents(filter: AgentSearchFilter = {}, sort: AgentSearchSo
|
|
|
149
151
|
clauses.push("spawned_by = ?");
|
|
150
152
|
params.push(filter.spawnedBy);
|
|
151
153
|
}
|
|
154
|
+
if (filter.teamId) {
|
|
155
|
+
clauses.push("team_id = ?");
|
|
156
|
+
params.push(filter.teamId);
|
|
157
|
+
}
|
|
152
158
|
|
|
153
159
|
const statuses = searchValues(filter.status);
|
|
154
160
|
if (!statuses.includes("all")) {
|
package/src/db/agents.ts
CHANGED
|
@@ -157,8 +157,8 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
|
|
|
157
157
|
const instanceProvided = Boolean(input.instanceId);
|
|
158
158
|
const contextState = mergeRelayContextMarkers(input.id, input.context);
|
|
159
159
|
const stmt = getDb().query(`
|
|
160
|
-
INSERT INTO agents (id, name, kind, label, tags, machine, rig, capabilities, ready, status, instance_id, epoch, provider_capabilities, context_state, meta, spawned_by, last_seen, created_at)
|
|
161
|
-
VALUES ($id, $name, $kind, $label, $tags, $machine, $rig, $capabilities, $ready, $status, $instanceId, $initialEpoch, $providerCapabilities, $contextState, $meta, $spawnedBy, $now, $now)
|
|
160
|
+
INSERT INTO agents (id, name, kind, label, tags, machine, rig, capabilities, ready, status, instance_id, epoch, provider_capabilities, context_state, meta, spawned_by, team_id, last_seen, created_at)
|
|
161
|
+
VALUES ($id, $name, $kind, $label, $tags, $machine, $rig, $capabilities, $ready, $status, $instanceId, $initialEpoch, $providerCapabilities, $contextState, $meta, $spawnedBy, $teamId, $now, $now)
|
|
162
162
|
ON CONFLICT(id) DO UPDATE SET
|
|
163
163
|
name = $name,
|
|
164
164
|
kind = $kind,
|
|
@@ -178,6 +178,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
|
|
|
178
178
|
context_state = coalesce($contextState, agents.context_state),
|
|
179
179
|
meta = $meta,
|
|
180
180
|
spawned_by = coalesce($spawnedBy, agents.spawned_by),
|
|
181
|
+
team_id = coalesce($teamId, agents.team_id),
|
|
181
182
|
last_seen = $now
|
|
182
183
|
`);
|
|
183
184
|
|
|
@@ -201,6 +202,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
|
|
|
201
202
|
$contextState: contextState ? JSON.stringify(contextState) : null,
|
|
202
203
|
$meta: JSON.stringify(input.meta ?? {}),
|
|
203
204
|
$spawnedBy: input.spawnedBy ?? null,
|
|
205
|
+
$teamId: input.teamId ?? null,
|
|
204
206
|
$now: now,
|
|
205
207
|
});
|
|
206
208
|
if (contextState) recordContextSnapshot(input.id, contextState, now);
|
|
@@ -235,6 +237,13 @@ export function setLabel(id: string, label: string | null): boolean {
|
|
|
235
237
|
);
|
|
236
238
|
}
|
|
237
239
|
|
|
240
|
+
export function setAgentTeam(agentId: string, teamId: string): AgentCard | null {
|
|
241
|
+
const result = getDb()
|
|
242
|
+
.query("UPDATE agents SET team_id = ?, last_seen = ? WHERE id = ?")
|
|
243
|
+
.run(teamId, Date.now(), agentId);
|
|
244
|
+
return result.changes > 0 ? getAgent(agentId) : null;
|
|
245
|
+
}
|
|
246
|
+
|
|
238
247
|
export function setTags(id: string, tags: string[]): AgentCard | null {
|
|
239
248
|
const normalized = [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
|
|
240
249
|
const result = getDb().query("UPDATE agents SET tags = ?, last_seen = ? WHERE id = ?").run(JSON.stringify(normalized), Date.now(), id);
|
package/src/db/channels.ts
CHANGED
|
@@ -108,6 +108,7 @@ export function rowToChannelBinding(row: any): ChannelBinding {
|
|
|
108
108
|
export function bindingTargetToLegacyTarget(target: ChannelRouteTarget): string {
|
|
109
109
|
if (target.type === "agent") return target.id;
|
|
110
110
|
if (target.type === "label") return `label:${target.id}`;
|
|
111
|
+
if (target.type === "team") return `team:${target.id}`;
|
|
111
112
|
if (target.type === "tag") return `tag:${target.id}`;
|
|
112
113
|
if (target.type === "capability") return `cap:${target.id}`;
|
|
113
114
|
if (target.type === "broadcast") return "broadcast";
|
|
@@ -160,6 +161,7 @@ export function channelTargetMatches(target: ChannelRouteTarget, channelId?: str
|
|
|
160
161
|
const channelCandidates = candidates.filter((agent) => agentCanServeChannel(agent, channelId));
|
|
161
162
|
if (target.type === "pool") return poolSelectorMatches(target.id, channelCandidates);
|
|
162
163
|
if (target.type === "label") return channelCandidates.filter((agent) => agent.label === target.id);
|
|
164
|
+
if (target.type === "team") return channelCandidates.filter((agent) => agent.teamId === target.id);
|
|
163
165
|
if (target.type === "tag") return channelCandidates.filter((agent) => agent.tags.includes(target.id));
|
|
164
166
|
if (target.type === "capability") return channelCandidates.filter((agent) => agent.capabilities.includes(target.id));
|
|
165
167
|
if (target.type === "broadcast") return channelCandidates;
|
|
@@ -573,4 +575,3 @@ export function resolveChannelRoutes(channelId: string, conversationId?: string)
|
|
|
573
575
|
const exact = rows.filter((row) => row.conversation_key === conversationId);
|
|
574
576
|
return (exact.length ? exact : rows.filter((row) => row.conversation_key === "")).map(rowToChannelBinding);
|
|
575
577
|
}
|
|
576
|
-
|
package/src/db/delivery.ts
CHANGED
|
@@ -174,6 +174,10 @@ export function matchingDeliveryAgents(target: string): AgentCard[] {
|
|
|
174
174
|
const label = target.slice(6);
|
|
175
175
|
return candidates.filter((agent) => agent.label === label);
|
|
176
176
|
}
|
|
177
|
+
if (target.startsWith("team:")) {
|
|
178
|
+
const teamId = target.slice(5);
|
|
179
|
+
return candidates.filter((agent) => agent.teamId === teamId);
|
|
180
|
+
}
|
|
177
181
|
return [];
|
|
178
182
|
}
|
|
179
183
|
|
|
@@ -392,4 +396,3 @@ export function expireQueuedMessages(now: number = Date.now()): Message[] {
|
|
|
392
396
|
})();
|
|
393
397
|
return ids.map((id) => getMessage(id)).filter((message): message is Message => Boolean(message));
|
|
394
398
|
}
|
|
395
|
-
|
package/src/db/index.ts
CHANGED
package/src/db/mappers.ts
CHANGED
|
@@ -130,6 +130,7 @@ export function rowToAgent(row: any): AgentCard {
|
|
|
130
130
|
context: parseJson<ContextState | undefined>(row.context_state, undefined),
|
|
131
131
|
meta: parseJson(row.meta, {}),
|
|
132
132
|
spawnedBy: row.spawned_by ?? undefined,
|
|
133
|
+
teamId: row.team_id ?? undefined,
|
|
133
134
|
lastSeen: row.last_seen,
|
|
134
135
|
createdAt: row.created_at,
|
|
135
136
|
};
|
|
@@ -394,4 +395,3 @@ export const MSG_SELECT = `SELECT m.*, (
|
|
|
394
395
|
) AS reactions_json FROM messages m`;
|
|
395
396
|
|
|
396
397
|
// --- Agents ---
|
|
397
|
-
|
package/src/db/message-reads.ts
CHANGED
|
@@ -162,6 +162,7 @@ export function pollMessages(query: PollQuery): Message[] {
|
|
|
162
162
|
const agentTags = agent?.tags ?? [];
|
|
163
163
|
const agentCaps = agent?.capabilities ?? [];
|
|
164
164
|
const agentLabel = agent?.label;
|
|
165
|
+
const agentTeamId = agent?.teamId;
|
|
165
166
|
const agentLegacyChannelTargets = legacyChannelTargets(agent);
|
|
166
167
|
|
|
167
168
|
const conditions: string[] = [];
|
|
@@ -186,11 +187,19 @@ export function pollMessages(query: PollQuery): Message[] {
|
|
|
186
187
|
targetClauses.push("to_target = ?");
|
|
187
188
|
params.push(`label:${agentLabel}`);
|
|
188
189
|
}
|
|
190
|
+
if (agentTeamId) {
|
|
191
|
+
targetClauses.push("to_target = ?");
|
|
192
|
+
params.push(`team:${agentTeamId}`);
|
|
193
|
+
}
|
|
189
194
|
for (const target of agentLegacyChannelTargets) {
|
|
190
195
|
targetClauses.push("to_target = ?");
|
|
191
196
|
params.push(target);
|
|
192
197
|
}
|
|
193
198
|
conditions.push(`(${targetClauses.join(" OR ")})`);
|
|
199
|
+
if (agentTeamId) {
|
|
200
|
+
conditions.push("(from_agent != ? OR to_target != ?)");
|
|
201
|
+
params.push(query.for, `team:${agentTeamId}`);
|
|
202
|
+
}
|
|
194
203
|
conditions.push("delivery_status != 'queued' AND delivery_status NOT IN ('failed', 'dead')");
|
|
195
204
|
|
|
196
205
|
// Hide active claims held by someone else, but let expired claims surface so
|
|
@@ -323,7 +332,7 @@ export function markRead(messageId: number, agentId: string): boolean {
|
|
|
323
332
|
delivery_next_retry_at = NULL,
|
|
324
333
|
delivery_poison_reason = NULL,
|
|
325
334
|
delivery_updated_at = ?
|
|
326
|
-
WHERE id = ? AND (to_target = ? OR resolved_to_agent = ? OR to_target = 'broadcast' OR to_target LIKE 'tag:%' OR to_target LIKE 'cap:%' OR to_target LIKE 'label:%')
|
|
335
|
+
WHERE id = ? AND (to_target = ? OR resolved_to_agent = ? OR to_target = 'broadcast' OR to_target LIKE 'tag:%' OR to_target LIKE 'cap:%' OR to_target LIKE 'label:%' OR to_target LIKE 'team:%')
|
|
327
336
|
`).run(Date.now(), messageId, agentId, agentId);
|
|
328
337
|
return true;
|
|
329
338
|
}
|
package/src/db/migrations.ts
CHANGED
|
@@ -279,15 +279,44 @@ export function applyMigrations(): void {
|
|
|
279
279
|
// which runs per persisted session step. Added here (not the base schema) because the column
|
|
280
280
|
// is migration-added for pre-hardening DBs. Without it that lookup full-scans messages.
|
|
281
281
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_idempotency ON messages(from_agent, idempotency_key)");
|
|
282
|
+
getDb().run(`
|
|
283
|
+
CREATE TABLE IF NOT EXISTS teams (
|
|
284
|
+
id TEXT PRIMARY KEY,
|
|
285
|
+
coordinator_id TEXT,
|
|
286
|
+
root_coordinator_id TEXT,
|
|
287
|
+
title TEXT,
|
|
288
|
+
brief TEXT,
|
|
289
|
+
definition_of_done TEXT,
|
|
290
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
291
|
+
outcome_status TEXT,
|
|
292
|
+
outcome_summary TEXT,
|
|
293
|
+
composition TEXT,
|
|
294
|
+
cost_tokens INTEGER,
|
|
295
|
+
cost_usd REAL,
|
|
296
|
+
wall_clock_ms INTEGER,
|
|
297
|
+
lands_clean INTEGER,
|
|
298
|
+
created_at INTEGER NOT NULL,
|
|
299
|
+
updated_at INTEGER NOT NULL,
|
|
300
|
+
dissolved_at INTEGER
|
|
301
|
+
)
|
|
302
|
+
`);
|
|
303
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_teams_coordinator ON teams(coordinator_id)");
|
|
304
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_teams_status ON teams(status)");
|
|
282
305
|
getDb().run(`
|
|
283
306
|
CREATE TABLE IF NOT EXISTS plans (
|
|
284
|
-
id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
|
|
307
|
+
id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, team_id TEXT, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
|
|
285
308
|
title TEXT, objective TEXT, nodes TEXT NOT NULL DEFAULT '[]', edges TEXT NOT NULL DEFAULT '[]',
|
|
286
309
|
created_by TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
287
310
|
)
|
|
288
311
|
`);
|
|
312
|
+
const planCols = getDb().query("PRAGMA table_info(plans)").all() as any[];
|
|
313
|
+
const planColNames = planCols.map((c: any) => c.name);
|
|
314
|
+
if (!planColNames.includes("team_id")) {
|
|
315
|
+
getDb().run("ALTER TABLE plans ADD COLUMN team_id TEXT");
|
|
316
|
+
}
|
|
289
317
|
getDb().run("CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_thread_channel ON plans(thread_id, coalesce(channel, ''))");
|
|
290
318
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_plans_updated ON plans(updated_at)");
|
|
319
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_plans_team ON plans(team_id)");
|
|
291
320
|
getDb().run(`
|
|
292
321
|
CREATE TABLE IF NOT EXISTS plan_versions (
|
|
293
322
|
id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
|
@@ -372,6 +401,10 @@ export function applyMigrations(): void {
|
|
|
372
401
|
if (!agentColNames.includes("spawned_by")) {
|
|
373
402
|
getDb().run("ALTER TABLE agents ADD COLUMN spawned_by TEXT");
|
|
374
403
|
}
|
|
404
|
+
if (!agentColNames.includes("team_id")) {
|
|
405
|
+
getDb().run("ALTER TABLE agents ADD COLUMN team_id TEXT");
|
|
406
|
+
}
|
|
407
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_agents_team ON agents(team_id)");
|
|
375
408
|
getDb().run(`
|
|
376
409
|
CREATE TABLE IF NOT EXISTS context_snapshots (
|
|
377
410
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
package/src/db/plans.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Plan, PlanEdge, PlanNode } from "../types";
|
|
|
6
6
|
interface PlanRow {
|
|
7
7
|
id: string;
|
|
8
8
|
thread_id: number;
|
|
9
|
+
team_id: string | null;
|
|
9
10
|
channel: string | null;
|
|
10
11
|
version: number;
|
|
11
12
|
title: string | null;
|
|
@@ -49,6 +50,7 @@ function rowToPlan(row: PlanRow): Plan {
|
|
|
49
50
|
return {
|
|
50
51
|
id: row.id,
|
|
51
52
|
threadId: String(row.thread_id),
|
|
53
|
+
...(row.team_id ? { teamId: row.team_id } : {}),
|
|
52
54
|
...(row.channel ? { channel: row.channel } : {}),
|
|
53
55
|
version: row.version,
|
|
54
56
|
...(row.title ? { title: row.title } : {}),
|
|
@@ -82,6 +84,7 @@ export function planThreadExists(threadId: number): boolean {
|
|
|
82
84
|
export function createPlanRow(input: {
|
|
83
85
|
id?: string;
|
|
84
86
|
threadId: number;
|
|
87
|
+
teamId?: string;
|
|
85
88
|
channel?: string;
|
|
86
89
|
title?: string;
|
|
87
90
|
objective?: string;
|
|
@@ -93,11 +96,12 @@ export function createPlanRow(input: {
|
|
|
93
96
|
const now = Date.now();
|
|
94
97
|
try {
|
|
95
98
|
getDb().query(`
|
|
96
|
-
INSERT INTO plans (id, thread_id, channel, version, title, objective, nodes, edges, created_by, created_at, updated_at)
|
|
97
|
-
VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
|
|
99
|
+
INSERT INTO plans (id, thread_id, team_id, channel, version, title, objective, nodes, edges, created_by, created_at, updated_at)
|
|
100
|
+
VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
|
|
98
101
|
`).run(
|
|
99
102
|
id,
|
|
100
103
|
input.threadId,
|
|
104
|
+
input.teamId ?? null,
|
|
101
105
|
input.channel ?? null,
|
|
102
106
|
input.title ?? null,
|
|
103
107
|
input.objective ?? null,
|
|
@@ -128,6 +132,17 @@ export function getPlanByThread(threadId: number, channel?: string): Plan | null
|
|
|
128
132
|
return row ? rowToPlan(row) : null;
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
export function getPlanByTeam(teamId: string, channel?: string): Plan | null {
|
|
136
|
+
const row = channel === undefined
|
|
137
|
+
? getDb()
|
|
138
|
+
.query("SELECT * FROM plans WHERE team_id = ? ORDER BY updated_at DESC LIMIT 1")
|
|
139
|
+
.get(teamId) as PlanRow | undefined
|
|
140
|
+
: getDb()
|
|
141
|
+
.query("SELECT * FROM plans WHERE team_id = ? AND coalesce(channel, '') = ? ORDER BY updated_at DESC LIMIT 1")
|
|
142
|
+
.get(teamId, channel) as PlanRow | undefined;
|
|
143
|
+
return row ? rowToPlan(row) : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
131
146
|
export function replacePlan(input: Plan, changedBy?: string): Plan {
|
|
132
147
|
const existing = getPlan(input.id);
|
|
133
148
|
if (!existing) throw new ValidationError(`plan ${input.id} not found`);
|
package/src/db/schema.ts
CHANGED
|
@@ -106,10 +106,10 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
106
106
|
context_state TEXT,
|
|
107
107
|
meta TEXT NOT NULL DEFAULT '{}',
|
|
108
108
|
spawned_by TEXT,
|
|
109
|
+
team_id TEXT,
|
|
109
110
|
last_seen INTEGER NOT NULL,
|
|
110
111
|
created_at INTEGER NOT NULL
|
|
111
112
|
);
|
|
112
|
-
|
|
113
113
|
CREATE TABLE IF NOT EXISTS messages (
|
|
114
114
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
115
115
|
from_agent TEXT NOT NULL,
|
|
@@ -150,8 +150,29 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
150
150
|
-- per candidate row (O(n^2)): ~8.6s at 4k rows, which blew the 5s Stop-hook
|
|
151
151
|
-- timeout and wedged turns in "busy" (#199).
|
|
152
152
|
CREATE INDEX IF NOT EXISTS idx_msg_reply_to ON messages(reply_to, from_agent);
|
|
153
|
+
CREATE TABLE IF NOT EXISTS teams (
|
|
154
|
+
id TEXT PRIMARY KEY,
|
|
155
|
+
coordinator_id TEXT,
|
|
156
|
+
root_coordinator_id TEXT,
|
|
157
|
+
title TEXT,
|
|
158
|
+
brief TEXT,
|
|
159
|
+
definition_of_done TEXT,
|
|
160
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
161
|
+
outcome_status TEXT,
|
|
162
|
+
outcome_summary TEXT,
|
|
163
|
+
composition TEXT,
|
|
164
|
+
cost_tokens INTEGER,
|
|
165
|
+
cost_usd REAL,
|
|
166
|
+
wall_clock_ms INTEGER,
|
|
167
|
+
lands_clean INTEGER,
|
|
168
|
+
created_at INTEGER NOT NULL,
|
|
169
|
+
updated_at INTEGER NOT NULL,
|
|
170
|
+
dissolved_at INTEGER
|
|
171
|
+
);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_teams_coordinator ON teams(coordinator_id);
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_teams_status ON teams(status);
|
|
153
174
|
CREATE TABLE IF NOT EXISTS plans (
|
|
154
|
-
id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
|
|
175
|
+
id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, team_id TEXT, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
|
|
155
176
|
title TEXT, objective TEXT, nodes TEXT NOT NULL DEFAULT '[]', edges TEXT NOT NULL DEFAULT '[]',
|
|
156
177
|
created_by TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
157
178
|
);
|
package/src/db/teams.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getAgent, setAgentTeam } from "./agents.ts";
|
|
3
|
+
import { getDb, ValidationError } from "./connection.ts";
|
|
4
|
+
import { rowToAgent } from "./mappers.ts";
|
|
5
|
+
import type { AgentCard, Team } from "../types";
|
|
6
|
+
|
|
7
|
+
interface TeamRow {
|
|
8
|
+
id: string;
|
|
9
|
+
coordinator_id: string | null;
|
|
10
|
+
root_coordinator_id: string | null;
|
|
11
|
+
title: string | null;
|
|
12
|
+
brief: string | null;
|
|
13
|
+
definition_of_done: string | null;
|
|
14
|
+
status: Team["status"];
|
|
15
|
+
outcome_status: Team["outcomeStatus"] | null;
|
|
16
|
+
outcome_summary: string | null;
|
|
17
|
+
composition: string | null;
|
|
18
|
+
cost_tokens: number | null;
|
|
19
|
+
cost_usd: number | null;
|
|
20
|
+
wall_clock_ms: number | null;
|
|
21
|
+
lands_clean: number | null;
|
|
22
|
+
created_at: number;
|
|
23
|
+
updated_at: number;
|
|
24
|
+
dissolved_at: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TeamDissolveInput {
|
|
28
|
+
outcomeStatus?: Team["outcomeStatus"];
|
|
29
|
+
outcomeSummary?: string;
|
|
30
|
+
landsClean?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function teamId(): string {
|
|
34
|
+
return `team_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rowToTeam(row: TeamRow): Team {
|
|
38
|
+
return {
|
|
39
|
+
id: row.id,
|
|
40
|
+
...(row.coordinator_id ? { coordinatorId: row.coordinator_id } : {}),
|
|
41
|
+
...(row.root_coordinator_id ? { rootCoordinatorId: row.root_coordinator_id } : {}),
|
|
42
|
+
...(row.title ? { title: row.title } : {}),
|
|
43
|
+
...(row.brief ? { brief: row.brief } : {}),
|
|
44
|
+
...(row.definition_of_done ? { definitionOfDone: row.definition_of_done } : {}),
|
|
45
|
+
status: row.status,
|
|
46
|
+
...(row.outcome_status ? { outcomeStatus: row.outcome_status } : {}),
|
|
47
|
+
...(row.outcome_summary ? { outcomeSummary: row.outcome_summary } : {}),
|
|
48
|
+
...(row.composition ? { composition: row.composition } : {}),
|
|
49
|
+
...(row.cost_tokens !== null ? { costTokens: row.cost_tokens } : {}),
|
|
50
|
+
...(row.cost_usd !== null ? { costUsd: row.cost_usd } : {}),
|
|
51
|
+
...(row.wall_clock_ms !== null ? { wallClockMs: row.wall_clock_ms } : {}),
|
|
52
|
+
...(row.lands_clean !== null ? { landsClean: row.lands_clean === 1 } : {}),
|
|
53
|
+
createdAt: row.created_at,
|
|
54
|
+
updatedAt: row.updated_at,
|
|
55
|
+
...(row.dissolved_at !== null ? { dissolvedAt: row.dissolved_at } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getTeam(id: string): Team | null {
|
|
60
|
+
const row = getDb().query("SELECT * FROM teams WHERE id = ?").get(id) as TeamRow | undefined;
|
|
61
|
+
return row ? rowToTeam(row) : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getTeamRoster(teamId: string): AgentCard[] {
|
|
65
|
+
const rows = getDb()
|
|
66
|
+
.query("SELECT * FROM agents WHERE team_id = ? ORDER BY created_at ASC, id ASC")
|
|
67
|
+
.all(teamId) as any[];
|
|
68
|
+
return rows.map(rowToAgent);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ensureTeam(coordinatorId: string): Team {
|
|
72
|
+
const coordinator = getAgent(coordinatorId);
|
|
73
|
+
if (!coordinator) throw new ValidationError(`coordinator agent ${coordinatorId} not found`);
|
|
74
|
+
if (coordinator.teamId) {
|
|
75
|
+
const existing = getTeam(coordinator.teamId);
|
|
76
|
+
if (existing) return existing;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const id = teamId();
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
getDb().transaction(() => {
|
|
82
|
+
getDb().query(`
|
|
83
|
+
INSERT INTO teams (id, coordinator_id, root_coordinator_id, status, created_at, updated_at)
|
|
84
|
+
VALUES (?, ?, ?, 'active', ?, ?)
|
|
85
|
+
`).run(id, coordinatorId, coordinatorId, now, now);
|
|
86
|
+
setAgentTeam(coordinatorId, id);
|
|
87
|
+
})();
|
|
88
|
+
return getTeam(id)!;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function setTeamBrief(input: {
|
|
92
|
+
teamId: string;
|
|
93
|
+
title?: string;
|
|
94
|
+
brief?: string;
|
|
95
|
+
definitionOfDone?: string;
|
|
96
|
+
}): Team {
|
|
97
|
+
const existing = getTeam(input.teamId);
|
|
98
|
+
if (!existing) throw new ValidationError(`team ${input.teamId} not found`);
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
getDb().query(`
|
|
101
|
+
UPDATE teams
|
|
102
|
+
SET title = ?, brief = ?, definition_of_done = ?, updated_at = ?
|
|
103
|
+
WHERE id = ?
|
|
104
|
+
`).run(
|
|
105
|
+
input.title ?? null,
|
|
106
|
+
input.brief ?? null,
|
|
107
|
+
input.definitionOfDone ?? null,
|
|
108
|
+
now,
|
|
109
|
+
input.teamId,
|
|
110
|
+
);
|
|
111
|
+
return getTeam(input.teamId)!;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function compositionForMembers(members: AgentCard[]): string {
|
|
115
|
+
return JSON.stringify(members.map((agent) => ({
|
|
116
|
+
id: agent.id,
|
|
117
|
+
label: agent.label,
|
|
118
|
+
role: typeof agent.meta?.role === "string" ? agent.meta.role : undefined,
|
|
119
|
+
provider: agent.providerCapabilities?.model?.provider,
|
|
120
|
+
model: agent.providerCapabilities?.model?.id ?? agent.providerCapabilities?.model?.alias,
|
|
121
|
+
})));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function dissolveTeam(teamId: string, input: TeamDissolveInput = {}): Team {
|
|
125
|
+
const existing = getTeam(teamId);
|
|
126
|
+
if (!existing) throw new ValidationError(`team ${teamId} not found`);
|
|
127
|
+
const members = getTeamRoster(teamId);
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
getDb().query(`
|
|
130
|
+
UPDATE teams
|
|
131
|
+
SET status = 'dissolved',
|
|
132
|
+
outcome_status = ?,
|
|
133
|
+
outcome_summary = ?,
|
|
134
|
+
composition = ?,
|
|
135
|
+
wall_clock_ms = ?,
|
|
136
|
+
lands_clean = ?,
|
|
137
|
+
dissolved_at = ?,
|
|
138
|
+
updated_at = ?
|
|
139
|
+
WHERE id = ?
|
|
140
|
+
`).run(
|
|
141
|
+
input.outcomeStatus ?? null,
|
|
142
|
+
input.outcomeSummary ?? null,
|
|
143
|
+
compositionForMembers(members),
|
|
144
|
+
Math.max(0, now - existing.createdAt),
|
|
145
|
+
input.landsClean === undefined ? null : input.landsClean ? 1 : 0,
|
|
146
|
+
now,
|
|
147
|
+
now,
|
|
148
|
+
teamId,
|
|
149
|
+
);
|
|
150
|
+
return getTeam(teamId)!;
|
|
151
|
+
}
|
package/src/mcp-plan-tools.ts
CHANGED
|
@@ -47,6 +47,7 @@ export const PLAN_TOOLS = [
|
|
|
47
47
|
type: "object",
|
|
48
48
|
properties: {
|
|
49
49
|
threadId: { oneOf: [{ type: "string" }, { type: "integer", minimum: 1 }] },
|
|
50
|
+
teamId: { type: "string" },
|
|
50
51
|
channel: { type: "string" },
|
|
51
52
|
title: { type: "string" },
|
|
52
53
|
objective: { type: "string" },
|