flagon-io/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

g1t/packages/contracts/src/context.ts

260 lines11,000 bytesCodeBlame
/**
 * The context hub: one place to ask about a workspace. The context service
 * keeps a **catalog** of what the workspace builds and runs (projects,
 * their apps, APIs, packages, languages, owners, environments, integrations
 * and docs, and how they relate), built by itself from the repositories and
 * from projects, deployments and integrations; **search** across the
 * catalog, docs, issues, pull requests and memory; and **scorecards** for
 * each project. Memory itself, candidates and their review, is kept by
 * the work service: see `memoryReviewClient`.
 */

import type { Memory, MemoryKind } from "./agents";
import type { ServiceBinding } from "./clients";
import type { User, Viewer } from "./identity";
import type { RepoPath } from "./repos";
import type { Result } from "./result";

export type EntityKind =
  | "project"
  | "app"
  | "api"
  | "package"
  | "language"
  | "owner"
  | "environment"
  | "integration"
  | "doc";

export const ENTITY_KINDS: EntityKind[] = ["project", "app", "api", "package", "language", "owner", "environment", "integration", "doc"];

export type RelationKind = "depends_on" | "owned_by" | "deploys_to" | "documented_by" | "exposes" | "uses";

export type Entity = {
  id: string;
  workspace: string;
  kind: EntityKind;
  /** Unique for its kind in the workspace: a project's slug, `npm:<name>`, a username. */
  key: string;
  name: string;
  summary: string | null;
  /** The project it belongs to, by slug; null for what the workspace shares (owners, languages, integrations). */
  project: string | null;
  /** Whether only members see it: it comes from a private repository. */
  private: boolean;
  /** What it is, by kind: a package's version and dependencies, an API's routes, an app's address. */
  data: Record<string, unknown>;
  /** Where it was found: `scan` (the repository), `projects`, `deployments`, `integrations`. */
  source: string;
  /** Where to see it: a path on g1t.sh, or an address. */
  ref: string | null;
  /** RFC 3339. */
  updatedAt: string;
};

export type Relation = { from: string; kind: RelationKind; to: string };

export type Catalog = {
  entities: Entity[];
  relations: Relation[];
  /** When the catalog was last built from the repositories. */
  builtAt: string | null;
};

export type EntityDetail = {
  entity: Entity;
  /** Each relation with the entity at its other end. */
  relations: { kind: RelationKind; direction: "out" | "in"; entity: Entity }[];
};

/** What search can return: a catalog entity's kind, or one of these. */
export type SearchKind = EntityKind | "memory" | "issue" | "pull";
export const SEARCH_KINDS: SearchKind[] = [...ENTITY_KINDS, "memory", "issue", "pull"];

export type SearchHit = {
  kind: SearchKind;
  id: string;
  title: string;
  snippet: string;
  /** The project it is about, by slug. */
  project: string | null;
  /** Where to see it on g1t.sh, or an address. */
  url: string | null;
  /** Higher is closer; text matches score below semantic ones. */
  score: number;
  /** Where it came from, such as `AGENTS.md`, `review on #12`, `catalog`. */
  source: string;
  /** Who wrote it, for memory, issues and pull requests. */
  by: string | null;
  /** RFC 3339. */
  updatedAt: string | null;
};

export type SearchResult = {
  query: string;
  hits: SearchHit[];
  /** `semantic` when the search index answered; `text` when it fell back to matching words. */
  mode: "semantic" | "text";
};

export type SearchOptions = { project?: string | null; kinds?: SearchKind[] | null; limit?: number | null };

export type ScoreRule = "has_owner" | "has_readme" | "has_agents_md" | "tests_in_ci" | "production_green" | "no_secret_findings";

export type RuleResult = {
  rule: ScoreRule;
  title: string;
  /** `na` when the rule does not apply, such as production for a project that does not deploy. */
  status: "pass" | "fail" | "na";
  detail: string;
  /** For a failing rule: an issue an agent can fix it from. */
  fix: { title: string; body: string; checks: string[] } | null;
};

export type Scorecard = {
  project: string;
  name: string;
  repo: RepoPath;
  passed: number;
  /** The rules that apply. */
  total: number;
  rules: RuleResult[];
};

export type Backfill = {
  workspace: string;
  status: "running" | "done" | "failed";
  by: string;
  projects: number;
  done: number;
  entities: number;
  /** Memory candidates added, and how many of them were kept at once. */
  candidates: number;
  kept: number;
  /** Things put in the search index. */
  indexed: number;
  error: string | null;
  startedAt: string;
  finishedAt: string | null;
};

export type ContextStatus = {
  backfill: Backfill | null;
  counts: Partial<Record<EntityKind, number>>;
  /** What the search index cost the workspace this month. */
  usage: { month: string; tokens: number; costMicros: number };
  /** Whether semantic search is available: set up, and on a paid plan or the trial. Text search always is. */
  semantic: boolean;
};

export type RunContext = {
  /** The Context section for an agent's prompt; null when there is nothing to say. */
  text: string | null;
  /** What it drew on, such as `catalog:web`, `memory:mem_…`. */
  sources: string[];
};

export interface ContextApi {
  /** The workspace's catalog, by kind and project. Members, or anyone for public projects' entries. */
  catalog(workspace: string, viewer: Viewer, filter?: { kind?: EntityKind | null; project?: string | null }): Promise<Result<Catalog>>;
  /** One entity, by its id or its key, with its relations. */
  entity(workspace: string, viewer: Viewer, kind: EntityKind, id: string): Promise<Result<EntityDetail>>;
  /** One search across the catalog, docs, issues, pull requests and, for members, memory. */
  search(workspace: string, viewer: Viewer, query: string, options?: SearchOptions): Promise<Result<SearchResult>>;
  /** Each project's scorecard. Members only. */
  scorecards(workspace: string, viewer: Viewer): Promise<Result<Scorecard[]>>;
  /** Builds the catalog for every project and seeds memory from docs and merged pull requests. Members only; once at a time. */
  backfill(actor: User, workspace: string): Promise<Result<Backfill>>;
  /** The hub's state for a workspace. Starts its first backfill when it has none. Members only. */
  status(workspace: string, viewer: Viewer): Promise<Result<ContextStatus>>;
  /**
   * For the runner: the Context section for an agent starting work in a
   * repository, within `budget` characters. `requester` is the person the
   * run acts for: it holds only what they may read. One who is not a member
   * of the workspace gets the project's memory only, never the workspace's,
   * and only the dependencies whose projects they can read.
   */
  runContext(repoId: string, task: string, budget?: number, requester?: Viewer): Promise<RunContext>;
}

async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
  const response = await service.fetch(`https://service/rpc/${method}`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(args),
  });
  if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
  return (await response.json()) as T;
}

export function contextClient(service: ServiceBinding): ContextApi {
  const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
  return {
    catalog: (workspace, viewer, filter = {}) => call("catalog", { workspace, viewer, ...filter }),
    entity: (workspace, viewer, kind, id) => call("entity", { workspace, viewer, kind, id }),
    search: (workspace, viewer, query, options = {}) => call("search", { workspace, viewer, query, ...options }),
    scorecards: (workspace, viewer) => call("scorecards", { workspace, viewer }),
    backfill: (actor, workspace) => call("backfill", { actor, workspace }),
    status: (workspace, viewer) => call("status", { workspace, viewer }),
    runContext: (repoId, task, budget, requester) => call("run_context", { repoId, task, budget, requester: requester ?? null }),
  };
}

// --- Memory capture and review (the work service) ---------------------------

/** Where a captured memory came from. */
export type CaptureSource = "run" | "review" | "pr" | "doc" | "manual";

export type CaptureItem = {
  scope: "project" | "workspace";
  /** For a project's memory: its repository's id. */
  repoId?: string | null;
  kind?: MemoryKind;
  text: string;
  confidence?: number | null;
  source: CaptureSource;
  /** One source: `doc:<repo id>:<path>`, `run:<id>`, `comment:<id>`, `pull:<repo id>#<n>`. */
  reference: string;
  evidence?: string | null;
  number?: number | null;
  runId?: string | null;
};

export type Captured = { added: number; merged: number; kept: number; refused: number };

export type ReviewDecision = "keep" | "dismiss";

export interface MemoryReviewApi {
  /** Candidates waiting for review in a workspace, or for one project. Members only. */
  listCandidates(viewer: Viewer, workspace: string, repo?: RepoPath | null): Promise<Result<Memory[]>>;
  /** Keep a candidate, edited or as it is, or dismiss it. Members only. */
  reviewMemory(
    actor: User,
    workspace: string,
    id: string,
    decision: ReviewDecision,
    change?: { text?: string; kind?: MemoryKind },
  ): Promise<Result<Memory>>;
  /** For services: candidates from docs and backfills. */
  captureMemories(workspace: string, items: CaptureItem[], by?: string): Promise<Captured>;
  /** For services: memories by id, in any status. */
  memoriesById(workspace: string, ids: string[]): Promise<Memory[]>;
  /** For services: kept memories with every word of `query`; the caller has checked the viewer may read them. */
  searchMemories(workspace: string, query: string | null, options?: { repoIds?: string[] | null; limit?: number }): Promise<Memory[]>;
  /** For services: decision and convention candidates from a repository's last merged pull requests. */
  seedFromPulls(repoId: string, limit?: number): Promise<Captured>;
}

/** Memory capture and review: methods of the work service. */
export function memoryReviewClient(service: ServiceBinding): MemoryReviewApi {
  const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
  return {
    listCandidates: (viewer, workspace, repo) => call("list_candidates", { viewer, workspace, repo: repo ?? null }),
    reviewMemory: (actor, workspace, id, decision, change = {}) => call("review_memory", { actor, workspace, id, decision, ...change }),
    captureMemories: (workspace, items, by) => call("capture_memories", { workspace, items, by: by ?? null }),
    memoriesById: (workspace, ids) => call("memories_by_id", { workspace, ids }),
    searchMemories: (workspace, query, options = {}) =>
      call("search_memories", { workspace, query, repoIds: options.repoIds ?? null, limit: options.limit ?? 20 }),
    seedFromPulls: (repoId, limit) => call("seed_from_pulls", { repoId, limit: limit ?? null }),
  };
}