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

Docs: a workspace knowledge base people and agents write together1/**
2 * Docs: the workspace's written knowledge, kept by the docs service
3 * (`services/docs`). Spaces hold trees of pages; each page is a CRDT
4 * document (Yjs) edited live over a socket, saved with a Markdown
5 * rendition that search, agents, export and the read view use. Plan and
6 * decisions: docs/WORKSPACE.md, "Docs".
7 *
8 * Wire shapes are snake_case end to end: the site, agents and the live
9 * socket all carry the same objects.
10 *
11 * Access, in one place:
12 *
13 * - A space has a kind: `workspace` (every member gets `default_role`),
14 * `team` (the team's members get `default_role`) or `private` (only the
15 * people, agents and teams listed as its members).
16 * - Members are listed with a role: view < comment < edit < manage. A
17 * person's role is the highest of the space's base role (when it applies
18 * to them) and every listing that names them or one of their teams.
19 * Workspace owners manage every workspace and team space; a private space
20 * is its members' alone.
21 * - An agent never sees or changes more than the person it acts for (the
22 * `viewer` on every agent call). It reads what that person can read,
23 * narrowed further to what every person in the `audience` can read; it
24 * suggests where that person can comment; and it edits directly only
25 * where that person can edit AND the space lets agents edit
26 * (`agent_mode: "edit"`). Otherwise its edit becomes a suggestion.
27 */
28import type { MemberProfile, Principal } from "./chat";
29import type { ServiceBinding } from "./clients";
30import type { User } from "./identity";
31import type { Result } from "./result";
32
33/** What someone may do in a space, weakest first. */
34export type DocRole = "view" | "comment" | "edit" | "manage";
35export const DOC_ROLES: readonly DocRole[] = ["view", "comment", "edit", "manage"];
36
37export const DOC_ROLE_LABELS: Record<DocRole, string> = {
38 view: "Can view",
39 comment: "Can comment",
40 edit: "Can edit",
41 manage: "Full access",
42};
43
44export const DOC_ROLE_SUMMARIES: Record<DocRole, string> = {
45 view: "Read pages and their history.",
46 comment: "Read pages and comment on them.",
47 edit: "Write and organize pages, and accept or reject suggestions.",
48 manage: "Edit, and change who has access and how agents work here.",
49};
50
51/** Who a space is for. */
52export type DocSpaceKind = "workspace" | "team" | "private";
53
54export const DOC_SPACE_KIND_LABELS: Record<DocSpaceKind, string> = {
55 workspace: "Everyone in the workspace",
56 team: "A team",
57 private: "Only its members",
58};
59
60/** How agents change pages in a space: as tracked suggestions (the default), or directly. */
61export type DocAgentMode = "suggest" | "edit";
62
63export const DOC_AGENT_MODE_LABELS: Record<DocAgentMode, string> = {
64 suggest: "Suggest changes",
65 edit: "Edit directly",
66};
67
68/** A key for a space member: `user:<id>`, `agent:<id>` or `team:<slug>`. */
69export type DocMemberKey = string;
70
71export type DocSpace = {
72 id: string;
73 workspace_id: string;
74 /** Unique in the workspace; in the URL: `/<workspace>/-/docs/<slug>`. */
75 slug: string;
76 name: string;
77 description: string | null;
78 /** An emoji, or null for the default book. */
79 icon: string | null;
80 kind: DocSpaceKind;
81 /** The team's slug, for a team space. */
82 team: string | null;
83 /** What every workspace member (workspace) or team member (team) gets; null for a private space. */
84 default_role: DocRole | null;
85 agent_mode: DocAgentMode;
86 /** The workspace's General space, made the first time Docs is opened. Can't be archived. */
87 is_default: boolean;
88 /** Projects (repositories, `owner/name`) the space is about: Docs filters by them. */
89 projects: string[];
90 created_by: Principal;
91 created_at: string;
92 archived_at: string | null;
93 /** The viewer's role in it. */
94 viewer_role: DocRole;
95 /** Pages in it that are not in the trash. */
96 page_count: number;
97};
98
99/** A space member, as the space's settings show it. */
100export type DocSpaceMember = {
101 key: DocMemberKey;
102 kind: "user" | "agent" | "team";
103 /** Username, agent handle or team slug. */
104 name: string;
105 display_name: string;
106 avatar: string | null;
107 avatar_seed?: string | null;
108 role: DocRole;
109};
110
111/** Enough to link to a page. */
112export type DocPageRef = {
113 id: string;
114 space_id: string;
115 space_slug: string;
116 title: string;
117 /** An emoji, or null for the default page icon. */
118 icon: string | null;
119 /** `<title-slug>-<id>`: the last part of the page's URL. */
120 slug: string;
121 /** `/<workspace>/-/docs/<space>/<slug>`. */
122 path: string;
123};
124
125export type DocPage = DocPageRef & {
126 parent_id: string | null;
127 position: number;
128 /** A cover image's URL, or a CSS gradient name (`gradient:<n>`); null for none. */
129 cover: string | null;
130 created_by: MemberProfile;
131 created_at: string;
132 updated_by: MemberProfile | null;
133 updated_at: string;
134 /** When it went to the trash; null when it is not there. */
135 archived_at: string | null;
136 has_children: boolean;
137 /** Projects (`owner/name`) the page is about, besides its space's. */
138 projects: string[];
139 owners: MemberProfile[];
140 /** The first lines of its text, for cards. */
141 excerpt: string;
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store142 /** Whether code it cites changed since someone last marked it current: possibly out of date. */
143 stale: boolean;
144};
145
146// ── Citations and staleness ───────────────────────────────────────────────
147//
148// A page can cite code: a path (a file, a folder, or a glob like
149// `src/export/**`) in a repository, optionally naming what at that path it
150// describes (a symbol, an endpoint, an environment variable). Citations
151// come from the page's text (the editor's citation chips, and links to
152// files in a repository: `/<owner>/<repo>/blob/<ref>/<path>`) and from
153// the page's header ("Describes"). When a merged pull request or a push
154// to a repository's default branch changes a cited path, the page is
155// marked possibly out of date with that change, until someone with edit
156// access marks it current again (or an agent updates it).
157
158/** What a citation names at its path. */
159export type DocCitationKind = "path" | "symbol" | "endpoint" | "env";
160export const DOC_CITATION_KINDS: readonly DocCitationKind[] = ["path", "symbol", "endpoint", "env"];
161
162export const DOC_CITATION_KIND_LABELS: Record<DocCitationKind, string> = {
163 path: "A file or folder",
164 symbol: "A symbol",
165 endpoint: "An endpoint",
166 env: "An environment variable",
167};
168
169export type DocCitation = {
170 /** `owner/name`, lowercased. */
171 repo: string;
172 /** A file, a folder (everything under it), or a glob (`*`, `**`, `?`). */
173 path: string;
174 kind: DocCitationKind;
175 /** The symbol, endpoint (`POST /v1/export`) or variable (`EXPORT_BUCKET`), for those kinds. */
176 label: string | null;
177 /** The commit it was cited at, when known. */
178 ref: string | null;
179 /** From the page's text, or from its header's "Describes". */
180 source: "body" | "header";
181};
182
183/** One entry of a page's "Describes": a repository and a path in it. */
184export type DocDescribes = { repo: string; path: string };
185
186/** A change that made a page possibly out of date. */
187export type DocStaleChange = {
188 /**
189 * False when the viewer can't read the repository: then `repo`, `pull`,
190 * `commit` and `paths` are blank, and the page only says that a change
191 * they can't see touched code it cites.
192 */
193 visible: boolean;
194 repo: string | null;
195 commit: string | null;
196 /** The merged pull request, when the change came from one. */
197 pull: { number: number; title: string | null } | null;
198 /** The cited paths it changed (the changed files, at most 20). */
199 paths: string[];
200 /** When it was noticed. RFC 3339. */
201 at: string;
202};
203
204/** Why a page is possibly out of date: every change since it was last marked current, newest first. */
205export type DocStaleness = { since: string; changes: DocStaleChange[] };
206
207/** A page an agent may bring up to date, with what changed. */
208export type DocStalePage = {
209 page: DocPageRef & { updated_at: string };
210 space: { id: string; slug: string; name: string; agent_mode: DocAgentMode };
211 can: DocAgentAbilities;
212 owners: MemberProfile[];
213 citations: DocCitation[];
214 /** Changes in repositories the viewer (and audience) can read; newest first. */
215 changes: DocStaleChange[];
216 since: string;
217};
218
219// ── A project's docs ──────────────────────────────────────────────────────
220//
221// A repository's `docs/` folder (and README.md), shown read-only in Docs
222// next to the workspace's spaces and found by the same search. It is read
223// from the default branch and kept up to date on every push to it. Each
224// reader sees only the repositories they can read. Changes go through the
225// repository: "Edit in Code" opens the file.
226
227export type DocRepoFile = {
228 /** From the repository's root: `docs/guide/setup.md`, `README.md`. */
229 path: string;
230 /** Its first heading, or its file name. */
231 title: string;
232};
233
234export type DocRepoSpace = {
235 id: string;
236 /** `owner/name`, as the repository is named now. */
237 repo: string;
238 default_branch: string;
239 /** The commit it was read at; null until it has been. */
240 commit: string | null;
241 indexed_at: string | null;
242 added_by: MemberProfile;
243 files: DocRepoFile[];
244 /** Whether the viewer may stop showing it (whoever added it, or an owner). */
245 can_remove: boolean;
Docs: a workspace knowledge base people and agents write together246};
247
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store248export type DocRepoPage = {
249 space: DocRepoSpace;
250 file: DocRepoFile & {
251 markdown: string;
252 /** `/<workspace>/-/docs/repo/<owner>/<name>/<path>`. */
253 href: string;
254 /** The file in Code, on the default branch. */
255 code_href: string;
256 };
257};
258
Docs: a workspace knowledge base people and agents write together259/** A page as the sidebar's tree lists it: flat, ordered by `position` within each parent. */
260export type DocTreeNode = {
261 id: string;
262 parent_id: string | null;
263 position: number;
264 title: string;
265 icon: string | null;
266 slug: string;
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store267 /** Possibly out of date (see `DocPage.stale`). */
268 stale?: boolean;
Docs: a workspace knowledge base people and agents write together269};
270
271export type DocsSidebarSpace = DocSpace & { pages: DocTreeNode[] };
272
273export type DocsSidebar = {
274 spaces: DocsSidebarSpace[];
275 favorites: DocPageRef[];
276 /** The pages the viewer opened last, newest first. */
277 recent: DocPageRef[];
278 /** Whether the viewer may make spaces (members of the workspace may). */
279 can_create_space: boolean;
280 trash_count: number;
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store281 /** Pages the viewer can read that are possibly out of date. */
282 stale_count: number;
283 /** Projects' docs folders shown in Docs, those whose repository the viewer can read. */
284 repos: DocRepoSpace[];
Docs: a workspace knowledge base people and agents write together285};
286
287export type DocsHome = {
288 /** Recently edited pages the viewer can read, newest first. */
289 recent: DocPage[];
290 /** Pages the viewer made or owns. */
291 mine: DocPage[];
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store292 /** Pages possibly out of date, most recently flagged first. */
293 stale: DocPage[];
Docs: a workspace knowledge base people and agents write together294 spaces: DocSpace[];
295 /** Every project some space or page is linked to, for the filter. */
296 projects: string[];
297 /** The project the lists are filtered to, or null. */
298 project: string | null;
299};
300
301/** A tracked change an agent proposed. `blocks` targets name top-level block ids from `page_markdown`. */
302export type DocEditTarget =
303 /** Add to the end of the page. */
304 | { kind: "append" }
305 /** Replace the whole page. */
306 | { kind: "document" }
307 /** Replace a section: the heading whose text matches (case-insensitive) and everything under it, up to the next heading of the same or a higher level. The new Markdown should include the heading if it is to stay. */
308 | { kind: "section"; heading: string }
309 /** Replace top-level blocks `from_block` through `to_block`, inclusive (with any blocks nested under them). */
310 | { kind: "blocks"; from_block: string; to_block: string };
311
312export type DocSuggestionStatus = "open" | "accepted" | "rejected" | "stale";
313
314export type DocSuggestion = {
315 id: string;
316 page_id: string;
317 /** The agent that suggested it. */
318 author: MemberProfile;
319 /** The person it acted for. */
320 asked_by: MemberProfile | null;
321 target: DocEditTarget;
322 /** The target's Markdown when it was suggested. */
323 before_markdown: string;
324 /** What it proposes instead. */
325 after_markdown: string;
326 /** Why, in a line. */
327 note: string | null;
328 status: DocSuggestionStatus;
329 created_at: string;
330 decided_by: MemberProfile | null;
331 decided_at: string | null;
332 /** The top-level blocks it covers now, so the editor can mark them; empty for an append or when the target is gone. */
333 block_ids: string[];
334};
335
336export type DocVersionKind = "created" | "edit" | "agent" | "suggestion" | "restore";
337
338export type DocVersion = {
339 id: string;
340 page_id: string;
341 created_at: string;
342 /** Everyone whose changes are in it, people and agents. */
343 authors: MemberProfile[];
344 kind: DocVersionKind;
345 /** "Suggested by @inky, accepted by @ana"; "Restored from Oct 3, 14:02". */
346 note: string | null;
347};
348
349export type DocDiffLine = { op: "same" | "add" | "del"; text: string };
350
351export type DocVersionDetail = DocVersion & {
352 markdown: string;
353 /** Against the version before it (or nothing, for the first). */
354 diff: DocDiffLine[];
355};
356
357export type DocPageDetail = {
358 page: DocPage;
359 space: DocSpace;
360 /** The page's ancestors, root first. */
361 breadcrumbs: DocPageRef[];
362 /** As last saved: the read view, and what the editor shows before the socket connects. */
363 markdown: string;
364 role: DocRole;
365 backlinks: DocPageRef[];
366 children: DocPageRef[];
367 favorite: boolean;
368 /** When the viewer last opened it, before now. */
369 last_viewed_at: string | null;
370 suggestions: DocSuggestion[];
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store371 /** Code the page cites: from its text and its header. */
372 citations: DocCitation[];
373 /** The header's "Describes" list. */
374 describes: DocDescribes[];
375 /** Why it is possibly out of date; null when it isn't. */
376 staleness: DocStaleness | null;
Docs: a workspace knowledge base people and agents write together377};
378
379export type DocSearchHit = DocPageRef & {
380 space_name: string;
381 /** Text around the match; `[[` and `]]` mark matched words. */
382 snippet: string;
383 updated_at: string;
384 projects: string[];
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store385 /** Set for a file from a project's docs folder (then `id` is `repo:<space>:<path>` and `path` its address in Docs). */
386 repo_file?: { repo: string; path: string } | null;
Docs: a workspace knowledge base people and agents write together387};
388
389export type DocTemplate = {
390 id: string;
391 name: string;
392 description: string;
393 icon: string;
394 /** Built in (meeting notes, RFC, ...) rather than saved by the workspace. */
395 builtin: boolean;
396 markdown: string;
397 created_by: Principal | null;
398};
399
400/** One top-level block, as agents see a page. */
401export type DocBlockOutline = {
402 id: string;
403 /** BlockNote's type: `heading`, `paragraph`, `bulletListItem`, `codeBlock`, `callout`, ... */
404 type: string;
405 /** For a heading. */
406 level: number | null;
407 /** The block and anything nested under it. */
408 markdown: string;
409};
410
411/** What an agent may do on a page, for the person it acts for. */
412export type DocAgentAbilities = { read: boolean; suggest: boolean; edit: boolean };
413
414export type DocAgentPage = {
415 page: DocPageRef & { updated_at: string };
416 space: { id: string; slug: string; name: string; agent_mode: DocAgentMode };
417 markdown: string;
418 blocks: DocBlockOutline[];
419 can: DocAgentAbilities;
420};
421
422/** Who will see what an agent says; it reads only what they all can. */
423export type DocAudience =
424 /** These people (by user id), e.g. a DM's or a private channel's members. */
425 | { kind: "people"; user_ids: string[] }
426 /** Everyone in the workspace, e.g. a public channel: only workspace-wide spaces. */
427 | { kind: "workspace" };
428
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store429/** An agent's edit to a page. */
430export type DocAgentEdit = {
431 target: DocEditTarget;
432 markdown: string;
433 note?: string | null;
434 /** The edit brings the page up to date with the code it cites: once applied, the page is no longer possibly out of date. */
435 marks_current?: boolean | null;
436};
437
Docs: a workspace knowledge base people and agents write together438/** What `apply_edit` did: applied it, or filed a suggestion because it may not edit there. */
439export type DocAgentEditResult =
440 | { mode: "applied"; version_id: string | null; page: DocPageRef }
441 | { mode: "suggested"; suggestion: DocSuggestion; page: DocPageRef };
442
443export type NewDocSpace = {
444 name: string;
445 slug?: string | null;
446 description?: string | null;
447 icon?: string | null;
448 kind: DocSpaceKind;
449 team?: string | null;
450 default_role?: DocRole | null;
451 agent_mode?: DocAgentMode | null;
452 projects?: string[] | null;
453};
454
455export type DocSpaceChange = Partial<Omit<NewDocSpace, "kind">> & { kind?: DocSpaceKind; archived?: boolean };
456
457export type NewDocPage = {
458 space_id: string;
459 parent_id?: string | null;
460 title?: string | null;
461 icon?: string | null;
462 /** A built-in template's id (`builtin:<name>`) or a saved one's. */
463 template_id?: string | null;
464 /** Starting Markdown, when there is no template. */
465 markdown?: string | null;
466 projects?: string[] | null;
467};
468
469export type DocPageChange = {
470 title?: string;
471 icon?: string | null;
472 cover?: string | null;
473 projects?: string[];
474 /** Member keys (`user:<id>`, `agent:<id>`). */
475 owners?: string[];
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store476 /** Replaces the header's "Describes" list (at most 20). */
477 describes?: DocDescribes[];
Docs: a workspace knowledge base people and agents write together478};
479
480/** Where a page goes: under `parent_id` (null for the top of the space), before `before_id` (null for the end). */
481export type DocMove = { space_id?: string | null; parent_id: string | null; before_id?: string | null };
482
483export type DocSearchQuery = {
484 query: string;
485 space_id?: string | null;
486 /** `owner/name`: pages linked to it, or in a space linked to it. */
487 project?: string | null;
488 limit?: number | null;
489};
490
491/**
492 * The comment operations of the editor's thread store (BlockNote's
493 * `RESTYjsThreadStore`), applied by the page's room to the `threads` map in
494 * the page's Yjs document, so every open editor sees them at once.
495 */
496export type DocThreadAction =
497 | { op: "create"; body: unknown; metadata?: unknown; page_level?: boolean }
498 /** Anchors a thread to the text between two Yjs relative positions (JSON). */
499 | { op: "anchor"; thread_id: string; anchor: unknown; head: unknown }
500 | { op: "comment"; thread_id: string; body: unknown; metadata?: unknown }
501 | { op: "edit_comment"; thread_id: string; comment_id: string; body: unknown; metadata?: unknown }
502 | { op: "delete_comment"; thread_id: string; comment_id: string; soft?: boolean }
503 | { op: "delete_thread"; thread_id: string }
504 | { op: "resolve" | "unresolve"; thread_id: string }
505 | { op: "react" | "unreact"; thread_id: string; comment_id: string; emoji: string };
506
507/** A comment thread, as agents and the inbox see it. */
508export type DocThread = {
509 id: string;
510 /** The text it is on; null for a comment on the whole page. */
511 quote: string | null;
512 resolved: boolean;
513 comments: { id: string; author: MemberProfile; text: string; created_at: string }[];
514};
515
516/** A file put in a page: served from the usercontent origin. */
517export type DocFile = { id: string; url: string; name: string; content_type: string; bytes: number };
518
519/**
520 * What a page's live socket carries besides the Yjs protocol (binary
521 * frames: y-protocols sync and awareness). These are JSON text frames.
522 */
523export type DocsLiveEvent =
524 | { type: "page.updated"; page: DocPage }
525 | { type: "page.archived"; page_id: string }
526 | { type: "suggestion.created" | "suggestion.updated"; suggestion: DocSuggestion }
527 | { type: "version.created"; version: DocVersion }
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store528 /** Whether it is possibly out of date changed: ask for the page again (what each reader sees of why depends on what they can read). */
529 | { type: "page.staleness" }
Docs: a workspace knowledge base people and agents write together530 /** The viewer's role changed, or their access ended (`role` null). */
531 | { type: "access"; role: DocRole | null };
532
533/**
534 * Header the site sets on a forwarded live socket and on uploads: the
535 * viewer, as JSON. Trusted only because the docs service is reachable
536 * through service bindings alone.
537 */
538export const DOCS_VIEWER_HEADER = "x-g1t-docs-viewer";
539
540/** The largest file a page takes, in bytes. */
541export const DOC_MAX_FILE_BYTES = 25 * 1024 * 1024;
542
543/** The name of the Yjs XML fragment that holds a page's blocks. */
544export const DOC_FRAGMENT = "document-store";
545/** The name of the Yjs map that holds a page's comment threads. */
546export const DOC_THREADS = "threads";
547
Agents recall what Docs say before they answer or work, and each has required reading548/** One passage of Docs, recalled for an agent: a section of a page or of a repository's docs. */
549export type DocPassage = {
550 /** The page; null for a repository's docs file. */
551 page: DocPageRef | null;
552 /** A repository's docs file: `owner/name`, its path, and where it reads in Docs. */
553 repo_file: { repo: string; path: string; href: string } | null;
554 space_name: string;
555 /** The heading the passage sits under, if any. */
556 heading: string | null;
557 /** The passage as Markdown, at most about 1,500 characters. */
558 text: string;
559 /** How close it is, 0 to 1. */
560 score: number;
561 updated_at: string;
562 /** The page is marked possibly out of date. */
563 stale: boolean;
564};
565
Docs: a workspace knowledge base people and agents write together566export type DocsApi = {
567 // ── The site ─────────────────────────────────────────────────────────
568
569 /** Spaces with their page trees, favorites, recent pages. Makes the General space the first time. */
570 sidebar(workspace: string, viewer: User): Promise<Result<DocsSidebar>>;
571 home(workspace: string, viewer: User, options?: { project?: string | null }): Promise<Result<DocsHome>>;
572 space(workspace: string, spaceSlug: string, viewer: User): Promise<Result<{ space: DocSpace; members: DocSpaceMember[]; pages: DocPage[] }>>;
573 createSpace(workspace: string, viewer: User, input: NewDocSpace): Promise<Result<DocSpace>>;
574 /** Manage role only. */
575 updateSpace(workspace: string, spaceId: string, viewer: User, change: DocSpaceChange): Promise<Result<DocSpace>>;
576 /** Adds, changes (`role`) or removes (`role` null) a member. Manage role only; the last manager stays. */
577 setSpaceMember(workspace: string, spaceId: string, viewer: User, member: DocMemberKey, role: DocRole | null): Promise<Result<DocSpaceMember[]>>;
578
579 /** A page, and the viewer's role in it; records the view. Not found when they can't read it. */
580 page(workspace: string, pageId: string, viewer: User): Promise<Result<DocPageDetail>>;
581 createPage(workspace: string, viewer: User, input: NewDocPage): Promise<Result<DocPage>>;
582 updatePage(workspace: string, pageId: string, viewer: User, change: DocPageChange): Promise<Result<DocPage>>;
583 /** Edit role in both spaces. Refuses moving a page under itself. */
584 movePage(workspace: string, pageId: string, viewer: User, move: DocMove): Promise<Result<DocPage>>;
585 duplicatePage(workspace: string, pageId: string, viewer: User): Promise<Result<DocPage>>;
586 /** To the trash, with every page under it. */
587 archivePage(workspace: string, pageId: string, viewer: User): Promise<Result<DocPage>>;
588 restorePage(workspace: string, pageId: string, viewer: User): Promise<Result<DocPage>>;
589 /** For good: only from the trash, manage role. */
590 deletePage(workspace: string, pageId: string, viewer: User): Promise<Result<boolean>>;
591 trash(workspace: string, viewer: User): Promise<Result<DocPage[]>>;
592 favorite(workspace: string, pageId: string, viewer: User, on: boolean): Promise<Result<boolean>>;
593
594 search(workspace: string, viewer: User, query: DocSearchQuery): Promise<Result<DocSearchHit[]>>;
595
596 versions(workspace: string, pageId: string, viewer: User): Promise<Result<DocVersion[]>>;
597 version(workspace: string, pageId: string, versionId: string, viewer: User): Promise<Result<DocVersionDetail>>;
598 /** Makes the page what it was then, as a new version. Edit role. */
599 restoreVersion(workspace: string, pageId: string, versionId: string, viewer: User): Promise<Result<DocVersion>>;
600
601 templates(workspace: string, viewer: User): Promise<Result<DocTemplate[]>>;
602 /** Saves a page as one of the workspace's templates. */
603 saveTemplate(workspace: string, viewer: User, input: { page_id: string; name: string; description?: string | null }): Promise<Result<DocTemplate>>;
604 deleteTemplate(workspace: string, templateId: string, viewer: User): Promise<Result<boolean>>;
605
606 exportPage(workspace: string, pageId: string, viewer: User): Promise<Result<{ filename: string; markdown: string }>>;
607 /** Every page in a space as Markdown, at paths that follow the tree. */
608 exportSpace(workspace: string, spaceId: string, viewer: User): Promise<Result<{ name: string; files: { path: string; markdown: string }[] }>>;
609
610 suggestions(workspace: string, pageId: string, viewer: User): Promise<Result<DocSuggestion[]>>;
611 /** Accept (applies it to the live document, attributed to both) or reject. Edit role. */
612 decideSuggestion(workspace: string, suggestionId: string, viewer: User, decision: "accept" | "reject"): Promise<Result<DocSuggestion>>;
613 acceptAll(workspace: string, pageId: string, viewer: User): Promise<Result<DocSuggestion[]>>;
614
615 /** A comment operation from the editor; comment role (deleting others' comments or threads needs edit). */
616 thread(workspace: string, pageId: string, viewer: User, action: DocThreadAction): Promise<Result<unknown>>;
617 threads(workspace: string, pageId: string, viewer: User): Promise<Result<DocThread[]>>;
618
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store619 /** Clears "possibly out of date": the page says what the code does now. Edit role. */
620 markCurrent(workspace: string, pageId: string, viewer: User): Promise<Result<boolean>>;
621 /** Pages the viewer can read that are possibly out of date, most recently flagged first; `repo` (`owner/name`) narrows to changes there. */
622 stalePages(workspace: string, viewer: User, options?: { repo?: string | null }): Promise<Result<DocPage[]>>;
623
624 /**
625 * Shows a repository's `docs/` folder and README.md in Docs, read from
626 * its default branch. Any member who can read the repository may; the
627 * files are read at once and again on every push to the default branch.
628 */
629 addRepoSpace(workspace: string, viewer: User, repo: string): Promise<Result<DocRepoSpace>>;
630 /** Stops showing it. Whoever added it, or a workspace owner. */
631 removeRepoSpace(workspace: string, viewer: User, id: string): Promise<Result<boolean>>;
632 /** One file of a project's docs, for a viewer who can read the repository; not found otherwise. */
633 repoPage(workspace: string, viewer: User, repo: string, path: string): Promise<Result<DocRepoPage>>;
634
Docs: a workspace knowledge base people and agents write together635 // ── Agents (services/agents) ─────────────────────────────────────────
636 //
637 // Each takes the agent and the person it acts for (`viewer`, the asker).
638 // The service checks the agent is a live agent of the workspace and caps
639 // everything by the viewer's access; `audience`, when given, narrows
640 // reads further to what every person in it can read. A page the agent
641 // may not read is not found, exactly as one that does not exist.
642
643 /** Spaces the agent may read for the viewer (and audience), with what it may do in each. */
644 spacesForAgent(workspace: string, agentId: string, viewer: User, audience?: DocAudience | null): Promise<Result<(Pick<DocSpace, "id" | "slug" | "name" | "description" | "kind" | "agent_mode" | "projects"> & { can: DocAgentAbilities })[]>>;
645 /** A page's Markdown and its top-level blocks (ids for `blocks` targets). */
646 pageMarkdown(workspace: string, agentId: string, viewer: User, pageId: string, audience?: DocAudience | null): Promise<Result<DocAgentPage>>;
647 /** Full-text search over pages every reader can read; at most 20, best first. */
648 searchForAgent(workspace: string, agentId: string, viewer: User, query: DocSearchQuery, audience?: DocAudience | null): Promise<Result<DocSearchHit[]>>;
649 /** Files a tracked suggestion; people with edit access accept or reject it inline. Needs the viewer's comment role. Notifies the page's owners. */
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store650 suggestEdit(workspace: string, agentId: string, viewer: User, pageId: string, edit: DocAgentEdit): Promise<Result<DocSuggestion>>;
Docs: a workspace knowledge base people and agents write together651 /**
652 * Applies an edit to the live document when the space lets agents edit
653 * and the viewer can edit; otherwise files it as a suggestion (and says
654 * so in `mode`). Attributed to the agent in the page's history.
655 */
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store656 applyEdit(workspace: string, agentId: string, viewer: User, pageId: string, edit: DocAgentEdit): Promise<Result<DocAgentEditResult>>;
Docs: a workspace knowledge base people and agents write together657 /**
658 * A new page, written by the agent: in `space_id` (the General space
659 * when null), under `parent_id`. Needs the viewer's edit role there
660 * (whatever the space's agent mode: a new page changes nothing anyone
661 * wrote). `source` links where it came from ("write this up").
662 */
663 createPageAsAgent(
664 workspace: string,
665 agentId: string,
666 viewer: User,
667 input: { space_id?: string | null; parent_id?: string | null; title: string; icon?: string | null; markdown: string; source?: { title: string; href: string } | null },
668 ): Promise<Result<DocPageRef>>;
Agents recall what Docs say before they answer or work, and each has required reading669 /**
670 * What the workspace's Docs say about `query`, for an agent about to
671 * answer: the passages closest in meaning (and, where meaning finds too
672 * little, in words), each with the page and heading it came from. Only
673 * from spaces the viewer can read and, with `audience`, everyone it
674 * covers; repository docs only from repositories they can all read.
675 * `spaces` narrows to these space ids first (an agent's required
676 * reading) and fills from the rest. Empty when nothing is close enough.
677 */
678 recallForAgent(
679 workspace: string,
680 agentId: string,
681 viewer: User,
682 input: { query: string; limit?: number | null; spaces?: string[] | null },
683 audience?: DocAudience | null,
684 ): Promise<Result<DocPassage[]>>;
Docs: a workspace knowledge base people and agents write together685 /** A page's comment threads, for an agent asked about them. */
686 threadsForAgent(workspace: string, agentId: string, viewer: User, pageId: string, audience?: DocAudience | null): Promise<Result<DocThread[]>>;
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store687 /**
688 * Pages possibly out of date that the agent may read for the viewer (and
689 * audience), each with the changes that made it so: for a documenter
690 * routine to bring them up to date. At most 50, most recently flagged
691 * first.
692 *
693 * - `repo` (`owner/name`): only pages made stale by a change there.
694 * - `since` (RFC 3339): only pages flagged at or after it.
695 *
696 * Changes in repositories the viewer can't read are left out, and a page
697 * whose every change is one of those is left out too: an agent never
698 * learns of code its person can't see. To update a page, read it
699 * (`pageMarkdown`), then `applyEdit` or `suggestEdit` with
700 * `marks_current: true`: the page is marked current when the edit is
701 * applied (at once, or when a person accepts the suggestion).
702 */
703 stalePagesForAgent(
704 workspace: string,
705 agentId: string,
706 viewer: User,
707 options?: { repo?: string | null; since?: string | null },
708 audience?: DocAudience | null,
709 ): Promise<Result<DocStalePage[]>>;
Docs: a workspace knowledge base people and agents write together710};
711
712async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
713 const response = await service.fetch(`https://service/rpc/${method}`, {
714 method: "POST",
715 headers: { "content-type": "application/json" },
716 body: JSON.stringify(args),
717 });
718 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
719 return (await response.json()) as T;
720}
721
722export function docsClient(service: ServiceBinding): DocsApi {
723 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
724 return {
725 sidebar: (workspace, viewer) => call("sidebar", { workspace, viewer }),
726 home: (workspace, viewer, options) => call("home", { workspace, viewer, project: options?.project ?? null }),
727 space: (workspace, spaceSlug, viewer) => call("space", { workspace, space: spaceSlug, viewer }),
728 createSpace: (workspace, viewer, input) => call("create_space", { workspace, viewer, input }),
729 updateSpace: (workspace, spaceId, viewer, change) => call("update_space", { workspace, space_id: spaceId, viewer, change }),
730 setSpaceMember: (workspace, spaceId, viewer, member, role) => call("set_space_member", { workspace, space_id: spaceId, viewer, member, role }),
731 page: (workspace, pageId, viewer) => call("page", { workspace, page_id: pageId, viewer }),
732 createPage: (workspace, viewer, input) => call("create_page", { workspace, viewer, input }),
733 updatePage: (workspace, pageId, viewer, change) => call("update_page", { workspace, page_id: pageId, viewer, change }),
734 movePage: (workspace, pageId, viewer, move) => call("move_page", { workspace, page_id: pageId, viewer, move }),
735 duplicatePage: (workspace, pageId, viewer) => call("duplicate_page", { workspace, page_id: pageId, viewer }),
736 archivePage: (workspace, pageId, viewer) => call("archive_page", { workspace, page_id: pageId, viewer }),
737 restorePage: (workspace, pageId, viewer) => call("restore_page", { workspace, page_id: pageId, viewer }),
738 deletePage: (workspace, pageId, viewer) => call("delete_page", { workspace, page_id: pageId, viewer }),
739 trash: (workspace, viewer) => call("trash", { workspace, viewer }),
740 favorite: (workspace, pageId, viewer, on) => call("favorite", { workspace, page_id: pageId, viewer, on }),
741 search: (workspace, viewer, query) => call("search", { workspace, viewer, query }),
742 versions: (workspace, pageId, viewer) => call("versions", { workspace, page_id: pageId, viewer }),
743 version: (workspace, pageId, versionId, viewer) => call("version", { workspace, page_id: pageId, version_id: versionId, viewer }),
744 restoreVersion: (workspace, pageId, versionId, viewer) => call("restore_version", { workspace, page_id: pageId, version_id: versionId, viewer }),
745 templates: (workspace, viewer) => call("templates", { workspace, viewer }),
746 saveTemplate: (workspace, viewer, input) => call("save_template", { workspace, viewer, input }),
747 deleteTemplate: (workspace, templateId, viewer) => call("delete_template", { workspace, template_id: templateId, viewer }),
748 exportPage: (workspace, pageId, viewer) => call("export_page", { workspace, page_id: pageId, viewer }),
749 exportSpace: (workspace, spaceId, viewer) => call("export_space", { workspace, space_id: spaceId, viewer }),
750 suggestions: (workspace, pageId, viewer) => call("suggestions", { workspace, page_id: pageId, viewer }),
751 decideSuggestion: (workspace, suggestionId, viewer, decision) => call("decide_suggestion", { workspace, suggestion_id: suggestionId, viewer, decision }),
752 acceptAll: (workspace, pageId, viewer) => call("accept_all", { workspace, page_id: pageId, viewer }),
753 thread: (workspace, pageId, viewer, action) => call("thread", { workspace, page_id: pageId, viewer, action }),
754 threads: (workspace, pageId, viewer) => call("threads", { workspace, page_id: pageId, viewer }),
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store755 markCurrent: (workspace, pageId, viewer) => call("mark_current", { workspace, page_id: pageId, viewer }),
756 stalePages: (workspace, viewer, options) => call("stale_pages", { workspace, viewer, repo: options?.repo ?? null }),
757 addRepoSpace: (workspace, viewer, repo) => call("add_repo_space", { workspace, viewer, repo }),
758 removeRepoSpace: (workspace, viewer, id) => call("remove_repo_space", { workspace, viewer, id }),
759 repoPage: (workspace, viewer, repo, path) => call("repo_page", { workspace, viewer, repo, path }),
Docs: a workspace knowledge base people and agents write together760 spacesForAgent: (workspace, agentId, viewer, audience) => call("spaces_for_agent", { workspace, agent_id: agentId, viewer, audience: audience ?? null }),
761 pageMarkdown: (workspace, agentId, viewer, pageId, audience) =>
762 call("page_markdown", { workspace, agent_id: agentId, viewer, page_id: pageId, audience: audience ?? null }),
763 searchForAgent: (workspace, agentId, viewer, query, audience) => call("search_for_agent", { workspace, agent_id: agentId, viewer, query, audience: audience ?? null }),
764 suggestEdit: (workspace, agentId, viewer, pageId, edit) => call("suggest_edit", { workspace, agent_id: agentId, viewer, page_id: pageId, edit }),
765 applyEdit: (workspace, agentId, viewer, pageId, edit) => call("apply_edit", { workspace, agent_id: agentId, viewer, page_id: pageId, edit }),
766 createPageAsAgent: (workspace, agentId, viewer, input) => call("create_page_as_agent", { workspace, agent_id: agentId, viewer, input }),
Agents recall what Docs say before they answer or work, and each has required reading767 recallForAgent: (workspace, agentId, viewer, input, audience) =>
768 call("recall_for_agent", { workspace, agent_id: agentId, viewer, ...input, audience: audience ?? null }),
Docs: a workspace knowledge base people and agents write together769 threadsForAgent: (workspace, agentId, viewer, pageId, audience) =>
770 call("threads_for_agent", { workspace, agent_id: agentId, viewer, page_id: pageId, audience: audience ?? null }),
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store771 stalePagesForAgent: (workspace, agentId, viewer, options, audience) =>
772 call("stale_pages_for_agent", { workspace, agent_id: agentId, viewer, repo: options?.repo ?? null, since: options?.since ?? null, audience: audience ?? null }),
Docs: a workspace knowledge base people and agents write together773 };
774}