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
15 changes: 15 additions & 0 deletions .changeset/context-tier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@bradygaster/squad-sdk": minor
"@bradygaster/squad-cli": minor
---

Thread a `contextTier` concept end-to-end through the agent spawning pipeline, mirroring `reasoningEffort`.

Adds the ability to select a model's context window / context tier (Default vs Long-context / 1M) wherever a model and reasoning effort can already be chosen:

- New `SquadContextTier = "default" | "long_context"` type and optional `SquadSessionConfig.contextTier`.
- Per-model `supportedContextTiers` / `defaultContextTier` in the model catalog so requests validate and clamp against what a model supports (e.g. Opus 4.8 → 264K default / 1M long context, inferred from billing token prices).
- `ModelConfig.defaultContextTier` and `agentContextTierOverrides` config-schema fields, with `readContextTier` / `writeContextTier` / `readAgentContextTierOverrides` / `writeAgentContextTierOverrides` / `resolveContextTier` / `clampContextTier` helpers mirroring the reasoning-effort resolvers (unsupported tier clamps to the model default; unknown treated as default; `auto` sentinel means "not set").
- Charter `## Model` → `**Context Tier:**` support in the charter compiler.
- `contextTier` threaded through `spawn-backend.ts` (camelCase for task spawns, `context_tier` snake_case for session kickoff) and `fan-out.ts`.
- New `squad config context-tier [<tier>] [--agent <name>] [--clear]` CLI subcommand parallel to `squad config model`.
27 changes: 27 additions & 0 deletions .github/agents/squad.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,33 @@ When the resolved reasoning effort is not `auto` or default, include it in the a

Follow `.squad/templates/model-selection-reference.md` for the base model-selection rules. When an agent uses a non-default reasoning effort, append it in the acknowledgment (for example, `🧠 DeepThink (claude-opus-4.7-1m-internal · xhigh) — deep architecture analysis`).

### Per-Agent Context Tier

Context tier controls the size of the model's context window — how much conversation, code, and instruction the model can hold at once. Larger tiers fit more context but cost more per token. This is SEPARATE from model selection and reasoning effort — you can run the same model at different context tiers.

Valid tiers: `default`, `long_context`. The value `auto` means "let the model decide" (platform default). A `long_context` request clamps to `default` on models that only support a single window.

**Resolution — check these layers in order (first match wins):**

1. **Persistent Config:** `.squad/config.json` → `agentContextTierOverrides.{agentName}`, then `defaultContextTier`
2. **User directive:** User says "use long context" or "1M window" → apply to this spawn
3. **Charter preference:** Agent's `## Model` section → `**Context Tier:** long_context`
4. **Default:** Do not set a context tier (platform decides)

**When user requests a larger window:** Use the SAME model with a different context tier — do NOT switch to a different model variant. Context tier is a session parameter, not a model choice.

- **When user says "always use long context" / "1M window by default":** Write `defaultContextTier` to `.squad/config.json`. Acknowledge: `✅ Context tier saved: long_context — all future sessions will use this until changed.`
- **When user says "use long context for {agent}":** Write to `agentContextTierOverrides.{agent}` in `.squad/config.json`. Acknowledge: `✅ {Agent} will always use long context — saved to config.`
- **When user says "clear context tier preference":** Remove context tier fields from `.squad/config.json`. Acknowledge: `✅ Context tier preference cleared — returning to automatic.`

**Passing context tier to spawns:**

When the resolved context tier is not `auto` or default, include it in the agent's charter-compiled spawn prompt or session config. The SDK threads it through to `SquadSessionConfig.contextTier` automatically, clamping to what the model supports.

**Spawn output format — show the model choice and tier:**

Follow `.squad/templates/model-selection-reference.md` for the base model-selection rules. When an agent uses a non-default context tier, append it in the acknowledgment (for example, `🧠 DeepThink (claude-opus-4.8 · long context) — 1M-token window for deep architecture analysis`).

### Client Compatibility

Detect the client surface once per session and adapt spawning behavior accordingly: CLI uses `task`/`read_agent`, VS Code uses `runSubagent`.
Expand Down
27 changes: 27 additions & 0 deletions .squad-templates/squad.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,33 @@ When the resolved reasoning effort is not `auto` or default, include it in the a

Follow `.squad/templates/model-selection-reference.md` for the base model-selection rules. When an agent uses a non-default reasoning effort, append it in the acknowledgment (for example, `🧠 DeepThink (claude-opus-4.7-1m-internal · xhigh) — deep architecture analysis`).

### Per-Agent Context Tier

Context tier controls the size of the model's context window — how much conversation, code, and instruction the model can hold at once. Larger tiers fit more context but cost more per token. This is SEPARATE from model selection and reasoning effort — you can run the same model at different context tiers.

Valid tiers: `default`, `long_context`. The value `auto` means "let the model decide" (platform default). A `long_context` request clamps to `default` on models that only support a single window.

**Resolution — check these layers in order (first match wins):**

1. **Persistent Config:** `.squad/config.json` → `agentContextTierOverrides.{agentName}`, then `defaultContextTier`
2. **User directive:** User says "use long context" or "1M window" → apply to this spawn
3. **Charter preference:** Agent's `## Model` section → `**Context Tier:** long_context`
4. **Default:** Do not set a context tier (platform decides)

**When user requests a larger window:** Use the SAME model with a different context tier — do NOT switch to a different model variant. Context tier is a session parameter, not a model choice.

- **When user says "always use long context" / "1M window by default":** Write `defaultContextTier` to `.squad/config.json`. Acknowledge: `✅ Context tier saved: long_context — all future sessions will use this until changed.`
- **When user says "use long context for {agent}":** Write to `agentContextTierOverrides.{agent}` in `.squad/config.json`. Acknowledge: `✅ {Agent} will always use long context — saved to config.`
- **When user says "clear context tier preference":** Remove context tier fields from `.squad/config.json`. Acknowledge: `✅ Context tier preference cleared — returning to automatic.`

**Passing context tier to spawns:**

When the resolved context tier is not `auto` or default, include it in the agent's charter-compiled spawn prompt or session config. The SDK threads it through to `SquadSessionConfig.contextTier` automatically, clamping to what the model supports.

**Spawn output format — show the model choice and tier:**

Follow `.squad/templates/model-selection-reference.md` for the base model-selection rules. When an agent uses a non-default context tier, append it in the acknowledgment (for example, `🧠 DeepThink (claude-opus-4.8 · long context) — 1M-token window for deep architecture analysis`).

### Client Compatibility

Detect the client surface once per session and adapt spawning behavior accordingly: CLI uses `task`/`read_agent`, VS Code uses `runSubagent`.
Expand Down
99 changes: 99 additions & 0 deletions packages/squad-cli/src/cli/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
* squad config model <model-name> --agent <n> — pin model to a specific agent
* squad config model --clear — clear default model override
* squad config model --clear --agent <n> — clear a specific agent's override
* squad config context-tier — show current context tier configuration
* squad config context-tier <tier> — set default context tier for all agents
* squad config context-tier <tier> --agent <n> — pin context tier to a specific agent
* squad config context-tier --clear — clear default context tier override
* squad config context-tier --clear --agent <n> — clear a specific agent's override
*/

import { join } from 'node:path';
Expand All @@ -16,6 +21,11 @@ import {
writeModelPreference,
readAgentModelOverrides,
writeAgentModelOverrides,
readContextTier,
writeContextTier,
readAgentContextTierOverrides,
writeAgentContextTierOverrides,
VALID_CONTEXT_TIERS,
MODEL_CATALOG,
} from '@bradygaster/squad-sdk/config';
import { fatal } from '../core/errors.js';
Expand Down Expand Up @@ -61,6 +71,14 @@ function showAvailableModels(): void {
console.log();
}

function isValidTier(name: string): boolean {
return (VALID_CONTEXT_TIERS as readonly string[]).includes(name);
}

function showAvailableTiers(): void {
console.log(`\n Available context tiers: ${DIM}${VALID_CONTEXT_TIERS.join(', ')}${RESET}\n`);
}

function parseFlags(args: string[]): { clear: boolean; agent: string | null; positional: string[] } {
let clear = false;
let agent: string | null = null;
Expand Down Expand Up @@ -153,6 +171,77 @@ async function runModelSubcommand(squadDir: string, subArgs: string[]): Promise<
console.log();
}

async function runContextTierSubcommand(squadDir: string, subArgs: string[]): Promise<void> {
const { clear, agent, positional } = parseFlags(subArgs);
const tierArg = positional[0] ?? null;

// --- Clear ---
if (clear) {
if (agent) {
const agents = listAgents(squadDir);
if (agents.length > 0 && !agents.includes(agent)) {
fatal(
`Unknown agent "${agent}".\n` +
` Known agents: ${agents.join(', ')}`,
);
}
const overrides = readAgentContextTierOverrides(squadDir);
delete overrides[agent];
writeAgentContextTierOverrides(squadDir, overrides);
console.log(`${GREEN}✓${RESET} Context tier override for ${BOLD}${agent}${RESET} cleared.`);
} else {
writeContextTier(squadDir, null);
console.log(`${GREEN}✓${RESET} Default context tier cleared (reverted to model default).`);
}
return;
}

// --- Set context tier ---
if (tierArg) {
if (!isValidTier(tierArg)) {
console.error(`${RED}✗${RESET} Unknown context tier: ${BOLD}${tierArg}${RESET}`);
showAvailableTiers();
process.exit(1);
}

if (agent) {
const agents = listAgents(squadDir);
if (agents.length > 0 && !agents.includes(agent)) {
fatal(
`Unknown agent "${agent}".\n` +
` Known agents: ${agents.join(', ')}`,
);
}
const overrides = readAgentContextTierOverrides(squadDir);
overrides[agent] = tierArg;
writeAgentContextTierOverrides(squadDir, overrides);
console.log(`${GREEN}✓${RESET} Context tier for ${BOLD}${agent}${RESET} set to ${BOLD}${tierArg}${RESET}`);
} else {
writeContextTier(squadDir, tierArg);
console.log(`${GREEN}✓${RESET} Default context tier set to ${BOLD}${tierArg}${RESET}`);
}
return;
}

// --- Show current config ---
const defaultTier = readContextTier(squadDir);
const overrides = readAgentContextTierOverrides(squadDir);
const overrideEntries = Object.entries(overrides);

console.log(`\n${BOLD}Context tier configuration:${RESET}`);
console.log(` Default context tier: ${defaultTier ? BOLD + defaultTier + RESET : `${DIM}(model default)${RESET}`}`);

if (overrideEntries.length > 0) {
console.log(`\n Agent overrides:`);
for (const [name, tier] of overrideEntries) {
console.log(` ${name} ${DIM}→${RESET} ${tier}`);
}
} else {
console.log(`\n ${DIM}No agent overrides configured.${RESET}`);
}
showAvailableTiers();
}

export async function runConfig(cwd: string, subArgs: string[]): Promise<void> {
const squadDir = resolveSquadDir(cwd);
if (!squadDir) {
Expand All @@ -167,11 +256,21 @@ export async function runConfig(cwd: string, subArgs: string[]): Promise<void> {
return;
}

if (sub === 'context-tier') {
await runContextTierSubcommand(squadDir, subArgs.slice(1));
return;
}

// No subcommand or unknown — show usage
console.log(`\n${BOLD}squad config${RESET} — manage squad configuration\n`);
console.log(` ${BOLD}squad config model${RESET} — show current model config`);
console.log(` ${BOLD}squad config model <model>${RESET} — set default model`);
console.log(` ${BOLD}squad config model <model> --agent <name>${RESET} — pin model to agent`);
console.log(` ${BOLD}squad config model --clear${RESET} — clear default model`);
console.log(` ${BOLD}squad config model --clear --agent <name>${RESET} — clear agent override\n`);
console.log(` ${BOLD}squad config context-tier${RESET} — show current context-tier config`);
console.log(` ${BOLD}squad config context-tier <tier>${RESET} — set default context tier`);
console.log(` ${BOLD}squad config context-tier <tier> --agent <name>${RESET} — pin context tier to agent`);
console.log(` ${BOLD}squad config context-tier --clear${RESET} — clear default context tier`);
console.log(` ${BOLD}squad config context-tier --clear --agent <name>${RESET} — clear agent override\n`);
}
27 changes: 27 additions & 0 deletions packages/squad-cli/templates/squad.agent.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,33 @@ When the resolved reasoning effort is not `auto` or default, include it in the a

Follow `.squad/templates/model-selection-reference.md` for the base model-selection rules. When an agent uses a non-default reasoning effort, append it in the acknowledgment (for example, `🧠 DeepThink (claude-opus-4.7-1m-internal · xhigh) — deep architecture analysis`).

### Per-Agent Context Tier

Context tier controls the size of the model's context window — how much conversation, code, and instruction the model can hold at once. Larger tiers fit more context but cost more per token. This is SEPARATE from model selection and reasoning effort — you can run the same model at different context tiers.

Valid tiers: `default`, `long_context`. The value `auto` means "let the model decide" (platform default). A `long_context` request clamps to `default` on models that only support a single window.

**Resolution — check these layers in order (first match wins):**

1. **Persistent Config:** `.squad/config.json` → `agentContextTierOverrides.{agentName}`, then `defaultContextTier`
2. **User directive:** User says "use long context" or "1M window" → apply to this spawn
3. **Charter preference:** Agent's `## Model` section → `**Context Tier:** long_context`
4. **Default:** Do not set a context tier (platform decides)

**When user requests a larger window:** Use the SAME model with a different context tier — do NOT switch to a different model variant. Context tier is a session parameter, not a model choice.

- **When user says "always use long context" / "1M window by default":** Write `defaultContextTier` to `.squad/config.json`. Acknowledge: `✅ Context tier saved: long_context — all future sessions will use this until changed.`
- **When user says "use long context for {agent}":** Write to `agentContextTierOverrides.{agent}` in `.squad/config.json`. Acknowledge: `✅ {Agent} will always use long context — saved to config.`
- **When user says "clear context tier preference":** Remove context tier fields from `.squad/config.json`. Acknowledge: `✅ Context tier preference cleared — returning to automatic.`

**Passing context tier to spawns:**

When the resolved context tier is not `auto` or default, include it in the agent's charter-compiled spawn prompt or session config. The SDK threads it through to `SquadSessionConfig.contextTier` automatically, clamping to what the model supports.

**Spawn output format — show the model choice and tier:**

Follow `.squad/templates/model-selection-reference.md` for the base model-selection rules. When an agent uses a non-default context tier, append it in the acknowledgment (for example, `🧠 DeepThink (claude-opus-4.8 · long context) — 1M-token window for deep architecture analysis`).

### Client Compatibility

Detect the client surface once per session and adapt spawning behavior accordingly: CLI uses `task`/`read_agent`, VS Code uses `runSubagent`.
Expand Down
8 changes: 8 additions & 0 deletions packages/squad-sdk/src/adapter/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,14 @@ export class SquadClient {
billing: m.billing,
supportedReasoningEfforts: m.supportedReasoningEfforts,
defaultReasoningEffort: m.defaultReasoningEffort,
// Infer context-tier support from billing: models that publish a
// long-context token-price schedule support the 1M window. The
// runtime does not expose supported tiers directly, so presence of
// `billing.tokenPrices.longContext` is the authoritative signal.
supportedContextTiers: m.billing?.tokenPrices?.longContext
? ["default", "long_context"]
: ["default"],
defaultContextTier: "default",
}));
} catch (error) {
if (this.shouldAttemptReconnect(error)) {
Expand Down
39 changes: 39 additions & 0 deletions packages/squad-sdk/src/adapter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ export interface SquadSessionConfig {
*/
reasoningEffort?: SquadReasoningEffort;

/**
* Context tier (context-window size) for models that support it.
* Selects between the default context window and an extended
* long-context window (e.g. Opus 4.8: 264K default vs 1M long context).
* Only valid for models where capabilities support multiple context tiers.
* @example "long_context"
*/
contextTier?: SquadContextTier;

/**
* Override the default configuration directory location.
* When specified, the session will use this directory for config and state.
Expand Down Expand Up @@ -759,6 +768,17 @@ export interface SquadInfiniteSessionConfig {
*/
export type SquadReasoningEffort = "low" | "medium" | "high" | "xhigh";

/**
* Valid context tiers (context-window sizes) for models that support them.
*
* `"default"` selects the model's standard context window; `"long_context"`
* selects an extended window (e.g. Opus 4.8: 264K default vs 1M long context).
* Canonical context-tier union for the SDK — import this type instead of
* re-declaring the union inline. The runtime list lives in config/models.ts as
* `VALID_CONTEXT_TIERS` (kept in sync via `satisfies`).
*/
export type SquadContextTier = "default" | "long_context";

// ============================================================================
// Session Interface
// ============================================================================
Expand Down Expand Up @@ -951,6 +971,14 @@ export interface SquadModelCapabilities {
supports: {
vision: boolean;
reasoningEffort: boolean;
/**
* Whether the model exposes a selectable context tier (extended context
* window). Optional because the underlying runtime does not model this as
* a capability flag; Squad infers per-model support from the billing
* signal and surfaces it via `supportedContextTiers` / `defaultContextTier`
* on {@link SquadModelInfo} instead.
*/
contextTier?: boolean;
};
limits: {
max_prompt_tokens?: number;
Expand Down Expand Up @@ -989,4 +1017,15 @@ export interface SquadModelInfo {
billing?: SquadModelBilling;
supportedReasoningEfforts?: SquadReasoningEffort[];
defaultReasoningEffort?: SquadReasoningEffort;
/**
* Context tiers (context-window sizes) this model supports. When a model
* exposes an extended/long context window this is `["default", "long_context"]`;
* otherwise it is `["default"]`. Inferred from the runtime billing signal.
*/
supportedContextTiers?: SquadContextTier[];
/**
* The context tier used when none is explicitly requested. Almost always
* `"default"`; models without an extended window only report `"default"`.
*/
defaultContextTier?: SquadContextTier;
}
Loading
Loading