mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 20:01:27 +00:00
Refactor Foundry GitHub and sandbox flows
This commit is contained in:
parent
4bccd5fc8d
commit
ec8e816d0d
112 changed files with 4026 additions and 2715 deletions
|
|
@ -18,6 +18,7 @@ import type {
|
|||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchSnapshot,
|
||||
WorkbenchTask,
|
||||
TaskWorkbenchTabInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
HistoryEvent,
|
||||
|
|
@ -34,7 +35,7 @@ import type {
|
|||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { ProcessCreateRequest, ProcessInfo, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
||||
import { createMockBackendClient } from "./mock/backend-client.js";
|
||||
import { sandboxInstanceKey, workspaceKey } from "./keys.js";
|
||||
import { sandboxInstanceKey, organizationKey, taskKey } from "./keys.js";
|
||||
|
||||
export type TaskAction = "push" | "sync" | "merge" | "archive" | "kill";
|
||||
|
||||
|
|
@ -103,6 +104,10 @@ interface WorkspaceHandle {
|
|||
revertWorkbenchFile(input: TaskWorkbenchDiffInput): Promise<void>;
|
||||
}
|
||||
|
||||
interface TaskHandle {
|
||||
getWorkbench(): Promise<WorkbenchTask>;
|
||||
}
|
||||
|
||||
interface SandboxInstanceHandle {
|
||||
createSession(input: {
|
||||
prompt: string;
|
||||
|
|
@ -124,9 +129,12 @@ interface SandboxInstanceHandle {
|
|||
}
|
||||
|
||||
interface RivetClient {
|
||||
workspace: {
|
||||
organization: {
|
||||
getOrCreate(key?: string | string[], opts?: { createWithInput?: unknown }): WorkspaceHandle;
|
||||
};
|
||||
task: {
|
||||
get(key?: string | string[]): TaskHandle;
|
||||
};
|
||||
sandboxInstance: {
|
||||
getOrCreate(key?: string | string[], opts?: { createWithInput?: unknown }): SandboxInstanceHandle;
|
||||
};
|
||||
|
|
@ -238,6 +246,7 @@ export interface BackendClient {
|
|||
): Promise<{ providerId: ProviderId; sandboxId: string; state: string; at: number }>;
|
||||
getSandboxAgentConnection(workspaceId: string, providerId: ProviderId, sandboxId: string): Promise<{ endpoint: string; token?: string }>;
|
||||
getWorkbench(workspaceId: string): Promise<TaskWorkbenchSnapshot>;
|
||||
getWorkbenchTask(workspaceId: string, taskId: string): Promise<WorkbenchTask>;
|
||||
subscribeWorkbench(workspaceId: string, listener: () => void): () => void;
|
||||
createWorkbenchTask(workspaceId: string, input: TaskWorkbenchCreateTaskInput): Promise<TaskWorkbenchCreateTaskResponse>;
|
||||
markWorkbenchUnread(workspaceId: string, input: TaskWorkbenchSelectInput): Promise<void>;
|
||||
|
|
@ -482,7 +491,8 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
const shouldUseCandidate = metadata.clientEndpoint ? await probeMetadataEndpoint(candidateEndpoint, metadata.clientNamespace, 1_500) : true;
|
||||
const resolvedEndpoint = shouldUseCandidate ? candidateEndpoint : options.endpoint;
|
||||
|
||||
return createClient({
|
||||
const buildClient = createClient as any;
|
||||
return buildClient({
|
||||
endpoint: resolvedEndpoint,
|
||||
namespace: metadata.clientNamespace,
|
||||
token: metadata.clientToken,
|
||||
|
|
@ -495,7 +505,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
};
|
||||
|
||||
const workspace = async (workspaceId: string): Promise<WorkspaceHandle> =>
|
||||
(await getClient()).workspace.getOrCreate(workspaceKey(workspaceId), {
|
||||
(await getClient()).organization.getOrCreate(organizationKey(workspaceId), {
|
||||
createWithInput: workspaceId,
|
||||
});
|
||||
|
||||
|
|
@ -504,6 +514,13 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return (client as any).sandboxInstance.get(sandboxInstanceKey(workspaceId, providerId, sandboxId));
|
||||
};
|
||||
|
||||
const taskById = async (workspaceId: string, taskId: string): Promise<TaskHandle> => {
|
||||
const ws = await workspace(workspaceId);
|
||||
const detail = await ws.getTask({ workspaceId, taskId });
|
||||
const client = await getClient();
|
||||
return client.task.get(taskKey(workspaceId, detail.repoId, taskId));
|
||||
};
|
||||
|
||||
function isActorNotFoundError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message.includes("Actor not found");
|
||||
|
|
@ -576,8 +593,14 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
|
||||
entry.listeners.add(listener);
|
||||
|
||||
if (!entry.disposeConnPromise) {
|
||||
entry.disposeConnPromise = (async () => {
|
||||
const ensureConnection = (currentEntry: NonNullable<typeof entry>) => {
|
||||
if (currentEntry.disposeConnPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
let reconnecting = false;
|
||||
let disposeConnPromise: Promise<(() => Promise<void>) | null> | null = null;
|
||||
disposeConnPromise = (async () => {
|
||||
const handle = await workspace(workspaceId);
|
||||
const conn = (handle as any).connect();
|
||||
const unsubscribeEvent = conn.on("workbenchUpdated", () => {
|
||||
|
|
@ -589,14 +612,39 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
currentListener();
|
||||
}
|
||||
});
|
||||
const unsubscribeError = conn.onError(() => {});
|
||||
const unsubscribeError = conn.onError(() => {
|
||||
if (reconnecting) {
|
||||
return;
|
||||
}
|
||||
reconnecting = true;
|
||||
|
||||
const current = workbenchSubscriptions.get(workspaceId);
|
||||
if (!current || current.disposeConnPromise !== disposeConnPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
current.disposeConnPromise = null;
|
||||
void disposeConnPromise?.then(async (disposeConn) => {
|
||||
await disposeConn?.();
|
||||
});
|
||||
|
||||
if (current.listeners.size > 0) {
|
||||
ensureConnection(current);
|
||||
for (const currentListener of [...current.listeners]) {
|
||||
currentListener();
|
||||
}
|
||||
}
|
||||
});
|
||||
return async () => {
|
||||
unsubscribeEvent();
|
||||
unsubscribeError();
|
||||
await conn.dispose();
|
||||
};
|
||||
})().catch(() => null);
|
||||
}
|
||||
currentEntry.disposeConnPromise = disposeConnPromise;
|
||||
};
|
||||
|
||||
ensureConnection(entry);
|
||||
|
||||
return () => {
|
||||
const current = workbenchSubscriptions.get(workspaceId);
|
||||
|
|
@ -984,6 +1032,10 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return (await workspace(workspaceId)).getWorkbench({ workspaceId });
|
||||
},
|
||||
|
||||
async getWorkbenchTask(workspaceId: string, taskId: string): Promise<WorkbenchTask> {
|
||||
return (await taskById(workspaceId, taskId)).getWorkbench();
|
||||
},
|
||||
|
||||
subscribeWorkbench(workspaceId: string, listener: () => void): () => void {
|
||||
return subscribeWorkbench(workspaceId, listener);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,34 +1,30 @@
|
|||
export type ActorKey = string[];
|
||||
|
||||
export function workspaceKey(workspaceId: string): ActorKey {
|
||||
return ["ws", workspaceId];
|
||||
export function organizationKey(organizationId: string): ActorKey {
|
||||
return ["org", organizationId];
|
||||
}
|
||||
|
||||
export function projectKey(workspaceId: string, repoId: string): ActorKey {
|
||||
return ["ws", workspaceId, "project", repoId];
|
||||
export function repositoryKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repo", repoId];
|
||||
}
|
||||
|
||||
export function taskKey(workspaceId: string, repoId: string, taskId: string): ActorKey {
|
||||
return ["ws", workspaceId, "project", repoId, "task", taskId];
|
||||
export function githubStateKey(organizationId: string): ActorKey {
|
||||
return ["org", organizationId, "github"];
|
||||
}
|
||||
|
||||
export function sandboxInstanceKey(workspaceId: string, providerId: string, sandboxId: string): ActorKey {
|
||||
return ["ws", workspaceId, "provider", providerId, "sandbox", sandboxId];
|
||||
export function taskKey(organizationId: string, repoId: string, taskId: string): ActorKey {
|
||||
return ["org", organizationId, "repo", repoId, "task", taskId];
|
||||
}
|
||||
|
||||
export function historyKey(workspaceId: string, repoId: string): ActorKey {
|
||||
return ["ws", workspaceId, "project", repoId, "history"];
|
||||
export function sandboxInstanceKey(organizationId: string, providerId: string, sandboxId: string): ActorKey {
|
||||
return ["org", organizationId, "provider", providerId, "sandbox", sandboxId];
|
||||
}
|
||||
|
||||
export function projectPrSyncKey(workspaceId: string, repoId: string): ActorKey {
|
||||
return ["ws", workspaceId, "project", repoId, "pr-sync"];
|
||||
export function historyKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repo", repoId, "history"];
|
||||
}
|
||||
|
||||
export function projectBranchSyncKey(workspaceId: string, repoId: string): ActorKey {
|
||||
return ["ws", workspaceId, "project", repoId, "branch-sync"];
|
||||
}
|
||||
|
||||
export function taskStatusSyncKey(workspaceId: string, repoId: string, taskId: string, sandboxId: string, sessionId: string): ActorKey {
|
||||
export function taskStatusSyncKey(organizationId: string, repoId: string, taskId: string, sandboxId: string, sessionId: string): ActorKey {
|
||||
// Include sandbox + session so multiple sandboxes/sessions can be tracked per task.
|
||||
return ["ws", workspaceId, "project", repoId, "task", taskId, "status-sync", sandboxId, sessionId];
|
||||
return ["org", organizationId, "repo", repoId, "task", taskId, "status-sync", sandboxId, sessionId];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ export type MockGithubInstallationStatus = "connected" | "install_required" | "r
|
|||
export type MockGithubSyncStatus = "pending" | "syncing" | "synced" | "error";
|
||||
export type MockOrganizationKind = "personal" | "organization";
|
||||
export type MockStarterRepoStatus = "pending" | "starred" | "skipped";
|
||||
export type MockActorRuntimeStatus = "healthy" | "error";
|
||||
export type MockActorRuntimeType = "organization" | "repository" | "task" | "history" | "sandbox_instance" | "task_status_sync";
|
||||
|
||||
export interface MockFoundryUser {
|
||||
id: string;
|
||||
|
|
@ -52,6 +54,27 @@ export interface MockFoundryGithubState {
|
|||
lastSyncAt: number | null;
|
||||
}
|
||||
|
||||
export interface MockFoundryActorRuntimeIssue {
|
||||
actorId: string;
|
||||
actorType: MockActorRuntimeType;
|
||||
scopeId: string | null;
|
||||
scopeLabel: string;
|
||||
message: string;
|
||||
workflowId: string | null;
|
||||
stepName: string | null;
|
||||
attempt: number | null;
|
||||
willRetry: boolean;
|
||||
retryDelayMs: number | null;
|
||||
occurredAt: number;
|
||||
}
|
||||
|
||||
export interface MockFoundryActorRuntimeState {
|
||||
status: MockActorRuntimeStatus;
|
||||
errorCount: number;
|
||||
lastErrorAt: number | null;
|
||||
issues: MockFoundryActorRuntimeIssue[];
|
||||
}
|
||||
|
||||
export interface MockFoundryOrganizationSettings {
|
||||
displayName: string;
|
||||
slug: string;
|
||||
|
|
@ -67,6 +90,7 @@ export interface MockFoundryOrganization {
|
|||
kind: MockOrganizationKind;
|
||||
settings: MockFoundryOrganizationSettings;
|
||||
github: MockFoundryGithubState;
|
||||
runtime: MockFoundryActorRuntimeState;
|
||||
billing: MockFoundryBillingState;
|
||||
members: MockFoundryOrganizationMember[];
|
||||
seatAssignments: string[];
|
||||
|
|
@ -140,6 +164,15 @@ function syncStatusFromLegacy(value: unknown): MockGithubSyncStatus {
|
|||
}
|
||||
}
|
||||
|
||||
function buildHealthyRuntimeState(): MockFoundryActorRuntimeState {
|
||||
return {
|
||||
status: "healthy",
|
||||
errorCount: 0,
|
||||
lastErrorAt: null,
|
||||
issues: [],
|
||||
};
|
||||
}
|
||||
|
||||
function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
||||
return {
|
||||
auth: {
|
||||
|
|
@ -203,6 +236,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
lastSyncLabel: "Synced just now",
|
||||
lastSyncAt: Date.now() - 60_000,
|
||||
},
|
||||
runtime: buildHealthyRuntimeState(),
|
||||
billing: {
|
||||
planId: "free",
|
||||
status: "active",
|
||||
|
|
@ -237,6 +271,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
lastSyncLabel: "Waiting for first import",
|
||||
lastSyncAt: null,
|
||||
},
|
||||
runtime: buildHealthyRuntimeState(),
|
||||
billing: {
|
||||
planId: "team",
|
||||
status: "active",
|
||||
|
|
@ -279,6 +314,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
lastSyncLabel: "Sync stalled 2 hours ago",
|
||||
lastSyncAt: Date.now() - 2 * 60 * 60_000,
|
||||
},
|
||||
runtime: buildHealthyRuntimeState(),
|
||||
billing: {
|
||||
planId: "team",
|
||||
status: "trialing",
|
||||
|
|
@ -317,6 +353,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
lastSyncLabel: "Synced yesterday",
|
||||
lastSyncAt: Date.now() - 24 * 60 * 60_000,
|
||||
},
|
||||
runtime: buildHealthyRuntimeState(),
|
||||
billing: {
|
||||
planId: "free",
|
||||
status: "active",
|
||||
|
|
@ -370,6 +407,7 @@ function parseStoredSnapshot(): MockFoundryAppSnapshot | null {
|
|||
syncStatus: syncStatusFromLegacy(organization.github?.syncStatus ?? organization.repoImportStatus),
|
||||
lastSyncAt: organization.github?.lastSyncAt ?? null,
|
||||
},
|
||||
runtime: organization.runtime ?? buildHealthyRuntimeState(),
|
||||
})),
|
||||
};
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,35 @@ function toTaskStatus(status: TaskRecord["status"], archived: boolean): TaskReco
|
|||
return status;
|
||||
}
|
||||
|
||||
function mapWorkbenchTaskStatus(task: TaskWorkbenchSnapshot["tasks"][number]): TaskRecord["status"] {
|
||||
if (task.status === "archived") {
|
||||
return "archived";
|
||||
}
|
||||
if (task.lifecycle?.state === "error") {
|
||||
return "error";
|
||||
}
|
||||
if (task.status === "idle") {
|
||||
return "idle";
|
||||
}
|
||||
if (task.status === "new") {
|
||||
return task.lifecycle?.code ?? "init_create_sandbox";
|
||||
}
|
||||
return "running";
|
||||
}
|
||||
|
||||
function mapWorkbenchTaskStatusMessage(task: TaskWorkbenchSnapshot["tasks"][number], status: TaskRecord["status"]): string {
|
||||
if (status === "archived") {
|
||||
return "archived";
|
||||
}
|
||||
if (status === "error") {
|
||||
return task.lifecycle?.message ?? "mock task initialization failed";
|
||||
}
|
||||
if (task.status === "new") {
|
||||
return task.lifecycle?.message ?? "mock sandbox provisioning";
|
||||
}
|
||||
return task.tabs.some((tab) => tab.status === "running") ? "agent responding" : "mock sandbox ready";
|
||||
}
|
||||
|
||||
export function createMockBackendClient(defaultWorkspaceId = "default"): BackendClient {
|
||||
const workbench = getSharedMockWorkbenchClient();
|
||||
const listenersBySandboxId = new Map<string, Set<() => void>>();
|
||||
|
|
@ -121,6 +150,8 @@ export function createMockBackendClient(defaultWorkspaceId = "default"): Backend
|
|||
const task = requireTask(taskId);
|
||||
const cwd = mockCwd(task.repoName, task.id);
|
||||
const archived = task.status === "archived";
|
||||
const taskStatus = mapWorkbenchTaskStatus(task);
|
||||
const sandboxAvailable = task.status !== "new" && taskStatus !== "error" && taskStatus !== "archived";
|
||||
return {
|
||||
workspaceId: defaultWorkspaceId,
|
||||
repoId: task.repoId,
|
||||
|
|
@ -130,21 +161,23 @@ export function createMockBackendClient(defaultWorkspaceId = "default"): Backend
|
|||
title: task.title,
|
||||
task: task.title,
|
||||
providerId: "local",
|
||||
status: toTaskStatus(archived ? "archived" : "running", archived),
|
||||
statusMessage: archived ? "archived" : "mock sandbox ready",
|
||||
activeSandboxId: task.id,
|
||||
activeSessionId: task.tabs[0]?.sessionId ?? null,
|
||||
sandboxes: [
|
||||
{
|
||||
sandboxId: task.id,
|
||||
providerId: "local",
|
||||
sandboxActorId: "mock-sandbox",
|
||||
switchTarget: `mock://${task.id}`,
|
||||
cwd,
|
||||
createdAt: task.updatedAtMs,
|
||||
updatedAt: task.updatedAtMs,
|
||||
},
|
||||
],
|
||||
status: toTaskStatus(taskStatus, archived),
|
||||
statusMessage: mapWorkbenchTaskStatusMessage(task, taskStatus),
|
||||
activeSandboxId: sandboxAvailable ? task.id : null,
|
||||
activeSessionId: sandboxAvailable ? (task.tabs[0]?.sessionId ?? null) : null,
|
||||
sandboxes: sandboxAvailable
|
||||
? [
|
||||
{
|
||||
sandboxId: task.id,
|
||||
providerId: "local",
|
||||
sandboxActorId: "mock-sandbox",
|
||||
switchTarget: `mock://${task.id}`,
|
||||
cwd,
|
||||
createdAt: task.updatedAtMs,
|
||||
updatedAt: task.updatedAtMs,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
agentType: task.tabs[0]?.agent === "Codex" ? "codex" : "claude",
|
||||
prSubmitted: Boolean(task.pullRequest),
|
||||
diffStat: task.fileChanges.length > 0 ? `+${task.fileChanges.length}/-${task.fileChanges.length}` : "+0/-0",
|
||||
|
|
@ -272,7 +305,7 @@ export function createMockBackendClient(defaultWorkspaceId = "default"): Backend
|
|||
taskId: task.id,
|
||||
branchName: task.branch,
|
||||
title: task.title,
|
||||
status: task.status === "archived" ? "archived" : "running",
|
||||
status: mapWorkbenchTaskStatus(task),
|
||||
updatedAt: task.updatedAtMs,
|
||||
}));
|
||||
},
|
||||
|
|
@ -462,6 +495,10 @@ export function createMockBackendClient(defaultWorkspaceId = "default"): Backend
|
|||
return workbench.getSnapshot();
|
||||
},
|
||||
|
||||
async getWorkbenchTask(_workspaceId: string, taskId: string) {
|
||||
return requireTask(taskId);
|
||||
},
|
||||
|
||||
subscribeWorkbench(_workspaceId: string, listener: () => void): () => void {
|
||||
return workbench.subscribe(listener);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -267,6 +267,40 @@ export function removeFileTreePath(nodes: FileTreeNode[], targetPath: string): F
|
|||
export function buildInitialTasks(): Task[] {
|
||||
return [
|
||||
// ── rivet-dev/sandbox-agent ──
|
||||
{
|
||||
id: "h0",
|
||||
repoId: "sandbox-agent",
|
||||
title: "Recover from sandbox session bootstrap timeout",
|
||||
status: "idle",
|
||||
lifecycle: {
|
||||
code: "error",
|
||||
state: "error",
|
||||
label: "Session startup failed",
|
||||
message: "createSession failed after 3 attempts: upstream 504 Gateway Timeout",
|
||||
},
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(1),
|
||||
branch: "fix/session-bootstrap-timeout",
|
||||
pullRequest: null,
|
||||
tabs: [
|
||||
{
|
||||
id: "t0",
|
||||
sessionId: null,
|
||||
sessionName: "Failed startup",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
status: "error",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
created: false,
|
||||
draft: { text: "", attachments: [], updatedAtMs: null },
|
||||
transcript: [],
|
||||
},
|
||||
],
|
||||
fileChanges: [],
|
||||
diffs: {},
|
||||
fileTree: [],
|
||||
},
|
||||
{
|
||||
id: "h1",
|
||||
repoId: "sandbox-agent",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue