From ef3b7eae9d64bc45b81e5dda97eec0646422c939 Mon Sep 17 00:00:00 2001 From: Ben Tang Date: Wed, 6 May 2026 08:46:52 +0800 Subject: [PATCH 1/4] feat(subagents): add thinking override to subagent spawn params Expose thinking as a tool call parameter so callers can override the agent default directly. Priority: params.thinking > agent frontmatter thinking. --- pi-extension/subagents/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pi-extension/subagents/index.ts b/pi-extension/subagents/index.ts index cff79bb..bdf24db 100644 --- a/pi-extension/subagents/index.ts +++ b/pi-extension/subagents/index.ts @@ -97,6 +97,9 @@ const SubagentParams = Type.Object({ Type.String({ description: "Appended to system prompt (role instructions)" }), ), model: Type.Optional(Type.String({ description: "Model override (overrides agent default)" })), + thinking: Type.Optional( + Type.String({ description: "Thinking mode/budget override (overrides agent default)" }), + ), skills: Type.Optional( Type.String({ description: "Comma-separated skills (overrides agent default)" }), ), @@ -922,7 +925,7 @@ async function launchSubagent( const effectiveModel = params.model ?? agentDefs?.model; const effectiveTools = params.tools ?? agentDefs?.tools; const effectiveSkills = params.skills ?? agentDefs?.skills; - const effectiveThinking = agentDefs?.thinking; + const effectiveThinking = params.thinking ?? agentDefs?.thinking; const effectiveInteractive = resolveEffectiveInteractive(params, agentDefs); const sessionFile = ctx.sessionManager.getSessionFile(); From 3be7bd45102e1c6f3a10d3546025907171207639 Mon Sep 17 00:00:00 2001 From: Ben Tang Date: Wed, 6 May 2026 08:54:48 +0800 Subject: [PATCH 2/4] feat(subagents): add settings.json override for built-in agent model/thinking Read subagents.agentOverrides from ~/.pi/agent/settings.json (user) and .pi/settings.json (project, takes precedence). Only model and thinking fields are overridden; all other agent defaults remain unchanged. --- pi-extension/subagents/index.ts | 73 ++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/pi-extension/subagents/index.ts b/pi-extension/subagents/index.ts index bdf24db..623a68c 100644 --- a/pi-extension/subagents/index.ts +++ b/pi-extension/subagents/index.ts @@ -134,6 +134,72 @@ const SubagentParams = Type.Object({ type SubagentSessionMode = "standalone" | "lineage-only" | "fork"; +/** Overrides for model/thinking of built-in subagents from settings.json. */ +interface AgentOverride { + model?: string; + thinking?: string; +} + +function findProjectRoot(): string | null { + let dir = process.cwd(); + while (true) { + if (existsSync(join(dir, ".pi"))) return dir; + const parent = dirname(dir); + if (parent === dir) return null; + dir = parent; + } +} + +function readSubagentOverrides(filePath: string): Record { + if (!existsSync(filePath)) return {}; + let raw: string; + try { + raw = readFileSync(filePath, "utf8"); + } catch { + return {}; + } + let settings: unknown; + try { + settings = JSON.parse(raw); + } catch { + return {}; + } + if (!settings || typeof settings !== "object" || Array.isArray(settings)) return {}; + const s = settings as Record; + const subagents = s.subagents; + if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return {}; + const sa = subagents as Record; + const rawOverrides = sa.agentOverrides; + if (!rawOverrides || typeof rawOverrides !== "object" || Array.isArray(rawOverrides)) return {}; + const overrides: Record = {}; + for (const [name, value] of Object.entries(rawOverrides as Record)) { + if (!value || typeof value !== "object" || Array.isArray(value)) continue; + const v = value as Record; + const entry: AgentOverride = {}; + if ("model" in v && typeof v.model === "string") entry.model = v.model; + if ("thinking" in v && typeof v.thinking === "string") entry.thinking = v.thinking; + if (entry.model !== undefined || entry.thinking !== undefined) { + overrides[name] = entry; + } + } + return overrides; +} + +function getAgentOverride(agentName: string): AgentOverride | undefined { + const userSettingsPath = join(homedir(), ".pi", "agent", "settings.json"); + const projectRoot = findProjectRoot(); + const projectSettingsPath = projectRoot ? join(projectRoot, ".pi", "settings.json") : null; + + // Project takes precedence + if (projectSettingsPath) { + const projectOverrides = readSubagentOverrides(projectSettingsPath); + if (projectOverrides[agentName]) return projectOverrides[agentName]; + } + + const userOverrides = readSubagentOverrides(userSettingsPath); + return userOverrides[agentName]; +} + interface AgentDefaults { model?: string; tools?: string; @@ -370,7 +436,12 @@ function loadAgentDefaults(agentName: string): AgentDefaults | null { for (const p of paths) { if (!existsSync(p)) continue; const parsed = parseAgentDefinition(readFileSync(p, "utf8"), agentName); - if (parsed) return parsed; + if (parsed) { + const override = getAgentOverride(agentName); + if (override?.model !== undefined) parsed.model = override.model; + if (override?.thinking !== undefined) parsed.thinking = override.thinking; + return parsed; + } } return null; From be845db7305d77cd50bcd4364818bb720bf6be99 Mon Sep 17 00:00:00 2001 From: Ben Tang Date: Wed, 6 May 2026 09:03:08 +0800 Subject: [PATCH 3/4] fix(subagents): limit settings overrides to bundled agents Use the configured agent directory when reading user settings, merge project overrides per field, apply overrides only to bundled agents, and cover the settings behavior with tests. --- pi-extension/subagents/index.ts | 36 +++++++------- test/test.ts | 84 +++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 18 deletions(-) diff --git a/pi-extension/subagents/index.ts b/pi-extension/subagents/index.ts index 623a68c..51707a5 100644 --- a/pi-extension/subagents/index.ts +++ b/pi-extension/subagents/index.ts @@ -186,18 +186,20 @@ function readSubagentOverrides(filePath: string): Record } function getAgentOverride(agentName: string): AgentOverride | undefined { - const userSettingsPath = join(homedir(), ".pi", "agent", "settings.json"); + const userOverrides = readSubagentOverrides(join(getAgentConfigDir(), "settings.json")); const projectRoot = findProjectRoot(); - const projectSettingsPath = projectRoot ? join(projectRoot, ".pi", "settings.json") : null; - - // Project takes precedence - if (projectSettingsPath) { - const projectOverrides = readSubagentOverrides(projectSettingsPath); - if (projectOverrides[agentName]) return projectOverrides[agentName]; - } + const projectOverrides = projectRoot + ? readSubagentOverrides(join(projectRoot, ".pi", "settings.json")) + : {}; + const override = { ...userOverrides[agentName], ...projectOverrides[agentName] }; + return override.model !== undefined || override.thinking !== undefined ? override : undefined; +} - const userOverrides = readSubagentOverrides(userSettingsPath); - return userOverrides[agentName]; +function applyAgentOverride(agentName: string, agent: T): T { + const override = getAgentOverride(agentName); + if (override?.model !== undefined) agent.model = override.model; + if (override?.thinking !== undefined) agent.thinking = override.thinking; + return agent; } interface AgentDefaults { @@ -339,6 +341,7 @@ function discoverAgentDefinitions(): ListedAgentDefinition[] { file.replace(/\.md$/, ""), ); if (!parsed) continue; + if (source === "package") applyAgentOverride(parsed.name, parsed); agents.set(parsed.name, { ...parsed, source }); } } @@ -428,19 +431,16 @@ function resolveEffectiveInteractive( function loadAgentDefaults(agentName: string): AgentDefaults | null { const configDir = getAgentConfigDir(); const paths = [ - join(process.cwd(), ".pi", "agents", `${agentName}.md`), - join(configDir, "agents", `${agentName}.md`), - join(getBundledAgentsDir(), `${agentName}.md`), + { path: join(process.cwd(), ".pi", "agents", `${agentName}.md`), bundled: false }, + { path: join(configDir, "agents", `${agentName}.md`), bundled: false }, + { path: join(getBundledAgentsDir(), `${agentName}.md`), bundled: true }, ]; - for (const p of paths) { + for (const { path: p, bundled } of paths) { if (!existsSync(p)) continue; const parsed = parseAgentDefinition(readFileSync(p, "utf8"), agentName); if (parsed) { - const override = getAgentOverride(agentName); - if (override?.model !== undefined) parsed.model = override.model; - if (override?.thinking !== undefined) parsed.thinking = override.thinking; - return parsed; + return bundled ? applyAgentOverride(agentName, parsed) : parsed; } } diff --git a/test/test.ts b/test/test.ts index 98ed9fe..62cf7cb 100644 --- a/test/test.ts +++ b/test/test.ts @@ -950,6 +950,90 @@ describe("subagent discovery", () => { ); }); + it("applies settings.json model/thinking overrides to bundled agents", async () => { + await withIsolatedAgentEnv(async ({ globalDir }) => { + writeFileSync( + join(globalDir, "settings.json"), + JSON.stringify({ + subagents: { + agentOverrides: { + worker: { model: "anthropic/override-worker", thinking: "high" }, + }, + }, + }), + ); + + const defs = testApi.loadAgentDefaults("worker"); + assert.ok(defs, "expected bundled worker to be discoverable"); + assert.equal(defs.model, "anthropic/override-worker"); + assert.equal(defs.thinking, "high"); + + const listed = testApi.discoverAgentDefinitions().find((agent: any) => agent.name === "worker"); + assert.equal(listed?.model, "anthropic/override-worker"); + assert.equal(listed?.thinking, "high"); + }); + }); + + it("lets project settings.json override user settings per model/thinking field", async () => { + await withIsolatedAgentEnv(async ({ globalDir, projectDir }) => { + writeFileSync( + join(globalDir, "settings.json"), + JSON.stringify({ + subagents: { + agentOverrides: { + worker: { model: "anthropic/user-worker", thinking: "low" }, + }, + }, + }), + ); + writeFileSync( + join(projectDir, ".pi", "settings.json"), + JSON.stringify({ + subagents: { + agentOverrides: { + worker: { thinking: "xhigh" }, + }, + }, + }), + ); + + const defs = testApi.loadAgentDefaults("worker"); + assert.ok(defs, "expected bundled worker to be discoverable"); + assert.equal(defs.model, "anthropic/user-worker"); + assert.equal(defs.thinking, "xhigh"); + }); + }); + + it("does not apply built-in settings.json overrides to project agents", async () => { + await withIsolatedAgentEnv(async ({ projectAgentsDir, globalDir }) => { + writeFileSync( + join(globalDir, "settings.json"), + JSON.stringify({ + subagents: { + agentOverrides: { + worker: { model: "anthropic/override-worker", thinking: "high" }, + }, + }, + }), + ); + writeAgentFile( + projectAgentsDir, + "worker", + [ + "name: worker", + "description: Project worker", + "model: anthropic/project-worker", + "thinking: low", + ].join("\n"), + ); + + const defs = testApi.loadAgentDefaults("worker"); + assert.ok(defs, "expected project worker to load"); + assert.equal(defs.model, "anthropic/project-worker"); + assert.equal(defs.thinking, "low"); + }); + }); + it("ignores invalid session-mode values", async () => { await withIsolatedAgentEnv(async ({ projectAgentsDir }) => { writeAgentFile( From 27088d29375cc88b391ae56a87b063de7c50ad4c Mon Sep 17 00:00:00 2001 From: Ben Tang Date: Wed, 6 May 2026 10:31:17 +0800 Subject: [PATCH 4/4] feat(subagents): support tools override in settings.json Add tools?: string | false to AgentOverride so bundled agent tool lists can be replaced via settings.json. tools: false clears the list to get all default tools. --- pi-extension/subagents/index.ts | 9 +++++--- test/test.ts | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/pi-extension/subagents/index.ts b/pi-extension/subagents/index.ts index 51707a5..09f5262 100644 --- a/pi-extension/subagents/index.ts +++ b/pi-extension/subagents/index.ts @@ -134,10 +134,11 @@ const SubagentParams = Type.Object({ type SubagentSessionMode = "standalone" | "lineage-only" | "fork"; -/** Overrides for model/thinking of built-in subagents from settings.json. */ +/** Overrides for built-in subagents from settings.json. */ interface AgentOverride { model?: string; thinking?: string; + tools?: string | false; } function findProjectRoot(): string | null { @@ -178,7 +179,8 @@ function readSubagentOverrides(filePath: string): Record const entry: AgentOverride = {}; if ("model" in v && typeof v.model === "string") entry.model = v.model; if ("thinking" in v && typeof v.thinking === "string") entry.thinking = v.thinking; - if (entry.model !== undefined || entry.thinking !== undefined) { + if ("tools" in v && (typeof v.tools === "string" || v.tools === false)) entry.tools = v.tools; + if (entry.model !== undefined || entry.thinking !== undefined || entry.tools !== undefined) { overrides[name] = entry; } } @@ -192,13 +194,14 @@ function getAgentOverride(agentName: string): AgentOverride | undefined { ? readSubagentOverrides(join(projectRoot, ".pi", "settings.json")) : {}; const override = { ...userOverrides[agentName], ...projectOverrides[agentName] }; - return override.model !== undefined || override.thinking !== undefined ? override : undefined; + return override.model !== undefined || override.thinking !== undefined || override.tools !== undefined ? override : undefined; } function applyAgentOverride(agentName: string, agent: T): T { const override = getAgentOverride(agentName); if (override?.model !== undefined) agent.model = override.model; if (override?.thinking !== undefined) agent.thinking = override.thinking; + if (override?.tools !== undefined) agent.tools = override.tools === false ? undefined : override.tools; return agent; } diff --git a/test/test.ts b/test/test.ts index 62cf7cb..8f86fc4 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1034,6 +1034,44 @@ describe("subagent discovery", () => { }); }); + it("applies settings.json tools override to bundled agents", async () => { + await withIsolatedAgentEnv(async ({ globalDir }) => { + writeFileSync( + join(globalDir, "settings.json"), + JSON.stringify({ + subagents: { + agentOverrides: { + scout: { tools: "read, bash, grep, find, ls" }, + }, + }, + }), + ); + + const defs = testApi.loadAgentDefaults("scout"); + assert.ok(defs, "expected bundled scout to be discoverable"); + assert.equal(defs.tools, "read, bash, grep, find, ls"); + }); + }); + + it("supports tools: false to clear bundled agent tools", async () => { + await withIsolatedAgentEnv(async ({ globalDir }) => { + writeFileSync( + join(globalDir, "settings.json"), + JSON.stringify({ + subagents: { + agentOverrides: { + worker: { tools: false }, + }, + }, + }), + ); + + const defs = testApi.loadAgentDefaults("worker"); + assert.ok(defs, "expected bundled worker to be discoverable"); + assert.equal(defs.tools, undefined); + }); + }); + it("ignores invalid session-mode values", async () => { await withIsolatedAgentEnv(async ({ projectAgentsDir }) => { writeAgentFile(