Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/hooks/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -595,14 +595,15 @@ If a value is too large for the environment, it may be omitted (not set). Mux al
</details>

<details>
<summary>task (8)</summary>
<summary>task (9)</summary>

| Env var | JSON path | Type | Description |
| ---------------------------------- | ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MUX_TOOL_INPUT_AGENT_ID` | `agentId` | string | — |
| `MUX_TOOL_INPUT_N` | `n` | number | Optional best-of count. Use n when several agents should try the same prompt independently. Mutually exclusive with variants; omit both for a single task. Only use grouped runs for sub-agents without interfering side effects, such as read-only agents like explore. |
| `MUX_TOOL_INPUT_PROMPT` | `prompt` | string | — |
| `MUX_TOOL_INPUT_RUN_IN_BACKGROUND` | `run_in_background` | boolean | — |
| `MUX_TOOL_INPUT_STICKY` | `sticky` | boolean | Only set this when the user explicitly asks to keep or preserve the child workspace after reporting; do not decide to set it on your own. When true, the completed sub-agent workspace is preserved for inspection instead of being auto-deleted. |
| `MUX_TOOL_INPUT_SUBAGENT_TYPE` | `subagent_type` | string | — |
| `MUX_TOOL_INPUT_TITLE` | `title` | string | — |
| `MUX_TOOL_INPUT_VARIANTS_<INDEX>` | `variants[<INDEX>]` | string | Optional labels for sibling runs of the same prompt template. Use variants when the task should be repeated across labeled lanes such as issue numbers, commit windows, or frontend/backend/tests/docs review lanes. Mutually exclusive with n. When provided, Mux launches one sibling per label and substitutes ${variant} in the prompt. |
Expand Down
4 changes: 4 additions & 0 deletions src/common/orpc/schemas/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export const WorkspaceMetadataSchema = z.object({
bestOf: BestOfGroupSchema.optional().meta({
description: "Grouping metadata for child tasks spawned from the same parent tool call.",
}),
taskSticky: z.boolean().optional().meta({
description:
"If true, this completed child task workspace is preserved instead of being auto-deleted.",
}),
taskStatus: z
.enum(["queued", "running", "awaiting_report", "interrupted", "reported"])
.optional()
Expand Down
4 changes: 4 additions & 0 deletions src/common/schemas/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export const WorkspaceConfigSchema = z.object({
bestOf: BestOfGroupSchema.optional().meta({
description: "Grouping metadata for child tasks spawned from the same parent tool call.",
}),
taskSticky: z.boolean().optional().meta({
description:
"If true, this completed child task workspace is preserved instead of being auto-deleted.",
}),
taskStatus: z
.enum(["queued", "running", "awaiting_report", "interrupted", "reported"])
.optional()
Expand Down
14 changes: 14 additions & 0 deletions src/common/utils/tools/toolDefinitions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ describe("TOOL_DEFINITIONS", () => {
}
});

it("accepts optional sticky task tool flag", () => {
const parsed = TaskToolArgsSchema.safeParse({
subagent_type: "explore",
prompt: "do the thing",
title: "Test",
sticky: true,
});

expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.sticky).toBe(true);
}
});

it("accepts task tool best-of counts between 1 and 20", () => {
expect(
TaskToolArgsSchema.safeParse({
Expand Down
10 changes: 9 additions & 1 deletion src/common/utils/tools/toolDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ export function buildTaskToolDescription(runtimeMode: RuntimeMode | undefined):
"Spawn a sub-agent task (child workspace). " +
"\n\nIMPORTANT: Whether a sub-agent can see uncommitted changes depends on the runtime. " +
`${getTaskRuntimeVisibilityGuidance(runtimeMode)} ` +
"\n\nProvide agentId (preferred) or subagent_type, prompt, title, run_in_background, and optional n or variants. " +
"\n\nProvide agentId (preferred) or subagent_type, prompt, title, run_in_background, optional sticky, and optional n or variants. " +
"Do not set sticky on your own; set sticky=true only when the user explicitly asks to keep or preserve the child workspace after reporting. Sticky sub-agents are not auto-deleted. " +
`Use n when you want several agents to try the same prompt independently. Use variants when you want several agents to run the same prompt template with a different ${TASK_VARIANT_PLACEHOLDER} substituted into each run. ` +
"Examples: solve GitHub issues 45, 32, and 69 with one shared issue-solving template; investigate a regression across commit windows like A..B and B..C with one shared investigation template; or split a review into frontend/backend/tests/docs lanes with one shared review template. " +
`For variants, keep the shared template in the prompt and put the per-lane difference into ${TASK_VARIANT_PLACEHOLDER}. ` +
Expand Down Expand Up @@ -244,6 +245,12 @@ const TaskToolAgentArgsSchema = z
prompt: z.string().min(1),
title: z.string().min(1),
run_in_background: z.boolean().default(false),
sticky: z
.boolean()
.nullish()
.describe(
"Only set this when the user explicitly asks to keep or preserve the child workspace after reporting; do not decide to set it on your own. When true, the completed sub-agent workspace is preserved for inspection instead of being auto-deleted."
),
n: TaskToolBestOfCountSchema.nullish().describe(
"Optional best-of count. Use n when several agents should try the same prompt independently. Mutually exclusive with variants; omit both for a single task. Only use grouped runs for sub-agents without interfering side effects, such as read-only agents like explore."
),
Expand Down Expand Up @@ -744,6 +751,7 @@ export const TaskListToolTaskSchema = z
createdAt: z.string().optional(),
modelString: z.string().optional(),
thinkingLevel: TaskListThinkingLevelSchema.optional(),
sticky: z.boolean().optional(),
depth: z.number().int().min(0),
})
.strict();
Expand Down
6 changes: 6 additions & 0 deletions src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,7 @@ export class Config {
agentType: workspace.agentType,
agentId: workspace.agentId,
bestOf: workspace.bestOf,
taskSticky: workspace.taskSticky,
taskStatus: workspace.taskStatus,
reportedAt: workspace.reportedAt,
taskModelString: workspace.taskModelString,
Expand Down Expand Up @@ -1604,6 +1605,7 @@ export class Config {
metadata.agentType ??= workspace.agentType;
metadata.agentId ??= workspace.agentId;
metadata.bestOf ??= workspace.bestOf;
metadata.taskSticky ??= workspace.taskSticky;
metadata.taskStatus ??= workspace.taskStatus;
metadata.reportedAt ??= workspace.reportedAt;
metadata.taskModelString ??= workspace.taskModelString;
Expand Down Expand Up @@ -1633,6 +1635,7 @@ export class Config {
workspace.createdAt = metadata.createdAt;
workspace.runtimeConfig = metadata.runtimeConfig;
workspace.forkFamilyBaseName = metadata.forkFamilyBaseName;
workspace.taskSticky = metadata.taskSticky;
configModified = true;

if (!workspace.projects && metadata.projects) {
Expand Down Expand Up @@ -1672,6 +1675,7 @@ export class Config {
agentType: workspace.agentType,
agentId: workspace.agentId,
bestOf: workspace.bestOf,
taskSticky: workspace.taskSticky,
taskStatus: workspace.taskStatus,
reportedAt: workspace.reportedAt,
taskModelString: workspace.taskModelString,
Expand Down Expand Up @@ -1722,6 +1726,7 @@ export class Config {
agentType: workspace.agentType,
agentId: workspace.agentId,
bestOf: workspace.bestOf,
taskSticky: workspace.taskSticky,
taskStatus: workspace.taskStatus,
reportedAt: workspace.reportedAt,
taskModelString: workspace.taskModelString,
Expand Down Expand Up @@ -1790,6 +1795,7 @@ export class Config {
agentType: metadata.agentType,
agentId: metadata.agentId,
bestOf: metadata.bestOf,
taskSticky: metadata.taskSticky,
taskStatus: metadata.taskStatus,
reportedAt: metadata.reportedAt,
taskModelString: metadata.taskModelString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4242,14 +4242,15 @@ export const BUILTIN_SKILL_FILES: Record<string, Record<string, string>> = {
"</details>",
"",
"<details>",
"<summary>task (8)</summary>",
"<summary>task (9)</summary>",
"",
"| Env var | JSON path | Type | Description |",
"| ---------------------------------- | ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |",
"| `MUX_TOOL_INPUT_AGENT_ID` | `agentId` | string | — |",
"| `MUX_TOOL_INPUT_N` | `n` | number | Optional best-of count. Use n when several agents should try the same prompt independently. Mutually exclusive with variants; omit both for a single task. Only use grouped runs for sub-agents without interfering side effects, such as read-only agents like explore. |",
"| `MUX_TOOL_INPUT_PROMPT` | `prompt` | string | — |",
"| `MUX_TOOL_INPUT_RUN_IN_BACKGROUND` | `run_in_background` | boolean | — |",
"| `MUX_TOOL_INPUT_STICKY` | `sticky` | boolean | Only set this when the user explicitly asks to keep or preserve the child workspace after reporting; do not decide to set it on your own. When true, the completed sub-agent workspace is preserved for inspection instead of being auto-deleted. |",
"| `MUX_TOOL_INPUT_SUBAGENT_TYPE` | `subagent_type` | string | — |",
"| `MUX_TOOL_INPUT_TITLE` | `title` | string | — |",
"| `MUX_TOOL_INPUT_VARIANTS_<INDEX>` | `variants[<INDEX>]` | string | Optional labels for sibling runs of the same prompt template. Use variants when the task should be repeated across labeled lanes such as issue numbers, commit windows, or frontend/backend/tests/docs review lanes. Mutually exclusive with n. When provided, Mux launches one sibling per label and substitutes ${variant} in the prompt. |",
Expand Down
65 changes: 65 additions & 0 deletions src/node/services/taskService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1746,6 +1746,29 @@ describe("TaskService", () => {
);
}, 20_000);

test("create persists sticky task preference on the child workspace", async () => {
const config = await createTestConfig(rootDir);
stubStableIds(config, ["aaaaaaaaaa"], "bbbbbbbbbb");
const { parentId } = await saveLocalParentWorkspace(config, rootDir);

const { workspaceService } = createWorkspaceServiceMocks();
const { taskService } = createTaskServiceHarness(config, { workspaceService });

const created = await taskService.create({
parentWorkspaceId: parentId,
kind: "agent",
agentType: "exec",
prompt: "run sticky exec task",
title: "Sticky task",
sticky: true,
});
expect(created.success).toBe(true);
if (!created.success) return;

const childEntry = findWorkspaceInConfig(config, created.data.taskId);
expect(childEntry?.taskSticky).toBe(true);
}, 20_000);

test("parent runtime AI settings outrank persisted parent workspace settings", async () => {
const config = await createTestConfig(rootDir);
stubStableIds(config, ["aaaaaaaaaa"], "bbbbbbbbbb");
Expand Down Expand Up @@ -8888,6 +8911,7 @@ describe("TaskService", () => {
name: string;
agentType: string;
taskStatus?: "reported" | "interrupted";
taskSticky?: boolean;
reportedAt?: string;
}

Expand Down Expand Up @@ -8960,6 +8984,7 @@ describe("TaskService", () => {
parentWorkspaceId,
agentType: task.agentType,
taskStatus: task.taskStatus ?? "reported",
taskSticky: task.taskSticky,
reportedAt: task.reportedAt,
});
parentWorkspaceId = task.id;
Expand Down Expand Up @@ -9128,6 +9153,46 @@ describe("TaskService", () => {
}
});

test("sticky completed descendants are never auto-cleaned", async () => {
const parentTaskId = "parent-222";
const childTaskId = "child-333";
const { config, remove, rootWorkspaceId, taskService, internal } =
await setupReportedTaskChain({
preserveSubagentsUntilArchive: false,
taskChain: [
{
id: parentTaskId,
directoryName: "parent-task",
name: "agent_exec_parent",
agentType: "exec",
taskStatus: "reported",
},
{
id: childTaskId,
directoryName: "child-task",
name: "agent_explore_child",
agentType: "explore",
taskStatus: "reported",
taskSticky: true,
},
],
});

await archiveWorkspaceInTestConfig(config, rootWorkspaceId);

const cleanupEligibility = await internal.canCleanupReportedTask(childTaskId);
expect(cleanupEligibility).toEqual({ ok: false, reason: "sticky" });

await internal.cleanupReportedLeafTask(childTaskId);
await taskService.cleanupReportedDescendantsAfterArchive(rootWorkspaceId);

expect(remove).not.toHaveBeenCalled();
expect(findWorkspaceInConfig(config, childTaskId)).toBeTruthy();
expect(findWorkspaceInConfig(config, parentTaskId)).toBeTruthy();
expect(taskService.hasStickyCompletedDescendants(rootWorkspaceId)).toBe(true);
expect(taskService.hasPreservedCompletedDescendants(rootWorkspaceId)).toBe(true);
});

test("with toggle off, current cleanup behavior remains unchanged", async () => {
const { config, remove, taskChain, internal } = await setupReportedTaskChain({
preserveSubagentsUntilArchive: false,
Expand Down
Loading
Loading