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
2 changes: 2 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#
# Runtime environment:
# OPENAI_API_KEY required for model calls (checked at call time)
# AGENT_MODEL, GUARDRAIL_MODEL, COMPACTOR_MODEL
# optional "provider:model" overrides of the models
# SANDBOX_PROVIDER "local" (default) or "modal"
# MODAL_TOKEN_ID/SECRET credentials for the modal sandbox provider
# RESTATE_ADMIN_URL admin API for dynamic tool discovery (default
Expand Down
11 changes: 11 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The agent service reads these environment variables:
| `MCP_SERVERS_JSON` | MCP servers the agents may use; see below |
| `SANDBOX_PROVIDER` | `local` (default) or `modal`, which also needs `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET` |
| `MODAL_APP_NAME`, `MODAL_SANDBOX_NAMESPACE`, `MODAL_SANDBOX_IMAGE`, `MODAL_SANDBOX_TIMEOUT_MS` | Optional Modal settings; see [sandboxes](sandboxes.md) |
| `AGENT_MODEL`, `GUARDRAIL_MODEL`, `COMPACTOR_MODEL` | Override the agent, guardrail and compactor models, as `provider:model`; see [models](#models) |
| `AGENT_MODEL_MAX_OUTPUT_TOKENS` | Output budget per model call, 1024–64000 (default 32000) |
| `RESTATE_ADMIN_URL`, `RESTATE_ADMIN_TOKEN` | Admin API used to discover dynamic tools |

Expand All @@ -44,6 +45,16 @@ models: {
},
```

Each can also be set from the environment without editing the code:
`AGENT_MODEL`, `GUARDRAIL_MODEL` and `COMPACTOR_MODEL` override the agent,
guardrail and compactor models, and an unset or empty variable keeps the
default from `agent-config.ts`. The service reads them at startup.

```sh
export AGENT_MODEL=anthropic:claude-sonnet-5
export COMPACTOR_MODEL=google:gemini-3.8-flash
```

Only the providers in use need an API key. Every call asks for low reasoning,
and each provider maps that to its own setting. When the agent model changes,
set `context.windowTokens` to its context window.
Expand Down
15 changes: 12 additions & 3 deletions packages/libs/core/src/agent-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,33 @@ import {
import {getWeatherTool} from "./tools/weather.js";
import {webSearchTool} from "./tools/web-search.js";

/** An environment variable's value, or the default when it is unset or blank. */
function fromEnv(name: string, fallback: string): string {
return process.env[name]?.trim() || fallback;
}

export const agentConfig = {
/**
* The models, as "provider:model", where the provider is openai,
* anthropic, google, xai, deepseek or openai-compatible (see
* model/provider.ts and docs/configuration.md). Each provider needs its API
* key in the service's environment. The three may use different providers.
*
* AGENT_MODEL, GUARDRAIL_MODEL and COMPACTOR_MODEL override them from the
* environment; they are read once, at module load, so a change needs a
* restart.
*
* A turn keeps one model: its working messages carry that provider's
* reasoning and tool-call data. Running turns finish on the version they
* started on, so a change here applies to new turns.
*/
models: {
/** Runs the agent's turns. */
agent: "openai:gpt-5.6-luna",
agent: fromEnv("AGENT_MODEL", "openai:gpt-5.6-luna"),
/** Evaluates and reviews proposed actions against the guardrails. */
guardrail: "openai:gpt-5.6-terra",
guardrail: fromEnv("GUARDRAIL_MODEL", "openai:gpt-5.6-terra"),
/** Summarizes older conversation for later turns. */
compactor: "openai:gpt-5.6-terra",
compactor: fromEnv("COMPACTOR_MODEL", "openai:gpt-5.6-terra"),
},

/**
Expand Down
46 changes: 46 additions & 0 deletions packages/libs/core/test/model-env.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import {execFile} from "node:child_process";
import {test} from "node:test";
import {promisify} from "node:util";

// agentConfig reads the model variables once, at module load, so each case
// loads it in a fresh process with its own environment.
const probe = `
const {agentConfig} = await import("./src/agent-config.ts");
console.log(JSON.stringify(agentConfig.models));
`;

async function modelsWith(env) {
const base = {...process.env};
for (const name of ["AGENT_MODEL", "GUARDRAIL_MODEL", "COMPACTOR_MODEL"])
delete base[name];
const {stdout} = await promisify(execFile)(
process.execPath,
["--import", "tsx", "--input-type=module", "-e", probe],
{cwd: new URL("..", import.meta.url), env: {...base, ...env}},
);
return JSON.parse(stdout.trim().split("\n").at(-1));
}

test("the model variables override the configured models", async () => {
const defaults = await modelsWith({});
assert.deepEqual(
await modelsWith({
AGENT_MODEL: "anthropic:claude-sonnet-5",
GUARDRAIL_MODEL: " xai:grok-5 ",
COMPACTOR_MODEL: "google:gemini-3.8-flash",
}),
{
agent: "anthropic:claude-sonnet-5",
guardrail: "xai:grok-5",
compactor: "google:gemini-3.8-flash",
},
);
assert.deepEqual(
await modelsWith({
AGENT_MODEL: "deepseek:deepseek-chat",
COMPACTOR_MODEL: "",
}),
{...defaults, agent: "deepseek:deepseek-chat"},
);
});
Loading