g1t/packages/contracts/src/context.ts
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.
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 1 | /** |
| 2 | * The context hub: one place to ask about a workspace. The context service | |
| 3 | * keeps a **catalog** of what the workspace builds and runs (projects, | |
| 4 | * their apps, APIs, packages, languages, owners, environments, integrations | |
| 5 | * and docs, and how they relate), built by itself from the repositories and | |
| 6 | * from projects, deployments and integrations; **search** across the | |
| 7 | * catalog, docs, issues, pull requests and memory; and **scorecards** for | |
| 8 | * each project. Memory itself, candidates and their review, is kept by | |
| 9 | * the work service: see `memoryReviewClient`. | |
| 10 | */ | |
| 11 | ||
| 12 | import type { Memory, MemoryKind } from "./agents"; | |
| 13 | import type { ServiceBinding } from "./clients"; | |
| 14 | import type { User, Viewer } from "./identity"; | |
| 15 | import type { RepoPath } from "./repos"; | |
| 16 | import type { Result } from "./result"; | |
| 17 | ||
| 18 | export type EntityKind = | |
| 19 | | "project" | |
| 20 | | "app" | |
| 21 | | "api" | |
| 22 | | "package" | |
| 23 | | "language" | |
| 24 | | "owner" | |
| 25 | | "environment" | |
| 26 | | "integration" | |
| 27 | | "doc"; | |
| 28 | ||
| 29 | export const ENTITY_KINDS: EntityKind[] = ["project", "app", "api", "package", "language", "owner", "environment", "integration", "doc"]; | |
| 30 | ||
| 31 | export type RelationKind = "depends_on" | "owned_by" | "deploys_to" | "documented_by" | "exposes" | "uses"; | |
| 32 | ||
| 33 | export type Entity = { | |
| 34 | id: string; | |
| 35 | workspace: string; | |
| 36 | kind: EntityKind; | |
| 37 | /** Unique for its kind in the workspace: a project's slug, `npm:<name>`, a username. */ | |
| 38 | key: string; | |
| 39 | name: string; | |
| 40 | summary: string | null; | |
| 41 | /** The project it belongs to, by slug; null for what the workspace shares (owners, languages, integrations). */ | |
| 42 | project: string | null; | |
| 43 | /** Whether only members see it: it comes from a private repository. */ | |
| 44 | private: boolean; | |
| 45 | /** What it is, by kind: a package's version and dependencies, an API's routes, an app's address. */ | |
| 46 | data: Record<string, unknown>; | |
| 47 | /** Where it was found: `scan` (the repository), `projects`, `deployments`, `integrations`. */ | |
| 48 | source: string; | |
| 49 | /** Where to see it: a path on g1t.sh, or an address. */ | |
| 50 | ref: string | null; | |
| 51 | /** RFC 3339. */ | |
| 52 | updatedAt: string; | |
| 53 | }; | |
| 54 | ||
| 55 | export type Relation = { from: string; kind: RelationKind; to: string }; | |
| 56 | ||
| 57 | export type Catalog = { | |
| 58 | entities: Entity[]; | |
| 59 | relations: Relation[]; | |
| 60 | /** When the catalog was last built from the repositories. */ | |
| 61 | builtAt: string | null; | |
| 62 | }; | |
| 63 | ||
| 64 | export type EntityDetail = { | |
| 65 | entity: Entity; | |
| 66 | /** Each relation with the entity at its other end. */ | |
| 67 | relations: { kind: RelationKind; direction: "out" | "in"; entity: Entity }[]; | |
| 68 | }; | |
| 69 | ||
| 70 | /** What search can return: a catalog entity's kind, or one of these. */ | |
| 71 | export type SearchKind = EntityKind | "memory" | "issue" | "pull"; | |
| 72 | export const SEARCH_KINDS: SearchKind[] = [...ENTITY_KINDS, "memory", "issue", "pull"]; | |
| 73 | ||
| 74 | export type SearchHit = { | |
| 75 | kind: SearchKind; | |
| 76 | id: string; | |
| 77 | title: string; | |
| 78 | snippet: string; | |
| 79 | /** The project it is about, by slug. */ | |
| 80 | project: string | null; | |
| 81 | /** Where to see it on g1t.sh, or an address. */ | |
| 82 | url: string | null; | |
| 83 | /** Higher is closer; text matches score below semantic ones. */ | |
| 84 | score: number; | |
| 85 | /** Where it came from, such as `AGENTS.md`, `review on #12`, `catalog`. */ | |
| 86 | source: string; | |
| 87 | /** Who wrote it, for memory, issues and pull requests. */ | |
| 88 | by: string | null; | |
| 89 | /** RFC 3339. */ | |
| 90 | updatedAt: string | null; | |
| 91 | }; | |
| 92 | ||
| 93 | export type SearchResult = { | |
| 94 | query: string; | |
| 95 | hits: SearchHit[]; | |
| 96 | /** `semantic` when the search index answered; `text` when it fell back to matching words. */ | |
| 97 | mode: "semantic" | "text"; | |
| 98 | }; | |
| 99 | ||
| 100 | export type SearchOptions = { project?: string | null; kinds?: SearchKind[] | null; limit?: number | null }; | |
| 101 | ||
| 102 | export type ScoreRule = "has_owner" | "has_readme" | "has_agents_md" | "tests_in_ci" | "production_green" | "no_secret_findings"; | |
| 103 | ||
| 104 | export type RuleResult = { | |
| 105 | rule: ScoreRule; | |
| 106 | title: string; | |
| 107 | /** `na` when the rule does not apply, such as production for a project that does not deploy. */ | |
| 108 | status: "pass" | "fail" | "na"; | |
| 109 | detail: string; | |
| 110 | /** For a failing rule: an issue an agent can fix it from. */ | |
| 111 | fix: { title: string; body: string; checks: string[] } | null; | |
| 112 | }; | |
| 113 | ||
| 114 | export type Scorecard = { | |
| 115 | project: string; | |
| 116 | name: string; | |
| 117 | repo: RepoPath; | |
| 118 | passed: number; | |
| 119 | /** The rules that apply. */ | |
| 120 | total: number; | |
| 121 | rules: RuleResult[]; | |
| 122 | }; | |
| 123 | ||
| 124 | export type Backfill = { | |
| 125 | workspace: string; | |
| 126 | status: "running" | "done" | "failed"; | |
| 127 | by: string; | |
| 128 | projects: number; | |
| 129 | done: number; | |
| 130 | entities: number; | |
| 131 | /** Memory candidates added, and how many of them were kept at once. */ | |
| 132 | candidates: number; | |
| 133 | kept: number; | |
| 134 | /** Things put in the search index. */ | |
| 135 | indexed: number; | |
| 136 | error: string | null; | |
| 137 | startedAt: string; | |
| 138 | finishedAt: string | null; | |
| 139 | }; | |
| 140 | ||
| 141 | export type ContextStatus = { | |
| 142 | backfill: Backfill | null; | |
| 143 | counts: Partial<Record<EntityKind, number>>; | |
| 144 | /** What the search index cost the workspace this month. */ | |
| 145 | usage: { month: string; tokens: number; costMicros: number }; | |
| 146 | /** Whether semantic search is available; text search always is. */ | |
| 147 | semantic: boolean; | |
| 148 | }; | |
| 149 | ||
| 150 | export type RunContext = { | |
| 151 | /** The Context section for an agent's prompt; null when there is nothing to say. */ | |
| 152 | text: string | null; | |
| 153 | /** What it drew on, such as `catalog:web`, `memory:mem_…`. */ | |
| 154 | sources: string[]; | |
| 155 | }; | |
| 156 | ||
| 157 | export interface ContextApi { | |
| 158 | /** The workspace's catalog, by kind and project. Members, or anyone for public projects' entries. */ | |
| 159 | catalog(workspace: string, viewer: Viewer, filter?: { kind?: EntityKind | null; project?: string | null }): Promise<Result<Catalog>>; | |
| 160 | /** One entity, by its id or its key, with its relations. */ | |
| 161 | entity(workspace: string, viewer: Viewer, kind: EntityKind, id: string): Promise<Result<EntityDetail>>; | |
| 162 | /** One search across the catalog, docs, issues, pull requests and, for members, memory. */ | |
| 163 | search(workspace: string, viewer: Viewer, query: string, options?: SearchOptions): Promise<Result<SearchResult>>; | |
| 164 | /** Each project's scorecard. Members only. */ | |
| 165 | scorecards(workspace: string, viewer: Viewer): Promise<Result<Scorecard[]>>; | |
| 166 | /** Builds the catalog for every project and seeds memory from docs and merged pull requests. Members only; once at a time. */ | |
| 167 | backfill(actor: User, workspace: string): Promise<Result<Backfill>>; | |
| 168 | /** The hub's state for a workspace. Starts its first backfill when it has none. Members only. */ | |
| 169 | status(workspace: string, viewer: Viewer): Promise<Result<ContextStatus>>; | |
| 170 | /** For the runner: the Context section for an agent starting work in a repository, within `budget` characters. */ | |
| 171 | runContext(repoId: string, task: string, budget?: number): Promise<RunContext>; | |
| 172 | } | |
| 173 | ||
| 174 | async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> { | |
| 175 | const response = await service.fetch(`https://service/rpc/${method}`, { | |
| 176 | method: "POST", | |
| 177 | headers: { "content-type": "application/json" }, | |
| 178 | body: JSON.stringify(args), | |
| 179 | }); | |
| 180 | if (!response.ok) throw new Error(`${method} failed with status ${response.status}`); | |
| 181 | return (await response.json()) as T; | |
| 182 | } | |
| 183 | ||
| 184 | export function contextClient(service: ServiceBinding): ContextApi { | |
| 185 | const call = <T>(method: string, args: object) => rpc<T>(service, method, args); | |
| 186 | return { | |
| 187 | catalog: (workspace, viewer, filter = {}) => call("catalog", { workspace, viewer, ...filter }), | |
| 188 | entity: (workspace, viewer, kind, id) => call("entity", { workspace, viewer, kind, id }), | |
| 189 | search: (workspace, viewer, query, options = {}) => call("search", { workspace, viewer, query, ...options }), | |
| 190 | scorecards: (workspace, viewer) => call("scorecards", { workspace, viewer }), | |
| 191 | backfill: (actor, workspace) => call("backfill", { actor, workspace }), | |
| 192 | status: (workspace, viewer) => call("status", { workspace, viewer }), | |
| 193 | runContext: (repoId, task, budget) => call("run_context", { repoId, task, budget }), | |
| 194 | }; | |
| 195 | } | |
| 196 | ||
| 197 | // --- Memory capture and review (the work service) --------------------------- | |
| 198 | ||
| 199 | /** Where a captured memory came from. */ | |
| 200 | export type CaptureSource = "run" | "review" | "pr" | "doc" | "manual"; | |
| 201 | ||
| 202 | export type CaptureItem = { | |
| 203 | scope: "project" | "workspace"; | |
| 204 | /** For a project's memory: its repository's id. */ | |
| 205 | repoId?: string | null; | |
| 206 | kind?: MemoryKind; | |
| 207 | text: string; | |
| 208 | confidence?: number | null; | |
| 209 | source: CaptureSource; | |
| 210 | /** One source: `doc:<repo id>:<path>`, `run:<id>`, `comment:<id>`, `pull:<repo id>#<n>`. */ | |
| 211 | reference: string; | |
| 212 | evidence?: string | null; | |
| 213 | number?: number | null; | |
| 214 | runId?: string | null; | |
| 215 | }; | |
| 216 | ||
| 217 | export type Captured = { added: number; merged: number; kept: number; refused: number }; | |
| 218 | ||
| 219 | export type ReviewDecision = "keep" | "dismiss"; | |
| 220 | ||
| 221 | export interface MemoryReviewApi { | |
| 222 | /** Candidates waiting for review in a workspace, or for one project. Members only. */ | |
| 223 | listCandidates(viewer: Viewer, workspace: string, repo?: RepoPath | null): Promise<Result<Memory[]>>; | |
| 224 | /** Keep a candidate, edited or as it is, or dismiss it. Members only. */ | |
| 225 | reviewMemory( | |
| 226 | actor: User, | |
| 227 | workspace: string, | |
| 228 | id: string, | |
| 229 | decision: ReviewDecision, | |
| 230 | change?: { text?: string; kind?: MemoryKind }, | |
| 231 | ): Promise<Result<Memory>>; | |
| 232 | /** For services: candidates from docs and backfills. */ | |
| 233 | captureMemories(workspace: string, items: CaptureItem[], by?: string): Promise<Captured>; | |
| 234 | /** For services: memories by id, in any status. */ | |
| 235 | memoriesById(workspace: string, ids: string[]): Promise<Memory[]>; | |
| 236 | /** For services: kept memories with every word of `query`; the caller has checked the viewer may read them. */ | |
| 237 | searchMemories(workspace: string, query: string | null, options?: { repoIds?: string[] | null; limit?: number }): Promise<Memory[]>; | |
| 238 | /** For services: decision and convention candidates from a repository's last merged pull requests. */ | |
| 239 | seedFromPulls(repoId: string, limit?: number): Promise<Captured>; | |
| 240 | } | |
| 241 | ||
| 242 | /** Memory capture and review: methods of the work service. */ | |
| 243 | export function memoryReviewClient(service: ServiceBinding): MemoryReviewApi { | |
| 244 | const call = <T>(method: string, args: object) => rpc<T>(service, method, args); | |
| 245 | return { | |
| 246 | listCandidates: (viewer, workspace, repo) => call("list_candidates", { viewer, workspace, repo: repo ?? null }), | |
| 247 | reviewMemory: (actor, workspace, id, decision, change = {}) => call("review_memory", { actor, workspace, id, decision, ...change }), | |
| 248 | captureMemories: (workspace, items, by) => call("capture_memories", { workspace, items, by: by ?? null }), | |
| 249 | memoriesById: (workspace, ids) => call("memories_by_id", { workspace, ids }), | |
| 250 | searchMemories: (workspace, query, options = {}) => | |
| 251 | call("search_memories", { workspace, query, repoIds: options.repoIds ?? null, limit: options.limit ?? 20 }), | |
| 252 | seedFromPulls: (repoId, limit) => call("seed_from_pulls", { repoId, limit: limit ?? null }), | |
| 253 | }; | |
| 254 | } |