Hinweis
Die ACP-Unterstützung in GitHub Copilot-CLI ist in Öffentliche Vorschau und kann geändert werden.
Übersicht
Das Agent-Client-Protokoll (ACP) ist ein Protokoll, das die Kommunikation zwischen Clients (z. B. Code-Editoren und IDEs) und Agenten (wie Copilot CLI) standardisiert. Weitere Informationen zu diesem Protokoll finden Sie in der offiziellen Einführung.
Anwendungsfälle
- IDE-Integrationen: Integrieren Sie Copilot Unterstützung in jede Editor- oder Entwicklungsumgebung.
- CI/CD-Pipelines: Koordinieren Sie agentische Codierungsaufgaben in automatisierten Workflows.
- Benutzerdefinierte Frontends: Erstellen Sie spezielle Schnittstellen für bestimmte Entwicklerworkflows.
- Multi-Agent-Systeme: Koordinieren sie Copilot mit anderen KI-Agents mithilfe eines Standardprotokolls.
Starten des ACP-Servers
Verwenden Sie die --acp Option des copilot Befehls, um den ACP-Server der CLI zu starten. Sie können den Transportmodus entweder mit den Optionen --stdio oder --port angeben. Wenn kein Transportmodus angegeben ist, wird der Server standardmäßig im Stdiomodus ausgeführt.
Auf jede Sitzung angewendete Optionen
Mit der ACP-Anforderung session/new kann ein Client nur einige Sitzungsparameter festlegen, z. B. das Arbeitsverzeichnis und die MCP-Server. Sie enthält keine Tool-Filter- oder Reasoning-Einstellungen. Um diese zu konfigurieren, übergeben Sie die entsprechenden Optionen, wenn Sie den Server starten. Der Server speichert die Werte und wendet sie als Initialkonfiguration für jede Sitzung an, die er erstellt oder lädt, für jeden Client, der eine Verbindung herstellt. Ein Client, der eine Verbindung herstellt, legt diese Werte nicht fest – sondern derjenige, der den Server startet.
| Serveroption | Akzeptierter Wert | Auswirkung auf jede Sitzung |
|---|---|---|
--available-tools=TOOL ... | Eine in Anführungszeichen gesetzte, durch Kommas getrennte Liste von Toolnamen | Die Sitzung kann nur die aufgelisteten Tools verwenden. |
--excluded-tools=TOOL ... | Eine in Anführungszeichen gesetzte, durch Kommas getrennte Liste von Toolnamen | Die aufgelisteten Tools werden aus der Sitzung entfernt. |
--effort=LEVEL, --reasoning-effort=LEVEL | ||
low, medium, high, xhigh oder max | Legt den anfänglichen Aufwand für das Schlussfolgern der Sitzung fest. |
Zum Beispiel startet dieser Befehl einen Server, bei dem alle Sitzungen maximale Schlussfolgerungsintensität nutzen und nur die Tools bash und view bereitstellen:
copilot --acp --port 3000 --effort=max --available-tools="bash,view"
Jede Sitzung, die der verbundene Client mit diesem Server öffnet, erbt diese Einstellungen. Da die Werte beim Starten des Servers behoben sind, kann ein Client sie nicht pro Sitzung durch session/newändern.
stdio-Modus
Der stdio-Modus wird standardmäßig erkannt, wenn Sie den ACP-Server starten. Sie können auch die Option --stdio zur Disambiguierung verwenden.
copilot --acp --stdio
TCP-Modus
Wenn die --port Option in Kombination mit der --acp Option bereitgestellt wird, wird der Server im TCP-Modus gestartet.
copilot --acp --port 3000
Auswahl zwischen Stdio und TCP
Beide Transportmodi übertragen dieselben ACP-Nachrichten, die als newline-delimited JSON (NDJSON) codiert sind. Sie unterscheiden sich nur darin, wie ein Client eine Verbindung mit dem Server herstellt und wie der Lebenszyklus des Servers verwaltet wird. Die beiden Modi schließen einander aus: Die gleichzeitige Übergabe von --stdio und --port wird abgelehnt.
| Aspect | stdio-Modus | TCP-Modus |
|---|---|---|
| Wie der Client eine Verbindung herstellt | Der Client startet copilot --acp als Kindprozess und tauscht Nachrichten über die Standardeingabe und -ausgabe dieses Prozesses aus. | Der Server öffnet einen TCP-Listener, mit dem Clients über einen Netzwerksocket eine Verbindung herstellen. Standardmäßig wird sie an die Loopbackadresse 127.0.0.1gebunden. |
| Anzahl der Clients | Ein einzelner Client – der Prozess, der den Server erzeugt hat und die Pipe besitzt. | Der Listener nimmt Socket-Verbindungen an, wobei jede als eigene Agentverbindung behandelt wird. |
| Lebenszyklus | An den übergeordneten Prozess gebunden. Wenn der Eingabestrom geschlossen wird, weil der übergeordnete Prozess beendet wird oder die Pipe schließt, wird der Server automatisch heruntergefahren. | Unabhängig von jedem einzelnen Client. Der Server überwacht den Port, bis er beendet wird, z. B. mit STRG+C. |
| Standardausgabe | Reserviert für den NDJSON-Protokolldatenstrom, sodass er nicht für Protokolle oder anderen Text verwendet werden kann. | Kostenlos für andere Zwecke, da der Protokolldatenverkehr über den Socket reist. |
Wann jeder Modus verwendet werden soll:
- Verwenden Sie den Stdiomodus , wenn ein Editor, eine IDE oder ein Skript direkt als Teilprozess spawnsiert Copilot CLI . Dies ist die Standard- und die empfohlene Konfiguration für die IDE-Integration, da der Transport beim Start des Prozesses automatisch aufgebaut und beim Beenden wieder abgebaut wird.
- Verwenden Sie den TCP-Modus , wenn ein Client den Server über einen Socket anstelle einer Pipe erreichen muss , z. B. von einem separaten Prozess oder Container oder beim Herstellen einer Verbindung mit einem längerlebigen Server auf einem bekannten Port.
Beispiel: Integration mit dem ACP-Server
Das folgende Beispiel ist eine Clientanwendung, die Copilot verwendet, indem sie mit dem ACP-Server von GitHub Copilot-CLI interagiert. Es startet den ACP-Server im stdio-Modus, öffnet eine Sitzung, fordert Sie zur Eingabe eines Prompts auf, sendet ihn und gibt die gestreamte Antwort aus.
Es gibt ein wachsendes Ökosystem von Bibliotheken für die programmgesteuerte Interaktion mit ACP-Servern. In diesem Beispiel wird die ACP TypeScript-Bibliothek verwendet.
Zum Ausführen dieses Beispiels benötigen Sie die folgenden Abhängigkeiten:
- Node.js Version 18 oder höher.
- GitHub Copilot-CLI, installiert und authentifiziert.
- Das
@agentclientprotocol/sdkPaket, das die ACP TypeScript-Bibliothek bereitstellt. Installieren Sie es, indem Sie ausführennpm install @agentclientprotocol/sdk.
import * as acp from "@agentclientprotocol/sdk";
import { spawn } from "node:child_process";
import { Readable, Writable } from "node:stream";
import * as readline from "node:readline/promises";
async function main() {
const executable = process.env.COPILOT_CLI_PATH ?? "copilot";
// ACP uses standard input/output (stdin/stdout) for transport; we pipe these for the NDJSON stream.
const copilotProcess = spawn(executable, ["--acp", "--stdio"], {
stdio: ["pipe", "pipe", "inherit"],
});
if (!copilotProcess.stdin || !copilotProcess.stdout) {
throw new Error("Failed to start Copilot ACP process with piped stdio.");
}
// Create ACP streams (NDJSON over stdio)
const output = Writable.toWeb(copilotProcess.stdin) as WritableStream<Uint8Array>;
const input = Readable.toWeb(copilotProcess.stdout) as ReadableStream<Uint8Array>;
const stream = acp.ndJsonStream(output, input);
const client: acp.Client = {
async requestPermission(params) {
// This example should not trigger tool calls; if it does, refuse.
return { outcome: { outcome: "cancelled" } };
},
async sessionUpdate(params) {
const update = params.update;
if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
process.stdout.write(update.content.text);
}
},
};
const connection = new acp.ClientSideConnection((_agent) => client, stream);
await connection.initialize({
protocolVersion: acp.PROTOCOL_VERSION,
clientCapabilities: {},
});
const sessionResult = await connection.newSession({
cwd: process.cwd(),
mcpServers: [],
});
process.stdout.write("Session started!\n");
// Ask the user to enter a prompt instead of using a hard-coded one.
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const promptText = await rl.question("Enter a prompt: ");
rl.close();
const promptResult = await connection.prompt({
sessionId: sessionResult.sessionId,
prompt: [{ type: "text", text: promptText }],
});
process.stdout.write("\n");
if (promptResult.stopReason !== "end_turn") {
process.stderr.write(`Prompt finished with stopReason=${promptResult.stopReason}\n`);
}
// Best-effort cleanup
copilotProcess.stdin.end();
copilotProcess.kill("SIGTERM");
await new Promise<void>((resolve) => {
copilotProcess.once("exit", () => resolve());
setTimeout(() => resolve(), 2000);
});
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
import * as acp from "@agentclientprotocol/sdk";
import { spawn } from "node:child_process";
import { Readable, Writable } from "node:stream";
import * as readline from "node:readline/promises";
async function main() {
const executable = process.env.COPILOT_CLI_PATH ?? "copilot";
// ACP uses standard input/output (stdin/stdout) for transport; we pipe these for the NDJSON stream.
const copilotProcess = spawn(executable, ["--acp", "--stdio"], {
stdio: ["pipe", "pipe", "inherit"],
});
if (!copilotProcess.stdin || !copilotProcess.stdout) {
throw new Error("Failed to start Copilot ACP process with piped stdio.");
}
// Create ACP streams (NDJSON over stdio)
const output = Writable.toWeb(copilotProcess.stdin) as WritableStream<Uint8Array>;
const input = Readable.toWeb(copilotProcess.stdout) as ReadableStream<Uint8Array>;
const stream = acp.ndJsonStream(output, input);
const client: acp.Client = {
async requestPermission(params) {
// This example should not trigger tool calls; if it does, refuse.
return { outcome: { outcome: "cancelled" } };
},
async sessionUpdate(params) {
const update = params.update;
if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
process.stdout.write(update.content.text);
}
},
};
const connection = new acp.ClientSideConnection((_agent) => client, stream);
await connection.initialize({
protocolVersion: acp.PROTOCOL_VERSION,
clientCapabilities: {},
});
const sessionResult = await connection.newSession({
cwd: process.cwd(),
mcpServers: [],
});
process.stdout.write("Session started!\n");
// Ask the user to enter a prompt instead of using a hard-coded one.
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const promptText = await rl.question("Enter a prompt: ");
rl.close();
const promptResult = await connection.prompt({
sessionId: sessionResult.sessionId,
prompt: [{ type: "text", text: promptText }],
});
process.stdout.write("\n");
if (promptResult.stopReason !== "end_turn") {
process.stderr.write(`Prompt finished with stopReason=${promptResult.stopReason}\n`);
}
// Best-effort cleanup
copilotProcess.stdin.end();
copilotProcess.kill("SIGTERM");
await new Promise<void>((resolve) => {
copilotProcess.once("exit", () => resolve());
setTimeout(() => resolve(), 2000);
});
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
So führen Sie das Beispiel aus:
-
Speichern Sie den obigen Code in einer Datei mit dem Namen
acp-client.ts. -
Führen Sie die Datei mit
npx tsxaus, wodurch TypeScript direkt ohne einen separaten Buildschritt ausgeführt wird:npx tsx acp-client.ts
Verwendung von Slash-Befehlen
Die integrierten Slash-Befehle von GitHub Copilot-CLI können über ACP ausgeführt werden. Um einen davon aufzurufen, senden Sie ihn als gewöhnlichen Prompt, dessen Text der Befehl ist, der als einzelner Textinhaltsblock übergeben wird – zum Beispiel /context oder /session info. Der Server erkennt den Befehl und führt ihn direkt aus: Informationsbefehle wie /usage oder /context geben ihre Ausgabe zurück, ohne das Modell aufzurufen, während Aktionsbefehle wie /plan oder /review die entsprechende Agentenaufgabe starten. Auf beide Weise wird der Befehlstext nicht als Frage an das Modell gesendet.
Ermitteln verfügbarer Befehle
Der Server kündigt die Befehle an, die er über die standardmäßige ACP-Sitzungsbenachrichtigung available_commands_update unterstützt. Sie wird gesendet, nachdem eine Sitzung erstellt oder geladen wurde, und erneut, wenn sich der Satz ändert – zum Beispiel, wenn die Fähigkeiten fertig geladen sind. Diese bekannt gegebene Liste ist die maßgebliche, stets aktuelle Liste der Befehle, die Sie über ACP ausführen können, und Clients zeigen sie typischerweise in einem Befehlsmenü an.
Die angekündigte Liste enthält:
- Integrierte Befehle, wie z. B.
/compact,/context,/usage,/env,/model,/mcp,/plan,/review,/research,/sessionund/rename. - Aktivierte, benutzerfreundliche Fähigkeiten, die als
/SKILL-NAMEBefehle angezeigt werden.
Befehle, die der Client selbst registriert, werden ihm nicht zurück bekanntgegeben.
Zugreifen auf die Liste von Ihrem Client
Da die Liste nicht als Reaktion auf eine Anforderung, sondern als Benachrichtigung eingeht, gibt es keine Methode, sie bei Bedarf abzurufen. Ihr Client greift darauf zu, indem die session/update Benachrichtigung verarbeitet und auf Updates reagiert wird, deren Typ lautet available_commands_update. Jeder Eintrag verfügt über einen name (ohne den führenden Schrägstrich), einen descriptionund einen optionalen input.hint Eintrag, der die Argumente des Befehls beschreibt. Die Benachrichtigung wird jedes Mal erneut gesendet, wenn sich der Satz ändert. Betrachten Sie daher jede Benachrichtigung als vollständigen Ersatz der Liste, die Sie zwischengespeichert haben.
Der folgende sessionUpdate Handler erfasst die angekündigten Befehle und erweitert das client Objekt aus dem zuvor gezeigten Beispiel.
// Track the latest advertised commands for the session.
let availableCommands: acp.AvailableCommand[] = [];
const client: acp.Client = {
async sessionUpdate(params) {
const update = params.update;
if (update.sessionUpdate === "available_commands_update") {
// This notification is a full snapshot—replace any cached list.
availableCommands = update.availableCommands;
for (const command of availableCommands) {
// command.name has no leading slash; invoke it by sending "/<name>" as a prompt.
console.log(`/${command.name} — ${command.description}`);
}
return;
}
// ...handle other updates, such as agent_message_chunk
},
// ...other client methods, such as requestPermission
};
// Track the latest advertised commands for the session.
let availableCommands: acp.AvailableCommand[] = [];
const client: acp.Client = {
async sessionUpdate(params) {
const update = params.update;
if (update.sessionUpdate === "available_commands_update") {
// This notification is a full snapshot—replace any cached list.
availableCommands = update.availableCommands;
for (const command of availableCommands) {
// command.name has no leading slash; invoke it by sending "/<name>" as a prompt.
console.log(`/${command.name} — ${command.description}`);
}
return;
}
// ...handle other updates, such as agent_message_chunk
},
// ...other client methods, such as requestPermission
};
Um einen der angekündigten Befehle auszuführen, senden Sie den Namen als Eingabeaufforderung in einem einzelnen Textinhaltsblock , { type: "text", text: "/context" }z. B. wie unter Verwenden von Schrägstrichbefehlen beschrieben.
Befehle, die nicht über ACP verwendet werden können
Slash-Befehle, die die interaktive Terminalschnittstelle voraussetzen, werden vom ACP-Server nicht verarbeitet. Dazu gehören Befehle, mit denen eine Auswahl, ein Dialogfeld oder eine Vollbildansicht geöffnet wird, beispielsweise /diff, /resume, /theme, /settings, /login, /help, /tasks und /undo. Wenn ein Befehl in der available_commands_update Liste nicht angezeigt wird, wird er in der Regel nicht über ACP ausgeführt: Der Server behandelt den Text als normale Eingabeaufforderung und leitet ihn an das Modell weiter, anstatt ihn auszuführen.
Da ACP-Clients keine interaktiven Auswahlen haben, gibt ein integrierter Befehl, der normalerweise ein Untermenü öffnet, seine Optionen als Text zurück. Geben Sie den Unterbefehl explizit an, um ein direktes Ergebnis zu erhalten – zum Beispiel /session info oder /mcp list statt /session oder /mcp für sich allein.
Eine vollständige Liste der Schrägstrichbefehle für Copilot CLI finden Sie unter GitHub Copilot CLI-Befehlsreferenz.