feat: auto-launch reflection via shared background task helper (#924)

This commit is contained in:
Charles Packer
2026-02-11 20:45:14 -08:00
committed by GitHub
parent 7f83035a09
commit 9630da190a
10 changed files with 750 additions and 275 deletions

View File

@@ -184,6 +184,7 @@ import {
parseMemoryPreference,
type ReflectionSettings,
reflectionSettingsToLegacyMode,
shouldFireStepCountTrigger,
} from "./helpers/memoryReminder";
import {
type QueuedMessage,
@@ -730,6 +731,19 @@ function formatReflectionSettings(settings: ReflectionSettings): string {
return `Step count (every ${settings.stepCount} turns, ${behaviorLabel})`;
}
const AUTO_REFLECTION_DESCRIPTION = "Reflect on recent conversations";
const AUTO_REFLECTION_PROMPT =
"Review recent conversation history and update memory files with important information worth preserving.";
function hasActiveReflectionSubagent(): boolean {
const snapshot = getSubagentSnapshot();
return snapshot.agents.some(
(agent) =>
agent.type.toLowerCase() === "reflection" &&
(agent.status === "pending" || agent.status === "running"),
);
}
function buildTextParts(
...parts: Array<string | undefined | null>
): Array<{ type: "text"; text: string }> {
@@ -7839,13 +7853,23 @@ ${SYSTEM_REMINDER_CLOSE}
bashCommandCacheRef.current = [];
}
// Build memory reminder if interval is set and we've reached the Nth turn
// When MemFS is enabled, this returns a reflection reminder instead
const memoryReminderContent = await buildMemoryReminder(
turnCountRef.current,
agentId,
);
const reflectionSettings = getReflectionSettings();
const memfsEnabledForAgent = settingsManager.isMemfsEnabled(agentId);
const shouldFireStepTrigger = shouldFireStepCountTrigger(
turnCountRef.current,
reflectionSettings,
);
let memoryReminderContent = "";
if (
shouldFireStepTrigger &&
(reflectionSettings.behavior === "reminder" || !memfsEnabledForAgent)
) {
// Step-count reminder mode (or non-memfs fallback)
memoryReminderContent = await buildMemoryReminder(
turnCountRef.current,
agentId,
);
}
// Increment turn count for next iteration
turnCountRef.current += 1;
@@ -7896,6 +7920,43 @@ ${SYSTEM_REMINDER_CLOSE}
if (!text) return;
reminderParts.push({ type: "text", text });
};
const maybeLaunchReflectionSubagent = async (
triggerSource: "step-count" | "compaction-event",
) => {
if (!memfsEnabledForAgent) {
return false;
}
if (hasActiveReflectionSubagent()) {
debugLog(
"memory",
`Skipping auto reflection launch (${triggerSource}) because one is already active`,
);
return false;
}
try {
const { spawnBackgroundSubagentTask } = await import(
"../tools/impl/Task"
);
spawnBackgroundSubagentTask({
subagentType: "reflection",
prompt: AUTO_REFLECTION_PROMPT,
description: AUTO_REFLECTION_DESCRIPTION,
});
debugLog(
"memory",
`Auto-launched reflection subagent (${triggerSource})`,
);
return true;
} catch (error) {
debugWarn(
"memory",
`Failed to auto-launch reflection subagent (${triggerSource}): ${
error instanceof Error ? error.message : String(error)
}`,
);
return false;
}
};
pushReminder(sessionContextReminder);
// Inject available skills as system-reminder (LET-7353)
@@ -7941,13 +8002,29 @@ ${SYSTEM_REMINDER_CLOSE}
pushReminder(userPromptSubmitHookFeedback);
pushReminder(memoryReminderContent);
// Consume compaction-triggered reflection/check reminder on next user turn.
// Step-count auto-launch mode: fire reflection in background on interval.
if (
shouldFireStepTrigger &&
reflectionSettings.trigger === "step-count" &&
reflectionSettings.behavior === "auto-launch"
) {
await maybeLaunchReflectionSubagent("step-count");
}
// Consume compaction-triggered reflection behavior on next user turn.
if (contextTrackerRef.current.pendingReflectionTrigger) {
contextTrackerRef.current.pendingReflectionTrigger = false;
if (reflectionSettings.trigger === "compaction-event") {
const compactionReminderContent =
await buildCompactionMemoryReminder(agentId);
pushReminder(compactionReminderContent);
if (
reflectionSettings.behavior === "auto-launch" &&
memfsEnabledForAgent
) {
await maybeLaunchReflectionSubagent("compaction-event");
} else {
const compactionReminderContent =
await buildCompactionMemoryReminder(agentId);
pushReminder(compactionReminderContent);
}
}
}

View File

@@ -77,6 +77,9 @@ const AgentRow = memo(
const isRunning = agent.status === "pending" || agent.status === "running";
const shouldDim = isRunning && !agent.isBackground;
const showStats = !(agent.isBackground && isRunning);
const hideBackgroundStatusLine =
agent.isBackground && isRunning && !agent.agentURL;
const stats = formatStats(
agent.toolCalls.length,
agent.totalTokens,
@@ -126,22 +129,24 @@ const AgentRow = memo(
</Text>
</Box>
{/* Simple status line */}
<Box flexDirection="row">
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" "}</Text>
{agent.status === "error" ? (
<Text color={colors.subagent.error}>Error</Text>
) : isComplete ? (
<Text dimColor>Done</Text>
) : agent.isBackground ? (
<Text dimColor>Running in the background</Text>
) : (
<Text dimColor>Running...</Text>
)}
</Box>
{!hideBackgroundStatusLine && (
<Box flexDirection="row">
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" "}</Text>
{agent.status === "error" ? (
<Text color={colors.subagent.error}>Error</Text>
) : isComplete ? (
<Text dimColor>Done</Text>
) : agent.isBackground ? (
<Text dimColor>Running in the background</Text>
) : (
<Text dimColor>Running...</Text>
)}
</Box>
)}
</Box>
);
}
@@ -177,10 +182,12 @@ const AgentRow = memo(
)}
</>
)}
<Text dimColor>
{" · "}
{stats}
</Text>
{showStats && (
<Text dimColor>
{" · "}
{stats}
</Text>
)}
</Text>
</Box>
@@ -215,61 +222,63 @@ const AgentRow = memo(
})}
{/* Status line */}
<Box flexDirection="row">
{agent.status === "completed" ? (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" Done"}</Text>
</>
) : agent.status === "error" ? (
<>
<Box width={gutterWidth} flexShrink={0}>
<Text>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
{!hideBackgroundStatusLine && (
<Box flexDirection="row">
{agent.status === "completed" ? (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" Done"}</Text>
</>
) : agent.status === "error" ? (
<>
<Box width={gutterWidth} flexShrink={0}>
<Text>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" "}</Text>
</Text>
<Text dimColor>{" "}</Text>
</Box>
<Box flexGrow={1} width={contentWidth}>
<Text wrap="wrap" color={colors.subagent.error}>
{agent.error}
</Text>
</Box>
</>
) : agent.isBackground ? (
<Text>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
</Box>
<Box flexGrow={1} width={contentWidth}>
<Text wrap="wrap" color={colors.subagent.error}>
{agent.error}
<Text dimColor>{" Running in the background"}</Text>
</Text>
) : lastTool ? (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
</Box>
</>
) : agent.isBackground ? (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" Running in the background"}</Text>
</>
) : lastTool ? (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>
{" "}
{lastTool.name}
</Text>
</>
) : (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" Starting..."}</Text>
</>
)}
</Box>
<Text dimColor>
{" "}
{lastTool.name}
</Text>
</>
) : (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" Starting..."}</Text>
</>
)}
</Box>
)}
</Box>
);
},

View File

@@ -62,6 +62,9 @@ const AgentRow = memo(({ agent, isLast }: AgentRowProps) => {
const isRunning = agent.status === "running";
const shouldDim = isRunning && !agent.isBackground;
const showStats = !(agent.isBackground && isRunning);
const hideBackgroundStatusLine =
agent.isBackground && isRunning && !agent.agentURL;
const stats = formatStats(agent.toolCount, agent.totalTokens, isRunning);
const modelDisplay = getSubagentModelDisplay(agent.model);
@@ -95,10 +98,12 @@ const AgentRow = memo(({ agent, isLast }: AgentRowProps) => {
)}
</>
)}
<Text dimColor>
{" · "}
{stats}
</Text>
{showStats && (
<Text dimColor>
{" · "}
{stats}
</Text>
)}
</Text>
</Box>
@@ -115,42 +120,44 @@ const AgentRow = memo(({ agent, isLast }: AgentRowProps) => {
)}
{/* Status line */}
<Box flexDirection="row">
{agent.status === "completed" && !agent.isBackground ? (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" Done"}</Text>
</>
) : agent.status === "error" ? (
<>
<Box width={gutterWidth} flexShrink={0}>
<Text>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
{!hideBackgroundStatusLine && (
<Box flexDirection="row">
{agent.status === "completed" && !agent.isBackground ? (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" Done"}</Text>
</>
) : agent.status === "error" ? (
<>
<Box width={gutterWidth} flexShrink={0}>
<Text>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" "}</Text>
</Text>
<Text dimColor>{" "}</Text>
</Box>
<Box flexGrow={1} width={contentWidth}>
<Text wrap="wrap" color={colors.subagent.error}>
{agent.error}
</Text>
</Box>
</>
) : (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
</Box>
<Box flexGrow={1} width={contentWidth}>
<Text wrap="wrap" color={colors.subagent.error}>
{agent.error}
</Text>
</Box>
</>
) : (
<>
<Text color={colors.subagent.treeChar}>
{" "}
{continueChar}
</Text>
<Text dimColor>{" Running in the background"}</Text>
</>
)}
</Box>
<Text dimColor>{" Running in the background"}</Text>
</>
)}
</Box>
)}
</Box>
);
});

View File

@@ -167,6 +167,17 @@ export function getMemoryReminderMode(): MemoryReminderMode {
return reflectionSettingsToLegacyMode(getReflectionSettings());
}
export function shouldFireStepCountTrigger(
turnCount: number,
settings: ReflectionSettings = getReflectionSettings(),
): boolean {
if (settings.trigger !== "step-count") {
return false;
}
const stepCount = normalizeStepCount(settings.stepCount, DEFAULT_STEP_COUNT);
return turnCount > 0 && turnCount % stepCount === 0;
}
async function buildMemfsAwareMemoryReminder(
agentId: string,
trigger: "interval" | "compaction",
@@ -221,12 +232,7 @@ export async function buildMemoryReminder(
return "";
}
if (
turnCount > 0 &&
turnCount %
normalizeStepCount(reflectionSettings.stepCount, DEFAULT_STEP_COUNT) ===
0
) {
if (shouldFireStepCountTrigger(turnCount, reflectionSettings)) {
debugLog(
"memory",
`Turn-based memory reminder fired (turn ${turnCount}, interval ${reflectionSettings.stepCount}, agent ${agentId})`,