add magenta log prefixes for delegated subagents (#387)

When delegate() runs multiple subagents in parallel, their logs
interleave without visual distinction. Use AsyncLocalStorage to
automatically prefix every log line inside runSubagent() with the
task label in magenta (e.g. [frontend-review]).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Colin McDonnell
2026-02-25 05:53:18 +00:00
committed by pullfrog[bot]
parent d5ab3706db
commit 1ed3da8273
5 changed files with 207 additions and 118 deletions
+66 -45
View File
@@ -135589,6 +135589,7 @@ var Effort = type.enumerated("mini", "auto", "max");
// utils/log.ts
var core = __toESM(require_core(), 1);
var import_table = __toESM(require_src(), 1);
import { AsyncLocalStorage } from "node:async_hooks";
// utils/globals.ts
import { existsSync } from "node:fs";
@@ -135597,6 +135598,23 @@ var isGitHubActions = !!process.env.GITHUB_ACTIONS;
var isInsideDocker = existsSync("/.dockerenv");
// utils/log.ts
var logContext = new AsyncLocalStorage();
var MAGENTA = "\x1B[35m";
var RESET = "\x1B[0m";
function withLogPrefix(prefix, fn2) {
return logContext.run({ prefix }, fn2);
}
function prefixLines(message) {
const ctx = logContext.getStore();
if (!ctx) return message;
const colored = `${MAGENTA}${ctx.prefix}${RESET} `;
return message.split("\n").map((line) => `${colored}${line}`).join("\n");
}
function prefixPlain(name) {
const ctx = logContext.getStore();
if (!ctx) return name;
return `${ctx.prefix} ${name}`;
}
var isRunnerDebugEnabled = () => core.isDebug();
var isLocalDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true";
var isDebugEnabled = () => isLocalDebugEnabled() || isRunnerDebugEnabled();
@@ -135612,10 +135630,11 @@ ${arg.stack}`;
}).join(" ");
}
function startGroup2(name) {
const prefixed = prefixPlain(name);
if (isGitHubActions) {
core.startGroup(name);
core.startGroup(prefixed);
} else {
console.group(name);
console.group(prefixed);
}
}
function endGroup2() {
@@ -135688,7 +135707,7 @@ function boxString(text, options) {
}
function box(text, options) {
const boxContent = boxString(text, options);
core.info(boxContent);
core.info(prefixLines(boxContent));
}
async function writeSummary(text) {
if (!isGitHubActions) return;
@@ -135708,42 +135727,42 @@ function printTable(rows, options) {
);
const formatted = (0, import_table.table)(tableData);
if (title) {
core.info(`
${title}`);
core.info(prefixLines(`
${title}`));
}
core.info(`
core.info(prefixLines(`
${formatted}
`);
`));
}
function separator(length = 50) {
const separatorText = "\u2500".repeat(length);
core.info(separatorText);
core.info(prefixLines(separatorText));
}
var log = {
/** Print info message */
info: (...args2) => {
core.info(`${ts()}${formatArgs(args2)}`);
core.info(prefixLines(`${ts()}${formatArgs(args2)}`));
},
/** Print a warning message. Use only for warnings that should be displayed in the job summary. */
warning: (...args2) => {
core.warning(`${ts()}${formatArgs(args2)}`);
core.warning(prefixLines(`${ts()}${formatArgs(args2)}`));
},
/** Print an error message. Use only for errors that should be displayed in the job summary. */
error: (...args2) => {
core.error(`${ts()}${formatArgs(args2)}`);
core.error(prefixLines(`${ts()}${formatArgs(args2)}`));
},
/** Print success message */
success: (...args2) => {
core.info(`${ts()}\xBB ${formatArgs(args2)}`);
core.info(prefixLines(`${ts()}\xBB ${formatArgs(args2)}`));
},
/** Print debug message (only when debug mode is enabled) */
debug: (...args2) => {
if (isRunnerDebugEnabled()) {
core.debug(formatArgs(args2));
core.debug(prefixLines(formatArgs(args2)));
return;
}
if (isLocalDebugEnabled()) {
core.info(`${ts()}[DEBUG] ${formatArgs(args2)}`);
core.info(prefixLines(`${ts()}[DEBUG] ${formatArgs(args2)}`));
}
},
/** Print a formatted box with text */
@@ -136443,41 +136462,43 @@ ${params.instructions}`;
};
}
async function runSubagent(params) {
params.subagent.keepAliveInterval = setInterval(markActivity, 3e4);
const mcpServer = await startSubagentMcpServer({
ctx: params.ctx,
subagentId: params.subagent.id
});
const subagentTmpdir = join(params.ctx.tmpdir, params.subagent.id);
mkdirSync(subagentTmpdir, { recursive: true });
try {
const subagentPayload = { ...params.ctx.payload, effort: params.effort };
const subagentInstructions = buildSubagentInstructions({
return withLogPrefix(`[${params.subagent.label}]`, async () => {
params.subagent.keepAliveInterval = setInterval(markActivity, 3e4);
const mcpServer = await startSubagentMcpServer({
ctx: params.ctx,
label: params.subagent.label,
instructions: params.instructions
subagentId: params.subagent.id
});
const result = await params.ctx.agent.run({
payload: subagentPayload,
mcpServerUrl: mcpServer.url,
tmpdir: subagentTmpdir,
instructions: subagentInstructions
});
params.subagent.usage = result.usage;
writeFileSync(params.subagent.stdoutFilePath, result.output ?? "", "utf-8");
completeSubagent({ ctx: params.ctx, subagent: params.subagent, success: result.success });
return { success: result.success, error: result.error };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
const subagentTmpdir = join(params.ctx.tmpdir, params.subagent.id);
mkdirSync(subagentTmpdir, { recursive: true });
try {
writeFileSync(params.subagent.stdoutFilePath, "", "utf-8");
} catch {
const subagentPayload = { ...params.ctx.payload, effort: params.effort };
const subagentInstructions = buildSubagentInstructions({
ctx: params.ctx,
label: params.subagent.label,
instructions: params.instructions
});
const result = await params.ctx.agent.run({
payload: subagentPayload,
mcpServerUrl: mcpServer.url,
tmpdir: subagentTmpdir,
instructions: subagentInstructions
});
params.subagent.usage = result.usage;
writeFileSync(params.subagent.stdoutFilePath, result.output ?? "", "utf-8");
completeSubagent({ ctx: params.ctx, subagent: params.subagent, success: result.success });
return { success: result.success, error: result.error };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
try {
writeFileSync(params.subagent.stdoutFilePath, "", "utf-8");
} catch {
}
completeSubagent({ ctx: params.ctx, subagent: params.subagent, success: false });
return { success: false, error: errorMessage };
} finally {
await mcpServer.stop();
}
completeSubagent({ ctx: params.ctx, subagent: params.subagent, success: false });
return { success: false, error: errorMessage };
} finally {
await mcpServer.stop();
}
});
}
// mcp/askQuestion.ts
+30 -14
View File
@@ -25507,6 +25507,7 @@ var core3 = __toESM(require_core(), 1);
// utils/log.ts
var core = __toESM(require_core(), 1);
var import_table = __toESM(require_src(), 1);
import { AsyncLocalStorage } from "node:async_hooks";
// utils/globals.ts
import { existsSync } from "node:fs";
@@ -25515,6 +25516,20 @@ var isGitHubActions = !!process.env.GITHUB_ACTIONS;
var isInsideDocker = existsSync("/.dockerenv");
// utils/log.ts
var logContext = new AsyncLocalStorage();
var MAGENTA = "\x1B[35m";
var RESET = "\x1B[0m";
function prefixLines(message) {
const ctx = logContext.getStore();
if (!ctx) return message;
const colored = `${MAGENTA}${ctx.prefix}${RESET} `;
return message.split("\n").map((line) => `${colored}${line}`).join("\n");
}
function prefixPlain(name) {
const ctx = logContext.getStore();
if (!ctx) return name;
return `${ctx.prefix} ${name}`;
}
var isRunnerDebugEnabled = () => core.isDebug();
var isLocalDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true";
var isDebugEnabled = () => isLocalDebugEnabled() || isRunnerDebugEnabled();
@@ -25530,10 +25545,11 @@ ${arg.stack}`;
}).join(" ");
}
function startGroup2(name) {
const prefixed = prefixPlain(name);
if (isGitHubActions) {
core.startGroup(name);
core.startGroup(prefixed);
} else {
console.group(name);
console.group(prefixed);
}
}
function endGroup2() {
@@ -25606,7 +25622,7 @@ function boxString(text, options) {
}
function box(text, options) {
const boxContent = boxString(text, options);
core.info(boxContent);
core.info(prefixLines(boxContent));
}
function printTable(rows, options) {
const { title } = options || {};
@@ -25620,42 +25636,42 @@ function printTable(rows, options) {
);
const formatted = (0, import_table.table)(tableData);
if (title) {
core.info(`
${title}`);
core.info(prefixLines(`
${title}`));
}
core.info(`
core.info(prefixLines(`
${formatted}
`);
`));
}
function separator(length = 50) {
const separatorText = "\u2500".repeat(length);
core.info(separatorText);
core.info(prefixLines(separatorText));
}
var log = {
/** Print info message */
info: (...args) => {
core.info(`${ts()}${formatArgs(args)}`);
core.info(prefixLines(`${ts()}${formatArgs(args)}`));
},
/** Print a warning message. Use only for warnings that should be displayed in the job summary. */
warning: (...args) => {
core.warning(`${ts()}${formatArgs(args)}`);
core.warning(prefixLines(`${ts()}${formatArgs(args)}`));
},
/** Print an error message. Use only for errors that should be displayed in the job summary. */
error: (...args) => {
core.error(`${ts()}${formatArgs(args)}`);
core.error(prefixLines(`${ts()}${formatArgs(args)}`));
},
/** Print success message */
success: (...args) => {
core.info(`${ts()}\xBB ${formatArgs(args)}`);
core.info(prefixLines(`${ts()}\xBB ${formatArgs(args)}`));
},
/** Print debug message (only when debug mode is enabled) */
debug: (...args) => {
if (isRunnerDebugEnabled()) {
core.debug(formatArgs(args));
core.debug(prefixLines(formatArgs(args)));
return;
}
if (isLocalDebugEnabled()) {
core.info(`${ts()}[DEBUG] ${formatArgs(args)}`);
core.info(prefixLines(`${ts()}[DEBUG] ${formatArgs(args)}`));
}
},
/** Print a formatted box with text */
+36 -33
View File
@@ -6,6 +6,7 @@ import type { Effort } from "../external.ts";
import { ghPullfrogMcpName } from "../external.ts";
import { markActivity } from "../utils/activity.ts";
import type { ResolvedInstructions } from "../utils/instructions.ts";
import { withLogPrefix } from "../utils/log.ts";
import { type SubagentState, startSubagentMcpServer, type ToolContext } from "./server.ts";
type CreateSubagentParams = {
@@ -132,41 +133,43 @@ type RunSubagentResult = {
};
export async function runSubagent(params: RunSubagentParams): Promise<RunSubagentResult> {
params.subagent.keepAliveInterval = setInterval(markActivity, 30_000);
const mcpServer = await startSubagentMcpServer({
ctx: params.ctx,
subagentId: params.subagent.id,
});
// each subagent gets its own tmpdir so parallel agents don't clobber config files
const subagentTmpdir = join(params.ctx.tmpdir, params.subagent.id);
mkdirSync(subagentTmpdir, { recursive: true });
try {
const subagentPayload = { ...params.ctx.payload, effort: params.effort };
const subagentInstructions = buildSubagentInstructions({
return withLogPrefix(`[${params.subagent.label}]`, async () => {
params.subagent.keepAliveInterval = setInterval(markActivity, 30_000);
const mcpServer = await startSubagentMcpServer({
ctx: params.ctx,
label: params.subagent.label,
instructions: params.instructions,
subagentId: params.subagent.id,
});
const result = await params.ctx.agent.run({
payload: subagentPayload,
mcpServerUrl: mcpServer.url,
tmpdir: subagentTmpdir,
instructions: subagentInstructions,
});
params.subagent.usage = result.usage;
writeFileSync(params.subagent.stdoutFilePath, result.output ?? "", "utf-8");
completeSubagent({ ctx: params.ctx, subagent: params.subagent, success: result.success });
return { success: result.success, error: result.error };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
// each subagent gets its own tmpdir so parallel agents don't clobber config files
const subagentTmpdir = join(params.ctx.tmpdir, params.subagent.id);
mkdirSync(subagentTmpdir, { recursive: true });
try {
writeFileSync(params.subagent.stdoutFilePath, "", "utf-8");
} catch {
// best-effort
const subagentPayload = { ...params.ctx.payload, effort: params.effort };
const subagentInstructions = buildSubagentInstructions({
ctx: params.ctx,
label: params.subagent.label,
instructions: params.instructions,
});
const result = await params.ctx.agent.run({
payload: subagentPayload,
mcpServerUrl: mcpServer.url,
tmpdir: subagentTmpdir,
instructions: subagentInstructions,
});
params.subagent.usage = result.usage;
writeFileSync(params.subagent.stdoutFilePath, result.output ?? "", "utf-8");
completeSubagent({ ctx: params.ctx, subagent: params.subagent, success: result.success });
return { success: result.success, error: result.error };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
try {
writeFileSync(params.subagent.stdoutFilePath, "", "utf-8");
} catch {
// best-effort
}
completeSubagent({ ctx: params.ctx, subagent: params.subagent, success: false });
return { success: false, error: errorMessage };
} finally {
await mcpServer.stop();
}
completeSubagent({ ctx: params.ctx, subagent: params.subagent, success: false });
return { success: false, error: errorMessage };
} finally {
await mcpServer.stop();
}
});
}
+30 -14
View File
@@ -28846,6 +28846,7 @@ var require_semver2 = __commonJS({
// utils/log.ts
var core = __toESM(require_core(), 1);
var import_table = __toESM(require_src(), 1);
import { AsyncLocalStorage } from "node:async_hooks";
// utils/globals.ts
import { existsSync } from "node:fs";
@@ -28854,6 +28855,20 @@ var isGitHubActions = !!process.env.GITHUB_ACTIONS;
var isInsideDocker = existsSync("/.dockerenv");
// utils/log.ts
var logContext = new AsyncLocalStorage();
var MAGENTA = "\x1B[35m";
var RESET = "\x1B[0m";
function prefixLines(message) {
const ctx = logContext.getStore();
if (!ctx) return message;
const colored = `${MAGENTA}${ctx.prefix}${RESET} `;
return message.split("\n").map((line) => `${colored}${line}`).join("\n");
}
function prefixPlain(name) {
const ctx = logContext.getStore();
if (!ctx) return name;
return `${ctx.prefix} ${name}`;
}
var isRunnerDebugEnabled = () => core.isDebug();
var isLocalDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true";
var isDebugEnabled = () => isLocalDebugEnabled() || isRunnerDebugEnabled();
@@ -28869,10 +28884,11 @@ ${arg.stack}`;
}).join(" ");
}
function startGroup2(name) {
const prefixed = prefixPlain(name);
if (isGitHubActions) {
core.startGroup(name);
core.startGroup(prefixed);
} else {
console.group(name);
console.group(prefixed);
}
}
function endGroup2() {
@@ -28945,7 +28961,7 @@ function boxString(text, options) {
}
function box(text, options) {
const boxContent = boxString(text, options);
core.info(boxContent);
core.info(prefixLines(boxContent));
}
function printTable(rows, options) {
const { title } = options || {};
@@ -28959,42 +28975,42 @@ function printTable(rows, options) {
);
const formatted = (0, import_table.table)(tableData);
if (title) {
core.info(`
${title}`);
core.info(prefixLines(`
${title}`));
}
core.info(`
core.info(prefixLines(`
${formatted}
`);
`));
}
function separator(length = 50) {
const separatorText = "\u2500".repeat(length);
core.info(separatorText);
core.info(prefixLines(separatorText));
}
var log = {
/** Print info message */
info: (...args2) => {
core.info(`${ts()}${formatArgs(args2)}`);
core.info(prefixLines(`${ts()}${formatArgs(args2)}`));
},
/** Print a warning message. Use only for warnings that should be displayed in the job summary. */
warning: (...args2) => {
core.warning(`${ts()}${formatArgs(args2)}`);
core.warning(prefixLines(`${ts()}${formatArgs(args2)}`));
},
/** Print an error message. Use only for errors that should be displayed in the job summary. */
error: (...args2) => {
core.error(`${ts()}${formatArgs(args2)}`);
core.error(prefixLines(`${ts()}${formatArgs(args2)}`));
},
/** Print success message */
success: (...args2) => {
core.info(`${ts()}\xBB ${formatArgs(args2)}`);
core.info(prefixLines(`${ts()}\xBB ${formatArgs(args2)}`));
},
/** Print debug message (only when debug mode is enabled) */
debug: (...args2) => {
if (isRunnerDebugEnabled()) {
core.debug(formatArgs(args2));
core.debug(prefixLines(formatArgs(args2)));
return;
}
if (isLocalDebugEnabled()) {
core.info(`${ts()}[DEBUG] ${formatArgs(args2)}`);
core.info(prefixLines(`${ts()}[DEBUG] ${formatArgs(args2)}`));
}
},
/** Print a formatted box with text */
+45 -12
View File
@@ -2,11 +2,43 @@
* Logging utilities that work well in both local and GitHub Actions environments
*/
import { AsyncLocalStorage } from "node:async_hooks";
import * as core from "@actions/core";
import { table } from "table";
import type { AgentUsage } from "../agents/shared.ts";
import { isGitHubActions, isInsideDocker } from "./globals.ts";
// --- subagent log prefix via AsyncLocalStorage ---
type LogContext = { prefix: string };
const logContext = new AsyncLocalStorage<LogContext>();
const MAGENTA = "\x1b[35m";
const RESET = "\x1b[0m";
/** run `fn` with every log line prefixed by `prefix` (e.g. "[task-label]") in magenta */
export function withLogPrefix<T>(prefix: string, fn: () => Promise<T>): Promise<T> {
return logContext.run({ prefix }, fn);
}
function prefixLines(message: string): string {
const ctx = logContext.getStore();
if (!ctx) return message;
const colored = `${MAGENTA}${ctx.prefix}${RESET} `;
return message
.split("\n")
.map((line) => `${colored}${line}`)
.join("\n");
}
/** plain-text prefix (no ANSI) for GitHub Actions group names */
function prefixPlain(name: string): string {
const ctx = logContext.getStore();
if (!ctx) return name;
return `${ctx.prefix} ${name}`;
}
const isRunnerDebugEnabled = () => core.isDebug();
const isLocalDebugEnabled = () =>
@@ -36,10 +68,11 @@ function formatArgs(args: unknown[]): string {
* Start a collapsed group (GitHub Actions) or regular group (local)
*/
function startGroup(name: string): void {
const prefixed = prefixPlain(name);
if (isGitHubActions) {
core.startGroup(name);
core.startGroup(prefixed);
} else {
console.group(name);
console.group(prefixed);
}
}
@@ -153,7 +186,7 @@ function box(
}
): void {
const boxContent = boxString(text, options);
core.info(boxContent);
core.info(prefixLines(boxContent));
}
/**
@@ -200,9 +233,9 @@ function printTable(
const formatted = table(tableData);
if (title) {
core.info(`\n${title}`);
core.info(prefixLines(`\n${title}`));
}
core.info(`\n${formatted}\n`);
core.info(prefixLines(`\n${formatted}\n`));
}
/**
@@ -210,7 +243,7 @@ function printTable(
*/
function separator(length: number = 50): void {
const separatorText = "─".repeat(length);
core.info(separatorText);
core.info(prefixLines(separatorText));
}
/**
@@ -219,32 +252,32 @@ function separator(length: number = 50): void {
export const log = {
/** Print info message */
info: (...args: unknown[]): void => {
core.info(`${ts()}${formatArgs(args)}`);
core.info(prefixLines(`${ts()}${formatArgs(args)}`));
},
/** Print a warning message. Use only for warnings that should be displayed in the job summary. */
warning: (...args: unknown[]): void => {
core.warning(`${ts()}${formatArgs(args)}`);
core.warning(prefixLines(`${ts()}${formatArgs(args)}`));
},
/** Print an error message. Use only for errors that should be displayed in the job summary. */
error: (...args: unknown[]): void => {
core.error(`${ts()}${formatArgs(args)}`);
core.error(prefixLines(`${ts()}${formatArgs(args)}`));
},
/** Print success message */
success: (...args: unknown[]): void => {
core.info(`${ts()}» ${formatArgs(args)}`);
core.info(prefixLines(`${ts()}» ${formatArgs(args)}`));
},
/** Print debug message (only when debug mode is enabled) */
debug: (...args: unknown[]): void => {
if (isRunnerDebugEnabled()) {
core.debug(formatArgs(args));
core.debug(prefixLines(formatArgs(args)));
return;
}
if (isLocalDebugEnabled()) {
core.info(`${ts()}[DEBUG] ${formatArgs(args)}`);
core.info(prefixLines(`${ts()}[DEBUG] ${formatArgs(args)}`));
}
},