| 1 | /** |
| 2 | * Starting work on an issue in a session on the person's own Claude. |
| 3 | * Mirrors `crates/contracts/src/agent_sessions.rs` (the integrations |
| 4 | * service's `start_agent_session`, `agent_sessions_setup`, |
| 5 | * `sync_agent_session` and `stop_agent_session`). |
| 6 | */ |
| 7 | import type { ServiceBinding } from "./clients"; |
| 8 | import type { User, Viewer } from "./identity"; |
| 9 | import type { RepoPath } from "./repos"; |
| 10 | import type { Result } from "./result"; |
| 11 | import type { WorkSession } from "./work"; |
| 12 | |
| 13 | /** What kind of work a session is asked for. */ |
| 14 | export type AgentMode = "plan" | "implement" | "review"; |
| 15 | |
| 16 | export const AGENT_MODES: { mode: AgentMode; label: string; hint: string }[] = [ |
| 17 | { mode: "implement", label: "Implement", hint: "Make the change and open a pull request." }, |
| 18 | { mode: "plan", label: "Plan", hint: "Work out how, and post the plan on the issue. No code." }, |
| 19 | { mode: "review", label: "Review", hint: "Review the issue's pull request." }, |
| 20 | ]; |
| 21 | |
| 22 | /** What `start_agent_session` started, or the hand-off to run. */ |
| 23 | export type AgentSessionStarted = { |
| 24 | issue: number; |
| 25 | session: WorkSession | null; |
| 26 | sessionUrl: string | null; |
| 27 | provider: string | null; |
| 28 | providerSessionId: string | null; |
| 29 | providerSessionUrl: string | null; |
| 30 | handoff: boolean; |
| 31 | command: string | null; |
| 32 | prompt: string; |
| 33 | }; |
| 34 | |
| 35 | /** Whether sessions can start on the workspace's Anthropic account. */ |
| 36 | export type AgentSessionsSetup = { |
| 37 | available: boolean; |
| 38 | connectionName: string | null; |
| 39 | model: string | null; |
| 40 | problem: string | null; |
| 41 | }; |
| 42 | |
| 43 | /** What to start: an issue, or a new one from a title and body. */ |
| 44 | export type StartAgentSessionInput = { |
| 45 | issue?: number; |
| 46 | title?: string; |
| 47 | body?: string; |
| 48 | instruction?: string; |
| 49 | mode?: AgentMode; |
| 50 | /** true: the Claude Code hand-off only; false: a session on the workspace's account, or a refusal. */ |
| 51 | handoff?: boolean; |
| 52 | }; |
| 53 | |
| 54 | /** The integrations service's methods for sessions on a workspace's Anthropic account. */ |
| 55 | export interface AgentSessionsApi { |
| 56 | setup(workspace: string, viewer: Viewer): Promise<Result<AgentSessionsSetup>>; |
| 57 | start(actor: User, repo: RepoPath, input: StartAgentSessionInput): Promise<Result<AgentSessionStarted>>; |
| 58 | /** Reads the session now (it is otherwise followed every minute). Whether it is still going. */ |
| 59 | sync(repo: RepoPath, id: string, viewer: Viewer): Promise<Result<boolean>>; |
| 60 | /** Interrupts it and ends its record. Only whoever started it. */ |
| 61 | stop(actor: User, repo: RepoPath, id: string): Promise<Result<boolean>>; |
| 62 | } |
| 63 | |
| 64 | async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> { |
| 65 | const response = await service.fetch(`https://service/rpc/${method}`, { |
| 66 | method: "POST", |
| 67 | headers: { "content-type": "application/json" }, |
| 68 | body: JSON.stringify(args), |
| 69 | }); |
| 70 | if (!response.ok) { |
| 71 | throw new Error(`${method} failed with status ${response.status}`); |
| 72 | } |
| 73 | return (await response.json()) as T; |
| 74 | } |
| 75 | |
| 76 | export function agentSessionsClient(service: ServiceBinding): AgentSessionsApi { |
| 77 | const call = <T>(method: string, args: object) => rpc<T>(service, method, args); |
| 78 | return { |
| 79 | setup: (workspace, viewer) => call("agent_sessions_setup", { workspace, viewer }), |
| 80 | start: (actor, repo, input) => |
| 81 | call("start_agent_session", { |
| 82 | actor, |
| 83 | repo, |
| 84 | issue: input.issue ?? null, |
| 85 | title: input.title ?? null, |
| 86 | body: input.body ?? null, |
| 87 | instruction: input.instruction ?? null, |
| 88 | mode: input.mode ?? null, |
| 89 | handoff: input.handoff ?? null, |
| 90 | }), |
| 91 | sync: (repo, id, viewer) => call("sync_agent_session", { repo, id, viewer }), |
| 92 | stop: (actor, repo, id) => call("stop_agent_session", { actor, repo, id }), |
| 93 | }; |
| 94 | } |