Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how. | 1 | /** |
| 2 | * An agent's teams on its profiles: the teams it is on and the ones the | |
| 3 | * viewer may add it to, and adding or taking it off one. An agent is on | |
| 4 | * teams the way a person is, through team membership in identity, so this | |
| 5 | * is the team's own add and remove, from the agent's side. | |
| 6 | */ | |
| 7 | import type { PeopleDirectory, Team, User } from "@g1t/contracts"; | |
| 8 | ||
| 9 | import type { AgentTeams, TeamChange } from "./people"; | |
| 10 | import { identity, workspaceAgents } from "./services.server"; | |
| 11 | ||
| 12 | /** The teams `agentId` is on and the teams the viewer may add it to; null when identity didn't answer. */ | |
| 13 | export async function agentTeamsFor(viewer: User, slug: string, agentId: string, directory?: PeopleDirectory | null): Promise<AgentTeams | null> { | |
| 14 | const [listed, people] = await Promise.all([ | |
| 15 | identity.listTeams(viewer, slug).catch(() => null), | |
| 16 | directory ? Promise.resolve(directory) : identity.peopleDirectory(viewer, slug).then((found) => (found.ok ? found.value : null), () => null), | |
| 17 | ]); | |
| 18 | if (!listed?.ok || !people) return null; | |
| 19 | const manage = new Map<string, Team>(listed.value.map((team) => [team.slug, team])); | |
| 20 | const on = people.teams | |
| 21 | .filter((team) => team.agent_ids.includes(agentId)) | |
| 22 | .map((team) => ({ | |
| 23 | slug: team.slug, | |
| 24 | name: team.name, | |
| 25 | people: team.people.length, | |
| 26 | agents: new Set(team.agent_ids).size, | |
| 27 | lead: team.lead?.kind === "agent" && team.lead.agent_id === agentId, | |
| 28 | can_manage: manage.get(team.slug)?.can_manage ?? false, | |
| 29 | })) | |
| 30 | .sort((a, b) => a.name.localeCompare(b.name)); | |
| 31 | const addable = listed.value | |
| 32 | .filter((team) => team.can_manage && !on.some((row) => row.slug === team.slug)) | |
| 33 | .map((team) => ({ slug: team.slug, name: team.name })) | |
| 34 | .sort((a, b) => a.name.localeCompare(b.name)); | |
| 35 | return { on, addable }; | |
| 36 | } | |
| 37 | ||
| 38 | /** | |
| 39 | * `join-team` or `leave-team` from an agent's profile: the agent must be | |
| 40 | * one of the workspace's (identity knows agents only by id), and a | |
| 41 | * personal agent joins no team. Null for any other intent. | |
| 42 | */ | |
| 43 | export async function changeAgentTeam(form: FormData, viewer: User, slug: string, handle: string): Promise<TeamChange | null> { | |
| 44 | const intent = String(form.get("intent") ?? ""); | |
| 45 | if (intent !== "join-team" && intent !== "leave-team") return null; | |
| 46 | const team = String(form.get("team") ?? "").trim().toLowerCase(); | |
| 47 | if (!team) return { intent, team, error: "Choose a team." }; | |
| 48 | const found = await workspaceAgents.get(slug, handle, viewer).catch(() => null); | |
| 49 | if (!found?.ok) return { intent, team, error: "That agent couldn't be found." }; | |
| 50 | if (intent === "join-team" && found.value.scope === "personal") return { intent, team, error: "A personal agent is on no team." }; | |
| 51 | const done = | |
| 52 | intent === "join-team" | |
| 53 | ? await identity.setTeamAgent(viewer, slug, team, found.value.id).catch(() => null) | |
| 54 | : await identity.removeTeamAgent(viewer, slug, team, found.value.id).catch(() => null); | |
| 55 | if (!done) return { intent, team, error: "Teams didn't answer. Try again in a moment." }; | |
| 56 | return done.ok ? { intent, team, error: null } : { intent, team, error: done.error.message }; | |
| 57 | } |