flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/packages/contracts/src/context.ts

254 lines10,640 bytesCodeBlame

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 API1/**
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
12import type { Memory, MemoryKind } from "./agents";
13import type { ServiceBinding } from "./clients";
14import type { User, Viewer } from "./identity";
15import type { RepoPath } from "./repos";
16import type { Result } from "./result";
17
18export type EntityKind =
19 | "project"
20 | "app"
21 | "api"
22 | "package"
23 | "language"
24 | "owner"
25 | "environment"
26 | "integration"
27 | "doc";
28
29export const ENTITY_KINDS: EntityKind[] = ["project", "app", "api", "package", "language", "owner", "environment", "integration", "doc"];
30
31export type RelationKind = "depends_on" | "owned_by" | "deploys_to" | "documented_by" | "exposes" | "uses";
32
33export 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
55export type Relation = { from: string; kind: RelationKind; to: string };
56
57export type Catalog = {
58 entities: Entity[];
59 relations: Relation[];
60 /** When the catalog was last built from the repositories. */
61 builtAt: string | null;
62};
63
64export 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. */
71export type SearchKind = EntityKind | "memory" | "issue" | "pull";
72export const SEARCH_KINDS: SearchKind[] = [...ENTITY_KINDS, "memory", "issue", "pull"];
73
74export 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
93export 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
100export type SearchOptions = { project?: string | null; kinds?: SearchKind[] | null; limit?: number | null };
101
102export type ScoreRule = "has_owner" | "has_readme" | "has_agents_md" | "tests_in_ci" | "production_green" | "no_secret_findings";
103
104export 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
114export 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
124export 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
141export 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
150export 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
157export 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
174async 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
184export 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. */
200export type CaptureSource = "run" | "review" | "pr" | "doc" | "manual";
201
202export 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
217export type Captured = { added: number; merged: number; kept: number; refused: number };
218
219export type ReviewDecision = "keep" | "dismiss";
220
221export 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. */
243export 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}