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 | */ | |
| 14 | import 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. | |
| 17 | import { runnerComputerClient } from "../../../packages/contracts/src/clients.ts"; | |
| 18 | import { fail, ok } from "../../../packages/contracts/src/result.ts"; | |
| 19 | import { canChange, canSeeAgent } from "./access.ts"; | |
| 20 | import type { Row } from "./store.ts"; | |
| 21 | import type { ComputerPorts } from "./tools.ts"; | |
| 22 | ||
| 23 | export type ComputerEnv = { RUNNER?: ServiceBinding }; | |
| 24 | ||
| 25 | /** The home on the computer, and where a session's work goes by default. */ | |
| 26 | export const COMPUTER_HOME = "/home/agent"; | |
| 27 | export 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. */ | |
| 32 | export 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 | ||
| 36 | const 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 | */ | |
| 44 | export 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. */ | |
| 72 | export 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. */ | |
| 75 | async 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 | ||
| 83 | const NO_AGENT = (handle: unknown) => fail("not_found", `There is no agent called @${String(handle ?? "")}.`); | |
| 84 | ||
| 85 | async 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. */ | |
| 94 | export 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. */ | |
| 101 | async 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 | ||
| 109 | export 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 | ||
| 117 | export 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 | ||
| 125 | export 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. */ | |
| 134 | export 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. */ | |
| 142 | export 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 | } |