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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ configureAgent(codexEngine());
configureAgent(geminiEngine());
```

If you do not call `configureAgent(...)`, Rig now auto-selects a default engine from environment variables: `COPILOT_SDK_URI` → `copilotEngine()`, then `RIG_ENGINE` (`copilot` | `anthropic` | `codex` | `gemini`) if set, then `ANTHROPIC_API_KEY` → `anthropicEngine()`, `OPENAI_API_KEY` → `codexEngine()`, `GEMINI_API_KEY`/`GOOGLE_API_KEY` → `geminiEngine()`, otherwise `copilotEngine()`.

`piEngine()` uses the maintained `@earendil-works/pi-agent-core` package and requires the provider for model lookup. `anthropicEngine()` uses `@anthropic-ai/sdk`. Both adapters preserve conversation state across repair turns and map Rig tools to their SDK tool runners.

`codexEngine()` uses `@openai/codex-sdk`, preserves its thread across repair turns, and accepts Codex client options plus thread options under `thread`. Rig system messages become Codex developer instructions. The Codex SDK does not expose custom tool registration, so the adapter rejects agents with Rig tools.
Expand All @@ -304,7 +306,7 @@ configureAgent(geminiEngine());

## Copilot SDK adapter

By default it connects to an already-running Copilot server via HTTP (`COPILOT_SDK_URI`, then `localhost:7777`).
When Copilot is selected, it connects to an already-running Copilot server via HTTP (`COPILOT_SDK_URI`, then `localhost:7777`).
Pass `--server` to spawn the server over stdio when launching a program.
Run `node skills/rig/rig.ts --help` for CLI usage; the launcher also accepts common help aliases such as `-h`, `help`, `/help`, and `/?`.

Expand Down
7 changes: 7 additions & 0 deletions skills/rig/references/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ configureAgent(codexEngine());
configureAgent(geminiEngine());
```

- If you do not call `configureAgent(...)`, Rig auto-selects an engine from env vars:
- `COPILOT_SDK_URI` → `copilotEngine()`
- `RIG_ENGINE` (`copilot` | `anthropic` | `codex` | `gemini`) to force a specific default when Copilot URI is not set.
- `ANTHROPIC_API_KEY` → `anthropicEngine()`
- `OPENAI_API_KEY` → `codexEngine()`
- `GEMINI_API_KEY` or `GOOGLE_API_KEY` → `geminiEngine()`
- otherwise → `copilotEngine()`
- `copilotEngine()` uses the Copilot SDK HTTP transport by default; launcher `--server` selects stdio.
- `piEngine({ provider })` uses `@earendil-works/pi-agent-core` and requires a provider for model lookup.
- `anthropicEngine()` uses `@anthropic-ai/sdk` and reads `ANTHROPIC_API_KEY`.
Expand Down
67 changes: 62 additions & 5 deletions skills/rig/rig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,63 @@ function resolveDefaultCopilotUri(): string {
return process.env["COPILOT_SDK_URI"] ?? "localhost:7777";
}

type DefaultEngineKind = "copilot" | "anthropic" | "codex" | "gemini";

type DefaultEngineOptions = {
cwd?: string;
startServer?: boolean;
};

function hasNonEmptyEnv(name: string): boolean {
const value = process.env[name];
return typeof value === "string" && value.trim().length > 0;
}

function resolveDefaultEngineKind(options: DefaultEngineOptions = {}): DefaultEngineKind {
if (options.startServer) {
return "copilot";
}
if (hasNonEmptyEnv("COPILOT_SDK_URI")) {
return "copilot";
}
const configuredEngine = process.env["RIG_ENGINE"]?.trim().toLowerCase();
if (configuredEngine === "copilot" || configuredEngine === "anthropic" || configuredEngine === "codex" || configuredEngine === "gemini") {
return configuredEngine;
}
if (hasNonEmptyEnv("ANTHROPIC_API_KEY")) {
return "anthropic";
}
if (hasNonEmptyEnv("OPENAI_API_KEY")) {
return "codex";
}
if (hasNonEmptyEnv("GEMINI_API_KEY") || hasNonEmptyEnv("GOOGLE_API_KEY")) {
return "gemini";
}
return "copilot";
}

function defaultAgentFactory(options: DefaultEngineOptions = {}): AgentFactory {
return async (agentOptions) => {
const kind = resolveDefaultEngineKind(options);
if (kind === "anthropic") {
const { anthropicEngine } = await import("./engines/anthropic.ts");
return anthropicEngine()(agentOptions);
}
if (kind === "codex") {
const { codexEngine } = await import("./engines/codex.ts");
return codexEngine(options.cwd ? { thread: { workingDirectory: options.cwd } } : {})(agentOptions);
}
if (kind === "gemini") {
const { geminiEngine } = await import("./engines/gemini.ts");
return geminiEngine(options.cwd ? { cwd: options.cwd } : {})(agentOptions);
}
const copilotOptions = options.cwd
? resolveCopilotOptions(options.cwd, options.startServer ? { startServer: true } : {})
: options.startServer ? { server: true } : {};
return copilotEngine(copilotOptions)(agentOptions);
};
}

export function copilotEngine(options: CopilotEngineOptions = {}): AgentFactory {
const { server, connection, ...clientOptions } = options;
return async (agentOptions) => {
Expand Down Expand Up @@ -1176,7 +1233,7 @@ export class AgentError extends Error {
}
}

let currentAgentFactory: AgentFactory = copilotEngine();
let currentAgentFactory: AgentFactory = defaultAgentFactory();

/**
* Mounts an engine and executes a rig program file.
Expand All @@ -1186,7 +1243,7 @@ export async function launchRigProgram(programPath: string, options: LaunchOptio
const cwd = options.cwd ?? process.cwd();
const resolvedPath = isAbsolute(programPath) ? programPath : resolve(cwd, programPath);

configureAgent(copilotEngine(resolveCopilotOptions(cwd, options)));
configureAgent(defaultAgentFactory({ cwd, ...(options.startServer ? { startServer: true } : {}) }));
await import(pathToFileURL(resolvedPath).href);
}

Expand Down Expand Up @@ -1426,7 +1483,7 @@ async function runRootAgentFromStdin(
throw new Error(`Usage: ${scriptName} <program-file>`);
}

configureAgent(copilotEngine(resolveCopilotOptions(cwd, options)));
configureAgent(defaultAgentFactory({ cwd, ...(options.startServer ? { startServer: true } : {}) }));
const mod = await import(pathToFileURL(resolvedPath).href);
const rootAgent = asRootProgram(mod.default, "launcher-root");
if (!rootAgent) {
Expand Down Expand Up @@ -1460,7 +1517,7 @@ async function runProgramCodeFromStdin(
io.stdout.write("typecheck passed\n");
return;
}
configureAgent(copilotEngine(resolveCopilotOptions(cwd, options)));
configureAgent(defaultAgentFactory({ cwd, ...(options.startServer ? { startServer: true } : {}) }));
const mod = await import(pathToFileURL(tempProgramPath).href);
const rootAgent = asRootProgram(mod.default, "launcher-inline-root");
if (!rootAgent) {
Expand Down Expand Up @@ -1502,7 +1559,7 @@ function renderLauncherUsage(scriptName: string): string {
}

/**
* Entry-point CLI that parses `argv`, wires a `copilotEngine`, and runs the
* Entry-point CLI that parses `argv`, wires the default engine selection, and runs the
* agent program. Two modes are supported:
*
* - **File mode** (`runLauncherCli(["path/to/prog.ts"])`): reads the agent
Expand Down
122 changes: 120 additions & 2 deletions src/launcher-default-engine.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, it, vi } from "vitest";
import { beforeEach, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => {
const approveAll = vi.fn();
Expand All @@ -13,17 +13,75 @@ const mocks = vi.hoisted(() => {
copilotClientCtor(options);
return { createSession, stop: stopClient };
};
return { approveAll, createSession, stopClient, copilotClientCtor, defaultForUri, forUri, CopilotClient };

const anthropicConstructor = vi.fn();
const anthropicToolRunner = vi.fn(() => ({
async runUntilDone() {
return { content: [{ type: "text", text: JSON.stringify("anthropic-mounted") }] };
},
params: { messages: [] },
}));
const Anthropic = function (this: unknown, options: unknown) {
anthropicConstructor(options);
return { beta: { messages: { toolRunner: anthropicToolRunner } } };
};
const betaTool = vi.fn((tool) => tool);

const codexConstructor = vi.fn();
const codexRun = vi.fn(async () => ({ finalResponse: JSON.stringify("codex-mounted") }));
const codexStartThread = vi.fn(() => ({ run: codexRun }));
const Codex = function (this: unknown, options: unknown) {
codexConstructor(options);
return { startThread: codexStartThread };
};

return {
approveAll,
createSession,
stopClient,
copilotClientCtor,
defaultForUri,
forUri,
CopilotClient,
anthropicConstructor,
anthropicToolRunner,
Anthropic,
betaTool,
codexConstructor,
codexRun,
codexStartThread,
Codex,
};
});

vi.mock("@github/copilot-sdk", () => ({
approveAll: mocks.approveAll,
CopilotClient: mocks.CopilotClient,
RuntimeConnection: { forUri: mocks.forUri, forStdio: vi.fn() },
}));
vi.mock("@anthropic-ai/sdk", () => ({ default: mocks.Anthropic }));
vi.mock("@anthropic-ai/sdk/helpers/beta/json-schema", () => ({ betaTool: mocks.betaTool }));
vi.mock("@openai/codex-sdk", () => ({ Codex: mocks.Codex }));

import { agent, launchRigProgram, s } from "rig";

beforeEach(() => {
mocks.copilotClientCtor.mockClear();
mocks.createSession.mockReset();
mocks.anthropicConstructor.mockClear();
mocks.anthropicToolRunner.mockClear();
mocks.betaTool.mockClear();
mocks.codexConstructor.mockClear();
mocks.codexRun.mockClear();
mocks.codexStartThread.mockClear();
delete process.env["COPILOT_SDK_URI"];
delete process.env["RIG_ENGINE"];
delete process.env["ANTHROPIC_API_KEY"];
delete process.env["OPENAI_API_KEY"];
delete process.env["GEMINI_API_KEY"];
delete process.env["GOOGLE_API_KEY"];
});

it("uses the launcher cwd when mounting the default copilot engine", async () => {
const sendAndWait = vi.fn().mockResolvedValue(JSON.stringify("default-mounted"));
mocks.createSession.mockResolvedValue({ sendAndWait });
Expand Down Expand Up @@ -70,3 +128,63 @@ it("uses COPILOT_SDK_URI when mounting the default copilot engine", async () =>
mocks.forUri.mockImplementation(mocks.defaultForUri);
}
});

it("prefers COPILOT_SDK_URI over RIG_ENGINE when mounting the default engine", async () => {
const sendAndWait = vi.fn().mockResolvedValue(JSON.stringify("copilot-preferred"));
mocks.createSession.mockResolvedValue({ sendAndWait });
process.env["COPILOT_SDK_URI"] = "http://127.0.0.1:4242";
process.env["RIG_ENGINE"] = "anthropic";
process.env["ANTHROPIC_API_KEY"] = "test-key";
mocks.forUri.mockImplementation(((url: string) => ({ kind: "uri", url })) as any);

const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.fixture.ts");

try {
await launchRigProgram(fixturePath);

const call = agent({
name: "launcher-default-engine-copilot-preferred-test",
input: s.object({}),
});
const result = await call({});
expect(result).toBe("copilot-preferred");
expect(mocks.forUri).toHaveBeenCalledWith("http://127.0.0.1:4242");
expect(mocks.copilotClientCtor).toHaveBeenCalled();
expect(mocks.anthropicConstructor).not.toHaveBeenCalled();
} finally {
mocks.forUri.mockImplementation(mocks.defaultForUri);
}
});

it("automatically mounts anthropicEngine when ANTHROPIC_API_KEY is set", async () => {
process.env["ANTHROPIC_API_KEY"] = "test-key";
const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.fixture.ts");

await launchRigProgram(fixturePath);

const call = agent({
name: "launcher-default-engine-anthropic-test",
input: s.object({}),
});
const result = await call({});
expect(result).toBe("anthropic-mounted");
expect(mocks.anthropicConstructor).toHaveBeenCalledWith({});
expect(mocks.copilotClientCtor).not.toHaveBeenCalled();
});

it("automatically mounts codexEngine when OPENAI_API_KEY is set", async () => {
process.env["OPENAI_API_KEY"] = "test-key";
const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.fixture.ts");

await launchRigProgram(fixturePath);

const call = agent({
name: "launcher-default-engine-codex-test",
input: s.object({}),
});
const result = await call({});
expect(result).toBe("codex-mounted");
expect(mocks.codexConstructor).toHaveBeenCalledWith({});
expect(mocks.codexStartThread).toHaveBeenCalledWith(expect.objectContaining({ model: "small" }));
expect(mocks.copilotClientCtor).not.toHaveBeenCalled();
});