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:
@@ -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}}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user