Skip to content
226 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.

Chat and workspace agents: channels, DMs and named agents you talk to1/**
2 * Where a conversation happens, as the reply loop sees it: read what was
3 * said, show that the agent is typing, post its answer. The loop knows
4 * nothing else about the chat it is in, so the same agent (its definition,
5 * budget and replies) answers wherever it is reached: g1t's own chat today,
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.6 * another chat app the workspace connected later. Only g1t's adapter exists.
Chat and workspace agents: channels, DMs and named agents you talk to7 */
Merge branch 'worktree-agent-a1398e81ad1a64c5f'8import type { AgentDelivery, ChatMessage, ConversationForAgent, MessageCard, ServiceBinding } from "@g1t/contracts";
Chat and workspace agents: channels, DMs and named agents you talk to9
10// By path, not the package: it imports only types, so the adapter is tested under Node.
11import { CHAT_MAX_HOPS, chatClient } from "../../../packages/contracts/src/chat.ts";
12
13/** One message, as the reply loop reads it, whatever the surface. */
14export type SurfaceMessage = {
15 id: string;
16 author: { kind: "user" | "agent"; id: string; name: string; display_name: string };
17 body: string;
18 /** A card's one-line summary, when the message is a card. */
19 card: string | null;
20 created_at: string;
21};
22
Merge branch 'worktree-agent-a1398e81ad1a64c5f'23/** One member of a conversation, as an agent is told about it. */
24export type ConversationMember = {
25 kind: "user" | "agent";
26 id: string;
27 /** What `@` mentions: a person's username, an agent's handle. */
28 name: string;
29 display_name: string;
30 /** An agent's title, or its role when it has none; null for a person. */
31 title: string | null;
32};
33
34/**
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.35 * Where an agent is answering and who is in it (docs.g1t.sh/guides/agents/,
36 * "Who is in the conversation"): only these members read what it says there.
Merge branch 'worktree-agent-a1398e81ad1a64c5f'37 */
38export type Conversation = {
39 kind: "dm" | "group_dm" | "private_channel" | "public_channel";
40 /** A channel's name; null for a direct message. */
41 name: string | null;
42 /** Every agent, then people up to a cap. */
43 members: ConversationMember[];
44 people: number;
45 agents: number;
46};
47
48/** What handing work to a colleague did. */
49export type HandedOff = { ok: true; where: "here" | "group_dm"; opened: boolean } | { ok: false; message: string };
50
Chat and workspace agents: channels, DMs and named agents you talk to51export interface Surface {
52 /** The latest `limit` messages of the conversation (the thread, or the DM or channel), oldest first. */
53 history(limit: number): Promise<SurfaceMessage[]>;
54 /** Shows the agent typing. Never throws: it is a courtesy. */
55 typing(): Promise<void>;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)56 /**
57 * Posts as the agent, in the conversation it was asked in, optionally as
58 * a card (a consult, say). Returns the new message's id.
59 */
60 post(body: string, card?: MessageCard | null): Promise<string>;
61 /**
62 * Reacts 👀 to the message that woke the agent, so people see it is on
63 * it. Never throws, and skipped where it adds nothing (see `reactsIn`).
64 */
65 acknowledge(): Promise<void>;
66 /**
67 * Once the reply has posted: 👀 becomes ✅ when it answered (`done`), or
68 * is taken away when it posted a notice or an apology (`withdrawn`).
69 * Never throws.
70 */
71 settle(outcome: "done" | "withdrawn"): Promise<void>;
Merge branch 'worktree-agent-a1398e81ad1a64c5f'72 /** The conversation and who is in it; null when it can't be read. Never throws. */
73 conversation(): Promise<Conversation | null>;
74 /**
75 * Hands work to a colleague agent for the person who asked: posted here
76 * when the colleague is in this channel or group DM, otherwise in the
77 * group DM of that person, this agent and the colleague, with a card
78 * here saying where it went. Wakes the colleague and nobody else.
79 */
80 handOff(colleagueId: string, brief: string): Promise<HandedOff>;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)81}
82
Merge branch 'worktree-agent-a1398e81ad1a64c5f'83/** A conversation as the chat service describes it, as the reply loop reads it. */
84export function conversationFrom(value: ConversationForAgent): Conversation {
85 const { channel } = value;
86 const kind =
87 channel.kind === "dm" ? (value.people + value.agents > 2 ? "group_dm" : "dm") : channel.private ? "private_channel" : "public_channel";
88 return {
89 kind,
90 name: channel.kind === "dm" ? null : channel.name,
91 members: value.members.map((m) => ({
92 kind: m.kind,
93 id: m.id,
94 name: m.name,
95 display_name: m.display_name,
96 title: m.kind === "agent" ? m.title || m.role || null : null,
97 })),
98 people: value.people,
99 agents: value.agents,
100 };
101}
102
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)103export const SEEN = "👀";
104export const DONE = "✅";
105
106/**
107 * Whether an agent reacts in a conversation: in channels and in direct
108 * messages with more than one person. In a DM with one person, the typing
109 * indicator says enough.
110 */
111export function reactsIn(kind: "channel" | "dm", people: number): boolean {
112 return kind === "channel" || people > 1;
Chat and workspace agents: channels, DMs and named agents you talk to113}
114
115/** A g1t chat message as the loop reads it. */
116export function fromChat(message: ChatMessage): SurfaceMessage {
117 const card = message.card
118 ? [message.card.kind, message.card.title, message.card.detail, message.card.state].filter(Boolean).join(" · ")
119 : null;
120 return {
121 id: message.id,
122 author: {
123 kind: message.author.kind,
124 id: message.author.id,
125 name: message.author.name,
126 display_name: message.author.display_name,
127 },
128 body: message.deleted_at ? "" : message.body,
129 card,
130 created_at: message.created_at,
131 };
132}
133
134/**
135 * g1t's own chat, over the chat service. Replies stay in the thread they
Merge branch 'worktree-agent-a1398e81ad1a64c5f'136 * were asked in, and carry the delivery's hops, `asked_by` and `asker` on.
137 * A reply wakes nobody, @mentions or not; a hand-off wakes its colleague
138 * one hop further along the same person's request, with that person's
139 * access.
Chat and workspace agents: channels, DMs and named agents you talk to140 */
141export function g1tSurface(chat: ServiceBinding, delivery: AgentDelivery): Surface {
142 const client = chatClient(chat);
143 const { workspace, channel_id: channel, agent_id: agent } = delivery;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)144 // Whether 👀 went on, so `settle` knows what to take back.
145 let acknowledged: Promise<boolean> = Promise.resolve(false);
Chat and workspace agents: channels, DMs and named agents you talk to146 return {
147 async history(limit) {
148 const page = await client.historyForAgent(workspace, channel, agent, { thread_root: delivery.thread_root, limit });
149 if (!page.ok) throw new Error(`reading the conversation failed: ${page.error.message}`);
150 return page.value.filter((message) => !message.deleted_at).map(fromChat);
151 },
152 async typing() {
153 await client.agentTyping(workspace, channel, agent).catch(() => undefined);
154 },
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)155 async acknowledge() {
156 acknowledged = (async () => {
157 try {
A new agent says nothing until you do. Creating one still opens your direct message with it, but no introduction is written there, nothing is charged to its budget and nothing waits in Chat; the desk's special first message, its fixed fallback and the prompt that asked for it are gone. The agents guide says so.158 // A DM's people decide.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)159 if (delivery.channel_kind === "dm") {
160 const audience = await client.audience(workspace, channel);
161 if (!audience.ok || !reactsIn("dm", audience.value.member_count)) return false;
162 }
163 const reacted = await client.reactAsAgent(workspace, channel, agent, delivery.message_id, SEEN);
164 return reacted.ok;
165 } catch {
166 return false;
167 }
168 })();
169 await acknowledged;
170 },
171 async settle(outcome) {
172 try {
173 if (!(await acknowledged)) return;
174 await client.reactAsAgent(workspace, channel, agent, delivery.message_id, SEEN, true);
175 if (outcome === "done") await client.reactAsAgent(workspace, channel, agent, delivery.message_id, DONE);
176 } catch {
177 // A reaction is a courtesy: the reply stands without it.
178 }
179 },
180 async post(body, card = null) {
Chat and workspace agents: channels, DMs and named agents you talk to181 const posted = await client.postAsAgent(workspace, channel, agent, {
182 body,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)183 card,
Chat and workspace agents: channels, DMs and named agents you talk to184 thread_root: delivery.thread_root,
185 hops: Math.min(delivery.hops, CHAT_MAX_HOPS),
186 asked_by: delivery.asked_by,
187 // Agents this reply wakes act for the same person, with their access.
188 asker: delivery.asker ?? null,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)189 // Chat adds this agent, and never hands the post back to the one that sent it the work.
190 chain: delivery.chain ?? [],
Chat and workspace agents: channels, DMs and named agents you talk to191 });
192 if (!posted.ok) throw new Error(`posting the reply failed: ${posted.error.message}`);
193 return posted.value.id;
194 },
Merge branch 'worktree-agent-a1398e81ad1a64c5f'195 async conversation() {
196 try {
197 const found = await client.conversationForAgent(workspace, channel, agent, delivery.asked_by);
198 return found.ok ? conversationFrom(found.value) : null;
199 } catch {
200 return null;
201 }
202 },
203 async handOff(colleagueId, brief) {
204 const done = await client.handOffAsAgent(workspace, channel, agent, {
205 colleague_id: colleagueId,
206 brief,
207 thread_root: delivery.thread_root,
208 hops: Math.min(delivery.hops, CHAT_MAX_HOPS),
209 asked_by: delivery.asked_by,
210 // The colleague works for the same person, with their access.
211 asker: delivery.asker ?? null,
212 chain: delivery.chain ?? [],
213 });
214 return done.ok ? { ok: true, where: done.value.where, opened: done.value.opened } : { ok: false, message: done.error.message };
215 },
Chat and workspace agents: channels, DMs and named agents you talk to216 };
217}
218
219/** The surface a delivery came from. Every delivery is g1t's today. */
220export function surfaceFor(chat: ServiceBinding, delivery: AgentDelivery): Surface {
221 switch (delivery.surface ?? "g1t") {
222 case "g1t":
223 default:
224 return g1tSurface(chat, delivery);
225 }
226}

This file's history is long; its oldest lines are credited to the oldest commit read.