revert #6130: doesn't really work well

This commit is contained in:
can1357
2026-07-22 23:54:15 +02:00
parent 59877a01bd
commit 242bcbef8c
11 changed files with 130 additions and 309 deletions
+13 -29
View File
@@ -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),
+9 -47
View File
@@ -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);
+82 -33
View File
@@ -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;
}
/**