Skip to content
241 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 {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.8 type Result,
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)9 type ServiceBinding,
10 type User,
11 chatClient,
Docs: a workspace knowledge base people and agents write together12 docsClient,
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.13 foliosClient,
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)14 identityClient,
15 reposClient,
16 searchClient,
17 workClient,
18} from "@g1t/contracts";
19
20import type { AudiencePorts, RepoRef } from "./audience.ts";
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.21import type { FolioDone, FoliosPorts, FoundMessage, ToolPorts } from "./tools.ts";
22import { RECALL_LIMIT, passageSource } 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)23
24export type PortsEnv = {
25 DB: D1Database;
26 CHAT: ServiceBinding;
27 IDENTITY: ServiceBinding;
28 REPOS: ServiceBinding;
29 WORK: ServiceBinding;
30 SEARCH: ServiceBinding;
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.31 /** The docs service, for agents reading and writing artifacts; absent on an installation without it. */
Docs: a workspace knowledge base people and agents write together32 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)33};
34
35const refOf = (repo: { id: string; namespace: string; name: string; isPrivate: boolean; defaultBranch: string; forkOf?: string | null }): RepoRef => ({
36 id: repo.id,
37 namespace: repo.namespace,
38 name: repo.name,
39 isPrivate: repo.isPrivate,
40 defaultBranch: repo.defaultBranch,
41 forkOf: repo.forkOf ?? null,
42});
43
44export function audiencePorts(env: PortsEnv, workspace: string, channelId: string): AudiencePorts {
45 const repos = reposClient(env.REPOS);
46 return {
47 async info() {
48 const found = await chatClient(env.CHAT).audience(workspace, channelId);
49 if (!found.ok) throw new Error(`the conversation's audience could not be read: ${found.error.message}`);
50 return found.value;
51 },
52 users: (ids) => identityClient(env.IDENTITY).usersForAudience(ids),
53 workspaceRepos: async (viewer) => (await repos.list(viewer, { namespace: workspace, memberOnly: true })).map(refOf),
54 readable: async (ids, viewer) => (await repos.readable(ids.slice(0, 500), viewer)).map(refOf),
55 };
56}
57
58const found = (m: { channel_id: string; channel: string | null; message: { id: string; author: { name: string }; body: string; created_at: string; card: { title: string } | null } }): FoundMessage => ({
59 channel: m.channel,
60 channel_id: m.channel_id,
61 id: m.message.id,
62 author: m.message.author.name,
63 body: m.message.body || (m.message.card ? `[card: ${m.message.card.title}]` : ""),
64 created_at: m.message.created_at,
65});
66
67export function toolPorts(
68 env: PortsEnv,
69 workspace: string,
70 workspaceId: string,
71 channelId: string,
72 consult: ToolPorts["consult"],
Docs: a workspace knowledge base people and agents write together73 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)74): ToolPorts {
75 const repos = reposClient(env.REPOS);
76 const work = workClient(env.WORK);
77 const path = (repo: RepoRef) => ({ namespace: repo.namespace, name: repo.name });
78 return {
79 async readFile(repo, viewer, ref, file) {
80 const blob = await repos.blob(path(repo), viewer, ref, file);
81 return blob.ok ? { text: blob.value.text, size: blob.value.size } : null;
82 },
83 async searchCode(viewer, query, repo) {
84 const results = await searchClient(env.SEARCH).search(viewer, repo ? `${query} repo:${repo.namespace}/${repo.name}` : query, { type: "code", perPage: 20 });
85 if (!results.ok) return [];
86 return results.value.hits
87 .filter((hit) => hit.kind === "code" && hit.repo && hit.path)
88 .map((hit) => ({
89 repo: hit.repo!,
90 path: hit.path!,
91 snippet: hit.lines.map((line) => `${line.number}: ${line.parts.map((part) => part.text).join("")}`).join("\n"),
92 }));
93 },
94 async listIssues(repo, viewer, state) {
95 const issues = await work.listIssues(path(repo), viewer, { state });
96 return issues.ok ? issues.value.map((i) => ({ number: i.number, title: i.title, state: i.state, labels: i.labels })) : null;
97 },
98 async getIssue(repo, number, viewer) {
99 const detail = await work.getIssue(path(repo), number, viewer);
100 if (!detail.ok) return null;
101 const { issue, comments } = detail.value;
102 return {
103 number: issue.number,
104 title: issue.title,
105 state: issue.state,
106 body: issue.body,
107 comments: comments.filter((c) => c.kind === "comment").slice(-20).map((c) => ({ author: c.author.username, body: c.body })),
108 };
109 },
110 async getPull(repo, number, viewer) {
111 const detail = await work.getPull(path(repo), number, viewer);
112 if (!detail.ok) return null;
113 const { pull, checks } = detail.value;
114 return {
115 number: pull.number,
116 title: pull.title,
117 status: pull.status,
118 body: pull.body ?? "",
119 checks: checks ? `${checks.status}${checks.results.length ? `: ${checks.results.map((r) => `${r.command} ${r.passed ? "passed" : "failed"}`).join(", ")}` : ""}` : null,
120 };
121 },
122 async recentPulls(list, viewer) {
123 const byId = new Map(list.map((repo) => [repo.id, repo]));
124 const groups = await work.pullsForRepos([...byId.keys()], viewer, 5);
125 return groups
126 .flatMap((group) => [...group.open, ...group.closed].map((pull) => ({ pull, repo: byId.get(group.repoId) })))
127 .filter((entry) => entry.repo)
128 .map(({ pull, repo }) => ({
129 repo: `${repo!.namespace}/${repo!.name}`,
130 number: pull.number,
131 title: pull.title,
132 status: pull.status,
133 updated_at: pull.mergedAt ?? pull.updatedAt,
134 }))
135 .sort((a, b) => b.updated_at.localeCompare(a.updated_at))
136 .slice(0, 25);
137 },
138 async searchMessages(query) {
139 const result = await chatClient(env.CHAT).searchForAgent(workspace, channelId, query, 20);
140 return result.ok ? result.value.map(found) : null;
141 },
142 async readThread(target, id) {
143 const result = await chatClient(env.CHAT).threadForAgent(workspace, channelId, target, id);
144 return result.ok ? result.value.map(found) : null;
145 },
146 async roster(viewer: User | null) {
147 const identity = identityClient(env.IDENTITY);
148 const [members, teams, agents] = await Promise.all([
149 viewer ? identity.listMembers(workspace, viewer).catch(() => null) : Promise.resolve(null),
150 viewer ? identity.listTeams(viewer, workspace).catch(() => null) : Promise.resolve(null),
151 env.DB.prepare("SELECT handle, display_name, title, role FROM agents WHERE workspace_id = ? AND archived_at IS NULL ORDER BY builtin DESC, handle")
152 .bind(workspaceId)
153 .all<{ handle: string; display_name: string; title: string; role: string }>(),
154 ]);
155 const people = members?.ok ? members.value.map((m) => `- @${m.username}${m.name ? ` (${m.name})` : ""}, ${m.role}`).join("\n") : "(people could not be listed)";
156 const teamLines = teams?.ok ? teams.value.map((t) => `- ${t.slug}: ${t.name}${t.description ? `, ${t.description}` : ""}`).join("\n") : "";
157 const agentLines = agents.results.map((a) => `- @${a.handle} (${a.display_name}): ${a.title || a.role}`).join("\n");
158 return `People:\n${people}${teamLines ? `\n\nTeams:\n${teamLines}` : ""}\n\nAgents:\n${agentLines}`;
159 },
160 consult,
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.161 ...(env.DOCS && agentId ? { folios: folioPorts(env.DOCS, env.CHAT, workspace, agentId) } : {}),
Docs: a workspace knowledge base people and agents write together162 };
163}
164
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.165/** A call's result as the tools take it: the value, or the error's code and sentence. */
166const done = <T>(result: Result<T>): FolioDone<T> => (result.ok ? result : { ok: false, code: result.error.code, message: result.error.message });
167
168/**
169 * Artifacts (folios) as an agent reads and writes them: Markdown in,
170 * Markdown out, every call checked by the docs service. Spaces still come
171 * from the docs service's `spaces_for_agent`: spaces are shared by pages
172 * and folios, and have no folio method of their own.
173 */
174function folioPorts(docs: ServiceBinding, chatBinding: ServiceBinding, workspace: string, agentId: string): FoliosPorts {
175 const folios = foliosClient(docs);
176 const where = (f: { title: string; path: string; id: string }) => `${f.title} (${f.path}, id ${f.id})`;
Docs: a workspace knowledge base people and agents write together177 return {
178 async spaces(viewer, audience) {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.179 const found = await docsClient(docs).spacesForAgent(workspace, agentId, viewer, audience);
Docs: a workspace knowledge base people and agents write together180 if (!found.ok) return null;
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.181 return found.value.map((s) => ({ id: s.id, slug: s.slug, name: s.name, description: s.description, kind: s.kind, projects: s.projects ?? [], can: s.can }));
Docs: a workspace knowledge base people and agents write together182 },
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.183 async recall(viewer, audience, query, spaces, kinds) {
184 const found = await folios.recallForAgent(workspace, agentId, viewer, { query, limit: RECALL_LIMIT, spaces, kinds: kinds ?? null }, audience);
Agents recall what Docs say before they answer or work, and each has required reading185 return found.ok ? found.value : null;
186 },
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.187 async search(viewer, audience, input) {
188 const kinds = input.kind ? [input.kind] : null;
189 // Passages too, when the search isn't narrowed to a space or project: recall can't be.
190 const [list, passages] = await Promise.all([
191 folios.foliosForAgent(workspace, agentId, viewer, { tab: "all", q: input.query, kinds, space_id: input.space_id, project: input.project, limit: 10 }, audience),
192 input.space_id || input.project ? Promise.resolve(null) : folios.recallForAgent(workspace, agentId, viewer, { query: input.query, limit: 4, kinds }, audience).catch(() => null),
193 ]);
194 if (!list.ok) return null;
195 const lines = list.value.items.map((f) => {
196 const space = f.space ? `in ${f.space.name}` : "not in a space";
197 return `- ${where(f)}: a ${f.kind} ${space}, edited ${f.edited_at.slice(0, 10)}${f.stale ? ", possibly out of date" : ""}${f.excerpt ? `: ${f.excerpt.replace(/\s+/g, " ").slice(0, 300)}` : ""}`;
198 });
199 const found = passages?.ok ? passages.value.map((p) => `### ${passageSource(p)}\n${p.text.trim().slice(0, 800)}`) : [];
200 if (!lines.length && !found.length) return "No artifacts found.";
201 return [lines.length ? lines.join("\n") : "No artifacts matched by title or words.", ...(found.length ? ["", "Passages that match:", "", found.join("\n\n")] : [])].join("\n");
Docs: a workspace knowledge base people and agents write together202 },
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.203 read: async (viewer, audience, folioId) => done(await folios.readForAgent(workspace, agentId, viewer, folioId, audience)),
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store204 async stale(viewer, audience, repo) {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.205 const found = await folios.staleForAgent(workspace, agentId, viewer, { repo }, audience);
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store206 if (!found.ok) return null;
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.207 if (!found.value.length) return "No artifacts are marked possibly out of date.";
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store208 return found.value
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.209 .map((f) => {
210 const can = f.viewer_role === "edit" || f.viewer_role === "manage" ? (f.agent_mode === "edit" ? "you can edit" : "you can suggest") : f.viewer_role === "comment" ? "you can suggest" : "read only";
211 return `- ${where(f)}, a ${f.kind} ${f.space ? `in ${f.space.name}` : "not in a space"} (${can}), edited ${f.edited_at.slice(0, 10)}`;
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store212 })
213 .join("\n");
214 },
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.215 async create(viewer, input) {
216 const made = await folios.createAsAgent(workspace, agentId, viewer, {
217 kind: input.kind,
218 title: input.title,
219 content: input.markdown === null ? null : { markdown: input.markdown },
220 template_id: input.template_id,
221 where: input.where,
222 parent_id: input.parent_id,
223 source: input.source,
224 });
225 return done(made);
226 },
227 edit: async (viewer, folioId, edit) => done(await folios.editAsAgent(workspace, agentId, viewer, folioId, edit)),
228 async share(viewer, audience, folioId, userIds, role) {
229 const shared = await folios.shareAsAgent(workspace, agentId, viewer, folioId, { user_ids: userIds, role }, audience);
230 return shared.ok ? { ok: true, value: null } : done(shared);
Docs: a workspace knowledge base people and agents write together231 },
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.232 async sendLink(asker, link, note) {
233 const chat = chatClient(chatBinding);
234 const dm = await chat.openDm(workspace, asker, [{ kind: "agent", id: agentId }]);
235 if (!dm.ok) return false;
236 // No mention, so it wakes nobody.
237 const posted = await chat.postAsAgent(workspace, dm.value.id, agentId, { body: `${note} [${link.title.replace(/[[\]]/g, "")}](${link.path})`, asked_by: asker.id });
238 return posted.ok;
Docs: a workspace knowledge base people and agents write together239 },
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)240 };
241}

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