Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,39 @@ Create a new conversation session.

Resume an existing session. Returns the session with `workspacePath` populated if infinite sessions were enabled.

##### `createCloudSession(options?: CloudSessionOptions): Promise<CloudSession>`

Create a sandbox-backed cloud session through Mission Control and attach to it as a remote-control client. The agent runtime runs inside the provisioned sandbox; this SDK instance polls task events and sends prompts or prompt responses through Mission Control.

```typescript
const client = new CopilotClient({ gitHubToken: process.env.GITHUB_TOKEN });

const session = await client.createCloudSession({
repository: { owner: "github", name: "copilot-sdk" },
onProgress: (event) => console.log(event.phase),
});
Comment thread
JasonEtco marked this conversation as resolved.

session.on("assistant.message", (event) => {
console.log(event.data.content);
});

await session.send({ prompt: "Summarize the project" });
```

Cloud sessions are separate from the `remote` client option. `remote: true` exports a local runtime session to Mission Control; `createCloudSession` provisions a cloud sandbox and controls the runtime running there.

Pass `repository` explicitly when the sandbox should be associated with a repository. Mission Control currently uses only repository owner/name for sandbox provisioning; `repository.branch`, when provided, is retained as SDK metadata only. For repo-less sandboxes, pass `owner` so Mission Control can bill and authorize the sandbox:

```typescript
const session = await client.createCloudSession({ owner: "github" });
```

For now, provide `gitHubToken`, `authToken`, or `COPILOT_MC_ACCESS_TOKEN` for Mission Control authentication. `missionControlBaseUrl`, `copilotApiBaseUrl`, `frontendBaseUrl`, and `pollIntervalMs` are available for enterprise hosts and tests.

##### `connectCloudSession(taskId: string, options?: CloudConnectOptions): Promise<CloudSession>`

Attach to an existing Mission Control cloud task and return the same remote-control `CloudSession` facade used by `createCloudSession`.

##### `ping(message?: string): Promise<{ message: string; timestamp: number }>`

Ping the server to check connectivity.
Expand Down
176 changes: 176 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,28 @@ import {
createInternalServerRpc,
registerClientSessionApiHandlers,
} from "./generated/rpc.js";
import { CloudSession } from "./cloud/cloudSession.js";
import { MissionControlClient } from "./cloud/missionControlClient.js";
import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js";
import { createSessionFsAdapter } from "./sessionFsProvider.js";
import { getTraceContext } from "./telemetry.js";
import { stripTrailingSlash } from "./url.js";
import type {
AutoModeSwitchRequest,
AutoModeSwitchResponse,
CloudConnectOptions,
CloudRepository,
CloudSessionMetadata,
CloudSessionOptions,
ConnectionState,
CopilotClientOptions,
ExitPlanModeRequest,
ExitPlanModeResult,
ForegroundSessionInfo,
GetAuthStatusResponse,
GetStatusResponse,
MissionControlTask,
ModelInfo,
ProviderConfig,
ResumeSessionConfig,
Expand Down Expand Up @@ -155,6 +163,11 @@ function getNodeExecPath(): string {
return process.execPath;
}

function normalizeToken(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}

/**
* Gets the path to the bundled CLI from the @github/copilot package.
* Uses index.js directly rather than npm-loader.js (which spawns the native binary).
Expand Down Expand Up @@ -436,6 +449,71 @@ export class CopilotClient {
return { host, port };
}

private createMissionControlClient(
options: CloudSessionOptions | CloudConnectOptions
): MissionControlClient {
const env = this.options.env;
const copilotApiBaseUrl = stripTrailingSlash(
options.copilotApiBaseUrl ??
env.COPILOT_API_BASE_URL ??
env.COPILOT_API_URL ??
"https://api.githubcopilot.com"
);
const baseUrl =
options.missionControlBaseUrl ??
env.COPILOT_MC_BASE_URL ??
`${copilotApiBaseUrl}/agents`;
const authToken =
normalizeToken(options.authToken) ??
normalizeToken(env.COPILOT_MC_ACCESS_TOKEN) ??
normalizeToken(this.options.gitHubToken);
const frontendBaseUrl =
options.frontendBaseUrl ?? env.COPILOT_MC_FRONTEND_URL ?? "https://github.com";

return new MissionControlClient({
baseUrl,
authToken,
integrationId: options.integrationId,
frontendBaseUrl,
});
}

private createCloudSessionMetadata(
task: MissionControlTask,
mcClient: MissionControlClient,
repository?: CloudRepository,
owner?: string
): CloudSessionMetadata {
return {
taskId: task.id,
missionControlSessionId: task.sessions?.at(-1)?.id,
frontendUrl: mcClient.getFrontendUrl(task.id),
owner,
repository,
createdAt: new Date(task.created_at),
updatedAt: new Date(task.updated_at),
state: task.state,
status: task.status,
};
}

private createFallbackCloudSessionMetadata(
taskId: string,
mcClient: MissionControlClient,
repository?: CloudRepository,
owner?: string
): CloudSessionMetadata {
const now = new Date();
return {
taskId,
frontendUrl: mcClient.getFrontendUrl(taskId),
owner,
repository,
createdAt: now,
updatedAt: now,
};
}

private validateSessionFsConfig(config: SessionFsConfig): void {
if (!config.initialCwd) {
throw new Error("sessionFs.initialCwd is required");
Expand Down Expand Up @@ -1075,6 +1153,104 @@ export class CopilotClient {
return result as GetAuthStatusResponse;
}

/**
* Create a sandbox-backed cloud session through Mission Control and attach
* to it as a remote-control client.
*
* This does not create a local runtime session. The agent runs inside the
* provisioned cloud sandbox; this SDK instance polls Mission Control for
* events and sends user actions through the task steer API.
*/
async createCloudSession(options: CloudSessionOptions = {}): Promise<CloudSession> {
const startedAt = Date.now();
const mcClient = this.createMissionControlClient(options);
const owner = normalizeToken(options.owner);
const repository = options.repository;

if (!repository && !owner) {
throw new Error("CloudSessionOptions.owner is required when repository is omitted");
}

options.onProgress?.({ phase: "creating_task", elapsedMs: 0 });
options.onProgress?.({
phase: "provisioning_sandbox",
elapsedMs: Date.now() - startedAt,
});
const task = await mcClient.createCloudTask({
owner,
repository: repository ? { owner: repository.owner, name: repository.name } : undefined,
});
Comment thread
JasonEtco marked this conversation as resolved.
options.onCloudTaskCreated?.(task);

options.onProgress?.({
phase: "waiting_for_session",
elapsedMs: Date.now() - startedAt,
taskId: task.id,
});

const session = new CloudSession({
client: mcClient,
metadata: this.createCloudSessionMetadata(task, mcClient, repository, owner),
pollIntervalMs: options.pollIntervalMs,
initialEventTimeoutMs: options.initialEventTimeoutMs,
initialEventPollIntervalMs: options.initialEventPollIntervalMs,
onEventPollError: options.onEventPollError,
});
await session.connect();

options.onProgress?.({
phase: "connected",
elapsedMs: Date.now() - startedAt,
taskId: task.id,
});

return session;
}

/**
* Attach to an existing Mission Control cloud task as a remote-control client.
*
* The identifier is treated as a task ID. If Mission Control can return task
* metadata, it is used to populate the session metadata; otherwise the SDK
* still attaches by polling task events for the provided task ID.
*/
async connectCloudSession(
taskId: string,
options: CloudConnectOptions = {}
): Promise<CloudSession> {
Comment thread
JasonEtco marked this conversation as resolved.
const startedAt = Date.now();
const mcClient = this.createMissionControlClient(options);
options.onProgress?.({
phase: "waiting_for_session",
elapsedMs: 0,
taskId,
});

const task = await mcClient.getTask(taskId);
const owner = normalizeToken(options.owner);
const metadata = task
? this.createCloudSessionMetadata(task, mcClient, options.repository, owner)
: this.createFallbackCloudSessionMetadata(taskId, mcClient, options.repository, owner);

const session = new CloudSession({
client: mcClient,
metadata,
pollIntervalMs: options.pollIntervalMs,
initialEventTimeoutMs: options.initialEventTimeoutMs,
initialEventPollIntervalMs: options.initialEventPollIntervalMs,
onEventPollError: options.onEventPollError,
});
await session.connect();

options.onProgress?.({
phase: "connected",
elapsedMs: Date.now() - startedAt,
taskId: metadata.taskId,
});

return session;
}

/**
* List available models with their metadata.
*
Expand Down
Loading
Loading