Skip to content

Commit 2be87b0

Browse files
kapaleshreyasclaude
andcommitted
feat: runtime overrides — model, temperature, baseUrl on ComputerAgent
Three first-class fields callers can pass to override what the GAP repo specifies, without forking the agent definition: new ComputerAgent({ source: "...", harness: "...", runtime: ..., model: "claude-haiku-4-5-20251001", // overrides agent.yaml model.preferred temperature: 0.2, // overrides constraints.temperature baseUrl: "https://my-proxy.example.com", // routes LLM calls through a proxy }); ## Cost source — no price table Same principle as #5: we don't compute or hardcode anything. `baseUrl` becomes the `ANTHROPIC_BASE_URL` env var, which the Anthropic SDK reads natively. `model` flows through the existing `mergeEngineOptions` chain. `temperature` is an engine-dependent passthrough — gitagent honors it (folds into gitclaw's `constraints` object), claude-agent-sdk warns once per process (Claude SDK v0.2.x doesn't expose temperature on its public Options type yet — fix lands when upstream adds it). ## What changed (additive — no breaking changes) packages/sdk/src/types.ts: + ComputerAgentOptions.model?: string + ComputerAgentOptions.temperature?: number + ComputerAgentOptions.baseUrl?: string packages/sdk/src/computer-agent.ts: Constructor computes `effectiveEnvs` (baseUrl injected as ANTHROPIC_BASE_URL when caller hasn't set that key explicitly) and `effectiveOptions` (model + temperature merged in). Both are memoized and used by both substrate boot and createSession so they stay consistent across multiple chats. Bad baseUrl throws at construction via `new URL(...)` parse — fail fast. packages/identity-gitagentprotocol/src/manifest.ts: + Typed `model.constraints.temperature` (was passthrough-only). packages/identity-gitagentprotocol/src/adapters/{claude-agent-sdk,gitagent}.ts: Map manifest.model.constraints.temperature → flat opts.temperature. Caller-supplied overrides still win via mergeEngineOptions on the harness side. packages/engine-gitagent/src/engine.ts: Fold flat ctx.options.temperature into gitclaw's nested `constraints.temperature` (gitclaw's actual interface). Strip the flat field before spread so it doesn't leak as an unknown top-level option. packages/engine-claude-agent-sdk/src/engine.ts: Strip flat temperature before spread (SDK doesn't accept it). Warn once per process via console.warn when temperature is set — explicitly tells the user to switch to gitagent if it matters, doesn't silently swallow. ## Tests added 10 new (314 → 324 total, all green): - sdk: 6 — model fold, model vs options precedence, temperature fold, baseUrl injection, baseUrl doesn't clobber explicit ANTHROPIC_BASE_URL, invalid baseUrl throws at construction - identity-gitagentprotocol/adapters: 4 — temperature mapped from constraints on both adapters, absent when not declared - engine-gitagent: 1 — flat temperature folds into constraints, pre-existing constraints field preserved, flat field stripped from the QueryOptions root ## Live-validated bun run examples/hello.ts # baseline still works bun run examples/_override-smoke.ts (now removed) # all 3 overrides applied → Response: "Hello there friend." (model=haiku honored) → 428 tokens • $0.0115 bad-url test (now removed): → ComputerAgent: invalid baseUrl "not a real url" — must be a valid URL (e.g. "https://api.anthropic.com"). ## Wire schema unchanged `CreateSessionBody.options` is already `Record<string, unknown>` — opaque passthrough. No zod schema change needed. Engine and adapter type augments are purely TS-side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 07bca6a commit 2be87b0

15 files changed

Lines changed: 594 additions & 24 deletions

File tree

examples/hello.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ await using agent = new ComputerAgent({
3232
runtime: new LocalSubstrate(),
3333
envs: { ANTHROPIC_API_KEY },
3434
options: { permissionMode: "bypassPermissions" },
35+
36+
// Optional runtime overrides — all three win over agent.yaml:
37+
// model: "claude-haiku-4-5-20251001", // force a specific model
38+
// temperature: 0.2, // override sampling temperature
39+
// baseUrl: "https://my-proxy.example.com", // route through a proxy / self-hosted endpoint
3540
});
3641

3742
const result = await agent.chat('Write a 3-line haiku about TypeScript to "haiku.txt".');

packages/engine-claude-agent-sdk/src/engine.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,16 @@ export class ClaudeAgentEngine implements EngineDriver<ClaudeAgentOptions> {
7474
}
7575
}
7676

77+
// `temperature` is part of our cross-engine options surface (Wedge 1.7)
78+
// but `@anthropic-ai/claude-agent-sdk` v0.2.x's public `Options` type
79+
// doesn't expose it. Warn once per process so callers know it's a no-op
80+
// here — they can switch to harness="gitagent" if temperature matters,
81+
// or wait for the upstream SDK to expose the field.
82+
const flatTemperature = (ctx.options as { temperature?: number }).temperature;
83+
if (flatTemperature !== undefined) warnTemperatureUnsupported();
84+
7785
const options: ClaudeAgentOptions = {
78-
...ctx.options,
86+
...stripFlatTemperature(ctx.options),
7987
cwd: ctx.workdir,
8088
env: { ...ctx.envs },
8189
includePartialMessages: true,
@@ -175,3 +183,29 @@ function signalToController(signal: AbortSignal): AbortController {
175183
else signal.addEventListener("abort", () => ctrl.abort(), { once: true });
176184
return ctrl;
177185
}
186+
187+
/**
188+
* Drop the flat `temperature` shortcut before spreading into ClaudeAgentOptions.
189+
* The Claude Agent SDK v0.2.x doesn't accept `temperature` on `Options`, so
190+
* leaving it in would land as an unknown property (mostly harmless but noisy).
191+
* See Wedge 1.7.
192+
*/
193+
function stripFlatTemperature<T extends Record<string, unknown>>(
194+
opts: T,
195+
): Omit<T, "temperature"> {
196+
const { temperature: _t, ...rest } = opts as T & { temperature?: number };
197+
return rest as Omit<T, "temperature">;
198+
}
199+
200+
let temperatureWarned = false;
201+
function warnTemperatureUnsupported(): void {
202+
if (temperatureWarned) return;
203+
temperatureWarned = true;
204+
// eslint-disable-next-line no-console
205+
console.warn(
206+
"[computeragent] `temperature` is set but the claude-agent-sdk engine (v0.2.x) " +
207+
"doesn't expose temperature on its public Options type — it has no effect. " +
208+
"Use `harness: \"gitagent\"` if temperature control matters, or wait for the " +
209+
"Anthropic SDK to add the field. (warned once per process)",
210+
);
211+
}

packages/engine-gitagent/src/engine.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,55 @@ describe("GitAgentEngine", () => {
3030
expect(typeof (stream as AsyncIterable<unknown>)[Symbol.asyncIterator]).toBe("function");
3131
});
3232

33+
it("flat opts.temperature is folded into gitclaw's constraints.temperature (Wedge 1.7)", async () => {
34+
let capturedOptions: Record<string, unknown> | undefined;
35+
vi.doMock("gitclaw", async () => ({
36+
query: async function* mockQuery(options: Record<string, unknown>) {
37+
capturedOptions = options;
38+
yield {
39+
type: "assistant",
40+
content: "ok",
41+
model: "anthropic/claude-sonnet",
42+
provider: "anthropic",
43+
stopReason: "stop",
44+
};
45+
},
46+
}));
47+
48+
vi.resetModules();
49+
const { GitAgentEngine: PatchedEngine } = await import("./engine.js");
50+
51+
const ctrl = new AbortController();
52+
const e = new PatchedEngine();
53+
const events: unknown[] = [];
54+
for await (const ev of e.startSession({
55+
sessionId: "sess_t",
56+
// GAP loader / SDK shortcut puts temperature flat on options.
57+
options: { temperature: 0.42, constraints: { stop_sequences: ["X"] } } as never,
58+
workdir: "/tmp",
59+
envs: {},
60+
userMessageQueue: (async function* () {
61+
yield { role: "user" as const, content: "hi" };
62+
})(),
63+
onPermissionRequest: async () => ({ behavior: "deny", message: "n/a" }),
64+
abortSignal: ctrl.signal,
65+
})) {
66+
events.push(ev);
67+
if (events.length >= 2) break;
68+
}
69+
70+
// gitclaw saw the temperature INSIDE constraints, not at the top level.
71+
expect(capturedOptions).toBeDefined();
72+
const constraints = (capturedOptions as { constraints?: Record<string, unknown> }).constraints;
73+
expect(constraints?.temperature).toBe(0.42);
74+
// Pre-existing constraints field is preserved.
75+
expect(constraints?.stop_sequences).toEqual(["X"]);
76+
// The flat shortcut was stripped (didn't leak into the QueryOptions root).
77+
expect((capturedOptions as { temperature?: number }).temperature).toBeUndefined();
78+
79+
vi.doUnmock("gitclaw");
80+
});
81+
3382
it("emits ca_usage_snapshot after each GCAssistantMessage with usage, delta semantic (issue #5)", async () => {
3483
// Mock gitclaw to yield an assistant message with usage attached.
3584
vi.doMock("gitclaw", async () => ({

packages/engine-gitagent/src/engine.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,28 @@ export class GitAgentEngine implements EngineDriver<GitclawForwardOptions & { di
114114
? (baseSuffix ? `${baseSuffix}\n\n${priorSuffix}` : priorSuffix)
115115
: (baseSuffix || undefined);
116116

117+
// Fold the flat `temperature` shortcut (set by `new ComputerAgent({...})`
118+
// or by the GAP loader from `agent.yaml`'s `model.constraints.temperature`)
119+
// into gitclaw's nested `constraints` object — that's where gitclaw
120+
// actually looks. See Wedge 1.7.
121+
const flatTemperature = (ctx.options as { temperature?: number }).temperature;
122+
const inheritedConstraints =
123+
(ctx.options as { constraints?: Record<string, unknown> }).constraints ?? {};
124+
const constraints: Record<string, unknown> =
125+
flatTemperature !== undefined
126+
? { ...inheritedConstraints, temperature: flatTemperature }
127+
: inheritedConstraints;
128+
117129
const options: QueryOptions = {
118130
prompt: singleMessageIterable(userText),
119131
dir: ctx.options.dir ?? ctx.workdir,
120132
sessionId: ctx.sessionId,
121133
abortController,
122134
hooks: { preToolUse: buildPreToolUse(ctx.onPermissionRequest) },
123-
...stripDir(ctx.options),
135+
...stripDirAndFlatTemperature(ctx.options),
136+
...(Object.keys(constraints).length > 0
137+
? { constraints: constraints as QueryOptions["constraints"] }
138+
: {}),
124139
...(systemPromptSuffix ? { systemPromptSuffix } : {}),
125140
};
126141

@@ -220,6 +235,19 @@ function stripDir<T extends { dir?: string }>(opts: T): Omit<T, "dir"> {
220235
return rest;
221236
}
222237

238+
/**
239+
* Strip both `dir` and the flat `temperature` shortcut before spreading
240+
* into gitclaw's QueryOptions. `temperature` is folded into `constraints`
241+
* by the engine; if it leaks through as a flat field gitclaw ignores it
242+
* but it's noisy in the typed options. See Wedge 1.7.
243+
*/
244+
function stripDirAndFlatTemperature<T extends { dir?: string; temperature?: number }>(
245+
opts: T,
246+
): Omit<T, "dir" | "temperature"> {
247+
const { dir: _dir, temperature: _temp, ...rest } = opts;
248+
return rest;
249+
}
250+
223251
/**
224252
* One-shot prompt iterable: yields a single user message then terminates.
225253
* Used by the per-turn `query()` loop so gitclaw consumes exactly one turn's

packages/identity-gitagentprotocol/src/adapters/claude-agent-sdk.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ export async function gapToClaudeAgentOptions(
3939
if (manifest.model?.preferred) opts.model = manifest.model.preferred;
4040
if (manifest.runtime?.max_turns) opts.maxTurns = manifest.runtime.max_turns;
4141
if (manifest.runtime?.budget_usd !== undefined) opts.maxBudgetUsd = manifest.runtime.budget_usd;
42+
// Carry the GAP-declared temperature through as a flat opt. The engine
43+
// decides what to do with it — gitclaw folds it into `constraints`,
44+
// claude-agent-sdk currently can't (no `temperature` on its public
45+
// `Options` type) and warns. Caller-supplied `temperature` overrides this
46+
// via the standard mergeEngineOptions chain. See Wedge 1.7.
47+
if (manifest.model?.constraints?.temperature !== undefined) {
48+
(opts as ClaudeAgentOptions & { temperature?: number }).temperature =
49+
manifest.model.constraints.temperature;
50+
}
4251

4352
const tools = await loadGapTools(workdir);
4453
if (tools.allowedTools.length > 0 || tools.mcpToolNames.length > 0) {

packages/identity-gitagentprotocol/src/adapters/compliance.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,24 @@ async function runHarden(
1616
return harden({ ...options, ...callerOpts });
1717
}
1818

19+
describe("manifest.model.constraints.temperature → opts.temperature (Wedge 1.7)", () => {
20+
it("maps a declared temperature to a flat opts.temperature field", async () => {
21+
const { options } = await gapToClaudeAgentOptions(
22+
{ ...base, model: { preferred: "claude-sonnet-4-5-20250929", constraints: { temperature: 0.3 } } },
23+
"/tmp/no-such",
24+
);
25+
expect((options as ClaudeAgentOptions & { temperature?: number }).temperature).toBe(0.3);
26+
});
27+
28+
it("omits temperature when not declared", async () => {
29+
const { options } = await gapToClaudeAgentOptions(
30+
{ ...base, model: { preferred: "claude-sonnet-4-5-20250929" } },
31+
"/tmp/no-such",
32+
);
33+
expect((options as ClaudeAgentOptions & { temperature?: number }).temperature).toBeUndefined();
34+
});
35+
});
36+
1937
describe("compliance.supervision.human_in_the_loop → permissionMode hardening", () => {
2038
it("'always' rewrites caller's bypassPermissions to default", async () => {
2139
const merged = await runHarden(

packages/identity-gitagentprotocol/src/adapters/gitagent.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,24 @@ describe("gapToGitagentOptions", () => {
2828
maxTurns: 12,
2929
});
3030
});
31+
32+
it("maps manifest.model.constraints.temperature to a flat temperature field (Wedge 1.7)", async () => {
33+
const { options } = await gapToGitagentOptions(
34+
{
35+
name: "x",
36+
version: "1.0.0",
37+
model: { preferred: "anthropic:claude-sonnet-4-5", constraints: { temperature: 0.2 } },
38+
} as never,
39+
"/tmp/agent",
40+
);
41+
expect((options as { temperature?: number }).temperature).toBe(0.2);
42+
});
43+
44+
it("omits temperature when not declared", async () => {
45+
const { options } = await gapToGitagentOptions(
46+
{ name: "x", version: "1.0.0", model: { preferred: "anthropic:claude-sonnet-4-5" } } as never,
47+
"/tmp/agent",
48+
);
49+
expect((options as { temperature?: number }).temperature).toBeUndefined();
50+
});
3151
});

packages/identity-gitagentprotocol/src/adapters/gitagent.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ export async function gapToGitagentOptions(
2828
const opts: GitagentOptions = { dir: workdir };
2929
if (manifest.model?.preferred) opts.model = manifest.model.preferred;
3030
if (manifest.runtime?.max_turns) opts.maxTurns = manifest.runtime.max_turns;
31+
// Pass GAP's declared temperature as a flat field; the engine folds it
32+
// into gitclaw's `constraints.temperature`. Caller-supplied `temperature`
33+
// overrides via the standard mergeEngineOptions chain. See Wedge 1.7.
34+
if (manifest.model?.constraints?.temperature !== undefined) {
35+
(opts as GitagentOptions & { temperature?: number }).temperature =
36+
manifest.model.constraints.temperature;
37+
}
3138
// gitclaw does not yet expose a permission-mode knob through its options
3239
// surface; HITL enforcement on this engine is a future-PR concern.
3340
return { options: opts, harden: (m) => m };

packages/identity-gitagentprotocol/src/manifest.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ export const GapManifest = z
1818
.object({
1919
preferred: z.string().optional(),
2020
fallback: z.array(z.string()).optional(),
21+
/**
22+
* Sampling constraints — engine-dependent how much of this gets honored.
23+
* `temperature` is typed because every shipped engine considers it (some
24+
* apply it, others ignore with a warning — see Wedge 1.7). Other fields
25+
* pass through unchanged.
26+
*/
27+
constraints: z
28+
.object({
29+
temperature: z.number().optional(),
30+
})
31+
.passthrough()
32+
.optional(),
2133
})
2234
.passthrough()
2335
.optional(),

0 commit comments

Comments
 (0)