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
10 changes: 10 additions & 0 deletions .changeset/harness-selector-support.md
Original file line number Diff line number Diff line change
@@ -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).
38 changes: 38 additions & 0 deletions packages/agent-interface/src/harness-capabilities.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import {
harnessHonorsEffort,
harnessHonorsModel,
harnessHonorsSelectors,
harnessProviders,
harnessReasoningEfforts,
harnessSupportsModel,
Expand Down Expand Up @@ -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);
});
});
42 changes: 42 additions & 0 deletions packages/agent-interface/src/harness-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HarnessType> = new Set([
"amp",
"openclaw",
"nanoclaw",
]);
const harnessIgnoresEffort: ReadonlySet<HarnessType> = 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);
}
Loading