mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
fix(core): tell the model the date once, from the history (#16362)
* fix(core): tell the model the date once, from the history The date reminder was deduplicated by an in-memory field that startChat reset and that never knew about the date the startup context carries. Every session's first turn repeated the date with a note that the two might differ, a rewind after midnight could leave only the stale startup date, and cron turns never refreshed it. The client now finds the last date the history gives the model (the startup context, including the legacy prelude, or an earlier date-only reminder in either wording) and adds a reminder on user and cron turns only when that date is not today: "Today's date is X." when there is none, and a short "the date has changed" note otherwise. Closes #16359 * fix(core): read the announced date from a marker on the date reminder part The history scan matched date reminder text anywhere in a user part, so a reminder the user pasted or @-included could stand in for the client's own: a pasted block dated today suppressed the real date-change reminder, and an older one triggered a false "the date has changed". The date reminder part now carries its date as part metadata, which never reaches the model, and the scan reads only that marker, the startup context, and the pre-marker wording as a whole part. Tests cover pasted reminders, a saved-record round trip, and the reminder the client sent being read back on the next turn; the tool-response IDE test sends a ToolResult turn again and keeps its strict no-reminder assertion. * fix(core): tell the date after a resume, compaction or retry A startup context rebuilt in front of earlier turns (resume, compaction) gave today's date, so the client sent no reminder although those turns may have run under another date. The startup context now records its date as part metadata only when it starts the conversation; a rebuilt one records none, and the next turn tells the date with a note that earlier parts may come from another date. A refresh keeps the date the old startup context recorded. A retry tells the date again when the failed entry it drops had told it.
This commit is contained in:
1 parent
c0782a8cb6
commit
0750e8bd02
7 files changed
+647
-133
No files matched your search
@@ -562,7 +562,7 @@ The suite runs in the integration job. The nightly E2E run, and one started by h
|
||||
- a cron turn cancelled between rounds keeps the results of its last tool batch in history, as a prompt does, and a notification turn whose response is retried shows only the retried text;
|
||||
- a cron turn cancelled after its echo and before its first request keeps its prompt in history and in the record, as a cron turn cancelled while its request is going out does today;
|
||||
|
||||
The ACP parity suite covers the common cases unchanged and asserts the date reminder, UserPromptSubmit on tool results, cancelling while that hook runs, that a batch's approvals come before its calls run, and the plan-mode notice.
|
||||
The ACP parity suite covers the common cases unchanged and asserts that a first prompt carries no date reminder, UserPromptSubmit on tool results, cancelling while that hook runs, that a batch's approvals come before its calls run, and the plan-mode notice.
|
||||
|
||||
- With the setting on, the Ink TUI differs from today in these intended ways, as on the other surfaces:
|
||||
- from PR 3b-1, the skipped siblings of a plan-mode entry no longer count toward the session's completed tool calls;
|
||||
|
||||
@@ -142,7 +142,8 @@ const VOLATILE_KEYS = new Set([
|
||||
const CONTINUATION_PROMPT_ID =
|
||||
/(########\d+)_(?:stop_hook|todo_stop_guard)_\d+/g;
|
||||
|
||||
const DATE_REMINDER = '<system-reminder>\nThe current date is:';
|
||||
const DATE_REMINDER =
|
||||
/^<system-reminder>\n(?:Today's date is |The date has changed since earlier in this conversation\. )/;
|
||||
|
||||
const toolResults = (messages: JsonObject[]) =>
|
||||
messages.filter((message) => message['role'] === 'tool').length;
|
||||
@@ -156,7 +157,7 @@ const userTurns = (messages: JsonObject[]) =>
|
||||
|
||||
const isDateReminder = (part: unknown) =>
|
||||
typeof (part as JsonObject)?.['text'] === 'string' &&
|
||||
((part as JsonObject)['text'] as string).startsWith(DATE_REMINDER);
|
||||
DATE_REMINDER.test((part as JsonObject)['text'] as string);
|
||||
|
||||
/** Removes the date reminder the client adds when the date changed. */
|
||||
function withoutDateReminder(content: unknown): unknown {
|
||||
@@ -1074,7 +1075,9 @@ const allowOnce = () => ({
|
||||
expect(written).toEqual([false, false]);
|
||||
}, 120_000);
|
||||
|
||||
it('sends the date reminder with a prompt, as other surfaces do', async () => {
|
||||
it('adds no date reminder to a first prompt, as other surfaces do', async () => {
|
||||
// The startup context already carries today's date; the client adds a
|
||||
// reminder only once the date changes.
|
||||
const { current, shared } = await both({
|
||||
name: 'date reminder',
|
||||
prompts: ['Say hello.'],
|
||||
@@ -1082,7 +1085,7 @@ const allowOnce = () => ({
|
||||
});
|
||||
|
||||
expect(current.dateReminders).toBe(0);
|
||||
expect(shared.dateReminders).toBe(1);
|
||||
expect(shared.dateReminders).toBe(0);
|
||||
}, 120_000);
|
||||
},
|
||||
);
|
||||
@@ -149,7 +149,8 @@ const VOLATILE_KEYS = new Set([
|
||||
// Timings, such as the telemetry recorded with each response.
|
||||
const TIMING_KEY = /(_ms|Ms)$/;
|
||||
|
||||
const DATE_REMINDER = '<system-reminder>\nThe current date is:';
|
||||
const DATE_REMINDER =
|
||||
/^<system-reminder>\n(?:Today's date is |The date has changed since earlier in this conversation\. )/;
|
||||
|
||||
const toolResults = (messages: JsonObject[]) =>
|
||||
messages.filter((message) => message['role'] === 'tool').length;
|
||||
@@ -163,7 +164,7 @@ const userTurns = (messages: JsonObject[]) =>
|
||||
|
||||
const isDateReminder = (part: unknown) =>
|
||||
typeof (part as JsonObject)?.['text'] === 'string' &&
|
||||
((part as JsonObject)['text'] as string).startsWith(DATE_REMINDER);
|
||||
DATE_REMINDER.test((part as JsonObject)['text'] as string);
|
||||
|
||||
/** `dir` is the workspace; `scratch`, which holds it, is the run's home. */
|
||||
function normalize(value: unknown, dir: string, scratch: string): unknown {
|
||||
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
Part,
|
||||
PartListUnion,
|
||||
} from '@google/genai';
|
||||
import { createUserContent } from '@google/genai';
|
||||
import {
|
||||
LlmClient,
|
||||
SendMessageType,
|
||||
@@ -128,7 +129,9 @@ import {
|
||||
buildChangedAgentsReminder,
|
||||
buildChangedMcpToolsReminder,
|
||||
buildChangedSkillsReminder,
|
||||
buildDateReminder,
|
||||
buildMcpServerInstructionsReminderFromEntries,
|
||||
DATE_REMINDER_METADATA,
|
||||
getDirectoryContextString,
|
||||
getInitialChatHistory,
|
||||
wrapSystemReminder,
|
||||
@@ -527,9 +530,15 @@ const ideContext = (openFiles: IdeOpenFile[]) => ({
|
||||
});
|
||||
|
||||
const dateReminderRe = (date = '') =>
|
||||
new RegExp(`^<system-reminder>\\nThe current date is:.*${date}`);
|
||||
/** Matches the date system-reminder that opens a user turn. */
|
||||
const dateReminder = (date = '') => expect.stringMatching(dateReminderRe(date));
|
||||
new RegExp(
|
||||
`^<system-reminder>\\n(?:Today's date is|The date has changed since earlier in this conversation\\. Today's date is now) [^\\n]*${date}`,
|
||||
);
|
||||
/** Matches the date system-reminder part that opens a user turn. */
|
||||
const dateReminder = (date = '') =>
|
||||
expect.objectContaining({
|
||||
text: expect.stringMatching(dateReminderRe(date)),
|
||||
partMetadata: { [DATE_REMINDER_METADATA]: expect.any(String) },
|
||||
});
|
||||
|
||||
/** turn.run args whose request contains every one of `parts`. */
|
||||
const requestWith = (...parts: unknown[]) =>
|
||||
@@ -2386,6 +2395,48 @@ describe('Gemini Client (client.ts)', () => {
|
||||
const reinstalled = vi.mocked(setHistory).mock.calls[0]![0] as Content[];
|
||||
expect(findApiHistoryPromptIndex(reinstalled, 'S########1')).toBe(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['the date the old one recorded', 'Thursday, June 4, 2026'],
|
||||
['no date when the old one recorded none', undefined],
|
||||
])(
|
||||
'records on the rebuilt startup context %s, for the turns it keeps',
|
||||
async (_title, date) => {
|
||||
const oldPrelude: Content = {
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
text: '<system-reminder>\nold prelude\n</system-reminder>',
|
||||
...(date && { partMetadata: { [DATE_REMINDER_METADATA]: date } }),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await refreshOver(
|
||||
[oldPrelude, userText('hello'), modelText('hi')],
|
||||
[userText('<system-reminder>\nfresh prelude\n</system-reminder>')],
|
||||
);
|
||||
|
||||
expect(getInitialChatHistory).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
undefined,
|
||||
expect.objectContaining({ announcedDate: date ?? null }),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('lets a startup context with no turns after it record its own date', async () => {
|
||||
await refreshOver(
|
||||
[userText('<system-reminder>\nold prelude\n</system-reminder>')],
|
||||
[userText('<system-reminder>\nfresh prelude\n</system-reminder>')],
|
||||
);
|
||||
|
||||
expect(getInitialChatHistory).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
undefined,
|
||||
expect.objectContaining({ announcedDate: undefined }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreStartupContextAfterCompaction', () => {
|
||||
@@ -2424,6 +2475,12 @@ describe('Gemini Client (client.ts)', () => {
|
||||
.calls[0]![0] as Content[];
|
||||
expect(reinstalled[0]).toEqual(prelude);
|
||||
expect(findApiHistoryPromptIndex(reinstalled, 'S########1')).toBe(1);
|
||||
// The summary and the kept turns may come from another date.
|
||||
expect(getInitialChatHistory).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
undefined,
|
||||
expect.objectContaining({ announcedDate: null }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4299,12 +4356,6 @@ describe('Gemini Client (client.ts)', () => {
|
||||
expect(hookSystem.fireSessionStartEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should reset lastInjectedDate', async () => {
|
||||
client['lastInjectedDate'] = 'Friday, June 5, 2026';
|
||||
await client.resetChat();
|
||||
expect(client['lastInjectedDate']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('resets Hook microcompaction checkpoint', async () => {
|
||||
client['lastHookMicrocompactionTimestamp'] = Date.now();
|
||||
|
||||
@@ -10474,27 +10525,198 @@ Other open files:
|
||||
expect(mockMemoryManager.recall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
/** Clears the injected date, pins the clock and stubs a replying chat. */
|
||||
const dateSession = (iso: string, reply = 'Hello') => {
|
||||
client['lastInjectedDate'] = undefined;
|
||||
/** Pins the clock and stubs a replying chat over `history`. */
|
||||
const dateSession = (iso: string, history: Content[] = []) => {
|
||||
vi.setSystemTime(new Date(iso));
|
||||
mockTurnRunFn.mockReturnValue(textTurn(reply));
|
||||
return installChat();
|
||||
mockTurnRunFn.mockReturnValue(textTurn('Hello'));
|
||||
return installChat({ getHistory: vi.fn().mockReturnValue(history) });
|
||||
};
|
||||
// A startup context that starts the conversation records its date; one
|
||||
// rebuilt in front of earlier turns (resume, compaction) records none.
|
||||
const startupText = (date: string) =>
|
||||
wrapSystemReminder(
|
||||
`This is the Qwen Code. We are setting up the context for our chat.\nToday's date is ${date}.\nMy operating system is: linux`,
|
||||
);
|
||||
const startupContext = (date: string): Content => ({
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
text: startupText(date),
|
||||
partMetadata: { [DATE_REMINDER_METADATA]: date },
|
||||
},
|
||||
],
|
||||
});
|
||||
const rebuiltStartupContext = (date: string) => userText(startupText(date));
|
||||
const dateReminderTurn = (reminder: string | Part, prompt: string) => ({
|
||||
role: 'user' as const,
|
||||
parts: [
|
||||
typeof reminder === 'string' ? { text: reminder } : reminder,
|
||||
{ text: prompt },
|
||||
],
|
||||
});
|
||||
const PLAIN = (date: string) => buildDateReminder([], date)!;
|
||||
const CHANGED = (date: string) =>
|
||||
buildDateReminder([startupContext('Monday, June 1, 2026')], date)!;
|
||||
const UNVERIFIED = (date: string) =>
|
||||
buildDateReminder(
|
||||
[rebuiltStartupContext(date), userText('Earlier')],
|
||||
date,
|
||||
)!;
|
||||
|
||||
it('should inject the current date on every UserQuery turn', async () => {
|
||||
it('tells the date when the history carries none', async () => {
|
||||
dateSession('2026-06-05T12:00:00Z');
|
||||
|
||||
await run([{ text: 'What day is it?' }], 'prompt-id-date-inject');
|
||||
|
||||
// The date reminder, wrapped in <system-reminder> tags, comes first.
|
||||
expect(mockTurnRunFn).toHaveBeenCalledWith(
|
||||
...exactRequest(dateReminder('June 5, 2026'), {
|
||||
...exactRequest(PLAIN('Friday, June 5, 2026'), {
|
||||
text: 'What day is it?',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['the startup context', [startupContext('Friday, June 5, 2026')]],
|
||||
[
|
||||
'an earlier date reminder',
|
||||
[
|
||||
startupContext('Thursday, June 4, 2026'),
|
||||
dateReminderTurn(CHANGED('Friday, June 5, 2026'), 'Earlier'),
|
||||
modelText('Hello'),
|
||||
],
|
||||
],
|
||||
])(
|
||||
'adds no date when %s already carries today',
|
||||
async (_title, history) => {
|
||||
dateSession('2026-06-05T12:00:00Z', history);
|
||||
|
||||
await run([{ text: 'Question' }], 'prompt-id-date-current');
|
||||
|
||||
expect(mockTurnRunFn).toHaveBeenCalledWith(
|
||||
...exactRequest({ text: 'Question' }),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
// Past midnight, or after a rewind dropped the reminder that
|
||||
// followed the startup context.
|
||||
'the startup context is from an earlier day',
|
||||
[startupContext('Thursday, June 4, 2026'), modelText('Hello')],
|
||||
],
|
||||
[
|
||||
// A compressed record keeps an older reminder; it is the last date
|
||||
// the model read.
|
||||
'a later entry carries an older date',
|
||||
[
|
||||
rebuiltStartupContext('Friday, June 5, 2026'),
|
||||
dateReminderTurn(
|
||||
'<system-reminder>\nThe current date is: Thursday, June 4, 2026. Note: This is the authoritative current date — it may differ from the "Today\'s date" mentioned earlier in the conversation startup context.\n</system-reminder>',
|
||||
'Kept turn',
|
||||
),
|
||||
modelText('Hello'),
|
||||
],
|
||||
],
|
||||
])('says the date has changed when %s', async (_title, history) => {
|
||||
dateSession('2026-06-05T12:00:00Z', history);
|
||||
|
||||
await run([{ text: 'Question' }], 'prompt-id-date-changed');
|
||||
|
||||
expect(mockTurnRunFn).toHaveBeenCalledWith(
|
||||
...exactRequest(CHANGED('Friday, June 5, 2026'), { text: 'Question' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('tells the date after a resume rebuilt the startup context', async () => {
|
||||
// The rebuilt startup context gives today, but the resumed turns ran
|
||||
// under a date the history does not record.
|
||||
dateSession('2026-06-05T12:00:00Z', [
|
||||
rebuiltStartupContext('Friday, June 5, 2026'),
|
||||
userText('What is the date today?'),
|
||||
modelText('Thursday, June 4, 2026.'),
|
||||
]);
|
||||
|
||||
await run([{ text: 'Question' }], 'prompt-id-date-resumed');
|
||||
|
||||
expect(mockTurnRunFn).toHaveBeenCalledWith(
|
||||
...exactRequest(UNVERIFIED('Friday, June 5, 2026'), {
|
||||
text: 'Question',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['as plain text', "Today's date is Friday, June 5, 2026."],
|
||||
[
|
||||
'inside a pasted date reminder',
|
||||
`Log:\n${PLAIN('Friday, June 5, 2026').text}\nWhat happened?`,
|
||||
],
|
||||
])('does not read a date the user wrote %s', async (_title, text) => {
|
||||
dateSession('2026-06-05T12:00:00Z', [
|
||||
startupContext('Thursday, June 4, 2026'),
|
||||
userText(text),
|
||||
modelText('Noted'),
|
||||
]);
|
||||
|
||||
await run([{ text: 'Question' }], 'prompt-id-date-user-text');
|
||||
|
||||
expect(mockTurnRunFn).toHaveBeenCalledWith(
|
||||
...exactRequest(CHANGED('Friday, June 5, 2026'), { text: 'Question' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('reads back the reminder it sent on the next turn', async () => {
|
||||
// The history keeps the request as the chat stores it: one user entry
|
||||
// built from the request parts, reminder parts included.
|
||||
const history: Content[] = [startupContext('Thursday, June 4, 2026')];
|
||||
dateSession('2026-06-05T12:00:00Z', history);
|
||||
|
||||
await run([{ text: 'First' }], 'prompt-id-date-first');
|
||||
history.push(
|
||||
createUserContent(mockTurnRunFn.mock.calls[0]![1] as PartListUnion),
|
||||
modelText('Hello'),
|
||||
);
|
||||
await run([{ text: 'Second' }], 'prompt-id-date-second');
|
||||
|
||||
expect(mockTurnRunFn.mock.calls[0]![1]).toEqual([
|
||||
CHANGED('Friday, June 5, 2026'),
|
||||
{ text: 'First' },
|
||||
]);
|
||||
expect(mockTurnRunFn).toHaveBeenLastCalledWith(
|
||||
...exactRequest({ text: 'Second' }),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
'tells the date again when the failed entry told it',
|
||||
[dateReminderTurn(CHANGED('Friday, June 5, 2026'), 'Question')],
|
||||
[CHANGED('Friday, June 5, 2026'), { text: 'Question' }],
|
||||
],
|
||||
[
|
||||
'adds no date when the failed entry told none',
|
||||
[userText('Question')],
|
||||
[{ text: 'Question' }],
|
||||
],
|
||||
])('on a retry, %s', async (_title, stripped, expected) => {
|
||||
dateSession('2026-06-05T12:00:00Z');
|
||||
installChat({
|
||||
// The strip already dropped the failed entry from the history.
|
||||
getHistory: vi
|
||||
.fn()
|
||||
.mockReturnValue([startupContext('Thursday, June 4, 2026')]),
|
||||
getHistoryLength: vi.fn().mockReturnValue(1),
|
||||
stripOrphanedUserEntriesFromHistory: vi.fn().mockReturnValue(stripped),
|
||||
});
|
||||
|
||||
await run([{ text: 'Question' }], 'prompt-id-date-retry', {
|
||||
type: SendMessageType.Retry,
|
||||
});
|
||||
|
||||
expect(mockTurnRunFn.mock.calls[0]![1]).toEqual(expected);
|
||||
});
|
||||
|
||||
describe('output style turn reminder', () => {
|
||||
const stubOutputStyle = (
|
||||
name: Parameters<typeof getBuiltInOutputStyle>[0],
|
||||
@@ -10713,79 +10935,30 @@ Other open files:
|
||||
},
|
||||
);
|
||||
|
||||
it('should not inject duplicate date on the same day', async () => {
|
||||
const mockChat = dateSession('2026-06-05T12:00:00Z');
|
||||
|
||||
// First query on June 5 — should inject date
|
||||
await run([{ text: 'First question' }], 'prompt-id-date-first');
|
||||
|
||||
expect(mockTurnRunFn).toHaveBeenLastCalledWith(
|
||||
...exactRequest(dateReminder('June 5, 2026'), {
|
||||
text: 'First question',
|
||||
}),
|
||||
);
|
||||
|
||||
// Second query the same day: no date prefix again.
|
||||
mockTurnRunFn.mockReturnValue(textTurn('World'));
|
||||
mockChat.getHistory = vi
|
||||
.fn()
|
||||
.mockReturnValue([userText('First question'), modelText('Hello')]);
|
||||
|
||||
await run([{ text: 'Second question' }], 'prompt-id-date-second');
|
||||
|
||||
const secondCall = mockTurnRunFn.mock.calls[1];
|
||||
expect(secondCall[1][0]).toEqual({ text: 'Second question' });
|
||||
});
|
||||
|
||||
it('should re-inject date when session spans midnight', async () => {
|
||||
const mockChat = dateSession('2026-06-04T12:00:00Z');
|
||||
|
||||
// First query on June 4 — should inject date
|
||||
await run([{ text: 'Day one' }], 'prompt-id-date-day-one');
|
||||
|
||||
expect(mockTurnRunFn).toHaveBeenLastCalledWith(
|
||||
...exactRequest(dateReminder('June 4, 2026'), { text: 'Day one' }),
|
||||
);
|
||||
|
||||
// Advance to June 5: the new date is injected.
|
||||
vi.setSystemTime(new Date('2026-06-05T12:00:00Z'));
|
||||
|
||||
mockTurnRunFn.mockReturnValue(textTurn('New day'));
|
||||
mockChat.getHistory = vi
|
||||
.fn()
|
||||
.mockReturnValue([userText('Day one'), modelText('Hello')]);
|
||||
|
||||
await run([{ text: 'Day two' }], 'prompt-id-date-day-two');
|
||||
|
||||
const secondCall = mockTurnRunFn.mock.calls[1];
|
||||
expect(secondCall[1][0]).toMatch(dateReminderRe('June 5, 2026'));
|
||||
});
|
||||
|
||||
it('should not inject date on Cron turns', async () => {
|
||||
const mockChat = dateSession('2026-06-05T12:00:00Z', 'Cron response');
|
||||
it('tells the date on a Cron turn past midnight', async () => {
|
||||
dateSession('2026-06-05T12:00:00Z', [
|
||||
startupContext('Thursday, June 4, 2026'),
|
||||
modelText('Hello'),
|
||||
]);
|
||||
|
||||
await run([{ text: 'cron-task' }], 'prompt-id-cron', {
|
||||
type: SendMessageType.Cron,
|
||||
});
|
||||
|
||||
// The date must be absent; other system reminders (e.g. PlanMode) may
|
||||
// be included, so check for the date reminder specifically.
|
||||
const cronCall = mockTurnRunFn.mock.calls[0];
|
||||
const cronRequest = cronCall[1].join('\n');
|
||||
expect(cronRequest).not.toContain(
|
||||
'<system-reminder>\nThe current date is:',
|
||||
expect(mockTurnRunFn.mock.calls[0]![1]).toContainEqual(
|
||||
CHANGED('Friday, June 5, 2026'),
|
||||
);
|
||||
});
|
||||
|
||||
// UserQuery after Cron should still inject date normally
|
||||
client['lastInjectedDate'] = undefined;
|
||||
mockChat.getHistory = vi.fn().mockReturnValue([]);
|
||||
mockTurnRunFn.mockReturnValue(textTurn('Hello'));
|
||||
await run([{ text: 'User question' }], 'prompt-id-cron-user');
|
||||
it('adds no date on a tool-result turn', async () => {
|
||||
dateSession('2026-06-05T12:00:00Z');
|
||||
|
||||
expect(mockTurnRunFn).toHaveBeenLastCalledWith(
|
||||
...exactRequest(dateReminder('June 5, 2026'), {
|
||||
text: 'User question',
|
||||
}),
|
||||
await run([fnResponse('read_file', { ok: true })], 'prompt-id-tool', {
|
||||
type: SendMessageType.ToolResult,
|
||||
});
|
||||
|
||||
expect(JSON.stringify(mockTurnRunFn.mock.calls[0]![1])).not.toContain(
|
||||
"Today's date",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11417,6 +11590,7 @@ Other open files:
|
||||
run(
|
||||
[fnResponse('some_tool', { success: true })],
|
||||
'prompt-id-tool-response',
|
||||
{ type: SendMessageType.ToolResult },
|
||||
);
|
||||
const editorContextAdded = expect.objectContaining({
|
||||
parts: expect.arrayContaining([
|
||||
|
||||
@@ -176,7 +176,9 @@ import {
|
||||
// Utilities
|
||||
import {
|
||||
AUTO_MEMORY_SNAPSHOT_METADATA,
|
||||
formatDateForContext,
|
||||
buildDateReminder,
|
||||
DATE_REMINDER_METADATA,
|
||||
findLastAnnouncedDate,
|
||||
buildChangedAgentsReminder,
|
||||
buildChangedMcpToolsReminder,
|
||||
buildChangedSkillsReminder,
|
||||
@@ -727,14 +729,6 @@ export class LlmClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the most recently injected date string to prevent injecting
|
||||
* duplicate or conflicting dates when a session spans midnight.
|
||||
* Only UserQuery turns inject dates; Cron/ToolResult turns reuse the
|
||||
* startup-context date which is still current within the same session.
|
||||
*/
|
||||
private lastInjectedDate: string | undefined;
|
||||
|
||||
/**
|
||||
* Promises for pending background memory tasks (dream / extract / skill review).
|
||||
* Each promise resolves with a count of memory files touched (0 = nothing written).
|
||||
@@ -2283,10 +2277,18 @@ export class LlmClient {
|
||||
// it…")] pair (getStartupContextLength === 2), so slice(1) would leave
|
||||
// the orphaned model-ack entry behind when re-prepending the prelude.
|
||||
const remaining = currentHistory.slice(startupLength);
|
||||
// The turns kept ran under the date the old startup context recorded.
|
||||
const [[startupContext], snapshotEntries] = await getInitialChatHistory(
|
||||
this.config,
|
||||
undefined,
|
||||
{ gitStatus: this.getCachedGitStatus() },
|
||||
{
|
||||
gitStatus: this.getCachedGitStatus(),
|
||||
announcedDate:
|
||||
remaining.length > 0
|
||||
? (findLastAnnouncedDate(currentHistory.slice(0, startupLength)) ??
|
||||
null)
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
this.seedSkillReminderDedupFromSnapshot(snapshotEntries);
|
||||
await this.seedAgentReminderDedupFromCurrent();
|
||||
@@ -2323,10 +2325,11 @@ export class LlmClient {
|
||||
return;
|
||||
}
|
||||
|
||||
// The summary and the turns kept may come from another date.
|
||||
const [[startupContext], snapshotEntries] = await getInitialChatHistory(
|
||||
this.config,
|
||||
undefined,
|
||||
{ gitStatus: this.getCachedGitStatus() },
|
||||
{ gitStatus: this.getCachedGitStatus(), announcedDate: null },
|
||||
);
|
||||
this.seedSkillReminderDedupFromSnapshot(snapshotEntries);
|
||||
await this.seedAgentReminderDedupFromCurrent();
|
||||
@@ -2912,7 +2915,6 @@ export class LlmClient {
|
||||
this.lastDeliveredMemoryTreeRevision = undefined;
|
||||
this.trustedUserAnswers.clear();
|
||||
this.forceFullIdeContext = true;
|
||||
this.lastInjectedDate = undefined;
|
||||
// Clear stale cache params on session reset to prevent cross-session leakage
|
||||
clearCacheSafeParams();
|
||||
|
||||
@@ -5082,21 +5084,16 @@ export class LlmClient {
|
||||
systemReminders.push(PAUSED_GOAL_SYSTEM_REMINDER);
|
||||
}
|
||||
|
||||
// Inject fresh date on UserQuery turns only; Cron and ToolResult turns
|
||||
// reuse the same session and the startup-context date is still current.
|
||||
if (messageType === SendMessageType.UserQuery) {
|
||||
const today = formatDateForContext();
|
||||
|
||||
// Only inject if the date has changed since the last injection.
|
||||
// This prevents accumulating conflicting dates when a session
|
||||
// spans midnight.
|
||||
if (today !== this.lastInjectedDate) {
|
||||
systemReminders.push(
|
||||
`<system-reminder>\nThe current date is: ${today}. Note: This is the authoritative current date — it may differ from the "Today's date" mentioned earlier in the conversation startup context.\n</system-reminder>`,
|
||||
);
|
||||
this.lastInjectedDate = today;
|
||||
}
|
||||
}
|
||||
// Tell the model today's date only when the last date the history
|
||||
// gives it is a different one, or there is none. The startup context
|
||||
// that starts a conversation carries the date, so the first turn adds
|
||||
// nothing; a reminder follows once the date changes, after a session
|
||||
// runs past midnight, a rewind drops the reminder, or a resume or
|
||||
// compaction rebuilds the startup context in front of earlier turns.
|
||||
// The reminder part carries its date as part metadata, which the next
|
||||
// turn reads back.
|
||||
const dateReminder = buildDateReminder(this.getHistoryShallow());
|
||||
if (dateReminder) systemReminders.push(dateReminder);
|
||||
|
||||
// add plan mode system reminder if approval mode is plan
|
||||
if (this.config.getApprovalMode() === ApprovalMode.PLAN) {
|
||||
@@ -5192,6 +5189,24 @@ export class LlmClient {
|
||||
requestToSend = [...systemReminders, ...requestToSend];
|
||||
}
|
||||
|
||||
// A retry pops the failed entry and resends the parts the caller gave,
|
||||
// without the date reminder the client added to that entry. Tell the
|
||||
// date again, as the history now lacks it.
|
||||
if (messageType === SendMessageType.Retry) {
|
||||
const toldDate = (part: unknown) =>
|
||||
typeof part === 'object' &&
|
||||
part !== null &&
|
||||
typeof (part as Part).partMetadata?.[DATE_REMINDER_METADATA] ===
|
||||
'string';
|
||||
if (
|
||||
strippedRetryEntries.some((entry) => entry.parts?.some(toldDate)) &&
|
||||
!requestToSend.some(toldDate)
|
||||
) {
|
||||
const dateReminder = buildDateReminder(this.getHistoryShallow());
|
||||
if (dateReminder) requestToSend = [dateReminder, ...requestToSend];
|
||||
}
|
||||
}
|
||||
|
||||
if (!signal.aborted) {
|
||||
const workflowIntent = resolveWorkflowIntent(this.config, humanInput);
|
||||
const preparedWorkflow =
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { createUserContent, type Content } from '@google/genai';
|
||||
import { createUserContent, type Content, type Part } from '@google/genai';
|
||||
import {
|
||||
buildAddedMcpToolsReminder,
|
||||
buildAddedAgentsReminder,
|
||||
@@ -17,6 +17,9 @@ import {
|
||||
buildChangedAgentsReminder,
|
||||
buildChangedMcpToolsReminder,
|
||||
buildChangedSkillsReminder,
|
||||
buildDateReminder,
|
||||
DATE_REMINDER_METADATA,
|
||||
findLastAnnouncedDate,
|
||||
getEnvironmentContext,
|
||||
getDirectoryContextString,
|
||||
getInitialChatHistory,
|
||||
@@ -185,6 +188,226 @@ describe('getDirectoryContextString', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('date announcements', () => {
|
||||
const user = (...parts: Array<string | Part>): Content => ({
|
||||
role: 'user',
|
||||
parts: parts.map((part) =>
|
||||
typeof part === 'string' ? { text: part } : part,
|
||||
),
|
||||
});
|
||||
const model = (text: string): Content => ({
|
||||
role: 'model',
|
||||
parts: [{ text }],
|
||||
});
|
||||
const startupText = (date: string) =>
|
||||
wrapSystemReminder(
|
||||
`This is the Qwen Code. We are setting up the context for our chat.\nToday's date is ${date}.\nMy operating system is: linux`,
|
||||
);
|
||||
// A startup context that starts the conversation records its date.
|
||||
const startup = (date: string) =>
|
||||
user({
|
||||
text: startupText(date),
|
||||
partMetadata: { [DATE_REMINDER_METADATA]: date },
|
||||
});
|
||||
// A startup context rebuilt in front of earlier turns records none.
|
||||
const rebuiltStartup = (date: string) => user(startupText(date));
|
||||
const reminder = (date: string) => buildDateReminder([], date)!;
|
||||
const legacyReminder = (date: string) =>
|
||||
`<system-reminder>\nThe current date is: ${date}. Note: This is the authoritative current date — it may differ from the "Today's date" mentioned earlier in the conversation startup context.\n</system-reminder>`;
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('findLastAnnouncedDate', () => {
|
||||
it('reads the date a startup context records', () => {
|
||||
expect(findLastAnnouncedDate([startup('Thursday, June 4, 2026')])).toBe(
|
||||
'Thursday, June 4, 2026',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a rebuilt startup context', rebuiltStartup('Friday, June 5, 2026')],
|
||||
[
|
||||
'a legacy prelude without the reminder wrapper',
|
||||
user(
|
||||
"This is the Qwen Code. We are setting up the context for our chat.\nToday's date is Friday, June 5, 2026.\nMy operating system is: linux",
|
||||
),
|
||||
],
|
||||
])('reads no date from %s', (_title, entry) => {
|
||||
expect(
|
||||
findLastAnnouncedDate([
|
||||
entry,
|
||||
model('Got it. Thanks for the context!'),
|
||||
]),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a marked reminder', reminder('Friday, June 5, 2026')],
|
||||
[
|
||||
'a reminder from a saved record',
|
||||
JSON.parse(JSON.stringify(reminder('Friday, June 5, 2026'))) as Part,
|
||||
],
|
||||
['the wording recorded before', legacyReminder('Friday, June 5, 2026')],
|
||||
])('reads %s after the startup context', (_title, part) => {
|
||||
const history = [
|
||||
startup('Thursday, June 4, 2026'),
|
||||
user(part, 'Question'),
|
||||
model('Answer'),
|
||||
];
|
||||
|
||||
expect(findLastAnnouncedDate(history)).toBe('Friday, June 5, 2026');
|
||||
});
|
||||
|
||||
it('returns the latest of several reminders', () => {
|
||||
const history = [
|
||||
startup('Wednesday, June 3, 2026'),
|
||||
user(reminder('Thursday, June 4, 2026'), 'Day two'),
|
||||
model('Answer'),
|
||||
user(reminder('Friday, June 5, 2026'), 'Day three'),
|
||||
];
|
||||
|
||||
expect(findLastAnnouncedDate(history)).toBe('Friday, June 5, 2026');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['plain user text', "Today's date is Friday, June 5, 2026."],
|
||||
[
|
||||
'a reminder with other content',
|
||||
wrapSystemReminder("Note.\nToday's date is Friday, June 5, 2026."),
|
||||
],
|
||||
[
|
||||
'a pasted date reminder',
|
||||
`Log:\n${reminder('Friday, June 5, 2026').text}\nWhat happened?`,
|
||||
],
|
||||
[
|
||||
'a whole part copying the date reminder text',
|
||||
reminder('Friday, June 5, 2026').text!,
|
||||
],
|
||||
['a copied startup context', startupText('Friday, June 5, 2026')],
|
||||
[
|
||||
'a pasted reminder of the wording recorded before',
|
||||
`Log:\n${legacyReminder('Friday, June 5, 2026')}`,
|
||||
],
|
||||
])('ignores a date in %s', (_title, text) => {
|
||||
const history = [startup('Thursday, June 4, 2026'), user(text)];
|
||||
|
||||
expect(findLastAnnouncedDate(history)).toBe('Thursday, June 4, 2026');
|
||||
});
|
||||
|
||||
it('ignores model text and returns undefined with no announced date', () => {
|
||||
expect(
|
||||
findLastAnnouncedDate([
|
||||
user('Hi'),
|
||||
{ role: 'model', parts: [reminder('Friday, June 5, 2026')] },
|
||||
]),
|
||||
).toBeUndefined();
|
||||
expect(findLastAnnouncedDate([])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildDateReminder', () => {
|
||||
const today = 'Friday, June 5, 2026';
|
||||
const reminderText = (body: string) =>
|
||||
`<system-reminder>\n${body}\n</system-reminder>`;
|
||||
|
||||
it('adds nothing when the history already gives today', () => {
|
||||
expect(buildDateReminder([startup(today)], today)).toBeUndefined();
|
||||
expect(
|
||||
buildDateReminder(
|
||||
[startup('Thursday, June 4, 2026'), user(reminder(today), 'Hi')],
|
||||
today,
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('says the date has changed when the history gives an earlier one', () => {
|
||||
expect(
|
||||
buildDateReminder(
|
||||
[startup('Thursday, June 4, 2026'), user('Hi'), model('Hello')],
|
||||
today,
|
||||
),
|
||||
).toEqual({
|
||||
text: reminderText(
|
||||
"The date has changed since earlier in this conversation. Today's date is now Friday, June 5, 2026. Use this date from here on; there is no need to mention the change to the user.",
|
||||
),
|
||||
partMetadata: { [DATE_REMINDER_METADATA]: today },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['an empty history', []],
|
||||
['a startup context without a date', [user(wrapSystemReminder('MCP'))]],
|
||||
])('tells the date plainly with %s', (_title, history: Content[]) => {
|
||||
expect(buildDateReminder(history, today)).toEqual({
|
||||
text: reminderText("Today's date is Friday, June 5, 2026."),
|
||||
partMetadata: { [DATE_REMINDER_METADATA]: today },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
'a resumed conversation',
|
||||
[rebuiltStartup(today), user('Earlier question'), model('Answer')],
|
||||
],
|
||||
[
|
||||
'a compacted conversation',
|
||||
[
|
||||
rebuiltStartup(today),
|
||||
userText('<state_snapshot>Earlier work</state_snapshot>'),
|
||||
modelText('Got it. Thanks for the additional context!'),
|
||||
],
|
||||
],
|
||||
])(
|
||||
'says earlier turns may come from another date in %s',
|
||||
(_title, history: Content[]) => {
|
||||
expect(buildDateReminder(history, today)).toEqual({
|
||||
text: reminderText(
|
||||
"Today's date is Friday, June 5, 2026. Earlier parts of this conversation may come from another date; use this date from here on.",
|
||||
),
|
||||
partMetadata: { [DATE_REMINDER_METADATA]: today },
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('getInitialChatHistory', () => {
|
||||
const startupDate = async (
|
||||
extraHistory?: Content[],
|
||||
announcedDate?: string | null,
|
||||
) => {
|
||||
vi.mocked(getFolderStructure).mockResolvedValue('Mock Folder Structure');
|
||||
const { config } = chatHistorySetup();
|
||||
const [history] = await getInitialChatHistory(
|
||||
config as Config,
|
||||
extraHistory,
|
||||
announcedDate === undefined ? {} : { announcedDate },
|
||||
);
|
||||
const part = history[0]!.parts!.find((p) =>
|
||||
p.text?.includes("Today's date is"),
|
||||
)!;
|
||||
return part.partMetadata?.[DATE_REMINDER_METADATA];
|
||||
};
|
||||
|
||||
it('records the startup context date when it starts the conversation', async () => {
|
||||
expect(await startupDate()).toBe(formatDateForContext());
|
||||
expect(await startupDate([])).toBe(formatDateForContext());
|
||||
});
|
||||
|
||||
it('records no date in front of earlier turns', async () => {
|
||||
expect(await startupDate([userText('Earlier')])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('records the date the caller gives, or none', async () => {
|
||||
expect(await startupDate(undefined, 'Thursday, June 4, 2026')).toBe(
|
||||
'Thursday, June 4, 2026',
|
||||
);
|
||||
expect(await startupDate(undefined, null)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEnvironmentContext', () => {
|
||||
let mockConfig: Partial<Config>;
|
||||
|
||||
|
||||
@@ -76,6 +76,78 @@ export function formatDateForContext(date: Date = new Date()): string {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* `partMetadata` key on a part that tells the model the date; its value is
|
||||
* that date. The client puts it on the date reminders it adds and on a startup
|
||||
* context that starts a conversation. Part metadata never reaches the model,
|
||||
* so text the user writes, pastes or includes cannot carry it.
|
||||
*/
|
||||
export const DATE_REMINDER_METADATA = 'qwen-code:date-reminder';
|
||||
|
||||
// The date reminder sessions recorded before the date reminder carried
|
||||
// DATE_REMINDER_METADATA (a compressed record can keep one). Read only when it
|
||||
// is a whole part, as the client added it. Safe to remove once those sessions
|
||||
// have cycled out.
|
||||
const LEGACY_DATE_REMINDER_PATTERN = new RegExp(
|
||||
`^${SYSTEM_REMINDER_OPEN}\\nThe current date is: ([^\\n]+?)\\. Note: This is the authoritative current date[^\\n]*\\n${SYSTEM_REMINDER_CLOSE}$`,
|
||||
);
|
||||
|
||||
/**
|
||||
* The last date the history tells the model, read only from what the client
|
||||
* generates: the marked startup context and date reminder parts. The date
|
||||
* reminder is decided from this rather than from client state, so rewind,
|
||||
* resume, compaction and `/clear` cannot leave the two out of step.
|
||||
*/
|
||||
export function findLastAnnouncedDate(
|
||||
history: readonly Content[],
|
||||
): string | undefined {
|
||||
for (let index = history.length - 1; index >= 0; index--) {
|
||||
const content = history[index]!;
|
||||
if (content.role !== 'user') continue;
|
||||
const parts = content.parts ?? [];
|
||||
for (let partIndex = parts.length - 1; partIndex >= 0; partIndex--) {
|
||||
const part = parts[partIndex]!;
|
||||
const marked = part.partMetadata?.[DATE_REMINDER_METADATA];
|
||||
if (typeof marked === 'string') return marked;
|
||||
if (
|
||||
typeof part.text !== 'string' ||
|
||||
!part.text.startsWith(SYSTEM_REMINDER_OPEN)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const date = LEGACY_DATE_REMINDER_PATTERN.exec(part.text.trimEnd())?.[1];
|
||||
if (date !== undefined) return date;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The reminder part that tells the model today's date, or undefined when the
|
||||
* history already gives it. With no date in the history, a history that holds
|
||||
* earlier turns (a resumed or compacted conversation, whose startup context
|
||||
* was rebuilt) is told that those turns may come from another date.
|
||||
*/
|
||||
export function buildDateReminder(
|
||||
history: readonly Content[],
|
||||
today: string = formatDateForContext(),
|
||||
): Part | undefined {
|
||||
const announced = findLastAnnouncedDate(history);
|
||||
if (announced === today) return undefined;
|
||||
let text: string;
|
||||
if (announced !== undefined) {
|
||||
text = `The date has changed since earlier in this conversation. Today's date is now ${today}. Use this date from here on; there is no need to mention the change to the user.`;
|
||||
} else if (history.length > getStartupContextLength(history)) {
|
||||
text = `Today's date is ${today}. Earlier parts of this conversation may come from another date; use this date from here on.`;
|
||||
} else {
|
||||
text = `Today's date is ${today}.`;
|
||||
}
|
||||
return {
|
||||
text: wrapSystemReminder(text),
|
||||
partMetadata: { [DATE_REMINDER_METADATA]: today },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a string describing the current workspace directories and their structures.
|
||||
* @param {Config} config - The runtime configuration and services.
|
||||
@@ -120,8 +192,10 @@ ${folderStructure}`;
|
||||
* @param {Config} config - The runtime configuration and services.
|
||||
* @returns A promise that resolves to an array of `Part` objects containing environment information.
|
||||
*/
|
||||
export async function getEnvironmentContext(config: Config): Promise<Part[]> {
|
||||
const today = formatDateForContext();
|
||||
export async function getEnvironmentContext(
|
||||
config: Config,
|
||||
today: string = formatDateForContext(),
|
||||
): Promise<Part[]> {
|
||||
const platform = config.getExecutionEnvironment?.()
|
||||
? ''
|
||||
: `My operating system is: ${process.platform}\n`;
|
||||
@@ -575,8 +649,9 @@ export function buildChangedAgentsReminder(
|
||||
|
||||
export async function buildStartupContextReminder(
|
||||
config: Config,
|
||||
today?: string,
|
||||
): Promise<string> {
|
||||
const envParts = await getEnvironmentContext(config);
|
||||
const envParts = await getEnvironmentContext(config, today);
|
||||
const envContextString = envParts.map((part) => part.text || '').join('\n\n');
|
||||
return wrapSystemReminder(envContextString);
|
||||
}
|
||||
@@ -592,6 +667,13 @@ export interface InitialChatHistoryOptions {
|
||||
// prompt so a repository change between sessions does not invalidate the
|
||||
// provider-cached system prompt. Subagents leave it unset.
|
||||
gitStatus?: string | null;
|
||||
// The date the turns after the startup context ran under, recorded on the
|
||||
// startup context part for the date reminder (see findLastAnnouncedDate).
|
||||
// Defaults to the startup context's own date when it starts the
|
||||
// conversation, and to none when it goes in front of `extraHistory`, whose
|
||||
// date is unknown. A caller that puts the startup context in front of turns
|
||||
// whose date it knows passes that date; null records none.
|
||||
announcedDate?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -624,9 +706,16 @@ export async function getInitialChatHistory(
|
||||
const includeAvailableSkillsReminder =
|
||||
(options.includeAvailableSkillsReminder ?? true) &&
|
||||
toolRegistry.getAllToolNames().includes(ToolNames.SKILL);
|
||||
const today = formatDateForContext();
|
||||
const startupReminder = config.getSkipStartupContext()
|
||||
? null
|
||||
: await buildStartupContextReminder(config);
|
||||
: await buildStartupContextReminder(config, today);
|
||||
const announcedDate =
|
||||
options.announcedDate !== undefined
|
||||
? options.announcedDate
|
||||
: extraHistory?.length
|
||||
? null
|
||||
: today;
|
||||
const skillsResult = includeAvailableSkillsReminder
|
||||
? await buildAvailableSkillsReminder(config)
|
||||
: null;
|
||||
@@ -636,17 +725,26 @@ export async function getInitialChatHistory(
|
||||
// computed once per session, so it sits with the other session-constant
|
||||
// parts. Deferred-tools is last because progressive MCP discovery can
|
||||
// change it — only the tail recomputes.
|
||||
const asPart = (text: string | null): Part | null =>
|
||||
text === null ? null : { text };
|
||||
const reminderParts = [
|
||||
buildMcpServerInstructionsReminder(toolRegistry),
|
||||
skillsResult?.reminder ?? null,
|
||||
startupReminder,
|
||||
options.gitStatus ? wrapSystemReminder(options.gitStatus) : null,
|
||||
includeDeferredToolsReminder
|
||||
? buildDeferredToolsReminder(toolRegistry)
|
||||
: null,
|
||||
]
|
||||
.filter((text): text is string => text !== null)
|
||||
.map((text) => ({ text }));
|
||||
asPart(buildMcpServerInstructionsReminder(toolRegistry)),
|
||||
asPart(skillsResult?.reminder ?? null),
|
||||
startupReminder === null
|
||||
? null
|
||||
: {
|
||||
text: startupReminder,
|
||||
...(announcedDate !== null && {
|
||||
partMetadata: { [DATE_REMINDER_METADATA]: announcedDate },
|
||||
}),
|
||||
},
|
||||
asPart(options.gitStatus ? wrapSystemReminder(options.gitStatus) : null),
|
||||
asPart(
|
||||
includeDeferredToolsReminder
|
||||
? buildDeferredToolsReminder(toolRegistry)
|
||||
: null,
|
||||
),
|
||||
].filter((part): part is Part => part !== null);
|
||||
|
||||
const prelude =
|
||||
reminderParts.length === 0
|
||||
@@ -679,7 +777,7 @@ export async function getInitialChatHistory(
|
||||
* counted as real user prompts for rewind indexing.
|
||||
*/
|
||||
export function getStartupContextLength(
|
||||
history: Content[],
|
||||
history: readonly Content[],
|
||||
options: { includeCompressed?: boolean } = {},
|
||||
): number {
|
||||
const firstEntry = history[0];
|
||||
@@ -708,7 +806,7 @@ export function getStartupContextLength(
|
||||
}
|
||||
|
||||
function detectCompressedPrefixLength(
|
||||
history: Content[],
|
||||
history: readonly Content[],
|
||||
offset: number,
|
||||
): number {
|
||||
const firstEntry = history[offset];
|
||||
|
||||
Reference in new issue
Block a user