From 5bd08a1647149807dbbd1ac9ce739820c34e45da Mon Sep 17 00:00:00 2001 From: stxkxs <139715017+stxkxs@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:57:25 -0700 Subject: [PATCH] feat(transports): add opt-in per-role effort on the sdk + claude-cli runtimes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reachable half of the transport-parity follow-up. Fast-mode parity turned out to be unreachable through fab's programmatic seam — no SDK query() fastMode option, and `claude --help` shows `--effort`/`--model` but no `--fast` (it's a settings.json / interactive `/fast` feature). `effort`, by contrast, IS exposed on exactly those two transports, so that's what landed. - TeamMember gains an optional `effort?: EffortLevel` (low | medium | high | xhigh | max), unset by default — zero behavior change until a role sets it. - The sdk runtime passes it to query() as the `effort` option; claude-cli passes `--effort `. The Managed Agents agent-create surface does not expose effort, so it is a documented no-op there (like compaction and Tool Search). - Assign per role via the same pilot path as model tiering: set `effort` in src/team/*, run in sdk/claude-cli mode, keep or revert. docs/roster.md's Model tiering section is updated from "deferred" to the live mechanism. - Tests: buildClaudeArgs emits `--effort` when a role sets it, omits it when unset. Verified: npm run lint / build / format:check clean; npm test 302/302. Co-authored-by: stxkxsbot <275011021+stxkxsbot@users.noreply.github.com> --- __tests__/claude-cli.test.ts | 33 +++++++++++++++++++++++++++++++++ docs/roster.md | 2 +- src/runtimes/claude-cli.ts | 11 ++++++++++- src/runtimes/sdk.ts | 15 +++++++++++++-- src/types.ts | 11 +++++++++++ 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/__tests__/claude-cli.test.ts b/__tests__/claude-cli.test.ts index b4fd0d1..2e794a1 100644 --- a/__tests__/claude-cli.test.ts +++ b/__tests__/claude-cli.test.ts @@ -40,6 +40,39 @@ describe('buildClaudeArgs', () => { expect(args).not.toContain('--bare'); expect(args).not.toContain('--resume'); expect(args).not.toContain('--add-dir'); + expect(args).not.toContain('--effort'); // unset by default + }); + + it('adds --effort when a role sets an effort level', () => { + const args = buildClaudeArgs({ + sessionId: '00000000-0000-4000-8000-000000000002', + systemPrompt: 'role prompt', + model: 'claude-sonnet-4-6', + mcpConfigPath: null, + bare: false, + addDir: null, + resumeFrom: null, + title: undefined, + effort: 'high', + env: baseEnv, + }); + expect(args).toContain('--effort'); + expect(args[args.indexOf('--effort') + 1]).toBe('high'); + }); + + it('omits --effort when effort is unset', () => { + const args = buildClaudeArgs({ + sessionId: '00000000-0000-4000-8000-000000000003', + systemPrompt: 'role prompt', + model: 'claude-sonnet-4-6', + mcpConfigPath: null, + bare: false, + addDir: null, + resumeFrom: null, + title: undefined, + env: baseEnv, + }); + expect(args).not.toContain('--effort'); }); it('adds --bare and drops --setting-sources when bare mode is on', () => { diff --git a/docs/roster.md b/docs/roster.md index 6ac0c3e..b623b22 100644 --- a/docs/roster.md +++ b/docs/roster.md @@ -154,4 +154,4 @@ Every role declares a `model` in `src/team//.ts`. The current sprea 3. Grade the output — the merge gate + `external-reviewer` calibration for factory roles; a manual read (or `external-reviewer`) for firm roles. 4. Promote (edit the role's `model` in `src/team/*`) only if quality holds; otherwise `fab model clear ` to roll back. -**The `effort` parameter** (GA on the Messages API for Opus 4.6+) is deferred: it would need an `AgentCreateParams` shape change and — like context compaction and the Tool Search tool — is not currently exposed on the Managed Agents agent-create surface fab's default transport uses. Revisit once the Managed Agents API carries it. +**The `effort` parameter** (GA on the Messages API for Opus 4.6+) is wired as an optional per-role `effort` on `TeamMember` (`low | medium | high | xhigh | max`), unset by default. It applies only on the transports that expose it — the `sdk` runtime (`query()` `effort`) and `claude-cli` (`--effort`); the Managed Agents agent-create surface does not carry it (like compaction and Tool Search), so it's a no-op on the default transport. Assign per role the same way as model tiering: set `effort` in `src/team/*`, pilot in `sdk`/`claude-cli` mode, keep or revert. diff --git a/src/runtimes/claude-cli.ts b/src/runtimes/claude-cli.ts index ce7d4e7..77e12d7 100644 --- a/src/runtimes/claude-cli.ts +++ b/src/runtimes/claude-cli.ts @@ -6,7 +6,7 @@ import { join } from 'node:path'; import { createInterface } from 'node:readline'; import type { AgentRuntime, AgentSession, RunRoleOptions } from '../runtime.js'; -import type { AgentEvent, FabState, TeamMember, TeamRole, UserEvent } from '../types.js'; +import type { AgentEvent, EffortLevel, FabState, TeamMember, TeamRole, UserEvent } from '../types.js'; import { TEAM } from '../team.js'; import { buildSystemPrompt } from '../prompts.js'; import { loadState, getPrimaryRepo } from '../state.js'; @@ -55,6 +55,7 @@ export class ClaudeCliRuntime implements AgentRuntime { addDir: repo ? `/workspace/${repo.repo}` : null, resumeFrom: null, title: options?.title, + effort: member.effort ?? null, env: process.env, }); @@ -269,6 +270,7 @@ class ResumedClaudeCliSession implements AgentSession { addDir: repo ? `/workspace/${repo.repo}` : null, resumeFrom: this.id, title: undefined, + effort: null, // resume inherits the original session's effort env: process.env, }); this.liveSession = new ClaudeCliSession({ @@ -307,6 +309,7 @@ export interface BuildClaudeArgsOptions { addDir: string | null; resumeFrom: string | null; title: string | undefined; + effort?: EffortLevel | null; env: NodeJS.ProcessEnv; } @@ -344,6 +347,12 @@ export function buildClaudeArgs(opts: BuildClaudeArgsOptions): string[] { args.push('--model', opts.model); } + // Reasoning effort (claude `--effort`). Unset for most roles; set per role to + // trade latency/cost for depth. managed-agents has no equivalent on agent-create. + if (opts.effort) { + args.push('--effort', opts.effort); + } + // System prompt. `--append-system-prompt` layers on Claude Code's own // default; we keep the default so subprocess tooling stays intact and // append the role's prompt + factory preamble on top. diff --git a/src/runtimes/sdk.ts b/src/runtimes/sdk.ts index a58acc2..c2b79fe 100644 --- a/src/runtimes/sdk.ts +++ b/src/runtimes/sdk.ts @@ -1,5 +1,5 @@ import type { AgentRuntime, AgentSession, RunRoleOptions } from '../runtime.js'; -import type { AgentEvent, FabState, TeamRole, UserEvent } from '../types.js'; +import type { AgentEvent, EffortLevel, FabState, TeamRole, UserEvent } from '../types.js'; import { TEAM } from '../team.js'; import { buildSystemPrompt } from '../prompts.js'; import { loadState, getBudgetLimit } from '../state.js'; @@ -63,7 +63,16 @@ export class SdkRuntime implements AgentRuntime { } const sdk = await loadSdk(); - const session = new SdkAgentSession(sdk, model, systemPrompt, options, backend, budgetUsd, mcpServers); + const session = new SdkAgentSession( + sdk, + model, + systemPrompt, + options, + backend, + budgetUsd, + mcpServers, + member.effort, + ); await session.start(message); return session; } @@ -113,6 +122,7 @@ class SdkAgentSession implements AgentSession { private readonly backend: InferenceBackend = 'api', private readonly budgetUsd: number | null = null, private readonly mcpServers: Record = {}, + private readonly effort?: EffortLevel, ) {} get id(): string { @@ -148,6 +158,7 @@ class SdkAgentSession implements AgentSession { // Role's MCP servers, scoped strictly to fab's set (not the user's // ambient ~/.claude MCP config) — matches claude-cli's --strict-mcp-config. ...(Object.keys(this.mcpServers).length > 0 && { mcpServers: this.mcpServers, strictMcpConfig: true }), + ...(this.effort && { effort: this.effort }), ...(backendEnv && { env: { ...process.env, ...backendEnv } }), // Resources hint: the SDK uses cwd for filesystem-bound tools; // workflows.ts pre-creates branches on the cloud-mounted repos diff --git a/src/types.ts b/src/types.ts index 904f5b3..ae09e6c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -499,6 +499,13 @@ export type TeamRole = | 'prompt-optimizer' | 'learner'; +/** + * Reasoning-effort level. GA on the Messages API (Opus 4.6+); reachable on the + * sdk (`query()` `effort`) and claude-cli (`--effort`) transports, but NOT the + * managed-agents agent-create surface. + */ +export type EffortLevel = 'low' | 'medium' | 'high' | 'xhigh' | 'max'; + export interface TeamMember { role: TeamRole; group?: TeamGroup; @@ -508,6 +515,10 @@ export interface TeamMember { system: string; mcpServers: string[]; // server names from mcp.ts registry briefTemplate?: string; // nanohype brief template name for skill generation + // Optional per-role reasoning effort. Unset = the model's default. Applied + // only on the sdk / claude-cli transports (managed-agents doesn't expose it); + // assign per role via the same pilot path as model tiering (see docs/roster.md). + effort?: EffortLevel; } // ── Git Resources ───────────────────────────────────────────────────