Skip to content
169 linesCodeBlameRaw

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.

People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.1/**
2 * Agents know their teammates (docs.g1t.sh/guides/people-and-teams/,
3 * "What agents are told"): every turn, an agent is told the visible teams
4 * it is on, from the team pages: who leads each, who is on it and what
5 * they own, who reports to whom, who is around now, and who to page when
6 * a person is needed. It also carries the teams' budgets, which cap what
7 * the team's agents spend together in a month.
8 *
9 * `teamsSection` and `teamBudgetBlock` are pure, so they are tested on
10 * their own (teammates.test.ts); `loadTeams` (ports.ts) reads identity,
11 * the presence rooms (notify) and this service's own agents.
12 */
13import type { AgentTeam, PresenceEntry } from "@g1t/contracts";
14
15import { dollars } from "./money.ts";
16
17/** An agent as its teammates are told of it. */
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.18export type TeamAgentInfo = { id: string; handle: string; display_name: string; title: string };
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.19
20/** Everything about an agent's teams that one turn needs. */
21export type TeamsHere = {
22 teams: AgentTeam[];
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.23 /** Every agent on those teams, by id. */
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.24 agents: TeamAgentInfo[];
25 /** How the people on those teams show now; someone missing is offline. */
26 presence: PresenceEntry[];
27};
28
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.29/** The agents on `team`, by id: its members, as the team page adds them. */
30export function agentIdsOn(team: AgentTeam): string[] {
31 return [...new Set(team.agent_ids)];
32}
33
34/** "QA", "QA and Web", "QA, Web and Support". */
35export function listOf(names: readonly string[]): string {
36 if (names.length < 2) return names.join("");
37 return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.38}
39
40/** "18:12", their local time, or null when the zone isn't one. */
41export function localTime(zone: string | null, now: Date): string | null {
42 if (!zone) return null;
43 try {
44 return new Intl.DateTimeFormat("en-GB", { timeZone: zone, hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(now);
45 } catch {
46 return null;
47 }
48}
49
50/** How someone shows now, in a few words; null when presence wasn't read. */
51export function showing(entry: PresenceEntry | null | undefined, zone: string | null, now: Date, known: boolean): string | null {
52 if (!known) return null;
53 if (!entry || entry.presence === "offline") return "offline";
54 const parts: string[] = [];
55 const dnd = entry.dnd_until && Date.parse(entry.dnd_until) > now.getTime() ? entry.dnd_until : null;
56 if (dnd) {
57 const until = localTime(zone, new Date(dnd));
58 parts.push(`focusing in Do Not Disturb${until ? ` until ${until} their time` : ""}`);
59 } else {
60 parts.push(entry.presence === "active" ? "online" : "away");
61 }
62 if (entry.status?.text) parts.push(`status "${[entry.status.emoji, entry.status.text].filter(Boolean).join(" ")}"`);
63 return parts.join("; ");
64}
65
66/** Whether someone can be reached now: online and not in Do Not Disturb. */
67function reachable(entry: PresenceEntry | null | undefined, now: Date): boolean {
68 if (!entry || entry.presence !== "active") return false;
69 return !(entry.dnd_until && Date.parse(entry.dnd_until) > now.getTime());
70}
71
72function personLabel(person: { username: string; name: string | null }): string {
73 return person.name && person.name.toLowerCase() !== person.username.toLowerCase() ? `${person.name} (@${person.username})` : `@${person.username}`;
74}
75
76/**
77 * Who to page on a team when a person is needed: its lead when they can
78 * be reached, else a maintainer, else anyone on it who can. Null when
79 * no one can, or presence wasn't read.
80 */
81export function whoToPage(team: AgentTeam, presence: Map<string, PresenceEntry>, now: Date, known: boolean): AgentTeam["people"][number] | null {
82 if (!known) return null;
83 const lead = team.lead?.kind === "user" ? team.people.find((p) => p.username === (team.lead as { username: string }).username) : undefined;
84 const order = [...(lead ? [lead] : []), ...team.people.filter((p) => p.maintainer && p !== lead), ...team.people.filter((p) => !p.maintainer && p !== lead)];
85 return order.find((p) => reachable(presence.get(p.user_id), now)) ?? null;
86}
87
88/** "Your teams", for the system prompt; null when the agent is on none. */
89export function teamsSection(selfId: string, here: TeamsHere | null, now: Date): string | null {
90 if (!here || !here.teams.length) return null;
91 const known = here.presence.length > 0;
92 const presence = new Map(here.presence.map((entry) => [entry.user_id, entry]));
93 const byId = new Map(here.agents.map((agent) => [agent.id, agent]));
94 const blocks = here.teams.map((team) => {
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.95 const agentIds = agentIdsOn(team);
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.96 const agents = agentIds.map((id) => byId.get(id)).filter((a): a is TeamAgentInfo => !!a);
97 const facts: string[] = [];
98 if (team.description) facts.push(team.description.replace(/\s+/g, " ").trim().replace(/\.?$/, "."));
99 if (team.lead?.kind === "user") {
100 const username = team.lead.username;
101 const lead = team.people.find((p) => p.username === username);
102 facts.push(`Led by ${lead ? personLabel(lead) : `@${username}`}.`);
103 } else if (team.lead?.kind === "agent") {
104 const id = team.lead.agent_id;
105 const lead = byId.get(id);
106 facts.push(id === selfId ? "You lead it." : lead ? `Led by @${lead.handle}, an agent.` : "Led by an agent.");
107 }
108 if (team.channel) facts.push(`Its channel is #${team.channel.name}.`);
109 if (team.budget_micros) facts.push(`Its agents share a budget of ${dollars(team.budget_micros)} a month.`);
110 const lines = team.people.map((person) => {
111 const bits = [personLabel(person)];
112 const role = [person.title, team.lead?.kind === "user" && team.lead.username === person.username ? "the lead" : null, person.maintainer ? "a maintainer" : null].filter(Boolean);
113 if (role.length) bits.push(role.join(", "));
114 if (person.owns.length) bits.push(`owns ${person.owns.join(", ")}`);
115 if (person.manager) bits.push(`reports to @${person.manager}`);
116 const shows = showing(presence.get(person.user_id), person.timezone, now, known);
117 const time = localTime(person.timezone, now);
118 const now_ = [shows, time ? `it's ${time} for them` : null].filter(Boolean).join("; ");
119 return `- ${bits.join("; ")}${now_ ? ` (${now_})` : ""}`;
120 });
121 for (const agent of agents) {
122 lines.push(agent.id === selfId ? `- @${agent.handle}: you` : `- @${agent.handle} (${agent.display_name}), an agent${agent.title ? `: ${agent.title}` : ""}`);
123 }
124 if (!lines.length) lines.push("- No one else yet.");
125 const page = whoToPage(team, presence, now, known);
126 const pageLine = !known
127 ? null
128 : page
129 ? `When a person is needed: ${personLabel(page)}, who is online now.`
130 : team.people.length
131 ? `No one on ${team.name} can be reached now; ${team.channel ? `post in #${team.channel.name} and ` : ""}say they'll see it when they're back.`
132 : null;
133 return [`### ${team.name}`, "", ...(facts.length ? [facts.join(" "), ""] : []), ...lines, ...(pageLine ? ["", pageLine] : [])].join("\n");
134 });
135 return [
136 "## Your teams",
137 "",
138 "From your teams' pages, and how people show right now. Use it to know who leads, who owns what and who to ask or hand work to. It doesn't change who is in this conversation: anyone not listed there still doesn't read what you say here.",
139 "",
140 "- Ask the person who owns something before guessing. Hold non-urgent questions for someone focusing or away, and say so.",
141 "",
142 blocks.join("\n\n"),
143 ].join("\n");
144}
145
146/** One team's month so far: its budget and what its agents spent together. */
147export type TeamSpend = { name: string; budget_micros: number; spent_micros: number };
148
149/** Why work may not start: a team the agent is on has used its budget for the month. Null when it may. */
150export function teamBudgetBlock(spends: readonly TeamSpend[]): string | null {
151 const over = spends.find((team) => team.budget_micros > 0 && team.spent_micros >= team.budget_micros);
152 if (!over) return null;
153 return `${over.name}'s agents have used the team's budget of ${dollars(over.budget_micros)} for this month. Someone who manages the team can raise it on its settings.`;
154}
155
156/** Each team with a budget, with what its agents spent this month (`month` is `YYYY-MM`). */
157export async function teamSpends(db: D1Database, here: TeamsHere, month: string): Promise<TeamSpend[]> {
158 const budgeted = here.teams.filter((team) => team.budget_micros && team.budget_micros > 0);
159 return Promise.all(
160 budgeted.map(async (team) => {
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.161 const ids = agentIdsOn(team);
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.162 const row = await db
163 .prepare("SELECT COALESCE(SUM(micros), 0) AS micros FROM agent_spend WHERE period = ?1 AND agent_id IN (SELECT value FROM json_each(?2))")
164 .bind(month, JSON.stringify(ids))
165 .first<{ micros: number | null }>();
166 return { name: team.name, budget_micros: team.budget_micros!, spent_micros: row?.micros ?? 0 };
167 }),
168 );
169}