Skip to content
Open
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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,44 @@ cp config.json.example config.json

---

### Model & Thinking Overrides via Settings

Override the default model or thinking level for any bundled or custom agent via `settings.json` — no need to edit agent `.md` files or pass `model:` on every spawn.

**Resolution order** (highest → lowest):
1. Explicit `model` or `thinking` tool parameter
2. `settings.json` → `subagents.agentOverrides.<agent>.model`/`.thinking`
3. Agent definition frontmatter (`model:`, `thinking:`)

Supported settings locations (project overrides global):

| Location | Scope |
|----------|-------|
| `~/.pi/agent/settings.json` | Global (all projects) |
| `.pi/settings.json` | Project (current directory) |

Example:

```json
{
"subagents": {
"agentOverrides": {
"scout": {
"model": "llm-gateway/gpt-5.4"
},
"planner": {
"model": "anthropic/claude-opus-4-6",
"thinking": "high"
}
}
}
}
```

Changes take effect immediately on the next spawn — no `/reload` needed.

---

## Spawning Subagents

```typescript
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 52 additions & 2 deletions pi-extension/subagents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ interface AgentDefaults {
disableModelInvocation?: boolean;
}

interface AgentOverrideConfig {
model?: string;
thinking?: string;
}

interface AgentSettings {
subagents?: {
agentOverrides?: Record<string, AgentOverrideConfig>;
};
}

type AgentSource = "package" | "global" | "project";

interface AgentDefinition extends AgentDefaults {
Expand Down Expand Up @@ -368,6 +379,42 @@ function loadAgentDefaults(agentName: string): AgentDefaults | null {
return null;
}

/**
* Read global and project-local settings.json to resolve agent overrides.
* Project settings override global settings per-agent (shallow merge per agent).
*/
function loadAgentSettings(): AgentSettings {
const globalPath = join(getAgentConfigDir(), "settings.json");
const projectPath = join(process.cwd(), ".pi", "settings.json");

const readSettings = (path: string): AgentSettings => {
if (!existsSync(path)) return {};
try {
const raw = readFileSync(path, "utf8");
return JSON.parse(raw) as AgentSettings;
} catch {
return {};
}
};

const globalSettings = readSettings(globalPath);
const projectSettings = readSettings(projectPath);

if (!globalSettings.subagents?.agentOverrides && !projectSettings.subagents?.agentOverrides) {
return {};
}

const mergedOverrides: Record<string, AgentOverrideConfig> = {
...globalSettings.subagents?.agentOverrides,
};

for (const [name, override] of Object.entries(projectSettings.subagents?.agentOverrides ?? {})) {
mergedOverrides[name] = { ...mergedOverrides[name], ...override };
}

return { subagents: { agentOverrides: mergedOverrides } };
}

function formatElapsed(seconds: number): string {
if (seconds < 60) return `${seconds}s`;
const m = Math.floor(seconds / 60);
Expand Down Expand Up @@ -842,6 +889,7 @@ export const __test__ = {
getShellReadyDelayMs,
renderSubagentWidgetLines,
loadAgentDefaults,
loadAgentSettings,
discoverAgentDefinitions,
resolveEffectiveSessionMode,
resolveLaunchBehavior,
Expand Down Expand Up @@ -881,10 +929,12 @@ async function launchSubagent(
const id = Math.random().toString(16).slice(2, 10);

const agentDefs = params.agent ? loadAgentDefaults(params.agent) : null;
const effectiveModel = params.model ?? agentDefs?.model;
const settings = loadAgentSettings();
const configOverride = params.agent ? settings.subagents?.agentOverrides?.[params.agent] : undefined;
const effectiveModel = params.model ?? configOverride?.model ?? agentDefs?.model;
const effectiveTools = params.tools ?? agentDefs?.tools;
const effectiveSkills = params.skills ?? agentDefs?.skills;
const effectiveThinking = agentDefs?.thinking;
const effectiveThinking = configOverride?.thinking ?? agentDefs?.thinking;
const effectiveInteractive = resolveEffectiveInteractive(params, agentDefs);

const sessionFile = ctx.sessionManager.getSessionFile();
Expand Down
82 changes: 82 additions & 0 deletions test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ function writeAgentFile(
writeFileSync(join(agentsDir, `${name}.md`), `---\n${frontmatter}\n---\n\n${body}\n`);
}

function writeSettingsFile(settingsPath: string, data: object) {
mkdirSync(settingsPath, { recursive: true });
writeFileSync(join(settingsPath, "settings.json"), JSON.stringify(data, null, 2));
}

async function withIsolatedAgentEnv(
fn: (paths: {
projectDir: string;
Expand Down Expand Up @@ -955,6 +960,83 @@ describe("subagent discovery", () => {
});
});

it("loadAgentSettings returns empty when no settings files exist", async () => {
await withIsolatedAgentEnv(async () => {
const settings = testApi.loadAgentSettings();
assert.deepEqual(settings, {});
});
});

it("loadAgentSettings reads global settings.json", async () => {
await withIsolatedAgentEnv(async ({ globalDir }) => {
writeSettingsFile(globalDir, {
subagents: {
agentOverrides: {
scout: { model: "llm-gateway/gpt-5.4" },
},
},
});

const settings = testApi.loadAgentSettings();
assert.equal(settings.subagents?.agentOverrides?.scout?.model, "llm-gateway/gpt-5.4");
});
});

it("loadAgentSettings project overrides global per-agent", async () => {
await withIsolatedAgentEnv(async ({ projectDir, globalDir }) => {
writeSettingsFile(globalDir, {
subagents: {
agentOverrides: {
scout: { model: "llm-gateway/gpt-5.4" },
worker: { model: "llm-gateway/Kimi-K2.6" },
},
},
});
writeSettingsFile(join(projectDir, ".pi"), {
subagents: {
agentOverrides: {
scout: { model: "anthropic/claude-sonnet-4-6" },
},
},
});

const settings = testApi.loadAgentSettings();
assert.equal(settings.subagents?.agentOverrides?.scout?.model, "anthropic/claude-sonnet-4-6");
assert.equal(settings.subagents?.agentOverrides?.worker?.model, "llm-gateway/Kimi-K2.6");
});
});

it("loadAgentSettings merges model and thinking per-agent from global and project", async () => {
await withIsolatedAgentEnv(async ({ projectDir, globalDir }) => {
writeSettingsFile(globalDir, {
subagents: {
agentOverrides: {
scout: { model: "llm-gateway/gpt-5.4" },
},
},
});
writeSettingsFile(join(projectDir, ".pi"), {
subagents: {
agentOverrides: {
scout: { thinking: "high" },
},
},
});

const settings = testApi.loadAgentSettings();
assert.equal(settings.subagents?.agentOverrides?.scout?.model, "llm-gateway/gpt-5.4");
assert.equal(settings.subagents?.agentOverrides?.scout?.thinking, "high");
});
});

it("loadAgentSettings ignores malformed settings.json", async () => {
await withIsolatedAgentEnv(async ({ globalDir }) => {
writeFileSync(join(globalDir, "settings.json"), "not json");
const settings = testApi.loadAgentSettings();
assert.deepEqual(settings, {});
});
});

it("resolves session mode with fork override precedence", () => {
assert.equal(testApi.resolveEffectiveSessionMode({ name: "A", task: "T" }, null), "standalone");
assert.equal(
Expand Down