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

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)1/**
2 * The reply's audience and tools, wired to g1t's services. Each call here
3 * runs only after `Audience` and `ToolBox` decided it may (audience.ts,
4 * tools.ts); the backing services check the viewer again, as they do for
5 * every request.
6 */
7import {
8 type ServiceBinding,
9 type User,
Docs: a workspace knowledge base people and agents write together10 type DocPageRef,
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)11 chatClient,
Docs: a workspace knowledge base people and agents write together12 docsClient,
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)13 identityClient,
14 reposClient,
15 searchClient,
16 workClient,
17} from "@g1t/contracts";
18
19import type { AudiencePorts, RepoRef } from "./audience.ts";
Docs: a workspace knowledge base people and agents write together20import type { DocsPorts, FoundMessage, ToolPorts } from "./tools.ts";
Agents recall what Docs say before they answer or work, and each has required reading21import { RECALL_LIMIT } from "./recall.ts";
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)22
23export type PortsEnv = {
24 DB: D1Database;
25 CHAT: ServiceBinding;
26 IDENTITY: ServiceBinding;
27 REPOS: ServiceBinding;
28 WORK: ServiceBinding;
29 SEARCH: ServiceBinding;
Docs: a workspace knowledge base people and agents write together30 /** Docs, for agents reading and writing pages; absent on an installation without it. */
31 DOCS?: ServiceBinding;
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)32};
33
34const refOf = (repo: { id: string; namespace: string; name: string; isPrivate: boolean; defaultBranch: string; forkOf?: string | null }): RepoRef => ({
35 id: repo.id,
36 namespace: repo.namespace,
37 name: repo.name,
38 isPrivate: repo.isPrivate,
39 defaultBranch: repo.defaultBranch,
40 forkOf: repo.forkOf ?? null,
41});
42
43export function audiencePorts(env: PortsEnv, workspace: string, channelId: string): AudiencePorts {
44 const repos = reposClient(env.REPOS);
45 return {
46 async info() {
47 const found = await chatClient(env.CHAT).audience(workspace, channelId);
48 if (!found.ok) throw new Error(`the conversation's audience could not be read: ${found.error.message}`);
49 return found.value;
50 },
51 users: (ids) => identityClient(env.IDENTITY).usersForAudience(ids),
52 workspaceRepos: async (viewer) => (await repos.list(viewer, { namespace: workspace, memberOnly: true })).map(refOf),
53 readable: async (ids, viewer) => (await repos.readable(ids.slice(0, 500), viewer)).map(refOf),
54 };
55}
56
57const found = (m: { channel_id: string; channel: string | null; message: { id: string; author: { name: string }; body: string; created_at: string; card: { title: string } | null } }): FoundMessage => ({
58 channel: m.channel,
59 channel_id: m.channel_id,
60 id: m.message.id,
61 author: m.message.author.name,
62 body: m.message.body || (m.message.card ? `[card: ${m.message.card.title}]` : ""),
63 created_at: m.message.created_at,
64});
65
66export function toolPorts(
67 env: PortsEnv,
68 workspace: string,
69 workspaceId: string,
70 channelId: string,
71 consult: ToolPorts["consult"],
Docs: a workspace knowledge base people and agents write together72 agentId: string | null = 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)73): ToolPorts {
74 const repos = reposClient(env.REPOS);
75 const work = workClient(env.WORK);
76 const path = (repo: RepoRef) => ({ namespace: repo.namespace, name: repo.name });
77 return {
78 async readFile(repo, viewer, ref, file) {
79 const blob = await repos.blob(path(repo), viewer, ref, file);
80 return blob.ok ? { text: blob.value.text, size: blob.value.size } : null;
81 },
82 async searchCode(viewer, query, repo) {
83 const results = await searchClient(env.SEARCH).search(viewer, repo ? `${query} repo:${repo.namespace}/${repo.name}` : query, { type: "code", perPage: 20 });
84 if (!results.ok) return [];
85 return results.value.hits
86 .filter((hit) => hit.kind === "code" && hit.repo && hit.path)
87 .map((hit) => ({
88 repo: hit.repo!,
89 path: hit.path!,
90 snippet: hit.lines.map((line) => `${line.number}: ${line.parts.map((part) => part.text).join("")}`).join("\n"),
91 }));
92 },
93 async listIssues(repo, viewer, state) {
94 const issues = await work.listIssues(path(repo), viewer, { state });
95 return issues.ok ? issues.value.map((i) => ({ number: i.number, title: i.title, state: i.state, labels: i.labels })) : null;
96 },
97 async getIssue(repo, number, viewer) {
98 const detail = await work.getIssue(path(repo), number, viewer);
99 if (!detail.ok) return null;
100 const { issue, comments } = detail.value;
101 return {
102 number: issue.number,
103 title: issue.title,
104 state: issue.state,
105 body: issue.body,
106 comments: comments.filter((c) => c.kind === "comment").slice(-20).map((c) => ({ author: c.author.username, body: c.body })),
107 };
108 },
109 async getPull(repo, number, viewer) {
110 const detail = await work.getPull(path(repo), number, viewer);
111 if (!detail.ok) return null;
112 const { pull, checks } = detail.value;
113 return {
114 number: pull.number,
115 title: pull.title,
116 status: pull.status,
117 body: pull.body ?? "",
118 checks: checks ? `${checks.status}${checks.results.length ? `: ${checks.results.map((r) => `${r.command} ${r.passed ? "passed" : "failed"}`).join(", ")}` : ""}` : null,
119 };
120 },
121 async recentPulls(list, viewer) {
122 const byId = new Map(list.map((repo) => [repo.id, repo]));
123 const groups = await work.pullsForRepos([...byId.keys()], viewer, 5);
124 return groups
125 .flatMap((group) => [...group.open, ...group.closed].map((pull) => ({ pull, repo: byId.get(group.repoId) })))
126 .filter((entry) => entry.repo)
127 .map(({ pull, repo }) => ({
128 repo: `${repo!.namespace}/${repo!.name}`,
129 number: pull.number,
130 title: pull.title,
131 status: pull.status,
132 updated_at: pull.mergedAt ?? pull.updatedAt,
133 }))
134 .sort((a, b) => b.updated_at.localeCompare(a.updated_at))
135 .slice(0, 25);
136 },
137 async searchMessages(query) {
138 const result = await chatClient(env.CHAT).searchForAgent(workspace, channelId, query, 20);
139 return result.ok ? result.value.map(found) : null;
140 },
141 async readThread(target, id) {
142 const result = await chatClient(env.CHAT).threadForAgent(workspace, channelId, target, id);
143 return result.ok ? result.value.map(found) : null;
144 },
145 async roster(viewer: User | null) {
146 const identity = identityClient(env.IDENTITY);
147 const [members, teams, agents] = await Promise.all([
148 viewer ? identity.listMembers(workspace, viewer).catch(() => null) : Promise.resolve(null),
149 viewer ? identity.listTeams(viewer, workspace).catch(() => null) : Promise.resolve(null),
150 env.DB.prepare("SELECT handle, display_name, title, role FROM agents WHERE workspace_id = ? AND archived_at IS NULL ORDER BY builtin DESC, handle")
151 .bind(workspaceId)
152 .all<{ handle: string; display_name: string; title: string; role: string }>(),
153 ]);
154 const people = members?.ok ? members.value.map((m) => `- @${m.username}${m.name ? ` (${m.name})` : ""}, ${m.role}`).join("\n") : "(people could not be listed)";
155 const teamLines = teams?.ok ? teams.value.map((t) => `- ${t.slug}: ${t.name}${t.description ? `, ${t.description}` : ""}`).join("\n") : "";
156 const agentLines = agents.results.map((a) => `- @${a.handle} (${a.display_name}): ${a.title || a.role}`).join("\n");
157 return `People:\n${people}${teamLines ? `\n\nTeams:\n${teamLines}` : ""}\n\nAgents:\n${agentLines}`;
158 },
159 consult,
Docs: a workspace knowledge base people and agents write together160 ...(env.DOCS && agentId ? { docs: docsPorts(env.DOCS, workspace, agentId) } : {}),
161 };
162}
163
164/** Docs as an agent reads and writes them: Markdown in, Markdown out, every call checked by the docs service. */
165function docsPorts(binding: ServiceBinding, workspace: string, agentId: string): DocsPorts {
166 const docs = docsClient(binding);
167 const where = (page: DocPageRef) => `${page.title} (${page.path}, id ${page.id})`;
168 return {
169 async spaces(viewer, audience) {
170 const found = await docs.spacesForAgent(workspace, agentId, viewer, audience);
171 if (!found.ok) return null;
172 if (!found.value.length) return "There are no Docs spaces everyone here can read.";
173 return found.value
174 .map((s) => {
175 const can = s.can.edit ? "you can edit" : s.can.suggest ? "you can suggest edits" : "read only";
176 const projects = s.projects?.length ? `; about ${s.projects.join(", ")}` : "";
177 return `- ${s.name} (id ${s.id}, ${s.kind}; ${can}${projects})${s.description ? `: ${s.description}` : ""}`;
178 })
179 .join("\n");
180 },
Agents recall what Docs say before they answer or work, and each has required reading181 async recall(viewer, audience, query, spaces) {
182 const found = await docs.recallForAgent(workspace, agentId, viewer, { query, limit: RECALL_LIMIT, spaces }, audience);
183 return found.ok ? found.value : null;
184 },
Docs: a workspace knowledge base people and agents write together185 async search(viewer, audience, query, project) {
186 const found = await docs.searchForAgent(workspace, agentId, viewer, { query, project, limit: 10 }, audience);
187 if (!found.ok) return null;
188 if (!found.value.length) return "No pages found.";
189 return found.value.map((hit) => `- ${where(hit)} in ${hit.space_name}, updated ${hit.updated_at.slice(0, 10)}: ${hit.snippet.replace(/\[\[|\]\]/g, "")}`).join("\n");
190 },
191 async read(viewer, audience, pageId) {
192 const found = await docs.pageMarkdown(workspace, agentId, viewer, pageId, audience);
193 if (!found.ok) return null;
194 const p = found.value;
195 const can = p.can.edit ? "you can edit it" : p.can.suggest ? "you can suggest edits" : "you can only read it";
196 const blocks = p.blocks.map((b) => `${b.id} ${b.type}${b.level ? ` ${b.level}` : ""}`).join(", ");
197 return `# ${where(p.page)}\nSpace: ${p.space.name}; ${can}. Updated ${p.page.updated_at.slice(0, 16)}.\nTop-level blocks: ${blocks}\n\n${p.markdown}`;
198 },
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store199 async stale(viewer, audience, repo) {
200 const found = await docs.stalePagesForAgent(workspace, agentId, viewer, { repo }, audience);
201 if (!found.ok) return null;
202 if (!found.value.length) return "No pages are marked possibly out of date.";
203 return found.value
204 .map((s) => {
205 const changes = s.changes
206 .filter((c) => c.visible)
207 .slice(0, 3)
208 .map((c) => `${c.repo}${c.pull ? `#${c.pull.number}${c.pull.title ? ` (${c.pull.title})` : ""}` : `@${String(c.commit ?? "").slice(0, 8)}`} changed ${c.paths.slice(0, 5).join(", ")}`)
209 .join("; ");
210 const can = s.can.edit ? "you can edit" : s.can.suggest ? "you can suggest" : "read only";
211 return `- ${where(s.page)} (${can}), since ${s.since.slice(0, 10)}: ${changes}`;
212 })
213 .join("\n");
214 },
Docs: a workspace knowledge base people and agents write together215 async edit(viewer, pageId, edit, suggestOnly) {
216 const done = suggestOnly
217 ? await docs.suggestEdit(workspace, agentId, viewer, pageId, edit).then((r) => (r.ok ? { ok: true as const, value: { mode: "suggested" as const, suggestion: r.value, page: null } } : r))
218 : await docs.applyEdit(workspace, agentId, viewer, pageId, edit);
219 if (!done.ok) return { ok: false, message: `That didn't work: ${done.error.message}` };
220 const v = done.value;
221 const page = v.page ? ` on ${where(v.page)}` : "";
222 return v.mode === "applied"
223 ? { ok: true, message: `Changed${page}. It's in the page's history as yours.` }
224 : { ok: true, message: `Suggested${page}: people accept or reject it on the page. Link the page so they can.` };
225 },
226 async create(viewer, input) {
227 const made = await docs.createPageAsAgent(workspace, agentId, viewer, input);
228 if (!made.ok) return { ok: false, message: `The page couldn't be made: ${made.error.message}` };
229 return { ok: true, message: `Wrote ${where(made.value)}. Link it.` };
230 },
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)231 };
232}

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