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
9 changes: 9 additions & 0 deletions .changeset/improvement-surface-and-reasoning-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@tangle-network/agent-interface": minor
---

Give the improvement-surface list and the native reasoning control one owner each.

`AgentImprovementSurface` is now derived from the exported `AGENT_IMPROVEMENT_SURFACES` list, which the promotion schema's enum also reads, and it gains `rollout-policy`. A rollout-policy improvement can now be named in a proposal; before, `improve()` could produce a surface no proposal could report.

`nativeReasoningControl(harness, effort)` returns the exact control token a harness process receives — the value a materialization receipt carries as `reasoningEffort.applied`. Adapters that build harness argv and callers that verify the receipt now read one table instead of two hand-rolled copies that had already drifted apart. A harness with no native control answers `null`.
14 changes: 2 additions & 12 deletions packages/agent-interface/src/agent-candidate-promotion-schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { AGENT_IMPROVEMENT_SURFACES } from "./agent-candidate.js";
import type {
AgentCandidateExperiment,
AgentCandidateJsonValue,
Expand Down Expand Up @@ -35,18 +36,7 @@ import {
changedProfileImprovementSurfaces,
} from "./agent-profile-improvement-schema.js";

const improvementSurfaceSchema = z.enum([
"prompt",
"skills",
"tools",
"mcp",
"hooks",
"subagents",
"agent-profile",
"memory",
"code",
"knowledge",
]);
const improvementSurfaceSchema = z.enum(AGENT_IMPROVEMENT_SURFACES);

export const agentCandidateExperimentSchema = z
.object({
Expand Down
33 changes: 23 additions & 10 deletions packages/agent-interface/src/agent-candidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,17 +828,30 @@ export interface AgentCandidateRunReceipt {
digest: Sha256Digest;
}

/**
* Every surface an improvement proposal can name. One owner: the validator
* ({@link agentCandidatePromotionSchema}'s surface enum) and every producer read this
* list, so a new surface cannot be proposable in one place and unnameable in another.
*
* `rollout-policy` is the inference-time structural-rollout dials
* (`profile.extensions['structural-rollout']`); `knowledge` is the corpus lane.
*/
export const AGENT_IMPROVEMENT_SURFACES = Object.freeze([
"prompt",
"skills",
"tools",
"mcp",
"hooks",
"subagents",
"agent-profile",
"memory",
"code",
"knowledge",
"rollout-policy",
] as const);

export type AgentImprovementSurface =
| "prompt"
| "skills"
| "tools"
| "mcp"
| "hooks"
| "subagents"
| "agent-profile"
| "memory"
| "code"
| "knowledge";
(typeof AGENT_IMPROVEMENT_SURFACES)[number];

/** One paired Runtime execution from the exact signed experiment. */
export interface AgentCandidateExperimentMeasurement {
Expand Down
53 changes: 53 additions & 0 deletions packages/agent-interface/src/harness-capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import {
harnessSupportsModel,
harnessSystemPromptIntents,
modelProvider,
nativeReasoningControl,
preferredHarnessForModel,
reasoningEffortsFor,
snapHarnessToModel,
snapModelToHarness,
} from "./harness-capabilities.js";
import { harnessTypeSchema } from "./harness.js";
import { REASONING_EFFORTS } from "./agent-profile.js";

const CATALOG = [
"anthropic/claude-opus-4-6",
Expand Down Expand Up @@ -287,3 +289,54 @@ describe("system-prompt intents", () => {
expect([...appenders].sort()).toEqual(["claude-code", "opencode", "pi", "prime"]);
});
});

describe("nativeReasoningControl", () => {
it("maps the ladder onto each harness's own control token", () => {
// Pinned against the argv builders that spawn each CLI. A rung renamed upstream must be
// changed here, where both the adapter and the receipt check read it.
expect(REASONING_EFFORTS.map((e) => nativeReasoningControl("claude-code", e))).toEqual([
"low", "low", "low", "medium", "high", "xhigh", "max",
]);
expect(REASONING_EFFORTS.map((e) => nativeReasoningControl("codex", e))).toEqual([
"none", "minimal", "low", "medium", "high", "xhigh", "ultra",
]);
expect(REASONING_EFFORTS.map((e) => nativeReasoningControl("pi", e))).toEqual([
"off", "minimal", "low", "medium", "high", "xhigh", "xhigh",
]);
expect(REASONING_EFFORTS.map((e) => nativeReasoningControl("prime", e))).toEqual([
"off", "minimal", "low", "medium", "high", "xhigh", "max",
]);
expect(REASONING_EFFORTS.map((e) => nativeReasoningControl("kimi-code", e))).toEqual([
"--no-thinking", "--no-thinking", "--no-thinking", null, "--thinking", "--thinking", "--thinking",
]);
expect(REASONING_EFFORTS.map((e) => nativeReasoningControl("opencode", e))).toEqual([
...REASONING_EFFORTS,
]);
});

it("answers null for a harness that applies no native control", () => {
// gemini derives thinking from the model, and the rest plumb no thinking flag. Asserting a
// token for them refuses a legitimate run, which is why the default is `null`, not the request.
for (const harness of harnessTypeSchema.options) {
if (["claude-code", "codex", "pi", "prime", "kimi-code", "opencode"].includes(harness)) continue;
for (const effort of REASONING_EFFORTS) {
expect(nativeReasoningControl(harness, effort)).toBeNull();
}
}
});

it("applies no control when nothing was requested", () => {
for (const harness of harnessTypeSchema.options) {
expect(nativeReasoningControl(harness, null)).toBeNull();
}
});

it("never claims a control for a harness whose runner drops the effort", () => {
for (const harness of harnessTypeSchema.options) {
if (harnessHonorsEffort(harness)) continue;
for (const effort of REASONING_EFFORTS) {
expect(nativeReasoningControl(harness, effort)).toBeNull();
}
}
});
});
71 changes: 69 additions & 2 deletions packages/agent-interface/src/harness-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ export function snapHarnessToModel(
* - claude-code: `--effort` accepts `low|medium|high|xhigh|max`; canonical `ultracode` maps to
* native `max`. It cannot express `none` or `minimal`, and an unsupported value is warned about
* and silently replaced with the default rather than rejected — so the set must not overstate.
* - pi: `--thinking` accepts `off|minimal|low|medium|high|xhigh|max`; canonical `none` maps to
* `off` and `ultracode` to `max`.
* - pi: `--thinking` accepts `off|minimal|low|medium|high|xhigh`; canonical `none` maps to
* `off` and `ultracode` clamps to `xhigh`, its top rung.
* - prime: the prime fork of the pi line accepts the same `--thinking` set
* (`off|minimal|low|medium|high|xhigh|max`); canonical `none` maps to `off` and `ultracode` to
* `max`.
Expand Down Expand Up @@ -231,6 +231,73 @@ export function reasoningEffortsFor(
return efforts;
}

// ── Native reasoning control (the exact token the harness process receives) ───

/**
* Canonical effort → the harness's OWN control token, or `null` when the harness applies no
* native reasoning control for that request. This is the value a materialization receipt carries
* as `reasoningEffort.applied`, so a caller can check that the effort it asked for reached the
* process instead of trusting an echo of its own request.
*
* Read from the argv builders that actually spawn each CLI, not from help text:
* - claude-code — `--effort <value>`; it cannot express `none` or `minimal`, so both clamp to
* `low`, and `ultracode` becomes its ceiling `max`.
* - codex — `-c model_reasoning_effort="<value>"`; it takes the canonical rungs directly and
* names its ceiling `ultra`.
* - pi — `--thinking <value>`; `none` becomes `off` and `ultracode` clamps to `xhigh`, the
* highest rung the pi line accepts.
* - prime — `--thinking <value>`; the fork carries `max` above `xhigh`, so `ultracode` reaches
* `max`. This is the one rung where prime and pi differ.
* - kimi-code — the control is the FLAG itself, `--thinking` or `--no-thinking`, because kimi's
* thinking switch is binary. `medium` is its default and passes no flag at all.
* - opencode — the router-backed variant name is the canonical rung unchanged.
*
* A harness with no entry applies NO native control: either it derives thinking from the model
* (gemini's `--thinking-budget`) or it plumbs no thinking flag at all (see
* {@link harnessHonorsEffort}). Both answer `null`, which is what their receipts carry — so an
* unknown harness is never asserted to have applied a control it cannot apply.
*/
const harnessNativeReasoningControl: Partial<
Record<HarnessType, (effort: ReasoningEffort) => string | null>
> = {
"claude-code": (effort) => {
if (effort === "none" || effort === "minimal") return "low";
return effort === "ultracode" ? "max" : effort;
},
codex: (effort) => (effort === "ultracode" ? "ultra" : effort),
pi: (effort) => {
if (effort === "none") return "off";
return effort === "ultracode" ? "xhigh" : effort;
},
prime: (effort) => {
if (effort === "none") return "off";
return effort === "ultracode" ? "max" : effort;
},
"kimi-code": (effort) => {
if (effort === "medium") return null;
return effort === "none" || effort === "minimal" || effort === "low"
? "--no-thinking"
: "--thinking";
},
opencode: (effort) => effort,
};

/**
* The native control token a harness applies for one canonical effort, or `null` when it applies
* none. `effort: null` (nothing requested) is always `null`.
*
* One owner for both sides of the check: the adapter that builds the harness argv and the caller
* that verifies the receipt read this function, so a CLI that renames a rung moves both at once
* instead of turning into a refused run.
*/
export function nativeReasoningControl(
harness: HarnessType,
effort: ReasoningEffort | null,
): string | null {
if (effort === null) return null;
return harnessNativeReasoningControl[harness]?.(effort) ?? null;
}

// ── Per-turn selector support (does the harness honor the chat pickers?) ──────

/**
Expand Down
Loading