Skip to content

Commit aa8c70a

Browse files
committed
fix(core): allow disabling title temperature
1 parent 69b78fd commit aa8c70a

7 files changed

Lines changed: 275 additions & 5 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@voltagent/core": patch
3+
---
4+
5+
fix(core): allow disabling conversation title temperature
6+
7+
Conversation title generation now keeps the existing default `temperature: 0`, while allowing `generateTitle.temperature: null` to omit the parameter for reasoning models that do not support temperature. Unsupported temperature warnings are surfaced at warn level with guidance, and title generation failures are logged at warn level instead of debug.

packages/core/src/agent/agent.spec.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1907,6 +1907,180 @@ Use pandas and summarize findings.`.split("\n"),
19071907
return [...messages].reverse().find((message) => message.role === "assistant");
19081908
};
19091909

1910+
it("should use temperature 0 by default when generating conversation titles", async () => {
1911+
const memory = new Memory({
1912+
storage: new InMemoryStorageAdapter(),
1913+
generateTitle: true,
1914+
});
1915+
const agent = new Agent({
1916+
name: "TestAgent",
1917+
instructions: "Test",
1918+
model: mockModel as any,
1919+
memory,
1920+
});
1921+
const span = {
1922+
end: vi.fn(),
1923+
setStatus: vi.fn(),
1924+
setAttribute: vi.fn(),
1925+
setAttributes: vi.fn(),
1926+
recordException: vi.fn(),
1927+
};
1928+
const context = {
1929+
operationId: "test-operation-id",
1930+
logger: {
1931+
debug: vi.fn(),
1932+
info: vi.fn(),
1933+
warn: vi.fn(),
1934+
error: vi.fn(),
1935+
trace: vi.fn(),
1936+
fatal: vi.fn(),
1937+
child: vi.fn().mockReturnThis(),
1938+
},
1939+
context: new Map(),
1940+
systemContext: new Map(),
1941+
isActive: true,
1942+
traceContext: {
1943+
createChildSpan: vi.fn().mockReturnValue(span),
1944+
withSpan: vi.fn().mockImplementation(async (_span, fn) => await fn()),
1945+
},
1946+
abortController: new AbortController(),
1947+
startTime: new Date(),
1948+
};
1949+
1950+
vi.mocked(ai.generateText).mockResolvedValue({
1951+
text: "Rome Weekend Plan",
1952+
content: [{ type: "text", text: "Rome Weekend Plan" }],
1953+
reasoning: [],
1954+
files: [],
1955+
sources: [],
1956+
toolCalls: [],
1957+
toolResults: [],
1958+
finishReason: "stop",
1959+
usage: providerUsage,
1960+
warnings: [
1961+
{
1962+
type: "unsupported",
1963+
feature: "temperature",
1964+
details: "temperature is not supported for reasoning models",
1965+
},
1966+
],
1967+
request: {},
1968+
response: {
1969+
id: "test-response",
1970+
modelId: "test-model",
1971+
timestamp: new Date(),
1972+
messages: createAssistantResponseMessages("Rome Weekend Plan"),
1973+
},
1974+
steps: [],
1975+
} as any);
1976+
1977+
const titleGenerator = (agent as any).createConversationTitleGenerator(memory);
1978+
const title = await titleGenerator({
1979+
input: "Plan a weekend trip to Rome.",
1980+
context,
1981+
defaultTitle: "Conversation",
1982+
});
1983+
1984+
expect(title).toBe("Rome Weekend Plan");
1985+
const generateTextCall = vi.mocked(ai.generateText).mock.calls[0][0] as Record<
1986+
string,
1987+
unknown
1988+
>;
1989+
expect(generateTextCall.temperature).toBe(0);
1990+
expect(generateTextCall.maxOutputTokens).toBe(32);
1991+
const createChildSpanCall = context.traceContext.createChildSpan.mock.calls[0][2] as {
1992+
attributes: Record<string, unknown>;
1993+
};
1994+
expect(createChildSpanCall.attributes["llm.temperature"]).toBe(0);
1995+
expect(context.logger.warn).toHaveBeenCalledWith(
1996+
"[Memory] Conversation title generation model does not support temperature",
1997+
expect.objectContaining({
1998+
hint: expect.stringContaining("generateTitle.temperature"),
1999+
warning: expect.stringContaining("temperature"),
2000+
}),
2001+
);
2002+
});
2003+
2004+
it("should omit temperature when title generation temperature is null", async () => {
2005+
const memory = new Memory({
2006+
storage: new InMemoryStorageAdapter(),
2007+
generateTitle: { enabled: true, temperature: null },
2008+
});
2009+
const agent = new Agent({
2010+
name: "TestAgent",
2011+
instructions: "Test",
2012+
model: mockModel as any,
2013+
memory,
2014+
});
2015+
const span = {
2016+
end: vi.fn(),
2017+
setStatus: vi.fn(),
2018+
setAttribute: vi.fn(),
2019+
setAttributes: vi.fn(),
2020+
recordException: vi.fn(),
2021+
};
2022+
const context = {
2023+
operationId: "test-operation-id",
2024+
logger: {
2025+
debug: vi.fn(),
2026+
info: vi.fn(),
2027+
warn: vi.fn(),
2028+
error: vi.fn(),
2029+
trace: vi.fn(),
2030+
fatal: vi.fn(),
2031+
child: vi.fn().mockReturnThis(),
2032+
},
2033+
context: new Map(),
2034+
systemContext: new Map(),
2035+
isActive: true,
2036+
traceContext: {
2037+
createChildSpan: vi.fn().mockReturnValue(span),
2038+
withSpan: vi.fn().mockImplementation(async (_span, fn) => await fn()),
2039+
},
2040+
abortController: new AbortController(),
2041+
startTime: new Date(),
2042+
};
2043+
2044+
vi.mocked(ai.generateText).mockResolvedValue({
2045+
text: "Rome Weekend Plan",
2046+
content: [{ type: "text", text: "Rome Weekend Plan" }],
2047+
reasoning: [],
2048+
files: [],
2049+
sources: [],
2050+
toolCalls: [],
2051+
toolResults: [],
2052+
finishReason: "stop",
2053+
usage: providerUsage,
2054+
warnings: [],
2055+
request: {},
2056+
response: {
2057+
id: "test-response",
2058+
modelId: "test-model",
2059+
timestamp: new Date(),
2060+
messages: createAssistantResponseMessages("Rome Weekend Plan"),
2061+
},
2062+
steps: [],
2063+
} as any);
2064+
2065+
const titleGenerator = (agent as any).createConversationTitleGenerator(memory);
2066+
const title = await titleGenerator({
2067+
input: "Plan a weekend trip to Rome.",
2068+
context,
2069+
defaultTitle: "Conversation",
2070+
});
2071+
2072+
expect(title).toBe("Rome Weekend Plan");
2073+
const generateTextCall = vi.mocked(ai.generateText).mock.calls[0][0] as Record<
2074+
string,
2075+
unknown
2076+
>;
2077+
expect(generateTextCall).not.toHaveProperty("temperature");
2078+
const createChildSpanCall = context.traceContext.createChildSpan.mock.calls[0][2] as {
2079+
attributes: Record<string, unknown>;
2080+
};
2081+
expect(createChildSpanCall.attributes).not.toHaveProperty("llm.temperature");
2082+
});
2083+
19102084
it("should initialize with memory", () => {
19112085
const memory = new Memory({
19122086
storage: new InMemoryStorageAdapter(),

packages/core/src/agent/agent.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,34 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
258258
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
259259
isRecord(value) && !Array.isArray(value);
260260

261+
const stringIncludesTemperature = (value: unknown): boolean =>
262+
typeof value === "string" && value.toLowerCase().includes("temperature");
263+
264+
const isTemperatureWarning = (warning: Warning): boolean => {
265+
const warningRecord: Record<string, unknown> = warning;
266+
const warningType = warningRecord.type;
267+
268+
if (typeof warningType !== "string") {
269+
return false;
270+
}
271+
272+
if (warningType === "unsupported-setting") {
273+
return stringIncludesTemperature(warningRecord.setting);
274+
}
275+
276+
if (warningType === "unsupported" || warningType === "compatibility") {
277+
return (
278+
stringIncludesTemperature(warningRecord.feature) ||
279+
stringIncludesTemperature(warningRecord.details)
280+
);
281+
}
282+
283+
return (
284+
stringIncludesTemperature(warningRecord.details) ||
285+
stringIncludesTemperature(warningRecord.message)
286+
);
287+
};
288+
261289
const hasNonEmptyString = (value: unknown): value is string =>
262290
typeof value === "string" && value.trim().length > 0;
263291

@@ -4721,6 +4749,12 @@ export class Agent {
47214749
typeof normalized.maxLength === "number" && Number.isFinite(normalized.maxLength)
47224750
? Math.max(1, normalized.maxLength)
47234751
: DEFAULT_CONVERSATION_TITLE_MAX_CHARS;
4752+
const temperature =
4753+
normalized.temperature === null
4754+
? undefined
4755+
: typeof normalized.temperature === "number" && Number.isFinite(normalized.temperature)
4756+
? normalized.temperature
4757+
: 0;
47244758

47254759
const modelOverride = normalized.model;
47264760

@@ -4751,7 +4785,7 @@ export class Agent {
47514785
isStreaming: false,
47524786
messages,
47534787
callOptions: {
4754-
temperature: 0,
4788+
...(temperature !== undefined ? { temperature } : {}),
47554789
maxOutputTokens,
47564790
},
47574791
label: "Generate Conversation Title",
@@ -4764,12 +4798,23 @@ export class Agent {
47644798
generateText({
47654799
model: resolvedModel,
47664800
messages,
4767-
temperature: 0,
4801+
...(temperature !== undefined ? { temperature } : {}),
47684802
maxOutputTokens,
47694803
abortSignal: context.abortController.signal,
47704804
}),
47714805
);
47724806

4807+
const temperatureWarning = result.warnings?.find(isTemperatureWarning);
4808+
if (temperatureWarning) {
4809+
context.logger.warn(
4810+
"[Memory] Conversation title generation model does not support temperature",
4811+
{
4812+
warning: safeStringify(temperatureWarning),
4813+
hint: "Set generateTitle.temperature to null to omit temperature for title generation.",
4814+
},
4815+
);
4816+
}
4817+
47734818
const resolvedUsage = result.usage ? await Promise.resolve(result.usage) : undefined;
47744819
const title = sanitizeConversationTitle(result.text ?? "", maxLength);
47754820
if (title) {
@@ -4787,8 +4832,10 @@ export class Agent {
47874832
throw error;
47884833
}
47894834
} catch (error) {
4790-
context.logger.debug("[Memory] Failed to generate conversation title", {
4835+
context.logger.warn("[Memory] Failed to generate conversation title", {
47914836
error: safeStringify(error),
4837+
message: error instanceof Error ? error.message : undefined,
4838+
hint: "If your title generation model does not support temperature, set generateTitle.temperature to null.",
47924839
});
47934840
return null;
47944841
}

packages/core/src/memory/manager/memory-manager.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,38 @@ describe("MemoryManager", () => {
128128
expect(titleGenerator).toHaveBeenCalledTimes(1);
129129
});
130130

131+
it("should warn when title generation fails", async () => {
132+
const context = createMockOperationContext();
133+
context.input = "Plan a weekend trip to Rome.";
134+
const warnSpy = vi.spyOn(context.logger, "warn");
135+
const titleGenerator = vi.fn().mockRejectedValue(new Error("Unsupported temperature"));
136+
const managerWithTitle = new MemoryManager(
137+
"agent-1",
138+
memory,
139+
{},
140+
getGlobalLogger().child({ test: true }),
141+
titleGenerator,
142+
);
143+
144+
const message = createTestUIMessage({
145+
id: "msg-1",
146+
role: "assistant",
147+
parts: [{ type: "text", text: "Sure, let's plan it." }],
148+
});
149+
150+
await managerWithTitle.saveMessage(context, message, "user-1", "conv-title-warning");
151+
152+
const conversation = await memory.getConversation("conv-title-warning");
153+
expect(conversation?.title).toBe("Conversation");
154+
expect(warnSpy).toHaveBeenCalledWith(
155+
"[Memory] Failed to generate conversation title",
156+
expect.objectContaining({
157+
message: "Unsupported temperature",
158+
hint: expect.stringContaining("generateTitle.temperature"),
159+
}),
160+
);
161+
});
162+
131163
it("should handle errors gracefully", async () => {
132164
// Create manager with mocked memory that throws error
133165
const errorMemory = new Memory({

packages/core/src/memory/manager/memory-manager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,8 +610,10 @@ export class MemoryManager {
610610
return title.trim();
611611
}
612612
} catch (error) {
613-
context.logger.debug("[Memory] Failed to generate conversation title", {
613+
context.logger.warn("[Memory] Failed to generate conversation title", {
614614
error: safeStringify(error),
615+
message: error instanceof Error ? error.message : undefined,
616+
hint: "If your title generation model does not support temperature, set generateTitle.temperature to null.",
615617
});
616618
}
617619

packages/core/src/memory/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export type MemoryOptions = {};
104104
export type ConversationTitleConfig = {
105105
enabled?: boolean;
106106
model?: AgentModelValue;
107+
temperature?: number | null;
107108
maxOutputTokens?: number;
108109
maxLength?: number;
109110
systemPrompt?: string | null;

website/docs/agents/memory/overview.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ const memory = new Memory({
4949
enabled: true,
5050
model: "gpt-4o-mini", // default agent model
5151
systemPrompt: "Generate a short title (max 6 words).",
52+
temperature: null, // optional; default is 0, use null to omit
5253
maxLength: 60,
53-
maxOutputTokens: 24,
54+
maxOutputTokens: 128, // optional; default is 32
5455
},
5556
});
5657
```
@@ -59,6 +60,12 @@ Notes:
5960

6061
- The agent's main model is used unless `generateTitle.model` is provided.
6162
- `generateTitle.model` accepts either a provider/model string or an AI SDK model instance.
63+
- Title generation sends `temperature: 0` by default. Set `generateTitle.temperature` to `null` to
64+
omit the parameter for reasoning models such as OpenAI `o`-series and `gpt-5-mini`.
65+
- Reasoning models may need a higher `maxOutputTokens` value because reasoning tokens count toward
66+
the output budget. Configure `generateTitle.maxOutputTokens` if the default `32` is too low.
67+
- If title generation fails, VoltAgent keeps the default conversation title and logs the cause at
68+
`warn` level, including a hint to set `generateTitle.temperature` to `null` when appropriate.
6269
- Only the first user message is summarized.
6370
- If you create conversations manually via the Memory API, set `title` explicitly.
6471

0 commit comments

Comments
 (0)