Skip to content
997 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 * What an agent can read while it replies: code, issues, pull requests,
3 * chat, the roster, and its colleagues (docs/WORKSPACE.md, "What an agent
4 * can and can't know", "Agents know each other").
5 *
6 * Every tool goes through the reply's `Audience` before it reads
7 * anything, and the check is here, in code:
8 * - code tools are offered only when the audience may read code at all,
9 * and a repository is used only when it is on the audience's allow-list;
10 * - chat tools ask the chat service, which works out the audience from the
11 * conversation itself;
12 * - what the audience may not see comes back as one neutral line,
13 * `WITHHELD`, the same for a thing that is private and a thing that does
14 * not exist, and never names it.
15 *
16 * Whatever a tool returns is wrapped as untrusted data: text in files,
17 * issues and messages is never an instruction to the agent.
18 *
19 * Pure apart from its ports, so the rules are tested adversarially.
20 */
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 { DocEditTarget, FolioAgentEdit, FolioAgentEditResult, FolioAgentRead, FolioAudience, FolioKind, FolioPassage, FolioRef, User } from "@g1t/contracts";
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
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.23import { FOLIO_KINDS, folioIdFrom, isFolioKind } from "../../../packages/contracts/src/folios.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)24import { type Audience, type RepoRef, WITHHELD } from "./audience.ts";
25
26/** One tool, as the Messages API takes it. */
27export type ToolDef = { name: string; description: string; input_schema: Record<string, unknown> };
28
29export type FoundMessage = { channel: string | null; channel_id: string; id: string; author: string; body: string; created_at: string };
30
31/** What the tools reach outside this module. */
32export interface ToolPorts {
33 readFile(repo: RepoRef, viewer: User, ref: string, path: string): Promise<{ text: string | null; size: number } | null>;
34 searchCode(viewer: User, query: string, repo: RepoRef | null): Promise<{ repo: string; path: string; snippet: string }[]>;
35 listIssues(repo: RepoRef, viewer: User, state: "open" | "closed"): Promise<{ number: number; title: string; state: string; labels: string[] }[] | null>;
36 getIssue(repo: RepoRef, number: number, viewer: User): Promise<{ number: number; title: string; state: string; body: string; comments: { author: string; body: string }[] } | null>;
37 getPull(repo: RepoRef, number: number, viewer: User): Promise<{ number: number; title: string; status: string; body: string; checks: string | null } | null>;
38 recentPulls(repos: RepoRef[], viewer: User): Promise<{ repo: string; number: number; title: string; status: string; updated_at: string }[]>;
39 /** Chat's own audience rule applies; null when the search failed. */
40 searchMessages(query: string): Promise<FoundMessage[] | null>;
41 /** Null when the audience may not read it (or it does not exist). */
42 readThread(channelId: string, id: string): Promise<FoundMessage[] | null>;
43 roster(viewer: User | null): Promise<string>;
44 consult(handle: string, question: string): Promise<{ ok: true; colleague: string; answer: string } | { ok: false; message: string }>;
Docs: a workspace knowledge base people and agents write together45 /**
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.46 * The workspace's artifacts (Artifacts mode), as the docs service lets
47 * this agent use them for the person it acts for and everyone who will
48 * read the answer. Absent where there is no docs service.
Docs: a workspace knowledge base people and agents write together49 */
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.50 folios?: FoliosPorts;
Docs: a workspace knowledge base people and agents write together51}
52
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.53/** A space as an agent sees it, with what it may do there for the person it acts for. */
54export type FolioSpaceLine = {
55 id: string;
56 slug: string;
57 name: string;
58 description: string | null;
59 kind: string;
60 projects: string[];
61 can: { read: boolean; suggest: boolean; edit: boolean };
62};
63
64/** Where a new artifact goes: a space, its asker's Private, or Private shared with the conversation's people. */
65export type FolioWhere = { space_id: string } | "private" | { conversation: string[] };
66
67/** A call's answer: the value, or the docs service's error code and sentence. */
68export type FolioDone<T> = { ok: true; value: T } | { ok: false; code: string; message: string };
69
70/**
71 * Artifacts, as an agent uses them. Every call names the person it acts
72 * for, and the reads also who reads the answer; the docs service checks
73 * both.
74 */
75export interface FoliosPorts {
76 /** Spaces everyone here can read; null when the docs service couldn't answer. */
77 spaces(viewer: User, audience: FolioAudience): Promise<FolioSpaceLine[] | null>;
78 /** Passages closest in meaning to `query`; `spaces` (required reading) first. Null when the docs service couldn't answer. */
79 recall(viewer: User, audience: FolioAudience, query: string, spaces: string[], kinds?: FolioKind[]): Promise<FolioPassage[] | null>;
80 /** Artifacts matching `query` (words and meaning), as lines with links. */
81 search(viewer: User, audience: FolioAudience, input: { query: string; kind: FolioKind | null; space_id: string | null; project: string | null }): Promise<string | null>;
82 read(viewer: User, audience: FolioAudience, folioId: string): Promise<FolioDone<FolioAgentRead>>;
83 /** Artifacts possibly out of date since code they cite changed. */
84 stale(viewer: User, audience: FolioAudience, repo: string | null): Promise<string | null>;
85 create(
86 viewer: User,
87 input: { kind: FolioKind; title: string; markdown: string | null; template_id: string | null; where: FolioWhere; parent_id: string | null; source: { title: string; href: string } | null },
88 ): Promise<FolioDone<FolioRef>>;
89 edit(viewer: User, folioId: string, edit: FolioAgentEdit): Promise<FolioDone<FolioAgentEditResult>>;
90 /** `view` or `comment` for people already in this conversation. */
91 share(viewer: User, audience: FolioAudience, folioId: string, userIds: string[], role: "view" | "comment"): Promise<FolioDone<null>>;
92 /** Sends the asker a link directly, as a message from the agent in their DM with it; false when it couldn't. */
93 sendLink(asker: User, link: { title: string; path: string }, note: string): Promise<boolean>;
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)94}
95
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked96/**
97 * What an agent may do, beyond reading: remember, file an issue for the
98 * person who asked, and start or shape work. Each is checked here before it
99 * runs (the audience, the asker, the hop limit) and again by the service
100 * that does it.
101 */
102export interface ActionPorts {
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.103 /**
104 * `onlyForAsker`: this turn read an artifact the whole workspace can't
105 * read, so the fact is kept for the person who asked alone.
106 */
107 remember(body: string, scope: "workspace" | "channel" | "person" | null, onlyForAsker?: boolean): Promise<{ ok: boolean; message: string }>;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked108 forget(id: string): Promise<{ ok: boolean; message: string }>;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar109 /**
110 * Posts a draft issue as a card in the conversation, with File issue and
111 * Discard: whoever presses File files it as themselves, if they can read
112 * the repository. Nothing is filed by the agent.
113 */
114 draftIssue(repo: RepoRef, input: { title: string; body: string; labels: string[] }): Promise<{ ok: boolean; message: string }>;
115 /**
116 * Comments on an issue or pull request, or reviews a pull request, as the
117 * agent on behalf of the person who asked. Reviews are advisory: they
118 * never count toward required approvals.
119 */
120 comment?(repo: RepoRef, asker: User, number: number, body: string): Promise<{ ok: boolean; message: string }>;
121 review?(repo: RepoRef, asker: User, number: number, verdict: "comment" | "approve" | "request_changes", body: string): Promise<{ ok: boolean; message: string }>;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked122 /** From chat: spins off a session for real work. */
123 startSession?(title: string, goal: string): Promise<{ ok: boolean; message: string }>;
124 /** In a session: a short progress note in its thread. */
125 postUpdate?(text: string): Promise<{ ok: boolean; message: string }>;
126 /** In a session: one of the agent's own subagents takes part of the work. */
127 useSubagent?(name: string, brief: string): Promise<{ ok: boolean; message: string }>;
128 /** In a session: a colleague works on part of it, paid from this session's budget. */
129 bringIn?(handle: string, brief: string): Promise<{ ok: boolean; message: string }>;
Workspace agents are told where they are and who is in the conversation every turn, and hand work to a colleague with a hand_off tool instead of an @mention: the brief is posted where the colleague already is, or in a group message of the person who asked, the agent and the colleague with a card linking to it; an agent's messages no longer wake other agents, its mentions of anyone outside the conversation show as plain names, and the agents and chat guides and the workspace plan say how.130 /** From chat: a colleague takes the work on for the person who asked, here or in a group message. */
131 handOff?(handle: string, brief: string): Promise<{ ok: boolean; message: string }>;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked132}
133
Workspace agents are told where they are and who is in the conversation every turn, and hand work to a colleague with a hand_off tool instead of an @mention: the brief is posted where the colleague already is, or in a group message of the person who asked, the agent and the colleague with a card linking to it; an agent's messages no longer wake other agents, its mentions of anyone outside the conversation show as plain names, and the agents and chat guides and the workspace plan say how.134/** The most hand-offs one reply makes. */
135export const MAX_HAND_OFFS = 2;
136
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)137/** The most tool calls one reply makes. */
138export const MAX_TOOL_CALLS = 8;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked139/** The most tool calls one step of a session makes. */
140export const MAX_SESSION_TOOL_CALLS = 24;
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)141/** The most of a file or result an answer is given, in characters. */
142const MAX_RESULT = 20_000;
143
144export type ToolCall = {
145 tool: string;
146 /** Its arguments, with long text cut, as recorded. */
147 args: string;
148 outcome: "allowed" | "withheld" | "refused" | "error";
149 bytes: number;
150};
151
152export type ToolResult = { text: string; outcome: ToolCall["outcome"] };
153
154/**
155 * Text a tool read, marked as data. Anything in it that looks like the
156 * closing mark is defused, so content can't end the block early.
157 */
158export function untrusted(source: string, content: string): string {
159 const safe = (text: string) => text.replace(/<\/?untrusted/gi, (mark) => mark.replace("<", "&lt;"));
160 const body = content.length > MAX_RESULT ? `${content.slice(0, MAX_RESULT)}\n[cut: ${content.length - MAX_RESULT} more characters]` : content;
161 return `<untrusted source="${safe(source).replace(/"/g, "'")}">\n${safe(body)}\n</untrusted>`;
162}
163
164/** Arguments as recorded: every string cut to 120 characters. */
165export function redact(args: unknown): string {
166 const cut = (value: unknown): unknown => {
167 if (typeof value === "string") return value.length > 120 ? `${value.slice(0, 120)}…` : value;
168 if (Array.isArray(value)) return value.slice(0, 10).map(cut);
169 if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).slice(0, 10).map(([k, v]) => [k, cut(v)]));
170 return value;
171 };
172 return JSON.stringify(cut(args ?? {})).slice(0, 1000);
173}
174
175const CODE_TOOLS: ToolDef[] = [
176 {
177 name: "list_repositories",
178 description: "The workspace's repositories everyone in this conversation can read. Start here to know what you can look at.",
179 input_schema: { type: "object", properties: {} },
180 },
181 {
182 name: "search_code",
183 description: "Search code on default branches. Optionally only in one repository (`name` or `workspace/name`).",
184 input_schema: { type: "object", properties: { query: { type: "string" }, repo: { type: "string" } }, required: ["query"] },
185 },
186 {
187 name: "read_file",
188 description: "Read a file from a repository, at its default branch or a ref.",
189 input_schema: { type: "object", properties: { repo: { type: "string" }, path: { type: "string" }, ref: { type: "string" } }, required: ["repo", "path"] },
190 },
191 {
192 name: "list_issues",
193 description: "A repository's newest issues, open by default.",
194 input_schema: { type: "object", properties: { repo: { type: "string" }, state: { type: "string", enum: ["open", "closed"] } }, required: ["repo"] },
195 },
196 {
197 name: "get_issue",
198 description: "One issue with its comments.",
199 input_schema: { type: "object", properties: { repo: { type: "string" }, number: { type: "integer" } }, required: ["repo", "number"] },
200 },
201 {
202 name: "get_pull",
203 description: "One pull request: what it changes, its status and checks.",
204 input_schema: { type: "object", properties: { repo: { type: "string" }, number: { type: "integer" } }, required: ["repo", "number"] },
205 },
206 {
207 name: "recent_activity",
208 description: "Recently merged and open pull requests, in one repository or across those you can read.",
209 input_schema: { type: "object", properties: { repo: { type: "string" } } },
210 },
211];
212
213const CHAT_TOOLS: ToolDef[] = [
214 {
215 name: "search_messages",
216 description: "Search chat messages this conversation's people can all read.",
217 input_schema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
218 },
219 {
220 name: "read_thread",
221 description: "Read a chat thread by its channel id and a message id in it (from search_messages).",
222 input_schema: { type: "object", properties: { channel: { type: "string" }, id: { type: "string" } }, required: ["channel", "id"] },
223 },
224 {
225 name: "workspace_roster",
226 description: "The workspace's people and agents: names, teams, titles and roles.",
227 input_schema: { type: "object", properties: {} },
228 },
229];
230
231const ASK_COLLEAGUE: ToolDef = {
232 name: "ask_colleague",
233 description:
Workspace agents are told where they are and who is in the conversation every turn, and hand work to a colleague with a hand_off tool instead of an @mention: the brief is posted where the colleague already is, or in a group message of the person who asked, the agent and the colleague with a card linking to it; an agent's messages no longer wake other agents, its mentions of anyone outside the conversation show as plain names, and the agents and chat guides and the workspace plan say how.234 "Ask a colleague agent a quick question, privately: their answer comes back to you alone, they do no work in the conversation, and the work stays yours. Use it when their role knows something yours doesn't. To give them the work itself, use hand_off.",
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)235 input_schema: { type: "object", properties: { handle: { type: "string" }, question: { type: "string" } }, required: ["handle", "question"] },
236};
237
Workspace agents are told where they are and who is in the conversation every turn, and hand work to a colleague with a hand_off tool instead of an @mention: the brief is posted where the colleague already is, or in a group message of the person who asked, the agent and the colleague with a card linking to it; an agent's messages no longer wake other agents, its mentions of anyone outside the conversation show as plain names, and the agents and chat guides and the workspace plan say how.238const HAND_OFF: ToolDef = {
239 name: "hand_off",
240 description:
241 "Hand work to a colleague agent, for the person who asked: they take it on and answer that person themselves, with that person's access. If they are a member of this conversation (a channel or group message), your brief is posted here; otherwise a group message opens (or is reused) with the person who asked, you and them, your brief goes there, and a card here links to it. It is the only way to get a colleague working: an @mention in your message wakes nobody. Give their handle and a complete brief written to them: what is wanted, why, what done looks like, and what they need from this conversation. For a quick question you answer with, use ask_colleague instead.",
242 input_schema: { type: "object", properties: { handle: { type: "string" }, brief: { type: "string" } }, required: ["handle", "brief"] },
243};
244
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked245const REMEMBER: ToolDef = {
246 name: "remember",
247 description:
248 "Keep a short fact for later work: a preference, a decision, who owns what, how something works here. One fact per call, in your own words. It is kept where this conversation allows (this person, this conversation, or the workspace from a public channel), with this conversation as its source. Never keep secrets, credentials or customers' personal data.",
249 input_schema: {
250 type: "object",
251 properties: { fact: { type: "string" }, scope: { type: "string", enum: ["workspace", "channel", "person"] } },
252 required: ["fact"],
253 },
254};
255
256const FORGET: ToolDef = {
257 name: "forget",
258 description: "Forget one of the notes under 'What you remember', by its id, when it is wrong or out of date.",
259 input_schema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
260};
261
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar262const DRAFT_ISSUE: ToolDef = {
263 name: "draft_issue",
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked264 description:
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar265 "Draft an issue (a bug report or a feature request) for a repository with what you found. It appears in the conversation as a card with File issue and Discard buttons: the person files it themselves with one press, so don't ask them to confirm in words. Write it for the team that will fix it: what happens, what should happen, steps or evidence, and where in the code it likely is.",
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked266 input_schema: {
267 type: "object",
268 properties: {
269 repo: { type: "string" },
270 title: { type: "string" },
271 body: { type: "string" },
272 labels: { type: "array", items: { type: "string" } },
273 },
274 required: ["repo", "title", "body"],
275 },
276};
277
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar278const COMMENT: ToolDef = {
279 name: "comment",
280 description:
281 "Comment on an issue or pull request, as yourself on behalf of the person you're working for. Use it when the comment belongs on the issue or pull request (findings, a test plan, a question for its author), not for chatting.",
282 input_schema: {
283 type: "object",
284 properties: { repo: { type: "string" }, number: { type: "integer" }, body: { type: "string" } },
285 required: ["repo", "number", "body"],
286 },
287};
288
289const REVIEW_PULL: ToolDef = {
290 name: "review_pull",
291 description:
292 "Review a pull request on the pull request itself, as yourself on behalf of the person you're working for: approve, request changes, or just comment, with your review in the body. Your review is advisory: people still give the approvals a merge needs. Read the change first.",
293 input_schema: {
294 type: "object",
295 properties: {
296 repo: { type: "string" },
297 number: { type: "integer" },
298 verdict: { type: "string", enum: ["comment", "approve", "request_changes"] },
299 body: { type: "string" },
300 },
301 required: ["repo", "number", "verdict", "body"],
302 },
303};
304
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked305const START_SESSION: ToolDef = {
306 name: "start_session",
307 description:
308 "Spin off a session for work that needs more than a quick answer: investigating, reading a lot of code, writing something long, or anything that takes several steps. It runs on its own with its own context, shows a live card here, and reports back in this conversation when done. Give it a short title and a complete brief: the goal, what done looks like, and everything it needs from this conversation.",
309 input_schema: { type: "object", properties: { title: { type: "string" }, goal: { type: "string" } }, required: ["title", "goal"] },
310};
311
312const POST_UPDATE: ToolDef = {
313 name: "post_update",
314 description: "Post a short progress note in your session's thread, for the people following it. Use it for real milestones or a question, not for every step.",
315 input_schema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
316};
317
318const USE_SUBAGENT: ToolDef = {
319 name: "use_subagent",
320 description:
321 "Hand a well-defined part of this session to one of your subagents (listed under Subagents). It works in its own session, paid from this one, and its result comes back to you before you go on. Give a complete brief.",
322 input_schema: { type: "object", properties: { name: { type: "string" }, brief: { type: "string" } }, required: ["name", "brief"] },
323};
324
325const BRING_IN: ToolDef = {
326 name: "bring_in",
327 description:
328 "Bring a colleague in on part of this session when their role owns it. They work in their own session, paid from this one, and their result comes back to you before you go on. Give a complete brief.",
329 input_schema: { type: "object", properties: { handle: { type: "string" }, brief: { type: "string" } }, required: ["handle", "brief"] },
330};
331
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.332/** What every artifact tool's description starts from, so the model never mixes them up with workflow run artifacts. */
333const ARTIFACTS =
334 "Artifacts are the workspace's own documents, made and shared in its Artifacts section: docs now, and later slides, designs and dashboards. They are not a workflow run's build artifacts.";
335
336const FOLIO_TOOLS: ToolDef[] = [
Docs: a workspace knowledge base people and agents write together337 {
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.338 name: "search_artifacts",
339 description: `${ARTIFACTS} Search the artifacts everyone in this conversation can read (specs, runbooks, policies, onboarding, decisions), by words and meaning, plus projects' docs. Optionally only one kind, one space (its name or id from list_spaces) or one project (\`workspace/name\`). Each result has its link and id. Look here first for how things work and what was decided.`,
340 input_schema: {
341 type: "object",
342 properties: {
343 query: { type: "string" },
344 kind: { type: "string", enum: [...FOLIO_KINDS] },
345 space: { type: "string", description: "A space's name or id." },
346 project: { type: "string", description: "A repository, `workspace/name`." },
347 },
348 required: ["query"],
349 },
Docs: a workspace knowledge base people and agents write together350 },
351 {
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.352 name: "read_artifact",
353 description: `${ARTIFACTS} Read one artifact by its id (fol_…) or its link (…/-/artifacts/<name>-fol_…). A doc comes back as Markdown with the ids of its top-level blocks (for edit_artifact), and says what you may do with it.`,
354 input_schema: { type: "object", properties: { id: { type: "string", description: "Its id or link." } }, required: ["id"] },
Docs: a workspace knowledge base people and agents write together355 },
356 {
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.357 name: "list_spaces",
358 description: `${ARTIFACTS} The spaces whose artifacts everyone in this conversation can read, with what you may do in each (read, suggest, edit).`,
359 input_schema: { type: "object", properties: {} },
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store360 },
361 {
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.362 name: "stale_artifacts",
363 description: `${ARTIFACTS} Artifacts possibly out of date because code they cite changed. Optionally only for one repository (\`workspace/name\`). Start here when keeping the docs current: read each, then bring it up to date with edit_artifact and marks_current.`,
364 input_schema: { type: "object", properties: { repo: { type: "string" } } },
Docs: a workspace knowledge base people and agents write together365 },
366];
367
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.368const FOLIO_WRITE_TOOLS: ToolDef[] = [
369 {
370 name: "create_artifact",
371 description: `${ARTIFACTS} Make a new artifact ("write this up"). Only kind "doc" can be made for now. Give a title and its content as Markdown (or a template id). where: a space (its name or id from list_spaces) as { "space": "..." }, "private" for the person who asked alone, or "conversation" for them plus view access for this conversation's people. Left out: shared with this conversation in a direct message or private channel, the General space in a public channel. It belongs to the person who asked, and you can keep editing it. When it comes from a conversation, set source to that thread's link.`,
372 input_schema: {
373 type: "object",
374 properties: {
375 kind: { type: "string", enum: [...FOLIO_KINDS] },
376 title: { type: "string" },
377 content: { type: "string", description: "Markdown." },
378 template: { type: "string", description: "A template id, instead of content." },
379 where: {
380 description: '{ "space": "<name or id>" }, "private" or "conversation".',
381 anyOf: [
382 { type: "string", enum: ["private", "conversation"] },
383 { type: "object", properties: { space: { type: "string" } }, required: ["space"] },
384 ],
385 },
386 parent: { type: "string", description: "A doc to put it under: its id or link." },
387 source: { type: "string", description: "The link of the thread it was written up from." },
388 },
389 required: ["kind", "title"],
390 },
391 },
Docs: a workspace knowledge base people and agents write together392 {
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.393 name: "edit_artifact",
394 description: `${ARTIFACTS} Change a doc: replace a section (by its heading), a range of top-level blocks (ids from read_artifact), the whole doc, or add to the end. Where you may edit, it applies at once and shows in its history as yours; elsewhere it becomes a suggestion people accept or reject inline. Read it first. Write Markdown. Only docs can be changed this way for now.`,
Docs: a workspace knowledge base people and agents write together395 input_schema: {
396 type: "object",
397 properties: {
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.398 id: { type: "string", description: "Its id or link." },
Docs: a workspace knowledge base people and agents write together399 target: { type: "string", enum: ["append", "section", "blocks", "document"] },
400 heading: { type: "string", description: "For target section: the heading's text." },
401 from_block: { type: "string" },
402 to_block: { type: "string" },
403 markdown: { type: "string" },
404 note: { type: "string", description: "Why, in a line, for the history or the suggestion." },
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.405 marks_current: { type: "boolean", description: "This edit brings a doc marked possibly out of date up to date: it clears the mark when it applies or is accepted." },
Docs: a workspace knowledge base people and agents write together406 suggest_only: { type: "boolean", description: "Suggest even where you could edit." },
407 },
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.408 required: ["id", "target", "markdown"],
Docs: a workspace knowledge base people and agents write together409 },
410 },
411 {
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.412 name: "share_artifact",
413 description: `${ARTIFACTS} Let people in this conversation view or comment on an artifact, when the person who asked has full access to it. Only in a direct message or a private channel, and only with people already in it. You can't give edit or full access, or change who else can open it: for that, ask the person to use Share.`,
Docs: a workspace knowledge base people and agents write together414 input_schema: {
415 type: "object",
416 properties: {
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.417 id: { type: "string", description: "Its id or link." },
418 people: { type: "array", items: { type: "string" }, description: "Usernames of people in this conversation." },
419 role: { type: "string", enum: ["view", "comment"] },
Docs: a workspace knowledge base people and agents write together420 },
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.421 required: ["id", "people", "role"],
Docs: a workspace knowledge base people and agents write together422 },
423 },
424];
425
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.426const FOLIO_NAMES = new Set([...FOLIO_TOOLS, ...FOLIO_WRITE_TOOLS].map((tool) => tool.name));
Docs: a workspace knowledge base people and agents write together427
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)428const CODE_NAMES = new Set(CODE_TOOLS.map((tool) => tool.name));
429
430export type ToolContext = {
431 /** The agent replying. */
432 agentId: string;
433 /** Handles nobody may consult from here: the agent itself, and whoever sent it the work. */
434 notConsult: string[];
435 /** Hops so far: a consult is one more, and none is offered at the limit. */
436 hops: number;
437 maxHops: number;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked438 /** Whether this is a session's step (more calls, session tools) or a reply. */
439 session?: boolean;
440 /** Told of every call as it is made, for a session's transcript. */
441 onCall?: (call: ToolCall) => 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)442};
443
444export class ToolBox {
445 /** Every call this reply made, shared with the tool boxes of colleagues it consults: one budget for the reply. */
446 readonly calls: ToolCall[];
447 private readonly audience: Audience;
448 private readonly ports: ToolPorts;
449 private readonly context: ToolContext;
450
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked451 private readonly actions: ActionPorts | null;
452 /** Updates posted in this step. */
453 private updates = 0;
Workspace agents are told where they are and who is in the conversation every turn, and hand work to a colleague with a hand_off tool instead of an @mention: the brief is posted where the colleague already is, or in a group message of the person who asked, the agent and the colleague with a card linking to it; an agent's messages no longer wake other agents, its mentions of anyone outside the conversation show as plain names, and the agents and chat guides and the workspace plan say how.454 /** Hand-offs made in this reply. */
455 private handOffs = 0;
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.456 /** Artifacts read or made in this turn that not everyone here can read: never named here. */
457 private readonly notHere = new Set<string>();
458 /**
459 * Whether this turn read an artifact the whole workspace can't: then what
460 * it remembers is kept for the person who asked alone
461 * (docs/ARTIFACTS_MODE.md, section 4.3, rule 7).
462 */
463 private privateRead = false;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked464
465 constructor(audience: Audience, ports: ToolPorts, context: ToolContext, calls: ToolCall[] = [], actions: ActionPorts | 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)466 this.audience = audience;
467 this.ports = ports;
468 this.context = context;
469 this.calls = calls;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked470 this.actions = actions;
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)471 }
472
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked473 /** A colleague's tool box for a consult: the same audience, the same budget, one hop further, reading only. */
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)474 forColleague(ports: ToolPorts, context: ToolContext): ToolBox {
475 return new ToolBox(this.audience, ports, context, this.calls);
476 }
477
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked478 /** The most calls this box makes. */
479 get maxCalls(): number {
480 return this.context.session ? MAX_SESSION_TOOL_CALLS : MAX_TOOL_CALLS;
481 }
482
483 /** Whether the person who asked can be acted for: resolved, and able to read code here. */
484 private canFile(): boolean {
485 return !!this.actions && !!this.audience.asker && this.audience.codeAllowed();
486 }
487
488 /**
489 * The tools offered: no code tools for an audience that can't read code,
490 * no consults or hand-offs at the hop limit, session tools only in a
491 * session, and a spin-off only from chat.
492 */
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)493 definitions(): ToolDef[] {
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked494 const roomForHop = this.context.hops + 1 <= this.context.maxHops;
495 const actions = this.actions;
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)496 return [
497 ...(this.audience.codeAllowed() ? CODE_TOOLS : []),
498 ...CHAT_TOOLS,
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.499 // Artifacts are for everyone, Code or not: the docs service decides what this person and audience can read.
500 ...(this.ports.folios && this.audience.asker ? FOLIO_TOOLS : []),
501 ...(this.ports.folios && this.audience.asker && actions ? FOLIO_WRITE_TOOLS : []),
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked502 ...(roomForHop ? [ASK_COLLEAGUE] : []),
503 ...(actions ? [REMEMBER, FORGET] : []),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar504 ...(this.canFile() ? [DRAFT_ISSUE] : []),
505 ...(this.canFile() && actions?.comment ? [COMMENT] : []),
506 ...(this.canFile() && actions?.review ? [REVIEW_PULL] : []),
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked507 ...(actions?.startSession && !this.context.session ? [START_SESSION] : []),
Workspace agents are told where they are and who is in the conversation every turn, and hand work to a colleague with a hand_off tool instead of an @mention: the brief is posted where the colleague already is, or in a group message of the person who asked, the agent and the colleague with a card linking to it; an agent's messages no longer wake other agents, its mentions of anyone outside the conversation show as plain names, and the agents and chat guides and the workspace plan say how.508 ...(actions?.handOff && !this.context.session && roomForHop ? [HAND_OFF] : []),
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked509 ...(actions?.postUpdate && this.context.session ? [POST_UPDATE] : []),
510 ...(actions?.useSubagent && this.context.session && roomForHop ? [USE_SUBAGENT] : []),
511 ...(actions?.bringIn && this.context.session && roomForHop ? [BRING_IN] : []),
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)512 ];
513 }
514
515 /** Whether another call may be made. */
516 get spent(): boolean {
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked517 return this.calls.length >= this.maxCalls;
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)518 }
519
520 async run(name: string, input: Record<string, unknown>): Promise<ToolResult> {
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked521 const result = await this.attempt(name, input);
522 const call: ToolCall = { tool: name, args: redact(input), outcome: result.outcome, bytes: result.text.length };
523 this.calls.push(call);
524 this.context.onCall?.(call);
525 return result;
526 }
527
528 private async attempt(name: string, input: Record<string, unknown>): Promise<ToolResult> {
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)529 let result: ToolResult;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked530 const what = this.context.session ? "step" : "reply";
531 if (this.spent) result = { text: `No more tool calls in this ${what} (at most ${this.maxCalls}). Answer with what you have.`, outcome: "refused" };
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)532 else {
533 try {
534 result = await this.dispatch(name, input ?? {});
535 } catch (error) {
536 console.error("agents: a tool failed", name, String(error));
537 result = { text: "That didn't work just now. Answer with what you have.", outcome: "error" };
538 }
539 }
540 return result;
541 }
542
543 private withheld(): ToolResult {
544 return { text: WITHHELD, outcome: "withheld" };
545 }
546
547 private async dispatch(name: string, input: Record<string, unknown>): Promise<ToolResult> {
548 const asker = this.audience.asker;
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.549 if (FOLIO_NAMES.has(name)) {
550 if (!this.definitions().some((tool) => tool.name === name) || !asker || !this.ports.folios) return { text: `There is no tool called ${name} here.`, outcome: "refused" };
551 return this.folios(name, input, asker, this.ports.folios);
Docs: a workspace knowledge base people and agents write together552 }
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)553 if (CODE_NAMES.has(name)) {
554 // Not offered, and refused if asked for anyway: the check is here, not in the prompt.
555 if (!this.audience.codeAllowed() || !asker) return this.withheld();
556 return this.code(name, input, asker);
557 }
558 switch (name) {
559 case "search_messages": {
560 const query = String(input.query ?? "").trim();
561 if (query.length < 2) return { text: "Search for at least two characters.", outcome: "refused" };
562 const found = await this.ports.searchMessages(query);
563 if (found === null) return { text: "Search didn't work just now.", outcome: "error" };
564 if (!found.length) return { text: "No messages found.", outcome: "allowed" };
565 return { text: untrusted(`search_messages "${query}"`, found.map(messageLine).join("\n")), outcome: "allowed" };
566 }
567 case "read_thread": {
568 const thread = await this.ports.readThread(String(input.channel ?? ""), String(input.id ?? ""));
569 if (!thread || !thread.length) return this.withheld();
570 return { text: untrusted("read_thread", thread.map(messageLine).join("\n")), outcome: "allowed" };
571 }
572 case "workspace_roster":
573 return { text: untrusted("workspace_roster", await this.ports.roster(asker)), outcome: "allowed" };
574 case "ask_colleague": {
575 const handle = String(input.handle ?? "").trim().replace(/^@/, "").toLowerCase();
576 const question = String(input.question ?? "").trim();
577 if (!handle || !question) return { text: "Name the colleague and the question.", outcome: "refused" };
578 if (this.context.hops + 1 > this.context.maxHops) return { text: "This request has been passed along too many times; answer with what you have.", outcome: "refused" };
579 if (this.context.notConsult.includes(handle)) {
580 return { text: `You can't consult @${handle} here: they sent you this work, or it is you. Answer with what you have.`, outcome: "refused" };
581 }
582 const answer = await this.ports.consult(handle, question.slice(0, 2000));
583 if (!answer.ok) return { text: answer.message, outcome: "refused" };
584 return { text: untrusted(`@${answer.colleague}'s answer`, answer.answer), outcome: "allowed" };
585 }
586 default:
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked587 return this.act(name, input);
588 }
589 }
590
Agents recall what Docs say before they answer or work, and each has required reading591 /**
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.592 * What the workspace's artifacts say about `query`, for this person and
593 * this audience, before the agent answers: no tool call, nothing counted
594 * against its tools. Empty when there is no docs service or nothing
595 * relevant.
Agents recall what Docs say before they answer or work, and each has required reading596 */
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.597 async recall(query: string | null, spaces: string[]): Promise<FolioPassage[]> {
598 const folios = this.ports.folios;
Agents recall what Docs say before they answer or work, and each has required reading599 const asker = this.audience.asker;
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.600 if (!folios || !asker || !query) return [];
Agents recall what Docs say before they answer or work, and each has required reading601 try {
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.602 return (await folios.recall(asker, this.folioAudience(), query, spaces)) ?? [];
Agents recall what Docs say before they answer or work, and each has required reading603 } catch (error) {
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.604 console.error("agents: artifacts recall failed", String(error));
Agents recall what Docs say before they answer or work, and each has required reading605 return [];
606 }
607 }
608
Docs: a workspace knowledge base people and agents write together609 /** Who reads what an agent says here, as the docs service takes it. */
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.610 private folioAudience(): FolioAudience {
Docs: a workspace knowledge base people and agents write together611 return this.audience.shared ? { kind: "workspace" } : { kind: "people", user_ids: this.audience.members.map((m) => m.id) };
612 }
613
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.614 /** Whether anyone besides the person who asked reads what is said here. */
615 private othersHere(asker: User): boolean {
616 return this.audience.shared || this.audience.members.some((m) => m.id !== asker.id);
617 }
618
619 private async folios(name: string, input: Record<string, unknown>, asker: User, folios: FoliosPorts): Promise<ToolResult> {
Docs: a workspace knowledge base people and agents write together620 const text = (key: string, max: number) => String(input[key] ?? "").trim().slice(0, max);
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.621 const audience = this.folioAudience();
Docs: a workspace knowledge base people and agents write together622 switch (name) {
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.623 case "list_spaces": {
624 const spaces = await folios.spaces(asker, audience);
625 if (spaces === null) return { text: "Spaces couldn't be listed just now.", outcome: "error" };
626 if (!spaces.length) return { text: "There are no spaces everyone here can read.", outcome: "allowed" };
627 return { text: untrusted("list_spaces", spaces.map(spaceLine).join("\n")), outcome: "allowed" };
628 }
629 case "search_artifacts": {
Docs: a workspace knowledge base people and agents write together630 const query = text("query", 200);
631 if (query.length < 2) return { text: "Search for at least two characters.", outcome: "refused" };
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.632 const kind = text("kind", 20) || null;
633 if (kind && !isFolioKind(kind)) return { text: `There is no kind of artifact called ${kind}: it is doc, slides, design or dashboard.`, outcome: "refused" };
634 let spaceId: string | null = null;
635 if (text("space", 200)) {
636 const space = findSpace((await folios.spaces(asker, audience)) ?? [], text("space", 200));
637 if (!space) return this.withheld();
638 spaceId = space.id;
639 }
640 const found = await folios.search(asker, audience, { query, kind: kind as FolioKind | null, space_id: spaceId, project: text("project", 200).toLowerCase() || null });
641 if (found === null) return { text: "Search didn't work just now.", outcome: "error" };
642 return { text: untrusted(`search_artifacts "${query}"`, found), outcome: "allowed" };
643 }
644 case "stale_artifacts": {
645 const found = await folios.stale(asker, audience, text("repo", 200).toLowerCase() || null);
646 if (found === null) return { text: "Out-of-date artifacts couldn't be listed just now.", outcome: "error" };
647 return { text: untrusted("stale_artifacts", found), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together648 }
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.649 case "read_artifact": {
650 const id = folioRef(text("id", 500));
651 if (!id) return { text: "Give the artifact's id (fol_…) or its link.", outcome: "refused" };
652 const found = await folios.read(asker, audience, id);
653 if (!found.ok) return found.code === "not_found" || found.code === "forbidden" ? this.withheld() : { text: found.message, outcome: "refused" };
654 const read = found.value;
655 if (!this.audience.shared || !read.audience_can_read) this.privateRead = true;
656 if (!read.audience_can_read) return this.notForEveryone(asker, read.folio, folios, "found");
657 return { text: untrusted(`read_artifact ${id}`, folioReadText(read)), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together658 }
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.659 case "create_artifact": {
660 const kind = text("kind", 20) || "doc";
661 if (!isFolioKind(kind)) return { text: `There is no kind of artifact called ${kind}.`, outcome: "refused" };
662 if (kind !== "doc") return { text: NOT_YET, outcome: "refused" };
663 const title = text("title", 200);
664 const template = text("template", 100) || null;
665 const markdown = String(input.content ?? "").slice(0, 100_000);
666 if (!title) return { text: "An artifact needs a title.", outcome: "refused" };
667 if (!markdown.trim() && !template) return { text: "Give its content as Markdown, or a template.", outcome: "refused" };
668 const parentGiven = text("parent", 500);
669 const parent = parentGiven ? folioRef(parentGiven) : null;
670 if (parentGiven && !parent) return { text: "Give the parent doc's id or link.", outcome: "refused" };
671 const place = await this.whereFor(input.where, asker, folios);
672 if (!place.ok) return { text: place.message, outcome: "refused" };
673 const make = (where: FolioWhere) =>
674 folios.create(asker, { kind, title, markdown: template ? null : markdown, template_id: template, where, parent_id: parent, source: sourceLink(text("source", 2000)) });
675 let made = await make(place.where);
676 // The General space by default, unless the person who asked can't add there: then their Private.
677 if (!made.ok && place.fallback && made.code === "forbidden") made = await make("private");
678 if (!made.ok) return { text: `It couldn't be made: ${made.message}`, outcome: "refused" };
679 const ref = made.value;
680 if (this.othersHere(asker)) {
681 const check = await folios.read(asker, audience, ref.id).catch(() => null);
682 if (!check?.ok || !check.value.audience_can_read) return this.notForEveryone(asker, ref, folios, "made");
683 }
684 return { text: `Wrote ${ref.title} (${ref.path}, id ${ref.id}). Link it.`, outcome: "allowed" };
685 }
686 case "edit_artifact": {
687 const id = folioRef(text("id", 500));
Docs: a workspace knowledge base people and agents write together688 const markdown = String(input.markdown ?? "").slice(0, 100_000);
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.689 if (!id || !markdown.trim()) return { text: "Give the artifact's id or link, and the Markdown.", outcome: "refused" };
Docs: a workspace knowledge base people and agents write together690 const kind = text("target", 20);
691 const target: DocEditTarget | null =
692 kind === "append"
693 ? { kind: "append" }
694 : kind === "document"
695 ? { kind: "document" }
696 : kind === "section" && text("heading", 300)
697 ? { kind: "section", heading: text("heading", 300) }
698 : kind === "blocks" && text("from_block", 100) && text("to_block", 100)
699 ? { kind: "blocks", from_block: text("from_block", 100), to_block: text("to_block", 100) }
700 : null;
701 if (!target) return { text: "Say what to change: append, a section by its heading, blocks by their ids, or the whole document.", outcome: "refused" };
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.702 const edit: FolioAgentEdit = { kind: "doc", target, markdown, note: text("note", 300) || null, suggest_only: input.suggest_only === true, marks_current: input.marks_current === true };
703 const done = await folios.edit(asker, id, edit);
704 if (!done.ok) return { text: `That didn't work: ${done.message}`, outcome: "refused" };
705 return { text: editMessage(done.value, !this.notHere.has(done.value.folio.id)), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together706 }
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.707 case "share_artifact": {
708 if (this.audience.shared) return { text: "You can share only in a direct message or a private channel. Ask the person to use Share on the artifact instead.", outcome: "refused" };
709 const id = folioRef(text("id", 500));
710 if (!id) return { text: "Give the artifact's id (fol_…) or its link.", outcome: "refused" };
711 const role = input.role === "view" || input.role === "comment" ? input.role : null;
712 if (!role) return { text: "You can share to view or comment only. For more, ask the person to use Share.", outcome: "refused" };
713 const named = (Array.isArray(input.people) ? input.people : []).filter((p): p is string => typeof p === "string").map((p) => p.trim().replace(/^@/, "").toLowerCase()).filter(Boolean).slice(0, 50);
714 const people = named.map((n) => this.audience.members.find((m) => m.username.toLowerCase() === n || m.id === n) ?? n);
715 const outside = people.filter((p): p is string => typeof p === "string");
716 if (outside.length) return { text: `${outside.map((n) => `@${n}`).join(", ")} ${outside.length === 1 ? "isn't" : "aren't"} in this conversation: you can share only with people in it.`, outcome: "refused" };
717 const users = [...new Map((people as User[]).filter((u) => u.id !== asker.id).map((u) => [u.id, u])).values()];
718 if (!users.length) return { text: "Name who to share it with: people in this conversation besides the person who asked.", outcome: "refused" };
719 const done = await folios.share(asker, audience, id, users.map((u) => u.id), role);
720 if (!done.ok) return { text: `It couldn't be shared: ${done.message}`, outcome: "refused" };
721 return { text: `Shared with ${users.map((u) => `@${u.username}`).join(", ")}: they can ${role} it.`, outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together722 }
723 default:
724 return { text: `There is no tool called ${name}.`, outcome: "refused" };
725 }
726 }
727
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.728 /**
729 * Where a new artifact goes (docs/ARTIFACTS_MODE.md, section 4.1):
730 * - a space named by its name or id, among those everyone here can read;
731 * - "private": the asker's Private;
732 * - "conversation": Private, plus `view` for this conversation's people;
733 * - nothing: the conversation in a DM or private channel, and in a public
734 * channel the General space if the asker can add there (`fallback`:
735 * their Private if the docs service says they can't).
736 */
737 private async whereFor(given: unknown, asker: User, folios: FoliosPorts): Promise<{ ok: true; where: FolioWhere; fallback: boolean } | { ok: false; message: string }> {
738 const named = typeof given === "object" && given !== null ? (given as { space?: unknown }).space : given;
739 const wanted = typeof named === "string" ? named.trim().slice(0, 200) : "";
740 const others = this.audience.members.filter((m) => m.id !== asker.id).map((m) => m.id);
741 const byDefault = async (): Promise<{ ok: true; where: FolioWhere; fallback: boolean }> => {
742 if (!this.audience.shared) return { ok: true, where: others.length ? { conversation: this.audience.members.map((m) => m.id) } : "private", fallback: false };
743 const spaces = (await folios.spaces(asker, this.folioAudience())) ?? [];
744 const general = spaces.find((s) => s.slug === "general") ?? spaces.find((s) => s.name.toLowerCase() === "general");
745 return general && general.can.suggest ? { ok: true, where: { space_id: general.id }, fallback: true } : { ok: true, where: "private", fallback: false };
746 };
747 if (!wanted) return byDefault();
748 const lower = wanted.toLowerCase();
749 if (typeof given === "string" && lower === "private") return { ok: true, where: "private", fallback: false };
750 // A public channel has no list of people to share with: its default instead.
751 if (typeof given === "string" && lower === "conversation") return byDefault();
752 const space = findSpace((await folios.spaces(asker, this.folioAudience())) ?? [], wanted);
753 if (space) return { ok: true, where: { space_id: space.id }, fallback: false };
754 // An id the asker gave, for a space not everyone here can read: the docs service checks it.
755 if (/^spc_[A-Za-z0-9]+$/.test(wanted)) return { ok: true, where: { space_id: wanted }, fallback: false };
756 return { ok: false, message: `There's no space called ${wanted} that everyone here can read. Use list_spaces, or put it in "private" or "conversation".` };
757 }
758
759 /**
760 * An artifact someone here can't read (docs/ARTIFACTS_MODE.md, section
761 * 4.3, rule 2): the link goes to the asker directly, and the agent says
762 * only that it found or made something, never what.
763 */
764 private async notForEveryone(asker: User, folio: FolioRef, folios: FoliosPorts, what: "found" | "made"): Promise<ToolResult> {
765 this.notHere.add(folio.id);
766 const who = `@${asker.username}`;
767 const note =
768 what === "made"
769 ? "I made this for you. Not everyone in the conversation you asked from can open it, so here is the link:"
770 : "Here is what you asked about. Not everyone in the conversation you asked from can open it, so here is the link:";
771 const sent = await folios.sendLink(asker, { title: folio.title, path: folio.path }, note).catch(() => false);
772 const lead = `Not everyone in this conversation can read this artifact, so ${what === "made" ? "it" : "its content"} isn't shown here. Don't quote it, name it or describe it here;`;
773 const text = sent
774 ? `${lead} say you ${what} it and that you've sent the link to ${who} directly.`
775 : what === "made"
776 ? `${lead} say you made it and that ${who} will find it under Artifacts, in their Private or shared with them.`
777 : `${lead} say you found it but can't share it here, and ask ${who} to message you directly.`;
778 return { text, outcome: what === "made" ? "allowed" : "withheld" };
779 }
780
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked781 /** Doing, not reading: memory, issues, sessions. Each refused unless offered. */
782 private async act(name: string, input: Record<string, unknown>): Promise<ToolResult> {
783 const actions = this.actions;
784 const offered = this.definitions().some((tool) => tool.name === name);
785 if (!actions || !offered) return { text: `There is no tool called ${name} here.`, outcome: "refused" };
786 const said = (answer: { ok: boolean; message: string }): ToolResult => ({ text: answer.message, outcome: answer.ok ? "allowed" : "refused" });
787 const text = (key: string, max: number) => String(input[key] ?? "").trim().slice(0, max);
788 switch (name) {
789 case "remember": {
790 const fact = text("fact", 2000);
791 if (!fact) return { text: "Say what to remember.", outcome: "refused" };
792 const scope = input.scope === "workspace" || input.scope === "channel" || input.scope === "person" ? input.scope : 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.793 return said(await actions.remember(fact, scope, this.privateRead));
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked794 }
795 case "forget":
796 return said(await actions.forget(text("id", 100)));
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar797 case "draft_issue": {
798 if (!this.audience.asker || !this.audience.codeAllowed()) return this.withheld();
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked799 const repo = await this.audience.repo(input.repo);
800 if (!repo) return this.withheld();
801 const title = text("title", 200);
802 const body = text("body", 20_000);
803 if (!title || !body) return { text: "An issue needs a title and a body.", outcome: "refused" };
804 const labels = Array.isArray(input.labels)
805 ? input.labels.filter((l): l is string => typeof l === "string").map((l) => l.trim()).filter(Boolean).slice(0, 5)
806 : [];
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar807 return said(await actions.draftIssue(repo, { title, body, labels }));
808 }
809 case "comment":
810 case "review_pull": {
811 const asker = this.audience.asker;
812 if (!asker || !this.audience.codeAllowed()) return this.withheld();
813 const repo = await this.audience.repo(input.repo);
814 if (!repo) return this.withheld();
815 const number = Math.floor(Number(input.number));
816 if (!Number.isFinite(number) || number < 1) return { text: "Give the issue or pull request's number.", outcome: "refused" };
817 const body = text("body", 20_000);
818 if (name === "comment") {
819 if (!body) return { text: "Say what to comment.", outcome: "refused" };
820 return said(await actions.comment!(repo, asker, number, body));
821 }
822 const verdict = input.verdict === "approve" || input.verdict === "request_changes" ? input.verdict : "comment";
823 if (!body && verdict !== "approve") return { text: "A review needs its text.", outcome: "refused" };
824 return said(await actions.review!(repo, asker, number, verdict, body));
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked825 }
826 case "start_session": {
827 const title = text("title", 120);
828 const goal = text("goal", 8000);
829 if (!title || !goal) return { text: "A session needs a title and a goal.", outcome: "refused" };
830 return said(await actions.startSession!(title, goal));
831 }
832 case "post_update": {
833 const note = text("text", 2000);
834 if (!note) return { text: "Say what to post.", outcome: "refused" };
835 if (this.updates >= 3) return { text: "You've posted enough updates for this step; carry on with the work.", outcome: "refused" };
836 this.updates++;
837 return said(await actions.postUpdate!(note));
838 }
839 case "use_subagent": {
840 const helper = text("name", 60).toLowerCase();
841 const brief = text("brief", 8000);
842 if (!helper || !brief) return { text: "Name the subagent and give it a brief.", outcome: "refused" };
843 return said(await actions.useSubagent!(helper, brief));
844 }
845 case "bring_in": {
846 const handle = text("handle", 60).replace(/^@/, "").toLowerCase();
847 const brief = text("brief", 8000);
848 if (!handle || !brief) return { text: "Name the colleague and give them a brief.", outcome: "refused" };
849 if (this.context.notConsult.includes(handle)) return { text: `You can't bring in @${handle} here: they sent you this work, or it is you.`, outcome: "refused" };
850 return said(await actions.bringIn!(handle, brief));
851 }
Workspace agents are told where they are and who is in the conversation every turn, and hand work to a colleague with a hand_off tool instead of an @mention: the brief is posted where the colleague already is, or in a group message of the person who asked, the agent and the colleague with a card linking to it; an agent's messages no longer wake other agents, its mentions of anyone outside the conversation show as plain names, and the agents and chat guides and the workspace plan say how.852 case "hand_off": {
853 const handle = text("handle", 60).replace(/^@/, "").toLowerCase();
854 const brief = text("brief", 8000);
855 if (!handle || !brief) return { text: "Name the colleague and give them a brief.", outcome: "refused" };
856 if (this.context.notConsult.includes(handle)) {
857 return { text: `You can't hand work to @${handle}: they sent you this work, or it is you. Answer with what you have.`, outcome: "refused" };
858 }
859 if (this.handOffs >= MAX_HAND_OFFS) return { text: `You've handed off to ${MAX_HAND_OFFS} colleagues from this message; hand off the rest later.`, outcome: "refused" };
860 const done = await actions.handOff!(handle, brief);
861 if (done.ok) this.handOffs++;
862 return said(done);
863 }
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked864 default:
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)865 return { text: `There is no tool called ${name}.`, outcome: "refused" };
866 }
867 }
868
869 private async code(name: string, input: Record<string, unknown>, viewer: User): Promise<ToolResult> {
870 if (name === "list_repositories") {
871 const repos = [...(await this.audience.repos()).values()];
872 if (!repos.length) return { text: "There are no repositories everyone here can read.", outcome: "allowed" };
873 return { text: untrusted("list_repositories", repos.map((repo) => `${repo.namespace}/${repo.name}${repo.isPrivate ? " (private)" : ""}`).join("\n")), outcome: "allowed" };
874 }
875 if (name === "search_code") {
876 const query = String(input.query ?? "").trim();
877 if (query.length < 2) return { text: "Search for at least two characters.", outcome: "refused" };
878 const only = input.repo === undefined || input.repo === null || input.repo === "" ? null : await this.audience.repo(input.repo);
879 if (input.repo && !only) return this.withheld();
880 const allowed = await this.audience.repos();
881 // Whatever search returns, only hits in allowed repositories come through.
882 const hits = (await this.ports.searchCode(viewer, query, only)).filter((hit) => allowed.has(hit.repo.toLowerCase()) && (!only || hit.repo.toLowerCase() === `${only.namespace}/${only.name}`.toLowerCase()));
883 if (!hits.length) return { text: "No code found.", outcome: "allowed" };
884 return { text: untrusted(`search_code "${query}"`, hits.slice(0, 10).map((hit) => `${hit.repo}:${hit.path}\n${hit.snippet}`).join("\n\n")), outcome: "allowed" };
885 }
886 if (name === "recent_activity") {
887 const one = input.repo ? await this.audience.repo(input.repo) : null;
888 if (input.repo && !one) return this.withheld();
889 const repos = one ? [one] : [...(await this.audience.repos()).values()].slice(0, 20);
890 if (!repos.length) return { text: "There are no repositories everyone here can read.", outcome: "allowed" };
891 const pulls = await this.ports.recentPulls(repos, viewer);
892 if (!pulls.length) return { text: "No recent pull requests.", outcome: "allowed" };
893 return { text: untrusted("recent_activity", pulls.map((p) => `${p.repo}#${p.number} ${p.status}: ${p.title} (${p.updated_at})`).join("\n")), outcome: "allowed" };
894 }
895 // The rest name one repository; it must be on the allow-list.
896 const repo = await this.audience.repo(input.repo);
897 if (!repo) return this.withheld();
898 const full = `${repo.namespace}/${repo.name}`;
899 switch (name) {
900 case "read_file": {
901 const path = String(input.path ?? "").trim().replace(/^\/+/, "");
902 if (!path || path.split("/").some((part) => part === "..")) return { text: "Give a path inside the repository.", outcome: "refused" };
903 const ref = typeof input.ref === "string" && input.ref.trim() ? input.ref.trim() : repo.defaultBranch;
904 const file = await this.ports.readFile(repo, viewer, ref, path);
905 if (!file) return { text: `No file ${path} at ${ref} in ${full}.`, outcome: "allowed" };
906 if (file.text === null) return { text: `${full}:${path} is binary or too large to read (${file.size} bytes).`, outcome: "allowed" };
907 return { text: untrusted(`${full}:${path}@${ref}`, file.text), outcome: "allowed" };
908 }
909 case "list_issues": {
910 const state = input.state === "closed" ? "closed" : "open";
911 const issues = await this.ports.listIssues(repo, viewer, state);
912 if (!issues) return this.withheld();
913 if (!issues.length) return { text: `No ${state} issues in ${full}.`, outcome: "allowed" };
914 return { text: untrusted(`list_issues ${full}`, issues.slice(0, 30).map((i) => `#${i.number} [${i.state}] ${i.title}${i.labels.length ? ` (${i.labels.join(", ")})` : ""}`).join("\n")), outcome: "allowed" };
915 }
916 case "get_issue": {
917 const issue = await this.ports.getIssue(repo, Math.floor(Number(input.number)), viewer);
918 if (!issue) return { text: `No such issue in ${full}.`, outcome: "allowed" };
919 const comments = issue.comments.map((c) => `@${c.author}: ${c.body}`).join("\n\n");
920 return { text: untrusted(`${full}#${issue.number}`, `#${issue.number} [${issue.state}] ${issue.title}\n\n${issue.body}${comments ? `\n\nComments:\n\n${comments}` : ""}`), outcome: "allowed" };
921 }
922 case "get_pull": {
923 const pull = await this.ports.getPull(repo, Math.floor(Number(input.number)), viewer);
924 if (!pull) return { text: `No such pull request in ${full}.`, outcome: "allowed" };
925 return { text: untrusted(`${full}#${pull.number}`, `#${pull.number} [${pull.status}] ${pull.title}\n\n${pull.body}${pull.checks ? `\n\nChecks: ${pull.checks}` : ""}`), outcome: "allowed" };
926 }
927 default:
928 return { text: `There is no tool called ${name}.`, outcome: "refused" };
929 }
930 }
931}
932
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.933/** What an agent hears when it asks for a kind that isn't built yet. */
934const NOT_YET = "Slides, designs and dashboards aren't available yet: only docs can be made for now. Say so, and offer to write it as a doc instead.";
935
936/**
937 * A folio id from an id or any artifact link (`/acme/-/artifacts/runbook-fol_…`,
938 * with or without the site and a query); null when there is none.
939 */
940export function folioRef(given: string): string | null {
941 const last = given.trim().split(/[?#]/)[0].split("/").filter(Boolean).at(-1) ?? "";
942 return folioIdFrom(last);
943}
944
945/** Where an artifact was written up from: a thread's link, cut to the site path the docs service keeps; null when it isn't one. */
946export function sourceLink(given: string): { title: string; href: string } | null {
947 let href = given.trim();
948 if (!href) return null;
949 if (/^https?:\/\//i.test(href)) {
950 try {
951 const url = new URL(href);
952 href = `${url.pathname}${url.search}${url.hash}`;
953 } catch {
954 return null;
955 }
956 }
957 return href.startsWith("/") && !href.startsWith("//") ? { title: "A conversation", href } : null;
958}
959
960/** A space by its id, address or name (any case), among those given. */
961function findSpace(spaces: FolioSpaceLine[], given: string): FolioSpaceLine | null {
962 const wanted = given.trim().toLowerCase();
963 return spaces.find((s) => s.id === given.trim()) ?? spaces.find((s) => s.slug.toLowerCase() === wanted) ?? spaces.find((s) => s.name.toLowerCase() === wanted) ?? null;
964}
965
966function spaceLine(s: FolioSpaceLine): string {
967 const can = s.can.edit ? "you can edit" : s.can.suggest ? "you can suggest edits" : "read only";
968 const projects = s.projects.length ? `; about ${s.projects.join(", ")}` : "";
969 return `- ${s.name} (id ${s.id}, ${s.kind}; ${can}${projects})${s.description ? `: ${s.description}` : ""}`;
970}
971
972/** An artifact as the agent reads it: where it is, what it may do, and its content (a doc's Markdown, with its top-level block ids). */
973export function folioReadText(read: FolioAgentRead): string {
974 const f = read.folio;
975 const can = read.can.edit ? "you can edit it" : read.can.suggest ? "you can suggest edits" : "you can only read it";
976 const where = read.space ? `in the ${read.space.name} space` : "not in a space";
977 const blocks = read.blocks?.length ? `\nTop-level blocks: ${read.blocks.map((b) => `${b.id} ${b.type}${b.level ? ` ${b.level}` : ""}`).join(", ")}` : "";
978 return `# ${f.title} (${f.path}, id ${f.id})\nA ${f.kind}, ${where}; ${can}. Edited ${f.edited_at.slice(0, 16)}.${blocks}\n\n${read.content}`;
979}
980
981/** What an edit did, naming the artifact only where everyone here can read it. */
982function editMessage(result: FolioAgentEditResult, name: boolean): string {
983 const on = name ? ` on ${result.folio.title} (${result.folio.path})` : "";
984 switch (result.mode) {
985 case "applied":
986 return `Changed${on}. It's in its history as yours.`;
987 case "suggested":
988 return `Suggested${on}: people accept or reject it there.${name ? " Link it so they can." : ""}`;
989 case "proposed":
990 return `Proposed a change${on}: a person previews and applies it.`;
991 }
Docs: a workspace knowledge base people and agents write together992}
993
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)994function messageLine(m: FoundMessage): string {
995 const where = m.channel ? `#${m.channel}` : "a direct message";
996 return `[${m.created_at.slice(0, 16)} in ${where}, channel ${m.channel_id}, message ${m.id}] @${m.author}: ${m.body}`;
997}

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