feat(coding-agent/task): added task.enableEffort setting to gate effort parameter

- Introduce task.enableEffort setting defaulting to false to hide per-spawn effort parameters.
- Conditionally include effort in single and batch task schemas and descriptions based on the new setting.
This commit is contained in:
can1357
2026-07-27 07:01:09 +02:00
parent d1239fd3e5
commit 0388946e85
6 changed files with 118 additions and 66 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Changed
- Made the task tool's per-spawn `effort` parameter opt-in through `task.enableEffort`, which defaults to false and omits the field from flat and batch schemas and tool guidance until enabled.
## [17.1.5] - 2026-07-27
### Added
@@ -4476,6 +4476,18 @@ export const SETTINGS_SCHEMA = {
},
},
"task.enableEffort": {
type: "boolean",
default: false,
ui: {
tab: "tasks",
group: "Subagents",
label: "Per-Task Effort",
description:
"Expose the optional effort parameter on task spawns, allowing callers to override each subagent's thinking level",
},
},
"task.maxConcurrency": {
type: "number",
default: 32,
@@ -25,7 +25,8 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking
- `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
- `agent`: The agent type running this item (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
- `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
- `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
{{#if effortEnabled}} - `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
{{/if}}
- `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
- `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
{{#if isolationEnabled}}
@@ -39,7 +40,8 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking
- `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
- `agent`: The agent type to spawn (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
- `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
- `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
{{#if effortEnabled}}- `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
{{/if}}
- `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
- `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
{{#if isolationEnabled}}
+43 -32
View File
@@ -156,27 +156,31 @@ export function formatResultOutputFallback(result: Pick<SingleResult, "output" |
return result.requests > 0 ? `(no output) after ${result.requests} req` : "(no output)";
}
/**
* Render the tool description from a cached agent list and current settings.
*/
function renderDescription(
agents: AgentDefinition[],
isolationEnabled: boolean,
applyIsolatedChanges: boolean,
disabledAgents: string[],
batchEnabled: boolean,
asyncEnabled: boolean,
ircEnabled: boolean,
parentSpawns: string,
): string {
const spawnPolicy = resolveSpawnPolicy(parentSpawns);
interface TaskDescriptionOptions {
agents: AgentDefinition[];
isolationEnabled: boolean;
applyIsolatedChanges: boolean;
disabledAgents: string[];
batchEnabled: boolean;
effortEnabled: boolean;
asyncEnabled: boolean;
ircEnabled: boolean;
parentSpawns: string;
}
/** Render the tool description from a cached agent list and current settings. */
function renderDescription(options: TaskDescriptionOptions): string {
const spawnPolicy = resolveSpawnPolicy(options.parentSpawns);
const spawningDisabled = !spawnPolicy.enabled;
let filteredAgents = disabledAgents.length > 0 ? agents.filter(a => !disabledAgents.includes(a.name)) : agents;
let filteredAgents =
options.disabledAgents.length > 0
? options.agents.filter(agent => !options.disabledAgents.includes(agent.name))
: options.agents;
if (spawningDisabled) {
filteredAgents = [];
} else if (spawnPolicy.allowedAgents !== null) {
const allowed = new Set(spawnPolicy.allowedAgents);
filteredAgents = filteredAgents.filter(a => allowed.has(a.name));
filteredAgents = filteredAgents.filter(agent => allowed.has(agent.name));
}
const renderedAgents = filteredAgents.map(agent => ({
name: agent.name,
@@ -188,12 +192,13 @@ function renderDescription(
agents: renderedAgents,
spawningDisabled,
defaultAgent: spawnPolicy.defaultAgent,
isolationEnabled,
applyIsolatedChanges,
batchEnabled,
asyncEnabled,
isolationEnabled: options.isolationEnabled,
applyIsolatedChanges: options.applyIsolatedChanges,
batchEnabled: options.batchEnabled,
effortEnabled: options.effortEnabled,
asyncEnabled: options.asyncEnabled,
hasBlockingAgents: renderedAgents.some(agent => agent.blocking),
ircEnabled,
ircEnabled: options.ircEnabled,
});
}
@@ -581,28 +586,34 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
const planMode = this.session.getPlanModeState?.()?.enabled === true;
const isolationEnabled = !planMode && this.session.settings.get("task.isolation.mode") !== "none";
const defaultAgent = resolveSpawnPolicy(this.session.getSessionSpawns()).defaultAgent;
return getTaskSchema({ isolationEnabled, batchEnabled: this.#isBatchEnabled(), defaultAgent });
return getTaskSchema({
isolationEnabled,
batchEnabled: this.#isBatchEnabled(),
effortEnabled: this.session.settings.get("task.enableEffort"),
defaultAgent,
});
}
renderCall(args: unknown, options: Parameters<typeof renderTaskCall>[1], theme: Theme) {
return renderTaskCall(repairTaskParams(args as TaskParams), options, theme);
}
/** Dynamic description that reflects current disabled-agent settings */
/** Dynamic description that reflects current task settings. */
get description(): string {
const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
const planMode = this.session.getPlanModeState?.()?.enabled === true;
const isolationMode = this.session.settings.get("task.isolation.mode");
return renderDescription(
this.#discoveredAgents,
!planMode && isolationMode !== "none",
this.session.settings.get("task.isolation.apply"),
return renderDescription({
agents: this.#discoveredAgents,
isolationEnabled: !planMode && isolationMode !== "none",
applyIsolatedChanges: this.session.settings.get("task.isolation.apply"),
disabledAgents,
this.#isBatchEnabled(),
this.session.settings.get("async.enabled"),
isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
this.session.getSessionSpawns() ?? "*",
);
batchEnabled: this.#isBatchEnabled(),
effortEnabled: this.session.settings.get("task.enableEffort"),
asyncEnabled: this.session.settings.get("async.enabled"),
ircEnabled: isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
parentSpawns: this.session.getSessionSpawns() ?? "*",
});
}
private constructor(
private readonly session: ToolSession,
+12 -17
View File
@@ -115,7 +115,6 @@ export const taskItemSchema = type({
"name?": "string",
agent: "string = 'task'",
task: "string",
"effort?": effortRule,
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"+": "delete",
@@ -124,7 +123,6 @@ const taskItemSchemaIsolated = type({
"name?": "string",
agent: "string = 'task'",
task: "string",
"effort?": effortRule,
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"isolated?": "boolean",
@@ -153,7 +151,6 @@ export const taskSchema = type({
"name?": "string",
agent: "string = 'task'",
task: "string",
"effort?": effortRule,
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"isolated?": "boolean",
@@ -163,7 +160,6 @@ const taskSchemaNoIsolation = type({
"name?": "string",
agent: "string = 'task'",
task: "string",
"effort?": effortRule,
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"+": "delete",
@@ -200,15 +196,17 @@ function createTaskSchema(options: {
isolationEnabled: boolean;
batchEnabled: boolean;
defaultAgent: string;
effortEnabled: boolean;
}): BaseType {
const agent = taskAgentSchemaRule(options.defaultAgent);
const effortField = options.effortEnabled ? { "effort?": effortRule } : {};
if (options.batchEnabled) {
if (options.isolationEnabled) {
const item = type.raw({
"name?": "string",
agent,
task: "string",
"effort?": effortRule,
...effortField,
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"isolated?": "boolean",
@@ -224,7 +222,7 @@ function createTaskSchema(options: {
"name?": "string",
agent,
task: "string",
"effort?": effortRule,
...effortField,
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"+": "delete",
@@ -240,7 +238,7 @@ function createTaskSchema(options: {
"name?": "string",
agent,
task: "string",
"effort?": effortRule,
...effortField,
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"isolated?": "boolean",
@@ -251,33 +249,30 @@ function createTaskSchema(options: {
"name?": "string",
agent,
task: "string",
"effort?": effortRule,
...effortField,
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"+": "delete",
});
}
export function getTaskSchema(options: { isolationEnabled: boolean; batchEnabled: boolean }): DynamicTaskSchema;
export function getTaskSchema(options: {
isolationEnabled: boolean;
batchEnabled: boolean;
defaultAgent: string;
}): TaskToolSchemaInstance;
/** Build the task wire schema for the current settings and spawn policy. */
export function getTaskSchema(options: {
isolationEnabled: boolean;
batchEnabled: boolean;
effortEnabled?: boolean;
defaultAgent?: string;
}): TaskToolSchemaInstance {
const defaultAgent = options.defaultAgent ?? "task";
if (defaultAgent === "task") {
const effortEnabled = options.effortEnabled ?? false;
if (defaultAgent === "task" && !effortEnabled) {
if (options.batchEnabled) return options.isolationEnabled ? taskSchemaBatch : taskSchemaBatchNoIsolation;
return options.isolationEnabled ? taskSchema : taskSchemaNoIsolation;
}
const key = `${options.isolationEnabled ? "iso" : "flat"}:${options.batchEnabled ? "batch" : "single"}:${defaultAgent}`;
const key = `${options.isolationEnabled ? "iso" : "flat"}:${options.batchEnabled ? "batch" : "single"}:${effortEnabled ? "effort" : "default"}:${defaultAgent}`;
const cached = taskSchemaCache.get(key);
if (cached) return cached;
const schema = createTaskSchema({ ...options, defaultAgent });
const schema = createTaskSchema({ ...options, effortEnabled, defaultAgent });
taskSchemaCache.set(key, schema);
return schema;
}
@@ -24,6 +24,7 @@ import * as discoveryModule from "@oh-my-pi/pi-coding-agent/task/discovery";
import * as executorModule from "@oh-my-pi/pi-coding-agent/task/executor";
import type { AgentDefinition, SingleResult, TaskParams } from "@oh-my-pi/pi-coding-agent/task/types";
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { isRecord } from "@oh-my-pi/pi-utils";
const taskAgent: AgentDefinition = {
name: "task",
@@ -53,8 +54,14 @@ function createSession(
}
function getSchemaProperties(tool: TaskTool): Record<string, unknown> {
const wire = toolWireSchema(tool) as { properties?: Record<string, unknown> };
return wire.properties ?? {};
const properties = toolWireSchema(tool).properties;
return isRecord(properties) ? properties : {};
}
function getBatchItemProperties(tool: TaskTool): Record<string, unknown> {
const tasks = getSchemaProperties(tool).tasks;
if (!isRecord(tasks) || !isRecord(tasks.items) || !isRecord(tasks.items.properties)) return {};
return tasks.items.properties;
}
function getFirstText(result: { content: Array<{ type: string; text?: string }> }): string {
@@ -116,13 +123,35 @@ describe("task.batch schema gating", () => {
expect(onProperties.agent).toBeUndefined();
expect(onProperties.outputSchema).toBeUndefined();
expect(onProperties.schemaMode).toBeUndefined();
const items = (onProperties.tasks as { items?: { properties?: Record<string, unknown> } }).items;
expect(items?.properties?.task).toBeDefined();
expect(items?.properties?.name).toBeDefined();
expect(items?.properties?.agent).toBeDefined();
expect(items?.properties?.outputSchema).toBeDefined();
expect(typeof items?.properties?.outputSchema).toBe("object");
expect(items?.properties?.schemaMode).toBeDefined();
const itemProperties = getBatchItemProperties(on);
expect(itemProperties.task).toBeDefined();
expect(itemProperties.name).toBeDefined();
expect(itemProperties.agent).toBeDefined();
expect(itemProperties.outputSchema).toBeDefined();
expect(typeof itemProperties.outputSchema).toBe("object");
expect(itemProperties.schemaMode).toBeDefined();
});
it("hides effort by default and exposes it when task.enableEffort is enabled", async () => {
mockDiscovery();
const flatSession = createSession({ settings: { "task.batch": false } });
const flat = await TaskTool.create(flatSession);
expect(getSchemaProperties(flat).effort).toBeUndefined();
expect(flat.description).not.toContain("`effort`");
flatSession.settings.override("task.enableEffort", true);
expect(getSchemaProperties(flat).effort).toBeDefined();
expect(flat.description).toContain("`effort`");
const batchSession = createSession({ settings: { "task.batch": true } });
const batch = await TaskTool.create(batchSession);
expect(getBatchItemProperties(batch).effort).toBeUndefined();
expect(batch.description).not.toContain("`effort`");
batchSession.settings.override("task.enableEffort", true);
expect(getBatchItemProperties(batch).effort).toBeDefined();
expect(batch.description).toContain("`effort`");
});
it("keeps isolation boolean-only and describes the configured apply behavior", async () => {
@@ -133,13 +162,13 @@ describe("task.batch schema gating", () => {
);
const properties = getSchemaProperties(tool);
expect(properties.isolated).toBeUndefined();
const items = (properties.tasks as { items?: { properties?: Record<string, unknown> } }).items;
const isolatedSchema = items?.properties?.isolated;
const itemProperties = getBatchItemProperties(tool);
const isolatedSchema = itemProperties.isolated;
if (!isolatedSchema || typeof isolatedSchema !== "object" || !("type" in isolatedSchema)) {
throw new Error("Expected isolated to be a boolean schema");
}
expect(isolatedSchema.type).toBe("boolean");
expect(items?.properties?.apply).toBeUndefined();
expect(itemProperties.apply).toBeUndefined();
expect(tool.description).toContain("automatically applied to the parent checkout");
const captureTool = await TaskTool.create(
@@ -162,9 +191,8 @@ describe("task.batch schema gating", () => {
settings: { "task.batch": true, "task.isolation.mode": "auto" },
}),
);
const properties = getSchemaProperties(tool);
const items = (properties.tasks as { items?: { properties?: Record<string, unknown> } }).items;
expect(items?.properties?.isolated).toBeUndefined();
const itemProperties = getBatchItemProperties(tool);
expect(itemProperties.isolated).toBeUndefined();
expect(tool.description).not.toContain("`isolated`");
});