Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/agent-adapters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"./claude-code": "./src/claude/index.ts",
"./codex": "./src/codex/index.ts",
"./cursor": "./src/cursor/index.ts",
"./github-copilot": "./src/github-copilot/adapter.ts",
"./opencode": "./src/opencode/adapter.ts",
"./codex/config": "./src/codex/config.ts",
"./codex/docker": "./src/codex/docker.ts",
Expand Down
129 changes: 129 additions & 0 deletions packages/agent-adapters/src/github-copilot/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { runCommand } from "../shared/execute/shell";
import { renderAgentPrompt } from "../shared/skills/request-prompt";
import { emitStreamEvent } from "../shared/streaming/events";
import type {
AgentAdapter,
AgentAdapterRunRequest,
AgentAdapterRuntimeConfig,
AgentResult,
} from "../types/agent-adapter.types";
import {
validateAgentAdapterRunRequest,
validateAgentAdapterRuntimeConfig,
} from "../validation";
import { mapGitHubCopilotError } from "./errors";
import { extractFinalMessage } from "./output";

export class GitHubCopilotAdapter implements AgentAdapter {
constructor(config: AgentAdapterRuntimeConfig) {
this.config = validateAgentAdapterRuntimeConfig(config);
}

private config: AgentAdapterRuntimeConfig;

async runPlan(prompt: string): Promise<AgentResult> {
return this.runAgent({ role: "planning", prompt });
}

async runTaskIntake(prompt: string): Promise<AgentResult> {
return this.runAgent({ role: "task-intake", prompt });
}

async resume(sessionId: string, prompt: string): Promise<AgentResult> {
return this.runAgent({ role: "implementing", prompt, sessionId });
}

async runReview(prompt: string): Promise<AgentResult> {
return this.runAgent({ role: "review-testing", prompt });
}

async runGithubComment(prompt: string): Promise<AgentResult> {
return this.runAgent({ role: "github-comment", prompt });
}

async runAgent(request: AgentAdapterRunRequest): Promise<AgentResult> {
const validatedRequest = validateAgentAdapterRunRequest(request);
const prompt = this.renderPrompt(validatedRequest);
return this.runGitHubCopilot(this.buildArgs(prompt), validatedRequest);
}

private renderPrompt(request: AgentAdapterRunRequest): string {
const prompt = renderAgentPrompt(request);
if (!request.sessionId) {
return prompt;
}
return [
`Previous GitHub Copilot session id: ${request.sessionId}`,
"Continue the task using this session id as context.",
"",
prompt,
].join("\n");
}

private buildArgs(prompt: string): string[] {
const args = ["-p", prompt];
if (this.config.githubCopilot?.allowAllTools) {
args.push("--allow-all-tools");
}
for (const pattern of this.config.githubCopilot?.allowTools ?? []) {
args.push("--allow-tool", pattern);
}
for (const pattern of this.config.githubCopilot?.denyTools ?? []) {
args.push("--deny-tool", pattern);
}
return args;
}

private async runGitHubCopilot(
args: string[],
request: AgentAdapterRunRequest,
): Promise<AgentResult> {
const binary = this.config.githubCopilot?.binary ?? "copilot";
const cwd = this.config.executionPath;
const result = await runCommand(binary, args, {
cwd,
env: this.buildEnv(),
streamStdout:
this.config.githubCopilot?.streamLogs ?? this.config.codex.streamLogs,
streamStderr:
this.config.githubCopilot?.streamLogs ?? this.config.codex.streamLogs,
stdinMode: "ignore",
onStdout: (text) => emitStreamEvent(request, "stdout", text),
onStderr: (text) => emitStreamEvent(request, "stderr", text),
}).catch((error) => {
throw mapGitHubCopilotError(
binary,
args,
{
code: 127,
stdout: "",
stderr: error instanceof Error ? error.message : String(error),
},
{ cwd, request },
);
});

if (result.code !== 0) {
throw mapGitHubCopilotError(binary, args, result, { cwd, request });
}

return {
finalMessage: extractFinalMessage(result.stdout),
stdout: result.stdout,
stderr: result.stderr,
traceId: request.traceId,
backend: "github-copilot",
};
}

private buildEnv(): Record<string, string> | undefined {
const env: Record<string, string> = {};
const model = this.config.githubCopilot?.model?.trim();
if (model && model.toLowerCase() !== "auto") env.COPILOT_MODEL = model;
const copilotHome = this.config.githubCopilot?.copilotHome?.trim();
if (copilotHome) env.COPILOT_HOME = copilotHome;
const githubToken = this.config.githubCopilot?.githubToken?.trim();
if (githubToken) env.COPILOT_GITHUB_TOKEN = githubToken;
return Object.keys(env).length ? env : undefined;
}
}
79 changes: 79 additions & 0 deletions packages/agent-adapters/src/github-copilot/configuration-doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { AgentConfigurationDoc } from "../types/agent-registry.types";
import {
GITHUB_COPILOT_AVAILABLE_MODELS,
GITHUB_COPILOT_BACKEND,
GITHUB_COPILOT_DEFAULT_MODEL,
GITHUB_COPILOT_DESCRIPTION,
GITHUB_COPILOT_LABEL,
} from "./constants";

export const githubCopilotConfigurationDoc = {
backend: GITHUB_COPILOT_BACKEND,
label: GITHUB_COPILOT_LABEL,
description: GITHUB_COPILOT_DESCRIPTION,
env: [
field(
"AGENT_BACKEND",
"Agent backend",
"Use 'github-copilot' for GitHub Copilot CLI.",
),
field("GITHUB_COPILOT_BINARY", "Binary", "GitHub Copilot CLI binary path."),
field("GITHUB_COPILOT_MODEL", "Model", "GitHub Copilot CLI model id."),
field("GITHUB_COPILOT_HOME", "Home", "GitHub Copilot CLI home directory."),
field(
"GITHUB_COPILOT_TOKEN",
"Token",
"GitHub Copilot CLI authentication token.",
),
field(
"GITHUB_COPILOT_ALLOW_ALL_TOOLS",
"Allow all tools",
"Allow GitHub Copilot CLI to use all tools.",
),
field(
"GITHUB_COPILOT_ALLOW_TOOLS",
"Allow tools",
"Comma-separated GitHub Copilot CLI tool allow patterns.",
),
field(
"GITHUB_COPILOT_DENY_TOOLS",
"Deny tools",
"Comma-separated GitHub Copilot CLI tool deny patterns.",
),
],
configFields: [
field("agent.backend", "Agent backend", "Project agent backend."),
field("githubCopilot.binary", "Binary", "GitHub Copilot CLI binary path."),
field("githubCopilot.model", "Model", "GitHub Copilot CLI model id."),
field("githubCopilot.copilotHome", "Home", "GitHub Copilot CLI home."),
field("githubCopilot.githubToken", "Token", "GitHub Copilot token."),
field(
"githubCopilot.allowAllTools",
"Allow all tools",
"Allow GitHub Copilot CLI to use all tools.",
),
field(
"githubCopilot.allowTools",
"Allow tools",
"GitHub Copilot CLI tool allow patterns.",
),
field(
"githubCopilot.denyTools",
"Deny tools",
"GitHub Copilot CLI tool deny patterns.",
),
],
defaults: {
backend: GITHUB_COPILOT_BACKEND,
model: GITHUB_COPILOT_DEFAULT_MODEL,
},
availableModels: GITHUB_COPILOT_AVAILABLE_MODELS,
} satisfies AgentConfigurationDoc;

function field(
name: string,
label: string,
description: string,
): AgentConfigurationDoc["env"][number] {
return { name, label, description };
}
18 changes: 18 additions & 0 deletions packages/agent-adapters/src/github-copilot/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { AgentBackend } from "../types/agent-adapter.types";
import type { AgentModelDefinition } from "../types/agent-registry.types";

export const GITHUB_COPILOT_BACKEND = "github-copilot" satisfies AgentBackend;
export const GITHUB_COPILOT_LABEL = "GitHub Copilot";
export const GITHUB_COPILOT_DESCRIPTION =
"GitHub Copilot CLI runtime for local programmatic agent runs.";

export const GITHUB_COPILOT_DEFAULT_MODEL = "auto";

export const GITHUB_COPILOT_AVAILABLE_MODELS = [
{
id: "auto",
label: "Auto",
description: "Let GitHub Copilot CLI choose the model.",
defaultFor: ["brainstorm", "taskIntake", "plan", "implement", "reviewTest"],
},
] as const satisfies readonly AgentModelDefinition[];
95 changes: 95 additions & 0 deletions packages/agent-adapters/src/github-copilot/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { AgentAdapterError } from "../adapter-error";
import type { AgentAdapterRunRequest } from "../types/agent-adapter.types";

interface GitHubCopilotErrorContext {
cwd?: string;
request?: AgentAdapterRunRequest;
}

export function mapGitHubCopilotError(
command: string,
args: string[],
result: { code: number; stdout: string; stderr: string },
context: GitHubCopilotErrorContext = {},
): Error {
const output = result.stderr || result.stdout;
const lower = output.toLowerCase();
const base = `${command} ${args.join(" ")} failed with exit code ${result.code}`;

Comment on lines +9 to +18
if (
result.code === 127 ||
lower.includes("command not found") ||
lower.includes("enoent")
) {
return buildError(
`${base}\nGitHub Copilot CLI binary not found. Install GitHub Copilot CLI or set GITHUB_COPILOT_BINARY.`,
command,
args,
result,
context,
);
}
if (
lower.includes("auth") ||
lower.includes("token") ||
lower.includes("login") ||
lower.includes("unauthorized")
) {
return buildError(
`${base}\nGitHub Copilot CLI authentication failed. Run 'copilot auth' or set GITHUB_COPILOT_TOKEN.`,
command,
args,
result,
context,
);
}
if (lower.includes("subscription") || lower.includes("policy")) {
return buildError(
`${base}\nGitHub Copilot CLI is unavailable for this account. Check Copilot subscription and organization policy.`,
command,
args,
result,
context,
);
}
if (lower.includes("model") && lower.includes("not found")) {
return buildError(
`${base}\nThe specified GitHub Copilot model was not found. Check GITHUB_COPILOT_MODEL.`,
command,
args,
result,
context,
);
}
if (lower.includes("permission") || lower.includes("approval")) {
return buildError(
`${base}\nGitHub Copilot CLI tool permission flow blocked the run. Configure GitHub Copilot tool allow or deny settings.`,
command,
args,
result,
context,
);
}

return buildError(`${base}\n${output}`, command, args, result, context);
}

function buildError(
message: string,
command: string,
args: string[],
result: { code: number; stdout: string; stderr: string },
context: GitHubCopilotErrorContext,
): AgentAdapterError {
return new AgentAdapterError({
backend: "github-copilot",
message,
command,
args,
cwd: context.cwd,
code: result.code,
stdout: result.stdout,
stderr: result.stderr,
traceId: context.request?.traceId,
});
}
3 changes: 3 additions & 0 deletions packages/agent-adapters/src/github-copilot/output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function extractFinalMessage(output: string): string {
return output.trim() || output;
}
Comment on lines +1 to +3
18 changes: 18 additions & 0 deletions packages/agent-adapters/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ import {
CURSOR_DESCRIPTION,
CURSOR_LABEL,
} from "./cursor/constants";
import { GitHubCopilotAdapter } from "./github-copilot/adapter";
import { githubCopilotConfigurationDoc } from "./github-copilot/configuration-doc";
import {
GITHUB_COPILOT_AVAILABLE_MODELS,
GITHUB_COPILOT_BACKEND,
GITHUB_COPILOT_DEFAULT_MODEL,
GITHUB_COPILOT_DESCRIPTION,
GITHUB_COPILOT_LABEL,
} from "./github-copilot/constants";
import { OpenCodeAdapter } from "./opencode/adapter";
import { opencodeConfigurationDoc } from "./opencode/configuration-doc";
import {
Expand Down Expand Up @@ -69,6 +78,15 @@ const agentBackendDefinitions = {
configurationDoc: cursorConfigurationDoc,
createAdapter: (config) => new CursorAgentAdapter(config),
},
[GITHUB_COPILOT_BACKEND]: {
backend: GITHUB_COPILOT_BACKEND,
label: GITHUB_COPILOT_LABEL,
description: GITHUB_COPILOT_DESCRIPTION,
defaultModel: GITHUB_COPILOT_DEFAULT_MODEL,
availableModels: GITHUB_COPILOT_AVAILABLE_MODELS,
configurationDoc: githubCopilotConfigurationDoc,
createAdapter: (config) => new GitHubCopilotAdapter(config),
},
[OPENCODE_BACKEND]: {
backend: OPENCODE_BACKEND,
label: OPENCODE_LABEL,
Expand Down
11 changes: 11 additions & 0 deletions packages/agent-adapters/src/types/agent-adapter.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface AgentAdapter {
export type AgentBackend =
| "codex"
| "claude-code"
| "github-copilot"
| "cursor-agent"
| "opencode";

Expand Down Expand Up @@ -118,6 +119,16 @@ export interface AgentAdapterRuntimeConfig {
attach?: string;
dangerouslySkipPermissions?: boolean;
};
githubCopilot?: {
binary: string;
streamLogs: boolean;
model?: string;
copilotHome?: string;
githubToken?: string;
allowAllTools?: boolean;
allowTools?: string[];
denyTools?: string[];
};
claude?: {
model?: string;
maxTurns?: number;
Expand Down
Loading
Loading