Skip to content
Open
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
11 changes: 8 additions & 3 deletions ts/packages/aiclient/src/copilotModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,14 @@ export interface CopilotClientOptions {
cliUrl?: string | undefined;
}

function configuredCopilotPath(): string | undefined {
return (
process.env.TYPEAGENT_COPILOT_CLI_PATH ?? process.env.COPILOT_CLI_PATH
);
}

function findCopilotPath(): string | undefined {
const configuredPath =
process.env.TYPEAGENT_COPILOT_CLI_PATH ?? process.env.COPILOT_CLI_PATH;
const configuredPath = configuredCopilotPath();
if (configuredPath) {
debug(`Using configured copilot CLI: ${configuredPath}`);
return configuredPath;
Expand All @@ -204,7 +209,7 @@ function findCopilotPath(): string | undefined {

export function createCopilotRuntimeConnection(
cliUrl: string | undefined,
cliPath: string | undefined,
cliPath: string | undefined = configuredCopilotPath(),
) {
return cliUrl
? RuntimeConnection.forUri(cliUrl)
Expand Down
1 change: 1 addition & 0 deletions ts/packages/aiclient/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export * from "./requestConstraints.js";
export {
getCopilotClient,
getCopilotCliPath,
createCopilotRuntimeConnection,
stopCopilotClient,
createCopilotModelEndpointProvider,
warmupCopilotClient,
Expand Down
54 changes: 54 additions & 0 deletions ts/packages/aiclient/test/copilotTransport.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,27 @@ const RESPONSES_OK = {
};

describe("createCopilotRuntimeConnection", () => {
const envKeys = ["TYPEAGENT_COPILOT_CLI_PATH", "COPILOT_CLI_PATH"] as const;
let savedEnv: (string | undefined)[];

beforeEach(() => {
savedEnv = envKeys.map((key) => process.env[key]);
for (const key of envKeys) {
delete process.env[key];
}
});

afterEach(() => {
envKeys.forEach((key, index) => {
const value = savedEnv[index];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
});
});

test("uses a configured runtime URI instead of an executable path", () => {
expect(
createCopilotRuntimeConnection(
Expand All @@ -184,6 +205,8 @@ describe("createCopilotRuntimeConnection", () => {
});

test("preserves a concrete external executable path", () => {
process.env.TYPEAGENT_COPILOT_CLI_PATH = "C:\\managed\\copilot.exe";
process.env.COPILOT_CLI_PATH = "C:\\fallback\\copilot.exe";
expect(
createCopilotRuntimeConnection(undefined, "C:\\tools\\copilot.exe"),
).toEqual({
Expand All @@ -194,6 +217,37 @@ describe("createCopilotRuntimeConnection", () => {
});
});

test("prefers the TypeAgent runtime over the generic CLI setting", () => {
process.env.TYPEAGENT_COPILOT_CLI_PATH = "C:\\managed\\copilot.exe";
process.env.COPILOT_CLI_PATH = "C:\\fallback\\copilot.exe";
expect(
createCopilotRuntimeConnection(undefined, undefined),
).toMatchObject({
kind: "stdio",
path: "C:\\managed\\copilot.exe",
});
});

test("honors the generic CLI setting when no TypeAgent runtime is configured", () => {
process.env.COPILOT_CLI_PATH = "C:\\fallback\\copilot.exe";
expect(
createCopilotRuntimeConnection(undefined, undefined),
).toMatchObject({
kind: "stdio",
path: "C:\\fallback\\copilot.exe",
});
});

test("keeps a remote URI ahead of configured local runtimes", () => {
process.env.TYPEAGENT_COPILOT_CLI_PATH = "C:\\managed\\copilot.exe";
expect(
createCopilotRuntimeConnection("localhost:4321", undefined),
).toMatchObject({
kind: "uri",
url: "localhost:4321",
});
});

test("omits the path to select the SDK-managed runtime", () => {
expect(createCopilotRuntimeConnection(undefined, undefined)).toEqual({
kind: "stdio",
Expand Down
7 changes: 4 additions & 3 deletions ts/packages/dispatcher/dispatcher/src/reasoning/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
import { ReasoningAction } from "../context/dispatcher/schema/reasoningActionSchema.js";
import {
CopilotClient,
RuntimeConnection,
defineTool,
type AssistantMessageEvent,
type CopilotSession,
Expand All @@ -25,6 +24,7 @@ import {
type MessageOptions,
type SessionConfig,
} from "@github/copilot-sdk";
import { createCopilotRuntimeConnection } from "@typeagent/aiclient";
import registerDebug from "debug";
import os from "node:os";
import path from "node:path";
Expand Down Expand Up @@ -389,9 +389,10 @@ async function createCopilotClient(

const creditBudget = getCopilotCreditBudget();
const client = new CopilotClient({
connection: RuntimeConnection.forStdio(
connection: createCopilotRuntimeConnection(
undefined,
creditBudget && process.env.TYPEAGENT_GHCP_EVAL_CLI
? { path: process.env.TYPEAGENT_GHCP_EVAL_CLI }
? process.env.TYPEAGENT_GHCP_EVAL_CLI
: undefined,
),
...(creditBudget ? { requestHandler: creditBudget } : {}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { jest } from "@jest/globals";
import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk";
import { mkdtempSync, rmSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import type { CommandHandlerContext } from "../src/context/commandHandlerContext.js";
import { prewarmCopilotReasoning } from "../src/reasoning/copilot.js";

describe("Copilot reasoning runtime selection", () => {
const envKeys = [
"TYPEAGENT_COPILOT_CLI_PATH",
"COPILOT_CLI_PATH",
"TYPEAGENT_GHCP_EVAL_CLI",
"TYPEAGENT_COPILOT_CREDIT_LEDGER",
] as const;
let savedEnv: (string | undefined)[];
let tempDir: string;

beforeEach(() => {
savedEnv = envKeys.map((key) => process.env[key]);
for (const key of envKeys) {
delete process.env[key];
}
tempDir = mkdtempSync(path.join(os.tmpdir(), "ta-reasoning-runtime-"));
jest.spyOn(os, "tmpdir").mockReturnValue(tempDir);
jest.spyOn(CopilotClient.prototype, "start").mockRejectedValue(
new Error("Test does not launch a runtime"),
);
jest.spyOn(RuntimeConnection, "forStdio");
});

afterEach(() => {
jest.restoreAllMocks();
envKeys.forEach((key, index) => {
const value = savedEnv[index];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
});
rmSync(tempDir, { recursive: true, force: true });
});

test.each([
{
name: "prefers the TypeAgent managed executable",
managed: "C:\\managed\\copilot.exe",
generic: "C:\\generic\\copilot.exe",
expected: "C:\\managed\\copilot.exe",
},
{
name: "uses the generic configured executable",
generic: "C:\\generic\\copilot.exe",
expected: "C:\\generic\\copilot.exe",
},
{
name: "retains SDK discovery without configuration",
expected: undefined,
},
{
name: "ignores the evaluation executable without a credit budget",
managed: "C:\\managed\\copilot.exe",
evaluation: "C:\\evaluation\\copilot.exe",
expected: "C:\\managed\\copilot.exe",
},
{
name: "prefers the evaluation executable with a credit budget",
managed: "C:\\managed\\copilot.exe",
generic: "C:\\generic\\copilot.exe",
evaluation: "C:\\evaluation\\copilot.exe",
creditBudget: true,
expected: "C:\\evaluation\\copilot.exe",
},
{
name: "uses the managed executable for credit budgets without an evaluation override",
managed: "C:\\managed\\copilot.exe",
creditBudget: true,
expected: "C:\\managed\\copilot.exe",
},
])(
"$name",
async ({ managed, generic, evaluation, creditBudget, expected }) => {
if (managed) process.env.TYPEAGENT_COPILOT_CLI_PATH = managed;
if (generic) process.env.COPILOT_CLI_PATH = generic;
if (evaluation) process.env.TYPEAGENT_GHCP_EVAL_CLI = evaluation;
if (creditBudget) {
process.env.TYPEAGENT_COPILOT_CREDIT_LEDGER = path.join(
tempDir,
"ledger.json",
);
}
const context = {
session: {
getConfig: () => ({ execution: { reasoning: "copilot" } }),
},
} as unknown as CommandHandlerContext;

prewarmCopilotReasoning(context);
await new Promise<void>((resolve) => setImmediate(resolve));

expect(CopilotClient.prototype.start).toHaveBeenCalledTimes(1);
expect(RuntimeConnection.forStdio).toHaveBeenCalledWith(
expected ? { path: expected } : {},
);
},
);
});
13 changes: 13 additions & 0 deletions ts/tools/installers/wix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,19 @@ The managed Copilot CLI cache uses `TYPEAGENT_COPILOT_RUNTIME_ROOT`.
`TYPEAGENT_RUNTIME_ROOT` is reserved for the bundled agent-server's application
assets and must not be overridden by installer lifecycle actions.

The runtime helper ignores `TYPEAGENT_RUNTIME_ROOT` when locating the managed
cache. Without a dedicated override, it uses `%LOCALAPPDATA%\TypeAgent\runtimes`
on Windows and `$XDG_DATA_HOME/TypeAgent/runtimes` (or
`~/.local/share/TypeAgent/runtimes`) elsewhere. `--runtime-root` overrides these
defaults and `TYPEAGENT_COPILOT_RUNTIME_ROOT`.

Prerequisite setup starts the selected runtime and checks its SDK status even
with `--runtime-only`; it does not require sign-in or model access for this
check. Startup failures return a nonzero setup exit code. The MSI logs these as
incomplete setup warnings without rolling back the installation. Reasoning and model clients use
`TYPEAGENT_COPILOT_CLI_PATH` before `COPILOT_CLI_PATH`; an explicit client path,
including the credit-budget evaluation override, takes precedence.

**Output:**

```
Expand Down
12 changes: 7 additions & 5 deletions ts/tools/scripts/copilotRuntime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@ export function defaultRuntimeRoot(env = process.env) {
if (env.TYPEAGENT_COPILOT_RUNTIME_ROOT) {
return path.resolve(env.TYPEAGENT_COPILOT_RUNTIME_ROOT);
}
if (env.TYPEAGENT_RUNTIME_ROOT) {
return path.resolve(env.TYPEAGENT_RUNTIME_ROOT);
}
if (process.platform === "win32" && env.LOCALAPPDATA) {
return path.join(env.LOCALAPPDATA, "TypeAgent", "runtimes");
}
Expand Down Expand Up @@ -514,7 +511,7 @@ export function resolveCopilotRuntime(
);
}

async function inspectCopilot(executable) {
async function inspectCopilot(executable, { runtimeOnly = false } = {}) {
let sdk;
try {
sdk = await import("@github/copilot-sdk");
Expand All @@ -541,6 +538,9 @@ async function inspectCopilot(executable) {
try {
await client.start();
const runtime = await client.getStatus();
if (runtimeOnly) {
return { runtime };
}
const auth = await client.getAuthStatus();
const models = auth.isAuthenticated ? await client.listModels() : [];
return {
Expand Down Expand Up @@ -578,6 +578,8 @@ async function setupCopilot(manifest, options) {
console.log(`GitHub Copilot runtime: ${executable}`);

if (!options.login) {
await inspectCopilot(executable, { runtimeOnly: true });
console.log("GitHub Copilot runtime startup verified.");
return 0;
}

Expand Down Expand Up @@ -651,7 +653,7 @@ function printHelp() {
" --artifact-dir <path>",
" --manifest <path>",
" --runtime-root <path>",
" --runtime-only",
" --runtime-only Verify runtime startup without signing in",
" --non-interactive",
" --device-code",
" --host <github-host>",
Expand Down
Loading
Loading