revert #6130: doesn't really work well
This commit is contained in:
@@ -33,7 +33,6 @@ import {
|
||||
canSpawnAtDepth,
|
||||
getTaskSchema,
|
||||
type SingleResult,
|
||||
type TaskIsolationControls,
|
||||
type TaskItem,
|
||||
type TaskParams,
|
||||
type TaskToolDetails,
|
||||
@@ -49,12 +48,7 @@ import { AgentOutputManager } from "./output-manager";
|
||||
import { mapWithConcurrencyLimitAllSettled, Semaphore } from "./parallel";
|
||||
import { renderResult, renderCall as renderTaskCall } from "./render";
|
||||
import { repairTaskParams } from "./repair-args";
|
||||
import {
|
||||
resolveEffectiveSubagentPolicy,
|
||||
runStructuredSubagent,
|
||||
StructuredSubagentError,
|
||||
toStructuredSubagentIsolationControls,
|
||||
} from "./structured-subagent";
|
||||
import { resolveEffectiveSubagentPolicy, runStructuredSubagent, StructuredSubagentError } from "./structured-subagent";
|
||||
|
||||
function renderSubagentUserPrompt(assignment: string): string {
|
||||
return prompt.render(subagentUserPromptTemplate, {
|
||||
@@ -275,22 +269,10 @@ function validateSpawnParams(params: TaskParams, batchEnabled: boolean): string
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Copy the first explicitly supplied value for each task isolation control. */
|
||||
function copyTaskIsolationControls(
|
||||
target: TaskIsolationControls,
|
||||
primary: TaskIsolationControls,
|
||||
fallback?: TaskIsolationControls,
|
||||
): void {
|
||||
if (Object.hasOwn(primary, "isolated")) target.isolated = primary.isolated;
|
||||
else if (fallback && Object.hasOwn(fallback, "isolated")) target.isolated = fallback.isolated;
|
||||
if (Object.hasOwn(primary, "apply")) target.apply = primary.apply;
|
||||
else if (fallback && Object.hasOwn(fallback, "apply")) target.apply = fallback.apply;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a validated call into its spawn list: the `tasks[]` batch when
|
||||
* provided, otherwise the single top-level spawn. The flat form's isolation controls
|
||||
* are only materialized when the caller sent them — `#runSpawn`
|
||||
* provided, otherwise the single top-level spawn. The flat form's `isolated`
|
||||
* flag is only materialized when the caller sent one — `#runSpawn`
|
||||
* distinguishes an absent key from an explicit value.
|
||||
*/
|
||||
function resolveSpawnItems(params: TaskParams): TaskItem[] {
|
||||
@@ -300,7 +282,7 @@ function resolveSpawnItems(params: TaskParams): TaskItem[] {
|
||||
const item: TaskItem = { name: params.name, agent: params.agent, task: params.task };
|
||||
if ("outputSchema" in params) item.outputSchema = params.outputSchema;
|
||||
if ("schemaMode" in params) item.schemaMode = params.schemaMode;
|
||||
copyTaskIsolationControls(item, params);
|
||||
if ("isolated" in params) item.isolated = params.isolated;
|
||||
return [item];
|
||||
}
|
||||
|
||||
@@ -310,8 +292,8 @@ function resolveSpawnItems(params: TaskParams): TaskItem[] {
|
||||
* the item's own value, else `defaultAgent` from the session spawn policy.
|
||||
* `tasks` never leaks into a spawn; the shared `context` rides along
|
||||
* unchanged. Keys are only materialized when present — `#runSpawn`
|
||||
* distinguishes absent isolation controls from explicit values. Per-item
|
||||
* controls (batch form) win over top-level controls (flat compatibility form).
|
||||
* distinguishes an absent `isolated` from an explicit one. The item's
|
||||
* `isolated` (batch form) wins over the top-level flag (flat form).
|
||||
*/
|
||||
function spawnParamsFor(params: TaskParams, item: TaskItem, defaultAgent: string): TaskParams {
|
||||
const spawn: TaskParams = { agent: item.agent?.trim() || defaultAgent };
|
||||
@@ -320,7 +302,11 @@ function spawnParamsFor(params: TaskParams, item: TaskItem, defaultAgent: string
|
||||
if (params.context !== undefined) spawn.context = params.context;
|
||||
if ("outputSchema" in item) spawn.outputSchema = item.outputSchema;
|
||||
if ("schemaMode" in item) spawn.schemaMode = item.schemaMode;
|
||||
copyTaskIsolationControls(spawn, item, params);
|
||||
if (item.isolated !== undefined) {
|
||||
spawn.isolated = item.isolated;
|
||||
} else if ("isolated" in params) {
|
||||
spawn.isolated = params.isolated;
|
||||
}
|
||||
return spawn;
|
||||
}
|
||||
|
||||
@@ -620,7 +606,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
||||
* task wire contract.
|
||||
*/
|
||||
#resolveSpawnPreflight(params: TaskParams) {
|
||||
const isolation = toStructuredSubagentIsolationControls(params);
|
||||
return resolveEffectiveSubagentPolicy({
|
||||
session: this.session,
|
||||
invocationKind: "task",
|
||||
@@ -629,7 +614,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
||||
agent: params.agent,
|
||||
...(Object.hasOwn(params, "outputSchema") ? { outputSchema: params.outputSchema } : {}),
|
||||
...(Object.hasOwn(params, "schemaMode") ? { schemaMode: params.schemaMode } : {}),
|
||||
...(isolation ? { isolation } : {}),
|
||||
...("isolated" in params ? { isolation: { requested: params.isolated } } : {}),
|
||||
blockedAgent: this.#blockedAgent,
|
||||
enableLsp: (this.session.enableLsp ?? true) && this.session.settings.get("task.enableLsp"),
|
||||
enableIrc: isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
|
||||
@@ -1390,7 +1375,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
||||
const startTime = Date.now();
|
||||
const assignment = (params.task ?? "").trim();
|
||||
const context = this.#isBatchEnabled() ? params.context?.trim() || undefined : undefined;
|
||||
const isolation = toStructuredSubagentIsolationControls(params);
|
||||
let latestProgress: AgentProgress | undefined;
|
||||
try {
|
||||
const execution = await runStructuredSubagent({
|
||||
@@ -1407,7 +1391,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
||||
detached,
|
||||
invokedAt: launchTiming?.invokedAt,
|
||||
acquiredAt: launchTiming?.acquiredAt,
|
||||
...(isolation ? { isolation } : {}),
|
||||
...("isolated" in params ? { isolation: { requested: params.isolated } } : {}),
|
||||
blockedAgent: this.#blockedAgent,
|
||||
enableLsp: (this.session.enableLsp ?? true) && this.session.settings.get("task.enableLsp"),
|
||||
enableIrc: isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
|
||||
|
||||
@@ -709,29 +709,6 @@ function agentTypeBadge(agent: string | undefined, theme: Theme): string {
|
||||
return ` ${theme.fg("dim", `${theme.format.bracketLeft}${trimmed}${theme.format.bracketRight}`)}`;
|
||||
}
|
||||
|
||||
type IsolationDisplayInput = { isolated?: boolean; apply?: boolean };
|
||||
|
||||
function isolationMeta(input: IsolationDisplayInput | undefined): string | undefined {
|
||||
if (input?.isolated !== true) return undefined;
|
||||
return input.apply === false ? "isolated · capture-only" : "isolated";
|
||||
}
|
||||
|
||||
function isolationBadge(input: IsolationDisplayInput | undefined, theme: Theme): string {
|
||||
const meta = isolationMeta(input);
|
||||
return meta ? theme.fg("dim", ` [${meta.replace(" · ", ", ")}]`) : "";
|
||||
}
|
||||
|
||||
function appendIsolationBadge(
|
||||
lines: string[],
|
||||
args: Partial<TaskParams> | undefined,
|
||||
index: number,
|
||||
theme: Theme,
|
||||
): void {
|
||||
if (lines.length === 0) return;
|
||||
const input = Array.isArray(args?.tasks) ? args.tasks[index] : args;
|
||||
lines[0] += isolationBadge(input, theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the call preview lines for the single spawned agent. The
|
||||
* args stream in token by token, so every field access is defensive.
|
||||
@@ -784,7 +761,9 @@ function renderTaskItemLines(tasks: TaskItem[] | undefined, theme: Theme): strin
|
||||
line += `: ${theme.fg("muted", previewLine(brief, 64))}`;
|
||||
}
|
||||
line += agentTypeBadge(item?.agent, theme);
|
||||
line += isolationBadge(item, theme);
|
||||
if (item?.isolated === true) {
|
||||
line += theme.fg("dim", " [isolated]");
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
if (cap < tasks.length) {
|
||||
@@ -850,7 +829,6 @@ function createMarkdownSectionRenderer(text: string, theme: Theme): AssignmentSe
|
||||
*/
|
||||
export function renderCall(args: TaskParams, options: TaskRenderOptions, theme: Theme): Component {
|
||||
const showIsolated = "isolated" in args && args.isolated === true;
|
||||
const captureOnly = showIsolated && args.apply === false;
|
||||
// Dispatch glyph from the first frame: spawning is non-blocking, so a
|
||||
// pending/hourglass icon would misread the call as something the turn
|
||||
// waits on.
|
||||
@@ -888,7 +866,7 @@ export function renderCall(args: TaskParams, options: TaskRenderOptions, theme:
|
||||
|
||||
return {
|
||||
header,
|
||||
headerMeta: captureOnly ? "isolated · capture-only" : showIsolated ? "isolated" : undefined,
|
||||
headerMeta: showIsolated ? "isolated" : undefined,
|
||||
sections,
|
||||
state: "pending",
|
||||
borderColor: "borderMuted",
|
||||
@@ -1504,27 +1482,17 @@ export function renderResult(
|
||||
const agentLabel = formatAgentHeaderLabel(args);
|
||||
const assignmentSection = createAssignmentSectionRenderer(args, theme);
|
||||
const contextSection = createContextSectionRenderer(args, theme);
|
||||
const flatIsolationMeta = Array.isArray(args?.tasks) ? undefined : isolationMeta(args);
|
||||
|
||||
if (!details) {
|
||||
const text = result.content.find(c => c.type === "text")?.text || "";
|
||||
const errored = result.isError === true;
|
||||
const header = errored
|
||||
? renderStatusLine(
|
||||
{
|
||||
icon: "error",
|
||||
title: "Task",
|
||||
description: agentLabel,
|
||||
meta: flatIsolationMeta ? [flatIsolationMeta] : undefined,
|
||||
},
|
||||
theme,
|
||||
)
|
||||
? renderStatusLine({ icon: "error", title: "Task", description: agentLabel }, theme)
|
||||
: renderStatusLine(
|
||||
{
|
||||
iconOverride: theme.styledSymbol("status.done", "accent"),
|
||||
title: "Task",
|
||||
description: agentLabel,
|
||||
meta: flatIsolationMeta ? [flatIsolationMeta] : undefined,
|
||||
},
|
||||
theme,
|
||||
);
|
||||
@@ -1584,7 +1552,7 @@ export function renderResult(
|
||||
? theme.styledSymbol("status.done", "accent")
|
||||
: undefined,
|
||||
title: "Task",
|
||||
meta: [metaLabel, flatIsolationMeta].filter((label): label is string => label !== undefined),
|
||||
meta: metaLabel ? [metaLabel] : undefined,
|
||||
},
|
||||
theme,
|
||||
);
|
||||
@@ -1609,17 +1577,13 @@ export function renderResult(
|
||||
lines.push(formatHiddenProgressLine(ordered.slice(0, ordered.length - visible.length), theme));
|
||||
}
|
||||
for (const progress of visible) {
|
||||
const rendered = renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen);
|
||||
appendIsolationBadge(rendered, args, progress.index, theme);
|
||||
lines.push(...rendered);
|
||||
lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen));
|
||||
}
|
||||
} else if (details.results && details.results.length > 0) {
|
||||
const ordered = orderResultsForDisplay(details.results);
|
||||
const visible = expanded ? ordered : selectCollapsedResults(ordered);
|
||||
for (const res of visible) {
|
||||
const rendered = renderAgentResult(res, "", " ", expanded, theme);
|
||||
appendIsolationBadge(rendered, args, res.index, theme);
|
||||
lines.push(...rendered);
|
||||
lines.push(...renderAgentResult(res, "", " ", expanded, theme));
|
||||
}
|
||||
if (visible.length < ordered.length) {
|
||||
const hint = formatExpandHint(theme, false, true);
|
||||
@@ -1638,9 +1602,7 @@ export function renderResult(
|
||||
)
|
||||
: [];
|
||||
for (const progress of supplementalProgress) {
|
||||
const rendered = renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen);
|
||||
appendIsolationBadge(rendered, args, progress.index, theme);
|
||||
lines.push(...rendered);
|
||||
lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen));
|
||||
}
|
||||
|
||||
const summaryParts: string[] = [];
|
||||
|
||||
@@ -68,28 +68,6 @@ export interface StructuredSubagentIsolationControls {
|
||||
apply?: boolean;
|
||||
}
|
||||
|
||||
/** Model-facing isolation controls accepted by task and eval adapters. */
|
||||
export interface StructuredSubagentIsolationInput {
|
||||
isolated?: boolean;
|
||||
apply?: boolean;
|
||||
merge?: boolean;
|
||||
}
|
||||
|
||||
/** Translate model-facing isolation flags into the shared executor policy. */
|
||||
export function toStructuredSubagentIsolationControls(
|
||||
input: StructuredSubagentIsolationInput,
|
||||
): StructuredSubagentIsolationControls | undefined {
|
||||
const requested = input.isolated !== undefined ? input.isolated : undefined;
|
||||
const apply = input.apply !== undefined ? input.apply : undefined;
|
||||
const merge = input.merge === false ? "patch" : undefined;
|
||||
if (requested === undefined && apply === undefined && merge === undefined) return undefined;
|
||||
return {
|
||||
...(requested !== undefined ? { requested } : {}),
|
||||
...(merge !== undefined ? { merge } : {}),
|
||||
...(apply !== undefined ? { apply } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Identity and presentation metadata supplied by the calling surface. */
|
||||
export interface StructuredSubagentIdentity {
|
||||
/** A previously reserved output/registry id. */
|
||||
@@ -265,9 +243,6 @@ export async function resolveEffectiveSubagentPolicy(
|
||||
const agentName = request.agent?.trim() || spawnPolicy.defaultAgent;
|
||||
const planMode = request.session.getPlanModeState?.()?.enabled === true;
|
||||
assertPlanControlsAllowed(request, planMode);
|
||||
if (request.isolation?.apply !== undefined && request.isolation.requested !== true) {
|
||||
throw new StructuredSubagentError("preflight", "Subagent `apply` control requires `isolated: true`.");
|
||||
}
|
||||
assertDepthAndSpawnAllowed(request, agentName);
|
||||
|
||||
const discovery = await discoverAgents(request.session.cwd);
|
||||
|
||||
@@ -109,31 +109,26 @@ export const LABEL_MAX = 80;
|
||||
// Keep this explicit: ArkType serializes `unknown` as a boolean subschema, which llama.cpp grammars reject.
|
||||
const outputSchemaInputSchema = type("object | boolean | string | null");
|
||||
|
||||
const TASK_ITEM_FIELDS = {
|
||||
export const taskItemSchema = type({
|
||||
"name?": "string",
|
||||
agent: "string = 'task'",
|
||||
task: "string",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"+": "delete",
|
||||
});
|
||||
const taskItemSchemaIsolated = type({
|
||||
"name?": "string",
|
||||
agent: "string = 'task'",
|
||||
task: "string",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
} as const;
|
||||
const TASK_ISOLATION_FIELDS = {
|
||||
"isolated?": "boolean",
|
||||
"apply?": "boolean",
|
||||
} as const;
|
||||
|
||||
export const taskItemSchema = type({ ...TASK_ITEM_FIELDS, "+": "delete" });
|
||||
const taskItemSchemaIsolated = type({ ...TASK_ITEM_FIELDS, ...TASK_ISOLATION_FIELDS, "+": "delete" });
|
||||
|
||||
/** Model-facing controls for one task spawn's isolated worktree. */
|
||||
export interface TaskIsolationControls {
|
||||
/** Run this spawn in an isolated worktree. */
|
||||
isolated?: boolean;
|
||||
/** Apply isolated changes to the parent checkout; defaults to true. */
|
||||
apply?: boolean;
|
||||
}
|
||||
"+": "delete",
|
||||
});
|
||||
|
||||
/** Single task item. Fields are optional defensively: args stream in token by token. */
|
||||
export interface TaskItem extends TaskIsolationControls {
|
||||
export interface TaskItem {
|
||||
/** Stable agent name; becomes the registry/IRC id. Default = generated AdjectiveNoun. */
|
||||
name?: string;
|
||||
/** Agent type to run this item (e.g. "scout"). Defaults to the spawn policy's default agent. */
|
||||
@@ -144,10 +139,27 @@ export interface TaskItem extends TaskIsolationControls {
|
||||
outputSchema?: unknown;
|
||||
/** Validation behavior for a caller-provided or inherited output schema. */
|
||||
schemaMode?: "permissive" | "strict";
|
||||
/** Run this spawn in an isolated worktree (batch form; flat form carries it top-level). */
|
||||
isolated?: boolean;
|
||||
}
|
||||
|
||||
export const taskSchema = taskItemSchemaIsolated;
|
||||
const taskSchemaNoIsolation = taskItemSchema;
|
||||
export const taskSchema = type({
|
||||
"name?": "string",
|
||||
agent: "string = 'task'",
|
||||
task: "string",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"isolated?": "boolean",
|
||||
"+": "delete",
|
||||
});
|
||||
const taskSchemaNoIsolation = type({
|
||||
"name?": "string",
|
||||
agent: "string = 'task'",
|
||||
task: "string",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"+": "delete",
|
||||
});
|
||||
const taskSchemaBatch = type({
|
||||
context: "string",
|
||||
tasks: taskItemSchemaIsolated.array(),
|
||||
@@ -176,25 +188,60 @@ function taskAgentSchemaRule(defaultAgent: string): string {
|
||||
return "string";
|
||||
}
|
||||
|
||||
function createTaskItemSchema(agent: string, isolationEnabled: boolean): BaseType {
|
||||
return type.raw({
|
||||
...TASK_ITEM_FIELDS,
|
||||
agent,
|
||||
...(isolationEnabled ? TASK_ISOLATION_FIELDS : {}),
|
||||
"+": "delete",
|
||||
});
|
||||
}
|
||||
|
||||
function createTaskSchema(options: {
|
||||
isolationEnabled: boolean;
|
||||
batchEnabled: boolean;
|
||||
defaultAgent: string;
|
||||
}): BaseType {
|
||||
const item = createTaskItemSchema(taskAgentSchemaRule(options.defaultAgent), options.isolationEnabled);
|
||||
if (!options.batchEnabled) return item;
|
||||
const agent = taskAgentSchemaRule(options.defaultAgent);
|
||||
if (options.batchEnabled) {
|
||||
if (options.isolationEnabled) {
|
||||
const item = type.raw({
|
||||
"name?": "string",
|
||||
agent,
|
||||
task: "string",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"isolated?": "boolean",
|
||||
"+": "delete",
|
||||
});
|
||||
return type.raw({
|
||||
context: "string",
|
||||
tasks: item.array(),
|
||||
"+": "delete",
|
||||
});
|
||||
}
|
||||
const item = type.raw({
|
||||
"name?": "string",
|
||||
agent,
|
||||
task: "string",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"+": "delete",
|
||||
});
|
||||
return type.raw({
|
||||
context: "string",
|
||||
tasks: item.array(),
|
||||
"+": "delete",
|
||||
});
|
||||
}
|
||||
if (options.isolationEnabled) {
|
||||
return type.raw({
|
||||
"name?": "string",
|
||||
agent,
|
||||
task: "string",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"isolated?": "boolean",
|
||||
"+": "delete",
|
||||
});
|
||||
}
|
||||
return type.raw({
|
||||
context: "string",
|
||||
tasks: item.array(),
|
||||
"name?": "string",
|
||||
agent,
|
||||
task: "string",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"+": "delete",
|
||||
});
|
||||
}
|
||||
@@ -229,7 +276,7 @@ export function getTaskSchema(options: {
|
||||
* otherwise); runtime stays permissive so internal callers and stale
|
||||
* transcripts using the flat form keep working under either setting.
|
||||
*/
|
||||
export interface TaskParams extends TaskIsolationControls {
|
||||
export interface TaskParams {
|
||||
/** Stable agent name (flat form). */
|
||||
name?: string;
|
||||
/** Agent type to spawn (flat form); omitted values resolve from the session spawn policy. */
|
||||
@@ -244,6 +291,8 @@ export interface TaskParams extends TaskIsolationControls {
|
||||
tasks?: TaskItem[];
|
||||
/** Batch form: shared background prepended to every assignment; required by the batch schema. */
|
||||
context?: string;
|
||||
/** Run in an isolated worktree (flat form; per-item in batch form). */
|
||||
isolated?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user