g1t/packages/contracts/src/agents.ts

273 lines10,962 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 { Confidence, 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`, for its own agent and 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" | "abuse" | null;
81 /** How sure g1t was of the change as this run left it, for a run that made or revised one. */
82 confidence?: Confidence | null;
83 createdAt: string;
84 startedAt: string | null;
85 finishedAt: string | null;
86 updatedAt: string;
87};
88
89export type AgentRunTicket = { runId: string; token: string };
90
91export type OpenRunInput = {
92 actor: User;
93 repo: RepoPath;
94 number?: number | null;
95 pullId?: string | null;
96 title?: string | null;
97 kind: RunKind;
98 agent?: string | null;
99 model?: string | null;
100 sandbox: string;
101 startedBy?: string | null;
102 /** Its caps, from its guardrails. */
103 budgetUsd?: number | null;
104 timeCapMinutes?: number | null;
105};
106
107export type RunFilter = {
108 repo?: RepoPath;
109 workspace?: string;
110 active?: boolean;
111 kind?: RunKind;
112 status?: AgentRunStatus;
113 number?: number;
114 limit?: number;
115};
116
117export type SessionSummary = {
118 number: number;
119 title: string;
120 status: PullStatus;
121 agent: string;
122 entries: number;
123 tools: number;
124 prompt: string | null;
125 kinds: RunKind[];
126 runs: number;
127 costUsd: number | null;
128 active: boolean;
129 startedAt: string;
130 lastAt: string;
131};
132
133export type SessionFilter = { kind?: RunKind; outcome?: PullStatus; number?: number };
134
135export type SessionView = {
136 pull: Pull;
137 entries: SessionEntry[];
138 runs: AgentRun[];
139 costUsd: number | null;
140};
141
142export type MemoryScope = "project" | "workspace";
143export type MemoryKind = "fact" | "convention" | "decision" | "gotcha";
144export const MEMORY_KINDS: MemoryKind[] = ["fact", "convention", "decision", "gotcha"];
145
146export type MemorySource = {
147 /** `person`, `agent` or `run`; for captured memory `review`, `pr` or `doc` too (see `./context`). */
148 kind: string;
149 runId: string | null;
150 repo: RepoPath | null;
151 number: number | null;
152 /** What it was captured from: `run:<id>`, `comment:<id>`, `pull:<repo id>#<n>`, `doc:<repo id>:<path>`. */
153 reference?: string | null;
154 /** What it was learned from, quoted. */
155 evidence?: string | null;
156};
157
158/** Kept memories are given to agents; candidates wait for review; dismissed ones are never suggested again. */
159export type MemoryStatus = "candidate" | "kept" | "dismissed";
160
161export type Memory = {
162 id: string;
163 scope: MemoryScope;
164 workspace: string;
165 repo: RepoPath | null;
166 text: string;
167 kind: MemoryKind;
168 source: MemorySource;
169 createdBy: string;
170 pinned: boolean;
171 createdAt: string;
172 updatedAt: string;
173 lastUsedAt: string | null;
174 /** Absent from services that predate capture: `kept`. */
175 status?: MemoryStatus;
176 /** 0 to 1: how sure its source was. Null for what people wrote. */
177 confidence?: number | null;
178 /** How many independent sources said it. */
179 seen?: number;
180};
181
182export type Memories = { project: Memory[]; workspace: Memory[] };
183
184export type NewMemory = {
185 scope: MemoryScope;
186 repo?: RepoPath | null;
187 text: string;
188 kind?: MemoryKind;
189 pinned?: boolean;
190};
191
192export type MemoryChange = { text?: string; kind?: MemoryKind; pinned?: boolean };
193
194export interface AgentsApi {
195 /** For the runner: records a sandbox it is starting. */
196 openRun(input: OpenRunInput): Promise<Result<AgentRunTicket>>;
197 /** For the runner: ends a run when its sandbox stops. Refused once it has ended. */
198 closeRun(runId: string, token: string, outcome: "succeeded" | "failed", error?: string): Promise<Result<AgentRunStatus>>;
199 /** Marks a run stopped and says where its sandbox is. Members only. */
200 stopRun(actor: User, repo: RepoPath, id: string): Promise<Result<{ run: AgentRun; sandbox: string }>>;
201 listRuns(viewer: Viewer, filter: RunFilter): Promise<Result<AgentRun[]>>;
202 getRun(viewer: Viewer, repo: RepoPath, id: string): Promise<Result<AgentRun>>;
203 listSessions(viewer: Viewer, repo: RepoPath, filter?: SessionFilter): Promise<Result<SessionSummary[]>>;
204 getSession(viewer: Viewer, repo: RepoPath, number: number): Promise<Result<SessionView>>;
205 listMemories(viewer: Viewer, workspace: string, repo?: RepoPath | null): Promise<Result<Memories>>;
206 addMemory(actor: User, workspace: string, memory: NewMemory): Promise<Result<Memory>>;
207 updateMemory(actor: User, workspace: string, id: string, change: MemoryChange): Promise<Result<Memory>>;
208 deleteMemory(actor: User, workspace: string, id: string): Promise<Result<boolean>>;
209 /**
210 * For the runner: what to tell an agent starting in `repo`, marked used.
211 * `requester` is the person the run acts for: one who is not a member of
212 * the workspace (an outside collaborator) is given the project's memory
213 * only, never the workspace's.
214 */
215 memoryContext(repo: RepoPath, budget?: number, requester?: User | null): Promise<{ text: string | null; count: number }>;
216 /** For the runner: agent runs the workspace has queued or running, for its plan's cap. */
217 activeAgents(workspace: string): Promise<number>;
218 /** For the runner: what an issue's agents have spent; `number` may be one of its pull requests. */
219 issueSpend(repo: RepoPath, number: number): Promise<Result<IssueSpend>>;
220 /** For the runner: gives back a lifecycle step it could not start for want of a slot. */
221 waitForSlot(pullId: string, reason: string): Promise<boolean>;
222 /** For the runner: a comment from g1t on an issue or pull request. */
223 agentComment(repo: RepoPath, number: number, body: string): Promise<boolean>;
224 /** For the runner: a run a person asked for, waiting for a slot. */
225 addWait(workspace: string, kind: string, payload: unknown): Promise<Result<boolean>>;
226 waitingWorkspaces(): Promise<string[]>;
227 /** The oldest run waiting in a workspace, taken out of the queue. */
228 takeWait(workspace: string): Promise<AgentWait | null>;
229 /** What a run's model cost, in dollars, with the run's token. */
230 runCost(runId: string, token: string): Promise<number | null>;
231}
232
233/** What an issue's agents have spent so far. Mirrors `IssueSpend` in agents.rs. */
234export type IssueSpend = { issue: number; spentMicros: number };
235
236/** A run waiting for one of its workspace's agent slots. */
237export type AgentWait = { id: string; workspace: string; kind: string; payload: unknown; createdAt: string };
238
239/** The agents and memory methods of the work service. */
240export function agentsClient(service: ServiceBinding): AgentsApi {
241 const call = async <T>(method: string, args: object): Promise<T> => {
242 const response = await service.fetch(`https://service/rpc/${method}`, {
243 method: "POST",
244 headers: { "content-type": "application/json" },
245 body: JSON.stringify(args),
246 });
247 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
248 return (await response.json()) as T;
249 };
250 return {
251 openRun: (input) => call("open_run", input),
252 closeRun: (runId, token, outcome, error) => call("report_run", { runId, token, outcome, error }),
253 stopRun: (actor, repo, id) => call("stop_run", { actor, repo, id }),
254 listRuns: (viewer, filter) => call("list_runs", { viewer, ...filter }),
255 getRun: (viewer, repo, id) => call("get_run", { viewer, repo, id }),
256 listSessions: (viewer, repo, filter = {}) => call("list_sessions", { viewer, repo, ...filter }),
257 getSession: (viewer, repo, number) => call("get_session", { viewer, repo, number }),
258 listMemories: (viewer, workspace, repo) => call("list_memories", { viewer, workspace, repo: repo ?? null }),
259 addMemory: (actor, workspace, memory) =>
260 call("add_memory", { actor, workspace, repo: memory.repo ?? null, scope: memory.scope, text: memory.text, kind: memory.kind ?? "fact", pinned: memory.pinned ?? false }),
261 updateMemory: (actor, workspace, id, change) => call("update_memory", { actor, workspace, id, ...change }),
262 deleteMemory: (actor, workspace, id) => call("delete_memory", { actor, workspace, id }),
263 memoryContext: (repo, budget, requester) => call("memory_context", { repo, budget, requester: requester ?? null }),
264 activeAgents: (workspace) => call("active_agents", { workspace }),
265 issueSpend: (repo, number) => call("issue_spend", { repo, number }),
266 waitForSlot: (pullId, reason) => call("wait_for_slot", { pullId, reason }),
267 agentComment: (repo, number, body) => call("agent_comment", { repo, number, body }),
268 addWait: (workspace, kind, payload) => call("add_wait", { workspace, kind, payload }),
269 waitingWorkspaces: () => call("waiting_workspaces", {}),
270 takeWait: (workspace) => call("take_wait", { workspace }),
271 runCost: (runId, token) => call("run_cost", { runId, token }),
272 };
273}