Skip to content
147 linesCodeBlameRaw

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.

Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.1/**
2 * An agent's own computer, from the agents service's side
3 * (docs.g1t.sh/guides/agents/, "Its computer"). The computer itself is the
4 * runner's `AgentComputer` object (services/runner computer.ts), reached
5 * through the RUNNER binding; here is what a session's tools call, and what
6 * the Computer tab reads and presses.
7 *
8 * Sessions only: a chat reply never gets these ports, so a quick answer
9 * never wakes a machine. Each session works in its own directory under the
10 * home, `/home/agent/sessions/<id>`, and shares the home with every other
11 * session of the agent. Every command is a `command` entry in the session's
12 * transcript (transcript.ts), and is kept on the computer's own page.
13 */
14import type { AbilitySection, AgentComputerCommand, AgentComputerView, Result, ServiceBinding, User } from "@g1t/contracts";
15
16// Relative, with extensions, so Node runs the tests on this file as it is.
17import { runnerComputerClient } from "../../../packages/contracts/src/clients.ts";
18import { fail, ok } from "../../../packages/contracts/src/result.ts";
19import { canChange, canSeeAgent } from "./access.ts";
20import type { Row } from "./store.ts";
21import type { ComputerPorts } from "./tools.ts";
22
23export type ComputerEnv = { RUNNER?: ServiceBinding };
24
25/** The home on the computer, and where a session's work goes by default. */
26export const COMPUTER_HOME = "/home/agent";
27export function sessionCwd(sessionId: string): string {
28 return `${COMPUTER_HOME}/sessions/${sessionId}`;
29}
30
31/** Whether the agent's abilities give it a computer this turn: shell or files, ready and not Never. */
32export function hasComputer(sections: AbilitySection[]): boolean {
33 return sections.some((section) => section.group === "computer" && section.sources.some((source) => source.abilities.some((ability) => ability.status === "ready" && ability.level !== "never")));
34}
35
36const NOT_HERE = "Agents' computers aren't available on this installation.";
37
38/**
39 * The ports a session's tool box calls (tools.ts `useComputer`): each
40 * command names the agent, its workspace and who asked, so the runner
41 * meters the machine time to them; `onCommand` is told of every command
42 * that ran, for the transcript.
43 */
44export function computerPorts(
45 env: ComputerEnv,
46 input: { agent: Row; workspace: string; session: { id: string }; asker: { username: string | null }; onCommand: (command: AgentComputerCommand) => void },
47): ComputerPorts | null {
48 if (!env.RUNNER) return null;
49 const runner = runnerComputerClient(env.RUNNER);
50 const who = { agent_id: input.agent.id, workspace: input.workspace, agent_handle: input.agent.handle, asked_by: input.asker.username };
51 const cwd = sessionCwd(input.session.id);
52 const outcome = <T, U>(result: Result<T>, map: (value: T) => U) => (result.ok ? { ok: true as const, value: map(result.value) } : { ok: false as const, code: result.error.code, message: result.error.message });
53 return {
54 cwd,
55 async exec(cmd, dir, timeoutSeconds) {
56 const ran = await runner.computerExec({ ...who, cmd, cwd: dir ?? cwd, timeout_seconds: timeoutSeconds, session_id: input.session.id });
57 if (ran.ok) input.onCommand(ran.value.command);
58 return outcome(ran, (value) => value.command);
59 },
60 async readFile(path) {
61 return outcome(await runner.computerReadFile({ ...who, path, session_id: input.session.id }), (value) => value);
62 },
63 async writeFile(path, text) {
64 return outcome(await runner.computerWriteFile({ ...who, path, text, session_id: input.session.id }), (value) => value);
65 },
66 };
67}
68
69// ── The Computer tab ──────────────────────────────────────────────────────
70
71/** What the views need: the workspace, the viewer, and the agents' database. */
72export type ComputerCtx = { env: ComputerEnv; db: D1Database; slug: string; workspaceId: string; viewer: User };
73
74/** The agent by handle, if the viewer may see it: a personal agent only its member and the owners. */
75async function agentRow(ctx: ComputerCtx, handle: unknown): Promise<Row | null> {
76 const row = await ctx.db
77 .prepare("SELECT * FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL")
78 .bind(ctx.workspaceId, String(handle ?? "").trim().replace(/^@/, "").toLowerCase())
79 .first<Row>();
80 return row && canSeeAgent(ctx.viewer, ctx.slug, row) ? row : null;
81}
82
83const NO_AGENT = (handle: unknown) => fail("not_found", `There is no agent called @${String(handle ?? "")}.`);
84
85async function view(ctx: ComputerCtx, row: Row): Promise<Result<AgentComputerView>> {
86 if (!ctx.env.RUNNER) return fail("unavailable", NOT_HERE);
87 const runner = runnerComputerClient(ctx.env.RUNNER);
88 const [status, commands] = await Promise.all([runner.computerStatus(row.id), runner.computerCommands(row.id, null)]);
89 if (!status.ok) return status;
90 return ok({ status: status.value, commands: commands.ok ? commands.value : [], can_manage: canChange(ctx.viewer, ctx.slug, row) });
91}
92
93/** The agent's computer: its state, disk and recent commands, for anyone who may see the agent. */
94export async function computerView(ctx: ComputerCtx, handle: unknown): Promise<Result<AgentComputerView>> {
95 const row = await agentRow(ctx, handle);
96 if (!row) return NO_AGENT(handle);
97 return view(ctx, row);
98}
99
100/** The agent, if the viewer may act on its computer: owners a workspace agent's, its member a personal one's. */
101async function managed(ctx: ComputerCtx, handle: unknown): Promise<Result<Row>> {
102 const row = await agentRow(ctx, handle);
103 if (!row) return NO_AGENT(handle);
104 if (!canChange(ctx.viewer, ctx.slug, row)) return fail("forbidden", row.scope === "personal" ? "Only the person whose personal agent this is can manage its computer." : "Only the workspace's owners wake, sleep or reset an agent's computer.");
105 if (!ctx.env.RUNNER) return fail("unavailable", NOT_HERE);
106 return ok(row);
107}
108
109export async function wakeComputer(ctx: ComputerCtx, handle: unknown): Promise<Result<AgentComputerView>> {
110 const row = await managed(ctx, handle);
111 if (!row.ok) return row;
112 const woke = await runnerComputerClient(ctx.env.RUNNER!).computerWake({ agent_id: row.value.id, workspace: ctx.slug, agent_handle: row.value.handle, asked_by: ctx.viewer.username });
113 if (!woke.ok) return woke;
114 return view(ctx, row.value);
115}
116
117export async function sleepComputer(ctx: ComputerCtx, handle: unknown): Promise<Result<AgentComputerView>> {
118 const row = await managed(ctx, handle);
119 if (!row.ok) return row;
120 const slept = await runnerComputerClient(ctx.env.RUNNER!).computerSleep(row.value.id);
121 if (!slept.ok) return slept;
122 return view(ctx, row.value);
123}
124
125export async function resetComputer(ctx: ComputerCtx, handle: unknown): Promise<Result<AgentComputerView>> {
126 const row = await managed(ctx, handle);
127 if (!row.ok) return row;
128 const reset = await runnerComputerClient(ctx.env.RUNNER!).computerReset(row.value.id);
129 if (!reset.ok) return reset;
130 return view(ctx, row.value);
131}
132
133/** The computer's recent commands, newest first; `sessionId` narrows them to one session's. */
134export async function computerCommands(ctx: ComputerCtx, handle: unknown, sessionId: unknown): Promise<Result<AgentComputerCommand[]>> {
135 const row = await agentRow(ctx, handle);
136 if (!row) return NO_AGENT(handle);
137 if (!ctx.env.RUNNER) return fail("unavailable", NOT_HERE);
138 return runnerComputerClient(ctx.env.RUNNER).computerCommands(row.id, typeof sessionId === "string" && sessionId ? sessionId : null);
139}
140
141/** An archived agent's computer: its disk is kept 30 days, then deleted. Never fails the archive. */
142export async function forgetComputer(env: ComputerEnv, agentId: string): Promise<void> {
143 if (!env.RUNNER) return;
144 await runnerComputerClient(env.RUNNER)
145 .computerForget(agentId)
146 .catch((error: unknown) => console.error("agents: an archived agent's computer was not told", agentId, String(error)));
147}