feat: auto-launch reflection via shared background task helper (#924)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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})`,
|
||||
|
||||
Reference in New Issue
Block a user