mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 01:04:42 +00:00
Add foundry terminal and process pane
This commit is contained in:
parent
0471214d65
commit
28c4ac22ff
16 changed files with 2412 additions and 36 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
initCreateSessionActivity,
|
||||
initEnsureAgentActivity,
|
||||
initEnsureNameActivity,
|
||||
initExposeSandboxActivity,
|
||||
initFailedActivity,
|
||||
initStartSandboxInstanceActivity,
|
||||
initStartStatusSyncActivity,
|
||||
|
|
@ -93,6 +94,10 @@ const commandHandlers: Record<HandoffQueueName, WorkflowHandler> = {
|
|||
timeout: 60_000,
|
||||
run: async () => initStartSandboxInstanceActivity(loopCtx, body, sandbox, agent),
|
||||
});
|
||||
await loopCtx.step(
|
||||
"init-expose-sandbox",
|
||||
async () => initExposeSandboxActivity(loopCtx, body, sandbox, sandboxInstanceReady),
|
||||
);
|
||||
const session = await loopCtx.step({
|
||||
name: "init-create-session",
|
||||
timeout: 180_000,
|
||||
|
|
|
|||
|
|
@ -419,6 +419,62 @@ export async function initCreateSessionActivity(
|
|||
});
|
||||
}
|
||||
|
||||
export async function initExposeSandboxActivity(
|
||||
loopCtx: any,
|
||||
body: any,
|
||||
sandbox: any,
|
||||
sandboxInstanceReady?: { actorId?: string | null }
|
||||
): Promise<void> {
|
||||
const providerId = body?.providerId ?? loopCtx.state.providerId;
|
||||
const now = Date.now();
|
||||
const db = loopCtx.db;
|
||||
const activeCwd =
|
||||
sandbox.metadata && typeof (sandbox.metadata as any).cwd === "string"
|
||||
? ((sandbox.metadata as any).cwd as string)
|
||||
: null;
|
||||
const sandboxActorId =
|
||||
typeof sandboxInstanceReady?.actorId === "string" && sandboxInstanceReady.actorId.length > 0
|
||||
? sandboxInstanceReady.actorId
|
||||
: null;
|
||||
|
||||
await db
|
||||
.insert(handoffSandboxes)
|
||||
.values({
|
||||
sandboxId: sandbox.sandboxId,
|
||||
providerId,
|
||||
sandboxActorId,
|
||||
switchTarget: sandbox.switchTarget,
|
||||
cwd: activeCwd,
|
||||
statusMessage: "sandbox ready",
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: handoffSandboxes.sandboxId,
|
||||
set: {
|
||||
providerId,
|
||||
sandboxActorId,
|
||||
switchTarget: sandbox.switchTarget,
|
||||
cwd: activeCwd,
|
||||
statusMessage: "sandbox ready",
|
||||
updatedAt: now
|
||||
}
|
||||
})
|
||||
.run();
|
||||
|
||||
await db
|
||||
.update(handoffRuntime)
|
||||
.set({
|
||||
activeSandboxId: sandbox.sandboxId,
|
||||
activeSwitchTarget: sandbox.switchTarget,
|
||||
activeCwd,
|
||||
statusMessage: "sandbox ready",
|
||||
updatedAt: now
|
||||
})
|
||||
.where(eq(handoffRuntime.id, HANDOFF_ROW_ID))
|
||||
.run();
|
||||
}
|
||||
|
||||
export async function initWriteDbActivity(
|
||||
loopCtx: any,
|
||||
body: any,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,15 @@ import { eq } from "drizzle-orm";
|
|||
import { actor, queue } from "rivetkit";
|
||||
import { Loop, workflow } from "rivetkit/workflow";
|
||||
import type { ProviderId } from "@openhandoff/shared";
|
||||
import type { SessionEvent, SessionRecord } from "sandbox-agent";
|
||||
import type {
|
||||
ProcessCreateRequest,
|
||||
ProcessInfo,
|
||||
ProcessLogFollowQuery,
|
||||
ProcessLogsResponse,
|
||||
ProcessSignalQuery,
|
||||
SessionEvent,
|
||||
SessionRecord,
|
||||
} from "sandbox-agent";
|
||||
import { sandboxInstanceDb } from "./db/db.js";
|
||||
import { sandboxInstance as sandboxInstanceTable } from "./db/schema.js";
|
||||
import { SandboxInstancePersistDriver } from "./persist.js";
|
||||
|
|
@ -18,6 +26,11 @@ export interface SandboxInstanceInput {
|
|||
sandboxId: string;
|
||||
}
|
||||
|
||||
interface SandboxAgentConnection {
|
||||
endpoint: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
const SANDBOX_ROW_ID = 1;
|
||||
const CREATE_SESSION_MAX_ATTEMPTS = 3;
|
||||
const CREATE_SESSION_RETRY_BASE_MS = 1_000;
|
||||
|
|
@ -79,7 +92,7 @@ function parseMetadata(metadataJson: string): Record<string, unknown> {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadPersistedAgentConfig(c: any): Promise<{ endpoint: string; token?: string } | null> {
|
||||
async function loadPersistedAgentConfig(c: any): Promise<SandboxAgentConnection | null> {
|
||||
try {
|
||||
const row = await c.db
|
||||
.select({ metadataJson: sandboxInstanceTable.metadataJson })
|
||||
|
|
@ -101,7 +114,7 @@ async function loadPersistedAgentConfig(c: any): Promise<{ endpoint: string; tok
|
|||
return null;
|
||||
}
|
||||
|
||||
async function loadFreshDaytonaAgentConfig(c: any): Promise<{ endpoint: string; token?: string }> {
|
||||
async function loadFreshDaytonaAgentConfig(c: any): Promise<SandboxAgentConnection> {
|
||||
const { config, driver } = getActorRuntimeContext();
|
||||
const daytona = driver.daytona.createClient({
|
||||
apiUrl: config.providers.daytona.endpoint,
|
||||
|
|
@ -116,7 +129,7 @@ async function loadFreshDaytonaAgentConfig(c: any): Promise<{ endpoint: string;
|
|||
return preview.token ? { endpoint: preview.url, token: preview.token } : { endpoint: preview.url };
|
||||
}
|
||||
|
||||
async function loadFreshProviderAgentConfig(c: any): Promise<{ endpoint: string; token?: string }> {
|
||||
async function loadFreshProviderAgentConfig(c: any): Promise<SandboxAgentConnection> {
|
||||
const { providers } = getActorRuntimeContext();
|
||||
const provider = providers.get(c.state.providerId);
|
||||
return await provider.ensureSandboxAgent({
|
||||
|
|
@ -125,7 +138,7 @@ async function loadFreshProviderAgentConfig(c: any): Promise<{ endpoint: string;
|
|||
});
|
||||
}
|
||||
|
||||
async function loadAgentConfig(c: any): Promise<{ endpoint: string; token?: string }> {
|
||||
async function loadAgentConfig(c: any): Promise<SandboxAgentConnection> {
|
||||
const persisted = await loadPersistedAgentConfig(c);
|
||||
if (c.state.providerId === "daytona") {
|
||||
// Keep one stable signed preview endpoint per sandbox-instance actor.
|
||||
|
|
@ -282,6 +295,13 @@ async function getSandboxAgentClient(c: any) {
|
|||
});
|
||||
}
|
||||
|
||||
function broadcastProcessesUpdated(c: any): void {
|
||||
c.broadcast("processesUpdated", {
|
||||
sandboxId: c.state.sandboxId,
|
||||
at: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureSandboxMutation(c: any, command: EnsureSandboxCommand): Promise<void> {
|
||||
const now = Date.now();
|
||||
const metadata = {
|
||||
|
|
@ -478,6 +498,56 @@ export const sandboxInstance = actor({
|
|||
sandboxId: input.sandboxId,
|
||||
}),
|
||||
actions: {
|
||||
async sandboxAgentConnection(c: any): Promise<SandboxAgentConnection> {
|
||||
return await loadAgentConfig(c);
|
||||
},
|
||||
|
||||
async createProcess(c: any, request: ProcessCreateRequest): Promise<ProcessInfo> {
|
||||
const client = await getSandboxAgentClient(c);
|
||||
const created = await client.createProcess(request);
|
||||
broadcastProcessesUpdated(c);
|
||||
return created;
|
||||
},
|
||||
|
||||
async listProcesses(c: any): Promise<{ processes: ProcessInfo[] }> {
|
||||
const client = await getSandboxAgentClient(c);
|
||||
return await client.listProcesses();
|
||||
},
|
||||
|
||||
async getProcessLogs(
|
||||
c: any,
|
||||
request: { processId: string; query?: ProcessLogFollowQuery }
|
||||
): Promise<ProcessLogsResponse> {
|
||||
const client = await getSandboxAgentClient(c);
|
||||
return await client.getProcessLogs(request.processId, request.query);
|
||||
},
|
||||
|
||||
async stopProcess(
|
||||
c: any,
|
||||
request: { processId: string; query?: ProcessSignalQuery }
|
||||
): Promise<ProcessInfo> {
|
||||
const client = await getSandboxAgentClient(c);
|
||||
const stopped = await client.stopProcess(request.processId, request.query);
|
||||
broadcastProcessesUpdated(c);
|
||||
return stopped;
|
||||
},
|
||||
|
||||
async killProcess(
|
||||
c: any,
|
||||
request: { processId: string; query?: ProcessSignalQuery }
|
||||
): Promise<ProcessInfo> {
|
||||
const client = await getSandboxAgentClient(c);
|
||||
const killed = await client.killProcess(request.processId, request.query);
|
||||
broadcastProcessesUpdated(c);
|
||||
return killed;
|
||||
},
|
||||
|
||||
async deleteProcess(c: any, request: { processId: string }): Promise<void> {
|
||||
const client = await getSandboxAgentClient(c);
|
||||
await client.deleteProcess(request.processId);
|
||||
broadcastProcessesUpdated(c);
|
||||
},
|
||||
|
||||
async providerState(c: any): Promise<{ providerId: ProviderId; sandboxId: string; state: string; at: number }> {
|
||||
const at = Date.now();
|
||||
const { config, driver } = getActorRuntimeContext();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,18 @@ import type {
|
|||
SandboxAgentClientOptions,
|
||||
SandboxSessionCreateRequest
|
||||
} from "./integrations/sandbox-agent/client.js";
|
||||
import type { ListEventsRequest, ListPage, ListPageRequest, SessionEvent, SessionRecord } from "sandbox-agent";
|
||||
import type {
|
||||
ListEventsRequest,
|
||||
ListPage,
|
||||
ListPageRequest,
|
||||
ProcessCreateRequest,
|
||||
ProcessInfo,
|
||||
ProcessLogFollowQuery,
|
||||
ProcessLogsResponse,
|
||||
ProcessSignalQuery,
|
||||
SessionEvent,
|
||||
SessionRecord,
|
||||
} from "sandbox-agent";
|
||||
import type {
|
||||
DaytonaClientOptions,
|
||||
DaytonaCreateSandboxOptions,
|
||||
|
|
@ -80,6 +91,12 @@ export interface SandboxAgentClientLike {
|
|||
sessionStatus(sessionId: string): Promise<SandboxSession>;
|
||||
listSessions(request?: ListPageRequest): Promise<ListPage<SessionRecord>>;
|
||||
listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>>;
|
||||
createProcess(request: ProcessCreateRequest): Promise<ProcessInfo>;
|
||||
listProcesses(): Promise<{ processes: ProcessInfo[] }>;
|
||||
getProcessLogs(processId: string, query?: ProcessLogFollowQuery): Promise<ProcessLogsResponse>;
|
||||
stopProcess(processId: string, query?: ProcessSignalQuery): Promise<ProcessInfo>;
|
||||
killProcess(processId: string, query?: ProcessSignalQuery): Promise<ProcessInfo>;
|
||||
deleteProcess(processId: string): Promise<void>;
|
||||
sendPrompt(request: { sessionId: string; prompt: string; notification?: boolean }): Promise<void>;
|
||||
cancelSession(sessionId: string): Promise<void>;
|
||||
destroySession(sessionId: string): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@ import type {
|
|||
ListEventsRequest,
|
||||
ListPage,
|
||||
ListPageRequest,
|
||||
ProcessCreateRequest,
|
||||
ProcessInfo,
|
||||
ProcessLogFollowQuery,
|
||||
ProcessLogsResponse,
|
||||
ProcessSignalQuery,
|
||||
SessionEvent,
|
||||
SessionPersistDriver,
|
||||
SessionRecord
|
||||
|
|
@ -213,6 +218,39 @@ export class SandboxAgentClient {
|
|||
return sdk.getEvents(request);
|
||||
}
|
||||
|
||||
async createProcess(request: ProcessCreateRequest): Promise<ProcessInfo> {
|
||||
const sdk = await this.sdk();
|
||||
return await sdk.createProcess(request);
|
||||
}
|
||||
|
||||
async listProcesses(): Promise<{ processes: ProcessInfo[] }> {
|
||||
const sdk = await this.sdk();
|
||||
return await sdk.listProcesses();
|
||||
}
|
||||
|
||||
async getProcessLogs(
|
||||
processId: string,
|
||||
query: ProcessLogFollowQuery = {}
|
||||
): Promise<ProcessLogsResponse> {
|
||||
const sdk = await this.sdk();
|
||||
return await sdk.getProcessLogs(processId, query);
|
||||
}
|
||||
|
||||
async stopProcess(processId: string, query?: ProcessSignalQuery): Promise<ProcessInfo> {
|
||||
const sdk = await this.sdk();
|
||||
return await sdk.stopProcess(processId, query);
|
||||
}
|
||||
|
||||
async killProcess(processId: string, query?: ProcessSignalQuery): Promise<ProcessInfo> {
|
||||
const sdk = await this.sdk();
|
||||
return await sdk.killProcess(processId, query);
|
||||
}
|
||||
|
||||
async deleteProcess(processId: string): Promise<void> {
|
||||
const sdk = await this.sdk();
|
||||
await sdk.deleteProcess(processId);
|
||||
}
|
||||
|
||||
async sendPrompt(request: SandboxSessionPromptRequest): Promise<void> {
|
||||
const sdk = await this.sdk();
|
||||
const existing = await sdk.getSession(request.sessionId);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,15 @@ import type {
|
|||
SandboxAgentClientLike,
|
||||
TmuxDriver,
|
||||
} from "../../src/driver.js";
|
||||
import type { ListEventsRequest, ListPage, ListPageRequest, SessionEvent, SessionRecord } from "sandbox-agent";
|
||||
import type {
|
||||
ListEventsRequest,
|
||||
ListPage,
|
||||
ListPageRequest,
|
||||
ProcessInfo,
|
||||
ProcessLogsResponse,
|
||||
SessionEvent,
|
||||
SessionRecord,
|
||||
} from "sandbox-agent";
|
||||
|
||||
export function createTestDriver(overrides?: Partial<BackendDriver>): BackendDriver {
|
||||
return {
|
||||
|
|
@ -74,6 +82,25 @@ export function createTestSandboxAgentDriver(
|
|||
export function createTestSandboxAgentClient(
|
||||
overrides?: Partial<SandboxAgentClientLike>
|
||||
): SandboxAgentClientLike {
|
||||
const defaultProcess: ProcessInfo = {
|
||||
id: "process-1",
|
||||
command: "bash",
|
||||
args: ["-lc", "echo test"],
|
||||
createdAtMs: Date.now(),
|
||||
cwd: "/workspace",
|
||||
exitCode: null,
|
||||
exitedAtMs: null,
|
||||
interactive: true,
|
||||
pid: 123,
|
||||
status: "running",
|
||||
tty: true,
|
||||
};
|
||||
const defaultLogs: ProcessLogsResponse = {
|
||||
processId: defaultProcess.id,
|
||||
stream: "combined",
|
||||
entries: [],
|
||||
};
|
||||
|
||||
return {
|
||||
createSession: async (_prompt) => ({ id: "test-session-1", status: "running" }),
|
||||
sessionStatus: async (sessionId) => ({ id: sessionId, status: "running" }),
|
||||
|
|
@ -85,6 +112,12 @@ export function createTestSandboxAgentClient(
|
|||
items: [],
|
||||
nextCursor: undefined,
|
||||
}),
|
||||
createProcess: async () => defaultProcess,
|
||||
listProcesses: async () => ({ processes: [defaultProcess] }),
|
||||
getProcessLogs: async () => defaultLogs,
|
||||
stopProcess: async () => ({ ...defaultProcess, status: "exited", exitCode: 0, exitedAtMs: Date.now() }),
|
||||
killProcess: async () => ({ ...defaultProcess, status: "exited", exitCode: 137, exitedAtMs: Date.now() }),
|
||||
deleteProcess: async () => {},
|
||||
sendPrompt: async (_request) => {},
|
||||
cancelSession: async (_sessionId) => {},
|
||||
destroySession: async (_sessionId) => {},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue