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/agents.ts

237 lines8,530 bytesCodeBlame
1/**
2 * Agents at work and what they remember, kept by the work service. Mirrors
3 * `g1t_contracts::agents`.
4 */
5import type { ServiceBinding } from "./clients";
6import type { User, Viewer } from "./identity";
7import type { RepoPath } from "./repos";
8import type { Result } from "./result";
9import type { Pull, PullStatus, SessionEntry } from "./work";
10
11/** The work a run does. checks, queue and mergecheck run commands, not a model. */
12export type RunKind =
13 | "implement"
14 | "revise"
15 | "review"
16 | "answer"
17 | "update"
18 | "plan"
19 | "checks"
20 | "queue"
21 | "mergecheck";
22
23export const RUN_KINDS: RunKind[] = ["implement", "revise", "review", "answer", "update", "plan", "checks", "queue", "mergecheck"];
24
25/** How a kind of run reads in a sentence: "is implementing", "a review run". */
26export const RUN_KIND_LABEL: Record<RunKind, string> = {
27 implement: "Implementing",
28 revise: "Revising",
29 review: "Reviewing",
30 answer: "Answering",
31 update: "Catching up",
32 plan: "Planning",
33 checks: "Checks",
34 queue: "Merge queue",
35 mergecheck: "Merge check",
36};
37
38/** Whether a message reaches the agent while it runs, at its next step. */
39export function takesMessages(kind: RunKind): boolean {
40 return kind === "implement" || kind === "revise" || kind === "answer";
41}
42
43export function isAgentKind(kind: RunKind): boolean {
44 return kind !== "checks" && kind !== "queue" && kind !== "mergecheck";
45}
46
47export type AgentRunStatus = "queued" | "running" | "succeeded" | "failed" | "stopped";
48
49export function isActiveRun(status: AgentRunStatus): boolean {
50 return status === "queued" || status === "running";
51}
52
53export type RunStep = { at: string; text: string };
54
55export type AgentRun = {
56 id: string;
57 repo: RepoPath;
58 number: number | null;
59 title: string | null;
60 kind: RunKind;
61 /** `g1t-agent`, or `g1t` for a sandbox that runs commands. */
62 agent: string;
63 /** Members only. */
64 model: string | null;
65 status: AgentRunStatus;
66 step: string | null;
67 /** Oldest first; empty in lists. */
68 steps: RunStep[];
69 stepCount: number;
70 startedBy: string | null;
71 error: string | null;
72 /** Members only. */
73 costUsd: number | null;
74 turns: number | null;
75 /** The most it may cost, from its guardrails. Null: no cap, or not a member. */
76 budgetUsd?: number | null;
77 /** The longest it may take, in minutes, from its guardrails. */
78 timeCapMinutes?: number | null;
79 /** `budget` or `time` when g1t stopped it for reaching that cap. */
80 halted?: "budget" | "time" | null;
81 createdAt: string;
82 startedAt: string | null;
83 finishedAt: string | null;
84 updatedAt: string;
85};
86
87export type AgentRunTicket = { runId: string; token: string };
88
89export type OpenRunInput = {
90 actor: User;
91 repo: RepoPath;
92 number?: number | null;
93 pullId?: string | null;
94 title?: string | null;
95 kind: RunKind;
96 agent?: string | null;
97 model?: string | null;
98 sandbox: string;
99 startedBy?: string | null;
100 /** Its caps, from its guardrails. */
101 budgetUsd?: number | null;
102 timeCapMinutes?: number | null;
103};
104
105export type RunFilter = {
106 repo?: RepoPath;
107 workspace?: string;
108 active?: boolean;
109 kind?: RunKind;
110 status?: AgentRunStatus;
111 number?: number;
112 limit?: number;
113};
114
115export type SessionSummary = {
116 number: number;
117 title: string;
118 status: PullStatus;
119 agent: string;
120 entries: number;
121 tools: number;
122 prompt: string | null;
123 kinds: RunKind[];
124 runs: number;
125 costUsd: number | null;
126 active: boolean;
127 startedAt: string;
128 lastAt: string;
129};
130
131export type SessionFilter = { kind?: RunKind; outcome?: PullStatus; number?: number };
132
133export type SessionView = {
134 pull: Pull;
135 entries: SessionEntry[];
136 runs: AgentRun[];
137 costUsd: number | null;
138};
139
140export type MemoryScope = "project" | "workspace";
141export type MemoryKind = "fact" | "convention" | "decision" | "gotcha";
142export const MEMORY_KINDS: MemoryKind[] = ["fact", "convention", "decision", "gotcha"];
143
144export type MemorySource = {
145 /** `person`, `agent` or `run`; for captured memory `review`, `pr` or `doc` too (see `./context`). */
146 kind: string;
147 runId: string | null;
148 repo: RepoPath | null;
149 number: number | null;
150 /** What it was captured from: `run:<id>`, `comment:<id>`, `pull:<repo id>#<n>`, `doc:<repo id>:<path>`. */
151 reference?: string | null;
152 /** What it was learned from, quoted. */
153 evidence?: string | null;
154};
155
156/** Kept memories are given to agents; candidates wait for review; dismissed ones are never suggested again. */
157export type MemoryStatus = "candidate" | "kept" | "dismissed";
158
159export type Memory = {
160 id: string;
161 scope: MemoryScope;
162 workspace: string;
163 repo: RepoPath | null;
164 text: string;
165 kind: MemoryKind;
166 source: MemorySource;
167 createdBy: string;
168 pinned: boolean;
169 createdAt: string;
170 updatedAt: string;
171 lastUsedAt: string | null;
172 /** Absent from services that predate capture: `kept`. */
173 status?: MemoryStatus;
174 /** 0 to 1: how sure its source was. Null for what people wrote. */
175 confidence?: number | null;
176 /** How many independent sources said it. */
177 seen?: number;
178};
179
180export type Memories = { project: Memory[]; workspace: Memory[] };
181
182export type NewMemory = {
183 scope: MemoryScope;
184 repo?: RepoPath | null;
185 text: string;
186 kind?: MemoryKind;
187 pinned?: boolean;
188};
189
190export type MemoryChange = { text?: string; kind?: MemoryKind; pinned?: boolean };
191
192export interface AgentsApi {
193 /** For the runner: records a sandbox it is starting. */
194 openRun(input: OpenRunInput): Promise<Result<AgentRunTicket>>;
195 /** For the runner: ends a run when its sandbox stops. Refused once it has ended. */
196 closeRun(runId: string, token: string, outcome: "succeeded" | "failed", error?: string): Promise<Result<AgentRunStatus>>;
197 /** Marks a run stopped and says where its sandbox is. Members only. */
198 stopRun(actor: User, repo: RepoPath, id: string): Promise<Result<{ run: AgentRun; sandbox: string }>>;
199 listRuns(viewer: Viewer, filter: RunFilter): Promise<Result<AgentRun[]>>;
200 getRun(viewer: Viewer, repo: RepoPath, id: string): Promise<Result<AgentRun>>;
201 listSessions(viewer: Viewer, repo: RepoPath, filter?: SessionFilter): Promise<Result<SessionSummary[]>>;
202 getSession(viewer: Viewer, repo: RepoPath, number: number): Promise<Result<SessionView>>;
203 listMemories(viewer: Viewer, workspace: string, repo?: RepoPath | null): Promise<Result<Memories>>;
204 addMemory(actor: User, workspace: string, memory: NewMemory): Promise<Result<Memory>>;
205 updateMemory(actor: User, workspace: string, id: string, change: MemoryChange): Promise<Result<Memory>>;
206 deleteMemory(actor: User, workspace: string, id: string): Promise<Result<boolean>>;
207 /** For the runner: what to tell an agent starting in `repo`, marked used. */
208 memoryContext(repo: RepoPath, budget?: number): Promise<{ text: string | null; count: number }>;
209}
210
211/** The agents and memory methods of the work service. */
212export function agentsClient(service: ServiceBinding): AgentsApi {
213 const call = async <T>(method: string, args: object): Promise<T> => {
214 const response = await service.fetch(`https://service/rpc/${method}`, {
215 method: "POST",
216 headers: { "content-type": "application/json" },
217 body: JSON.stringify(args),
218 });
219 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
220 return (await response.json()) as T;
221 };
222 return {
223 openRun: (input) => call("open_run", input),
224 closeRun: (runId, token, outcome, error) => call("report_run", { runId, token, outcome, error }),
225 stopRun: (actor, repo, id) => call("stop_run", { actor, repo, id }),
226 listRuns: (viewer, filter) => call("list_runs", { viewer, ...filter }),
227 getRun: (viewer, repo, id) => call("get_run", { viewer, repo, id }),
228 listSessions: (viewer, repo, filter = {}) => call("list_sessions", { viewer, repo, ...filter }),
229 getSession: (viewer, repo, number) => call("get_session", { viewer, repo, number }),
230 listMemories: (viewer, workspace, repo) => call("list_memories", { viewer, workspace, repo: repo ?? null }),
231 addMemory: (actor, workspace, memory) =>
232 call("add_memory", { actor, workspace, repo: memory.repo ?? null, scope: memory.scope, text: memory.text, kind: memory.kind ?? "fact", pinned: memory.pinned ?? false }),
233 updateMemory: (actor, workspace, id, change) => call("update_memory", { actor, workspace, id, ...change }),
234 deleteMemory: (actor, workspace, id) => call("delete_memory", { actor, workspace, id }),
235 memoryContext: (repo, budget) => call("memory_context", { repo, budget }),
236 };
237}