Skip to content

Commit da69291

Browse files
authored
Auto-configure Rig default engine from environment when no engine is explicitly mounted (#148)
1 parent 8b62d22 commit da69291

4 files changed

Lines changed: 192 additions & 8 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ configureAgent(codexEngine());
296296
configureAgent(geminiEngine());
297297
```
298298

299+
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()`.
300+
299301
`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.
300302

301303
`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.
@@ -304,7 +306,7 @@ configureAgent(geminiEngine());
304306

305307
## Copilot SDK adapter
306308

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

skills/rig/references/runtime.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ configureAgent(codexEngine());
8585
configureAgent(geminiEngine());
8686
```
8787

88+
- If you do not call `configureAgent(...)`, Rig auto-selects an engine from env vars:
89+
- `COPILOT_SDK_URI``copilotEngine()`
90+
- `RIG_ENGINE` (`copilot` | `anthropic` | `codex` | `gemini`) to force a specific default when Copilot URI is not set.
91+
- `ANTHROPIC_API_KEY``anthropicEngine()`
92+
- `OPENAI_API_KEY``codexEngine()`
93+
- `GEMINI_API_KEY` or `GOOGLE_API_KEY``geminiEngine()`
94+
- otherwise → `copilotEngine()`
8895
- `copilotEngine()` uses the Copilot SDK HTTP transport by default; launcher `--server` selects stdio.
8996
- `piEngine({ provider })` uses `@earendil-works/pi-agent-core` and requires a provider for model lookup.
9097
- `anthropicEngine()` uses `@anthropic-ai/sdk` and reads `ANTHROPIC_API_KEY`.

skills/rig/rig.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,63 @@ function resolveDefaultCopilotUri(): string {
462462
return process.env["COPILOT_SDK_URI"] ?? "localhost:7777";
463463
}
464464

465+
type DefaultEngineKind = "copilot" | "anthropic" | "codex" | "gemini";
466+
467+
type DefaultEngineOptions = {
468+
cwd?: string;
469+
startServer?: boolean;
470+
};
471+
472+
function hasNonEmptyEnv(name: string): boolean {
473+
const value = process.env[name];
474+
return typeof value === "string" && value.trim().length > 0;
475+
}
476+
477+
function resolveDefaultEngineKind(options: DefaultEngineOptions = {}): DefaultEngineKind {
478+
if (options.startServer) {
479+
return "copilot";
480+
}
481+
if (hasNonEmptyEnv("COPILOT_SDK_URI")) {
482+
return "copilot";
483+
}
484+
const configuredEngine = process.env["RIG_ENGINE"]?.trim().toLowerCase();
485+
if (configuredEngine === "copilot" || configuredEngine === "anthropic" || configuredEngine === "codex" || configuredEngine === "gemini") {
486+
return configuredEngine;
487+
}
488+
if (hasNonEmptyEnv("ANTHROPIC_API_KEY")) {
489+
return "anthropic";
490+
}
491+
if (hasNonEmptyEnv("OPENAI_API_KEY")) {
492+
return "codex";
493+
}
494+
if (hasNonEmptyEnv("GEMINI_API_KEY") || hasNonEmptyEnv("GOOGLE_API_KEY")) {
495+
return "gemini";
496+
}
497+
return "copilot";
498+
}
499+
500+
function defaultAgentFactory(options: DefaultEngineOptions = {}): AgentFactory {
501+
return async (agentOptions) => {
502+
const kind = resolveDefaultEngineKind(options);
503+
if (kind === "anthropic") {
504+
const { anthropicEngine } = await import("./engines/anthropic.ts");
505+
return anthropicEngine()(agentOptions);
506+
}
507+
if (kind === "codex") {
508+
const { codexEngine } = await import("./engines/codex.ts");
509+
return codexEngine(options.cwd ? { thread: { workingDirectory: options.cwd } } : {})(agentOptions);
510+
}
511+
if (kind === "gemini") {
512+
const { geminiEngine } = await import("./engines/gemini.ts");
513+
return geminiEngine(options.cwd ? { cwd: options.cwd } : {})(agentOptions);
514+
}
515+
const copilotOptions = options.cwd
516+
? resolveCopilotOptions(options.cwd, options.startServer ? { startServer: true } : {})
517+
: options.startServer ? { server: true } : {};
518+
return copilotEngine(copilotOptions)(agentOptions);
519+
};
520+
}
521+
465522
export function copilotEngine(options: CopilotEngineOptions = {}): AgentFactory {
466523
const { server, connection, ...clientOptions } = options;
467524
return async (agentOptions) => {
@@ -1197,7 +1254,7 @@ export class AgentError extends Error {
11971254
}
11981255
}
11991256

1200-
let currentAgentFactory: AgentFactory = copilotEngine();
1257+
let currentAgentFactory: AgentFactory = defaultAgentFactory();
12011258

12021259
/**
12031260
* Mounts an engine and executes a rig program file.
@@ -1207,7 +1264,7 @@ export async function launchRigProgram(programPath: string, options: LaunchOptio
12071264
const cwd = options.cwd ?? process.cwd();
12081265
const resolvedPath = isAbsolute(programPath) ? programPath : resolve(cwd, programPath);
12091266

1210-
configureAgent(copilotEngine(resolveCopilotOptions(cwd, options)));
1267+
configureAgent(defaultAgentFactory({ cwd, ...(options.startServer ? { startServer: true } : {}) }));
12111268
await import(pathToFileURL(resolvedPath).href);
12121269
}
12131270

@@ -1447,7 +1504,7 @@ async function runRootAgentFromStdin(
14471504
throw new Error(`Usage: ${scriptName} <program-file>`);
14481505
}
14491506

1450-
configureAgent(copilotEngine(resolveCopilotOptions(cwd, options)));
1507+
configureAgent(defaultAgentFactory({ cwd, ...(options.startServer ? { startServer: true } : {}) }));
14511508
const mod = await import(pathToFileURL(resolvedPath).href);
14521509
const rootAgent = asRootProgram(mod.default, "launcher-root");
14531510
if (!rootAgent) {
@@ -1481,7 +1538,7 @@ async function runProgramCodeFromStdin(
14811538
io.stdout.write("typecheck passed\n");
14821539
return;
14831540
}
1484-
configureAgent(copilotEngine(resolveCopilotOptions(cwd, options)));
1541+
configureAgent(defaultAgentFactory({ cwd, ...(options.startServer ? { startServer: true } : {}) }));
14851542
const mod = await import(pathToFileURL(tempProgramPath).href);
14861543
const rootAgent = asRootProgram(mod.default, "launcher-inline-root");
14871544
if (!rootAgent) {
@@ -1523,7 +1580,7 @@ function renderLauncherUsage(scriptName: string): string {
15231580
}
15241581

15251582
/**
1526-
* Entry-point CLI that parses `argv`, wires a `copilotEngine`, and runs the
1583+
* Entry-point CLI that parses `argv`, wires the default engine selection, and runs the
15271584
* agent program. Two modes are supported:
15281585
*
15291586
* - **File mode** (`runLauncherCli(["path/to/prog.ts"])`): reads the agent

src/launcher-default-engine.test.ts

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { dirname, resolve } from "node:path";
22
import { fileURLToPath } from "node:url";
3-
import { expect, it, vi } from "vitest";
3+
import { beforeEach, expect, it, vi } from "vitest";
44

55
const mocks = vi.hoisted(() => {
66
const approveAll = vi.fn();
@@ -13,17 +13,75 @@ const mocks = vi.hoisted(() => {
1313
copilotClientCtor(options);
1414
return { createSession, stop: stopClient };
1515
};
16-
return { approveAll, createSession, stopClient, copilotClientCtor, defaultForUri, forUri, CopilotClient };
16+
17+
const anthropicConstructor = vi.fn();
18+
const anthropicToolRunner = vi.fn(() => ({
19+
async runUntilDone() {
20+
return { content: [{ type: "text", text: JSON.stringify("anthropic-mounted") }] };
21+
},
22+
params: { messages: [] },
23+
}));
24+
const Anthropic = function (this: unknown, options: unknown) {
25+
anthropicConstructor(options);
26+
return { beta: { messages: { toolRunner: anthropicToolRunner } } };
27+
};
28+
const betaTool = vi.fn((tool) => tool);
29+
30+
const codexConstructor = vi.fn();
31+
const codexRun = vi.fn(async () => ({ finalResponse: JSON.stringify("codex-mounted") }));
32+
const codexStartThread = vi.fn(() => ({ run: codexRun }));
33+
const Codex = function (this: unknown, options: unknown) {
34+
codexConstructor(options);
35+
return { startThread: codexStartThread };
36+
};
37+
38+
return {
39+
approveAll,
40+
createSession,
41+
stopClient,
42+
copilotClientCtor,
43+
defaultForUri,
44+
forUri,
45+
CopilotClient,
46+
anthropicConstructor,
47+
anthropicToolRunner,
48+
Anthropic,
49+
betaTool,
50+
codexConstructor,
51+
codexRun,
52+
codexStartThread,
53+
Codex,
54+
};
1755
});
1856

1957
vi.mock("@github/copilot-sdk", () => ({
2058
approveAll: mocks.approveAll,
2159
CopilotClient: mocks.CopilotClient,
2260
RuntimeConnection: { forUri: mocks.forUri, forStdio: vi.fn() },
2361
}));
62+
vi.mock("@anthropic-ai/sdk", () => ({ default: mocks.Anthropic }));
63+
vi.mock("@anthropic-ai/sdk/helpers/beta/json-schema", () => ({ betaTool: mocks.betaTool }));
64+
vi.mock("@openai/codex-sdk", () => ({ Codex: mocks.Codex }));
2465

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

68+
beforeEach(() => {
69+
mocks.copilotClientCtor.mockClear();
70+
mocks.createSession.mockReset();
71+
mocks.anthropicConstructor.mockClear();
72+
mocks.anthropicToolRunner.mockClear();
73+
mocks.betaTool.mockClear();
74+
mocks.codexConstructor.mockClear();
75+
mocks.codexRun.mockClear();
76+
mocks.codexStartThread.mockClear();
77+
delete process.env["COPILOT_SDK_URI"];
78+
delete process.env["RIG_ENGINE"];
79+
delete process.env["ANTHROPIC_API_KEY"];
80+
delete process.env["OPENAI_API_KEY"];
81+
delete process.env["GEMINI_API_KEY"];
82+
delete process.env["GOOGLE_API_KEY"];
83+
});
84+
2785
it("uses the launcher cwd when mounting the default copilot engine", async () => {
2886
const sendAndWait = vi.fn().mockResolvedValue(JSON.stringify("default-mounted"));
2987
mocks.createSession.mockResolvedValue({ sendAndWait });
@@ -70,3 +128,63 @@ it("uses COPILOT_SDK_URI when mounting the default copilot engine", async () =>
70128
mocks.forUri.mockImplementation(mocks.defaultForUri);
71129
}
72130
});
131+
132+
it("prefers COPILOT_SDK_URI over RIG_ENGINE when mounting the default engine", async () => {
133+
const sendAndWait = vi.fn().mockResolvedValue(JSON.stringify("copilot-preferred"));
134+
mocks.createSession.mockResolvedValue({ sendAndWait });
135+
process.env["COPILOT_SDK_URI"] = "http://127.0.0.1:4242";
136+
process.env["RIG_ENGINE"] = "anthropic";
137+
process.env["ANTHROPIC_API_KEY"] = "test-key";
138+
mocks.forUri.mockImplementation(((url: string) => ({ kind: "uri", url })) as any);
139+
140+
const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.fixture.ts");
141+
142+
try {
143+
await launchRigProgram(fixturePath);
144+
145+
const call = agent({
146+
name: "launcher-default-engine-copilot-preferred-test",
147+
input: s.object({}),
148+
});
149+
const result = await call({});
150+
expect(result).toBe("copilot-preferred");
151+
expect(mocks.forUri).toHaveBeenCalledWith("http://127.0.0.1:4242");
152+
expect(mocks.copilotClientCtor).toHaveBeenCalled();
153+
expect(mocks.anthropicConstructor).not.toHaveBeenCalled();
154+
} finally {
155+
mocks.forUri.mockImplementation(mocks.defaultForUri);
156+
}
157+
});
158+
159+
it("automatically mounts anthropicEngine when ANTHROPIC_API_KEY is set", async () => {
160+
process.env["ANTHROPIC_API_KEY"] = "test-key";
161+
const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.fixture.ts");
162+
163+
await launchRigProgram(fixturePath);
164+
165+
const call = agent({
166+
name: "launcher-default-engine-anthropic-test",
167+
input: s.object({}),
168+
});
169+
const result = await call({});
170+
expect(result).toBe("anthropic-mounted");
171+
expect(mocks.anthropicConstructor).toHaveBeenCalledWith({});
172+
expect(mocks.copilotClientCtor).not.toHaveBeenCalled();
173+
});
174+
175+
it("automatically mounts codexEngine when OPENAI_API_KEY is set", async () => {
176+
process.env["OPENAI_API_KEY"] = "test-key";
177+
const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.fixture.ts");
178+
179+
await launchRigProgram(fixturePath);
180+
181+
const call = agent({
182+
name: "launcher-default-engine-codex-test",
183+
input: s.object({}),
184+
});
185+
const result = await call({});
186+
expect(result).toBe("codex-mounted");
187+
expect(mocks.codexConstructor).toHaveBeenCalledWith({});
188+
expect(mocks.codexStartThread).toHaveBeenCalledWith(expect.objectContaining({ model: "small" }));
189+
expect(mocks.copilotClientCtor).not.toHaveBeenCalled();
190+
});

0 commit comments

Comments
 (0)