diff --git a/.changeset/harness-selector-support.md b/.changeset/harness-selector-support.md new file mode 100644 index 0000000..8ceba61 --- /dev/null +++ b/.changeset/harness-selector-support.md @@ -0,0 +1,10 @@ +--- +"@tangle-network/agent-interface": minor +--- + +Add harness selector-support capability: `harnessHonorsModel`, `harnessHonorsEffort`, and +`harnessHonorsSelectors`. These report whether a harness's runner actually honors the per-turn model +and reasoning-effort pickers (grounded in the cli-bridge adapter audit: `amp` drops both; +`openclaw`/`nanoclaw` drop the model; `factory-droids`/`hermes`/`nanoclaw` drop the effort). Chat +pickers use these to trim or mark harnesses up front, so a user's model/effort choice is never +silently ignored. Distinct from `reasoningEffortsFor` (which levels a harness can express). diff --git a/packages/agent-interface/src/harness-capabilities.test.ts b/packages/agent-interface/src/harness-capabilities.test.ts index d69f9e4..37d760a 100644 --- a/packages/agent-interface/src/harness-capabilities.test.ts +++ b/packages/agent-interface/src/harness-capabilities.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; import { + harnessHonorsEffort, + harnessHonorsModel, + harnessHonorsSelectors, harnessProviders, harnessReasoningEfforts, harnessSupportsModel, @@ -135,3 +138,38 @@ describe("reasoning effort support", () => { ); }); }); + +describe("per-turn selector support", () => { + it("honors both selectors for the mainstream agent harnesses", () => { + for (const h of ["opencode", "claude-code", "codex", "kimi-code"] as const) { + expect(harnessHonorsModel(h)).toBe(true); + expect(harnessHonorsEffort(h)).toBe(true); + expect(harnessHonorsSelectors(h)).toBe(true); + } + }); + + it("flags harnesses that drop the per-turn model", () => { + for (const h of ["amp", "openclaw", "nanoclaw"] as const) { + expect(harnessHonorsModel(h)).toBe(false); + } + expect(harnessHonorsModel("factory-droids")).toBe(true); // honors model, not effort + }); + + it("flags harnesses that drop the reasoning effort", () => { + for (const h of ["amp", "factory-droids", "hermes", "nanoclaw"] as const) { + expect(harnessHonorsEffort(h)).toBe(false); + } + expect(harnessHonorsEffort("openclaw")).toBe(true); // honors effort, not model + }); + + it("harnessHonorsSelectors is the AND of both", () => { + expect(harnessHonorsSelectors("amp")).toBe(false); + expect(harnessHonorsSelectors("factory-droids")).toBe(false); // model yes, effort no + expect(harnessHonorsSelectors("openclaw")).toBe(false); // effort yes, model no + }); + + it("resolves aliases before keying", () => { + // `claude` canonicalizes to `claude-code`, which honors both. + expect(harnessHonorsSelectors("claude")).toBe(true); + }); +}); diff --git a/packages/agent-interface/src/harness-capabilities.ts b/packages/agent-interface/src/harness-capabilities.ts index 75e6f8b..28dc746 100644 --- a/packages/agent-interface/src/harness-capabilities.ts +++ b/packages/agent-interface/src/harness-capabilities.ts @@ -196,3 +196,45 @@ export function reasoningEffortsFor( } return efforts; } + +// ── Per-turn selector support (does the harness honor the chat pickers?) ────── + +/** + * Harnesses whose runner DROPS a per-turn selector — grounded in the cli-bridge adapter audit, NOT a + * guess. Most harnesses honor both selectors, so only the exceptions are listed; a harness absent from + * a set honors that selector. Keyed by the BASE runner (aliases canonicalized). + * + * - model dropped: `amp` (own agent picks the model), `openclaw` (dispatcher routes by its own + * config), `nanoclaw` (socket-bridge runner is config/env-driven). + * - effort dropped: `amp` and `factory-droids`/`hermes`/`nanoclaw` (no thinking flag is plumbed to + * the underlying CLI). + * + * This is distinct from {@link reasoningEffortsFor} (which levels a harness can EXPRESS): a picker uses + * these to trim or mark harnesses up front, so a user's model/effort choice is never silently ignored. + */ +const harnessIgnoresModel: ReadonlySet = new Set([ + "amp", + "openclaw", + "nanoclaw", +]); +const harnessIgnoresEffort: ReadonlySet = new Set([ + "amp", + "factory-droids", + "hermes", + "nanoclaw", +]); + +/** Whether the harness's runner honors a per-turn MODEL override (vs. picking the model itself). */ +export function harnessHonorsModel(harness: HarnessType): boolean { + return !harnessIgnoresModel.has(canonicalizeHarness(harness)); +} + +/** Whether the harness's runner honors a reasoning-EFFORT override (vs. dropping it). */ +export function harnessHonorsEffort(harness: HarnessType): boolean { + return !harnessIgnoresEffort.has(canonicalizeHarness(harness)); +} + +/** Whether the harness honors BOTH chat selectors — i.e. the model and effort pickers are live. */ +export function harnessHonorsSelectors(harness: HarnessType): boolean { + return harnessHonorsModel(harness) && harnessHonorsEffort(harness); +}