Skip to content

Commit 307fe21

Browse files
stephentoubCopilot
andcommitted
Normalize 1.0.72 background-agent completion notification wording
The 1.0.72 CLI reworded the background-agent completion system_notification that the runtime injects as a user message: the status sentence changed from "has completed successfully." to "has finished processing and is now idle." and the advice changed from "to retrieve unread results." to "to read the results, or write_agent to send follow-up messages." Existing snapshots store the old canonical wording, so the replay proxy could not match live 1.0.72 requests, failing subagent_hooks, should_abort_a_session, and the background-agent task-details test across every SDK. Extend the existing task-completion-notification normalization (which already mapped "retrieve unread results" to "retrieve the full results") into normalizeAgentCompletionNotification, collapsing every known phrasing of both sentences to one canonical form on both the request and stored sides. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e94bab38-c22e-4a38-8d8b-c82c1991a33c
1 parent f83d703 commit 307fe21

2 files changed

Lines changed: 114 additions & 10 deletions

File tree

test/harness/replayingCapiProxy.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,38 @@ describe("ReplayingCapiProxy", () => {
379379
expect(result.conversations[0].messages[0].content).toBe(fullNotification);
380380
});
381381

382+
test("normalizes the idle-phrasing task completion notification", async () => {
383+
const idleNotification = [
384+
"<system_notification>",
385+
'Agent "sdk-background-agent" (general-purpose) has finished processing and is now idle. Use read_agent with agent_id "sdk-background-agent" to read the results, or write_agent to send follow-up messages.',
386+
"</system_notification>",
387+
].join("\n");
388+
const fullNotification = [
389+
"<system_notification>",
390+
'Agent "sdk-background-agent" (general-purpose) has completed successfully. Use read_agent with agent_id "sdk-background-agent" to retrieve the full results.',
391+
"</system_notification>",
392+
].join("\n");
393+
394+
const requestBody = JSON.stringify({
395+
messages: [
396+
{
397+
role: "user",
398+
content: idleNotification,
399+
},
400+
],
401+
});
402+
const responseBody = JSON.stringify({
403+
choices: [{ message: { role: "assistant", content: "Done" } }],
404+
});
405+
406+
const outputPath = await createProxy([
407+
{ url: "/chat/completions", requestBody, responseBody },
408+
]);
409+
410+
const result = await readYamlOutput(outputPath);
411+
expect(result.conversations[0].messages[0].content).toBe(fullNotification);
412+
});
413+
382414
test("strips agent_instructions from user messages", async () => {
383415
const requestBody = JSON.stringify({
384416
messages: [
@@ -1321,6 +1353,67 @@ Always include PINEAPPLE_COCONUT_42.
13211353
}
13221354
});
13231355

1356+
test("matches cached completion notification against the idle-phrasing runtime", async () => {
1357+
const cachePath = path.join(tempDir, "cache.yaml");
1358+
// Snapshot recorded with the canonical completion wording.
1359+
const canonicalNotification = [
1360+
"<system_notification>",
1361+
'Agent "read-file" (explore) has completed successfully. Use read_agent with agent_id "read-file" to retrieve the full results.',
1362+
"</system_notification>",
1363+
].join("\n");
1364+
// Newer runtime emits the idle phrasing with write_agent advice.
1365+
const idleNotification = [
1366+
"<system_notification>",
1367+
'Agent "read-file" (explore) has finished processing and is now idle. Use read_agent with agent_id "read-file" to read the results, or write_agent to send follow-up messages.',
1368+
"</system_notification>",
1369+
].join("\n");
1370+
1371+
const cacheContent = yaml.stringify({
1372+
models: ["test-model"],
1373+
conversations: [
1374+
{
1375+
messages: [
1376+
{ role: "system", content: "${system}" },
1377+
{ role: "user", content: "Hello" },
1378+
{ role: "assistant", content: "Hi!" },
1379+
{ role: "user", content: canonicalNotification },
1380+
{ role: "assistant", content: "Read agent completed." },
1381+
],
1382+
},
1383+
],
1384+
} satisfies NormalizedData);
1385+
await writeFile(cachePath, cacheContent);
1386+
1387+
const proxy = new ReplayingCapiProxy(
1388+
"http://localhost:9999",
1389+
cachePath,
1390+
workDir,
1391+
);
1392+
const proxyUrl = await proxy.start();
1393+
1394+
try {
1395+
const response = await makeRequest(proxyUrl, "/chat/completions", {
1396+
body: {
1397+
model: "test-model",
1398+
messages: [
1399+
{ role: "system", content: "Be helpful" },
1400+
{ role: "user", content: "Hello" },
1401+
{ role: "assistant", content: "Hi!" },
1402+
{ role: "user", content: idleNotification },
1403+
],
1404+
},
1405+
});
1406+
1407+
expect(response.status).toBe(200);
1408+
expect(
1409+
(JSON.parse(response.body) as ChatCompletion).choices[0].message
1410+
.content,
1411+
).toBe("Read agent completed.");
1412+
} finally {
1413+
await proxy.stop();
1414+
}
1415+
});
1416+
13241417
test("matches parallel tool results regardless of arrival order", async () => {
13251418
const cachePath = path.join(tempDir, "cache.yaml");
13261419
const cacheContent = yaml.stringify({

test/harness/replayingCapiProxy.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,8 +1223,9 @@ function transformOpenAIRequestMessage(
12231223
}
12241224

12251225
function normalizeUserMessage(content: string): string {
1226-
return normalizeSkillContextFrontmatter(content)
1227-
.replace(taskCompletionNotificationPattern, taskCompletionNotificationReplacement)
1226+
return normalizeAgentCompletionNotification(
1227+
normalizeSkillContextFrontmatter(content),
1228+
)
12281229
.replace(/<current_datetime>.*?<\/current_datetime>/g, "")
12291230
.replace(/<reminder>[\s\S]*?<\/reminder>/g, "")
12301231
.replace(/<system_reminder>[\s\S]*?<\/system_reminder>/g, "")
@@ -1237,19 +1238,29 @@ function normalizeUserMessage(content: string): string {
12371238
.trim();
12381239
}
12391240

1240-
const taskCompletionNotificationPattern =
1241-
/Use read_agent with agent_id "([^"]+)" to retrieve unread results\./g;
1242-
const taskCompletionNotificationReplacement =
1243-
'Use read_agent with agent_id "$1" to retrieve the full results.';
1241+
// The runtime's background-agent completion system_notification has been reworded
1242+
// across CLI versions — both the status sentence ("has completed successfully." vs
1243+
// "has finished processing and is now idle.") and the follow-up advice ("to retrieve
1244+
// unread results." vs "to read the results, or write_agent to send follow-up
1245+
// messages."). Collapse every known phrasing to one canonical form so snapshots keep
1246+
// matching regardless of which runtime version recorded them or replays against them.
1247+
function normalizeAgentCompletionNotification(content: string): string {
1248+
return content
1249+
.replace(
1250+
/Agent ("[^"]*" \([^)]*\)) has finished processing and is now idle\./g,
1251+
"Agent $1 has completed successfully.",
1252+
)
1253+
.replace(
1254+
/Use read_agent with agent_id "([^"]+)" to (?:retrieve unread results|read the results, or write_agent to send follow-up messages)\./g,
1255+
'Use read_agent with agent_id "$1" to retrieve the full results.',
1256+
);
1257+
}
12441258

12451259
function normalizeStoredUserMessages(conversations: NormalizedConversation[]) {
12461260
for (const conversation of conversations) {
12471261
for (const message of conversation.messages) {
12481262
if (message.role === "user" && typeof message.content === "string") {
1249-
message.content = message.content.replace(
1250-
taskCompletionNotificationPattern,
1251-
taskCompletionNotificationReplacement,
1252-
);
1263+
message.content = normalizeAgentCompletionNotification(message.content);
12531264
}
12541265
}
12551266
}

0 commit comments

Comments
 (0)