diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 30ae6ff27..c6d5af4a2 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -11440,6 +11440,9 @@ const ( HookTypeSubagentStop HookType = "subagentStop" // Runs after the user submits a prompt. HookTypeUserPromptSubmitted HookType = "userPromptSubmitted" + // Runs after the runtime transforms the submitted prompt for the model, before it is added + // to session history. + HookTypeUserPromptTransformed HookType = "userPromptTransformed" ) // Constant value. Always "github". diff --git a/java/pom.xml b/java/pom.xml index ac95090b8..b2a2d3805 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -86,7 +86,7 @@ DO NOT EDIT MANUALLY. Updated by the update-copilot-dependency workflow. --> - ^1.0.71 + ^1.0.72-0 diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index 372e3daab..003246e16 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.71", + "@github/copilot": "^1.0.72-0", "json-schema": "^0.4.0", "tsx": "^4.23.1" } @@ -428,9 +428,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.71.tgz", - "integrity": "sha512-F3axBi+sXSLYDJbxCBW36bM6MYKNC2rlyAf3Ivo/MjiHKKJW7j5AmaR1IRYS9Gt8r9mxOwWFM1cJFA+CuLaR8g==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.72-0.tgz", + "integrity": "sha512-JS8LxGYvtPokXUHuqMbQhOqhMBPuOVMUIUy8BftlYxMJKN00oVp/Xe1aAqng6ULRdi35HnuyJ8nFfdBvE6UYsQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -439,20 +439,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.71", - "@github/copilot-darwin-x64": "1.0.71", - "@github/copilot-linux-arm64": "1.0.71", - "@github/copilot-linux-x64": "1.0.71", - "@github/copilot-linuxmusl-arm64": "1.0.71", - "@github/copilot-linuxmusl-x64": "1.0.71", - "@github/copilot-win32-arm64": "1.0.71", - "@github/copilot-win32-x64": "1.0.71" + "@github/copilot-darwin-arm64": "1.0.72-0", + "@github/copilot-darwin-x64": "1.0.72-0", + "@github/copilot-linux-arm64": "1.0.72-0", + "@github/copilot-linux-x64": "1.0.72-0", + "@github/copilot-linuxmusl-arm64": "1.0.72-0", + "@github/copilot-linuxmusl-x64": "1.0.72-0", + "@github/copilot-win32-arm64": "1.0.72-0", + "@github/copilot-win32-x64": "1.0.72-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.71.tgz", - "integrity": "sha512-mEWzyqbqRAWgyU7i2uuSRoVPx/TwaFQX0nZmw0bc30aJ0BnO7cy2kYQyCHw8ykmf/tfxT0xauZ6k0BOFmWizzQ==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.72-0.tgz", + "integrity": "sha512-/tWTIYNBXOQbJeniN9Sktsk0/Nnjnmr7jhaiY0ysumITK7wNQ3r8A2sZbv+WeODlfH/TfRvVS4e056U8NjKqnQ==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.71.tgz", - "integrity": "sha512-Md9yEg406OBVBx3w4PeEj62TubulVLBcHleqmCoOoUmPgUxPZotUbrqz3rtbzADbXfrrD7JWvVsbd2UiNL194w==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.72-0.tgz", + "integrity": "sha512-Q6jFeRx1ch5S42EeJR616fGAg8eJ+Y9QlRx3s1xWUHPVQEUBbap8aD5REl1GSDr4+TkkPO+WDW5Hf9xhO5SK5A==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.71.tgz", - "integrity": "sha512-ykLJYOqBj3jRB5IJCDugLClAqbr7DmtTbUjlNY7+Jdq/n6i+d7xUQGclf1IWL5gnxbGQVAf+zkToD+sRM389Kg==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.72-0.tgz", + "integrity": "sha512-5A/DZUKSnXkP+nVIa1bPHslJlv6f+vagsoYbbDfsky8DrsjdR4KKyFXPsJkSD+o253f5zstl7CnpUejrZ1R/+Q==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.71.tgz", - "integrity": "sha512-pC0FNHG+BBwZd6yZlM85kkAGN+uJhM6o+THi76N2GnnSxmw7+remb1mvYxdgRVbdCm+LBUIbCKRWJLuMwrfb6A==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.72-0.tgz", + "integrity": "sha512-SyY9sHaH/YwiKipKGSxXaAU755VlVInpSWCvNPycF7Z5oAmWNHeF1AOa5dwCYIRYxm8EgFXSLj5wTgNxXCz9Iw==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.71.tgz", - "integrity": "sha512-hBmDljFTjacxqZTasCEy43H8EIzuXB/hHEBBCMFjhB9J00nIxsO6Dh0woTifKpx7knTYZdpTjjca3D0pAoZlUA==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.72-0.tgz", + "integrity": "sha512-QI5+rWpixbgcxwcpYwmHcGi2L996/Wmmoto4eTR+axoAHBCny84dkWP7DPoFO40GLTNTBWxZATxOP/awa1o5ww==", "cpu": [ "arm64" ], @@ -530,9 +530,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.71.tgz", - "integrity": "sha512-CfTXU8pa5dxRz22xQzoi3TiG1PJo9+WR8PRDiPSdkIBSyPJ1NvX87DJmfXjTgeAfR+wkjt/p0keDCaBBVhNmUA==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.72-0.tgz", + "integrity": "sha512-msZCZZ4DovCKGJrUsvolozjOh9jq2u4ojAY70maB25MnnKm4uuvqUrkATVq9bDNU+nAcZuoiKKVRwDuByOlMww==", "cpu": [ "x64" ], @@ -546,9 +546,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.71.tgz", - "integrity": "sha512-+HI1DokixXhHUahj06Fw67ZAigBuXKC58BFma4UJOGrQsDgwOSbqeTQHCw6vuymzjKlg3sactfsCUTaefkjscQ==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.72-0.tgz", + "integrity": "sha512-OOOh39JxHrjIvzS9UT5c6H+V7IiRuLT96iDGpn5V1g8CnwSyNq/LbiRZxoYpqzqzmSAvqQOCQpZUwoe/JFIYNw==", "cpu": [ "arm64" ], @@ -562,9 +562,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.71.tgz", - "integrity": "sha512-02kXOBd9CwBbCaztuf71WYWn+uGapCuiaasomN4tcMH3HBVZ4gi3J0ZUoRcgcS80xh81uQyeBHbnUKzb/RE/9A==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.72-0.tgz", + "integrity": "sha512-Luix48UZ+dCs5xSCnc9i5/vJsIrvHhV62k6VyGUsbPVF5thjdXJU5T2Xi4Nsnt8ReLgQ5z/9YOBx9orUfDgGHg==", "cpu": [ "x64" ], diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index 32c609be4..8a3e173a5 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.71", + "@github/copilot": "^1.0.72-0", "json-schema": "^0.4.0", "tsx": "^4.23.1" } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/HookType.java b/java/src/generated/java/com/github/copilot/generated/rpc/HookType.java index 006f8a7c1..8d7cd913c 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/HookType.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/HookType.java @@ -26,6 +26,8 @@ public enum HookType { POSTTOOLUSEFAILURE("postToolUseFailure"), /** The {@code userPromptSubmitted} variant. */ USERPROMPTSUBMITTED("userPromptSubmitted"), + /** The {@code userPromptTransformed} variant. */ + USERPROMPTTRANSFORMED("userPromptTransformed"), /** The {@code sessionStart} variant. */ SESSIONSTART("sessionStart"), /** The {@code sessionEnd} variant. */ diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 799053c0f..270f73e73 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.71", + "@github/copilot": "^1.0.72-0", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -700,9 +700,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.71.tgz", - "integrity": "sha512-F3axBi+sXSLYDJbxCBW36bM6MYKNC2rlyAf3Ivo/MjiHKKJW7j5AmaR1IRYS9Gt8r9mxOwWFM1cJFA+CuLaR8g==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.72-0.tgz", + "integrity": "sha512-JS8LxGYvtPokXUHuqMbQhOqhMBPuOVMUIUy8BftlYxMJKN00oVp/Xe1aAqng6ULRdi35HnuyJ8nFfdBvE6UYsQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -711,20 +711,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.71", - "@github/copilot-darwin-x64": "1.0.71", - "@github/copilot-linux-arm64": "1.0.71", - "@github/copilot-linux-x64": "1.0.71", - "@github/copilot-linuxmusl-arm64": "1.0.71", - "@github/copilot-linuxmusl-x64": "1.0.71", - "@github/copilot-win32-arm64": "1.0.71", - "@github/copilot-win32-x64": "1.0.71" + "@github/copilot-darwin-arm64": "1.0.72-0", + "@github/copilot-darwin-x64": "1.0.72-0", + "@github/copilot-linux-arm64": "1.0.72-0", + "@github/copilot-linux-x64": "1.0.72-0", + "@github/copilot-linuxmusl-arm64": "1.0.72-0", + "@github/copilot-linuxmusl-x64": "1.0.72-0", + "@github/copilot-win32-arm64": "1.0.72-0", + "@github/copilot-win32-x64": "1.0.72-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.71.tgz", - "integrity": "sha512-mEWzyqbqRAWgyU7i2uuSRoVPx/TwaFQX0nZmw0bc30aJ0BnO7cy2kYQyCHw8ykmf/tfxT0xauZ6k0BOFmWizzQ==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.72-0.tgz", + "integrity": "sha512-/tWTIYNBXOQbJeniN9Sktsk0/Nnjnmr7jhaiY0ysumITK7wNQ3r8A2sZbv+WeODlfH/TfRvVS4e056U8NjKqnQ==", "cpu": [ "arm64" ], @@ -738,9 +738,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.71.tgz", - "integrity": "sha512-Md9yEg406OBVBx3w4PeEj62TubulVLBcHleqmCoOoUmPgUxPZotUbrqz3rtbzADbXfrrD7JWvVsbd2UiNL194w==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.72-0.tgz", + "integrity": "sha512-Q6jFeRx1ch5S42EeJR616fGAg8eJ+Y9QlRx3s1xWUHPVQEUBbap8aD5REl1GSDr4+TkkPO+WDW5Hf9xhO5SK5A==", "cpu": [ "x64" ], @@ -754,9 +754,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.71.tgz", - "integrity": "sha512-ykLJYOqBj3jRB5IJCDugLClAqbr7DmtTbUjlNY7+Jdq/n6i+d7xUQGclf1IWL5gnxbGQVAf+zkToD+sRM389Kg==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.72-0.tgz", + "integrity": "sha512-5A/DZUKSnXkP+nVIa1bPHslJlv6f+vagsoYbbDfsky8DrsjdR4KKyFXPsJkSD+o253f5zstl7CnpUejrZ1R/+Q==", "cpu": [ "arm64" ], @@ -770,9 +770,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.71.tgz", - "integrity": "sha512-pC0FNHG+BBwZd6yZlM85kkAGN+uJhM6o+THi76N2GnnSxmw7+remb1mvYxdgRVbdCm+LBUIbCKRWJLuMwrfb6A==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.72-0.tgz", + "integrity": "sha512-SyY9sHaH/YwiKipKGSxXaAU755VlVInpSWCvNPycF7Z5oAmWNHeF1AOa5dwCYIRYxm8EgFXSLj5wTgNxXCz9Iw==", "cpu": [ "x64" ], @@ -786,9 +786,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.71.tgz", - "integrity": "sha512-hBmDljFTjacxqZTasCEy43H8EIzuXB/hHEBBCMFjhB9J00nIxsO6Dh0woTifKpx7knTYZdpTjjca3D0pAoZlUA==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.72-0.tgz", + "integrity": "sha512-QI5+rWpixbgcxwcpYwmHcGi2L996/Wmmoto4eTR+axoAHBCny84dkWP7DPoFO40GLTNTBWxZATxOP/awa1o5ww==", "cpu": [ "arm64" ], @@ -802,9 +802,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.71.tgz", - "integrity": "sha512-CfTXU8pa5dxRz22xQzoi3TiG1PJo9+WR8PRDiPSdkIBSyPJ1NvX87DJmfXjTgeAfR+wkjt/p0keDCaBBVhNmUA==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.72-0.tgz", + "integrity": "sha512-msZCZZ4DovCKGJrUsvolozjOh9jq2u4ojAY70maB25MnnKm4uuvqUrkATVq9bDNU+nAcZuoiKKVRwDuByOlMww==", "cpu": [ "x64" ], @@ -818,9 +818,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.71.tgz", - "integrity": "sha512-+HI1DokixXhHUahj06Fw67ZAigBuXKC58BFma4UJOGrQsDgwOSbqeTQHCw6vuymzjKlg3sactfsCUTaefkjscQ==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.72-0.tgz", + "integrity": "sha512-OOOh39JxHrjIvzS9UT5c6H+V7IiRuLT96iDGpn5V1g8CnwSyNq/LbiRZxoYpqzqzmSAvqQOCQpZUwoe/JFIYNw==", "cpu": [ "arm64" ], @@ -834,9 +834,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.71.tgz", - "integrity": "sha512-02kXOBd9CwBbCaztuf71WYWn+uGapCuiaasomN4tcMH3HBVZ4gi3J0ZUoRcgcS80xh81uQyeBHbnUKzb/RE/9A==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.72-0.tgz", + "integrity": "sha512-Luix48UZ+dCs5xSCnc9i5/vJsIrvHhV62k6VyGUsbPVF5thjdXJU5T2Xi4Nsnt8ReLgQ5z/9YOBx9orUfDgGHg==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index 4f588640a..a840c642e 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.71", + "@github/copilot": "^1.0.72-0", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 89c74c153..2c7dc9c9d 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.71", + "@github/copilot": "^1.0.72-0", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 255a769d5..8e710cf24 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -540,6 +540,8 @@ export type HookType = | "postToolUseFailure" /** Runs after the user submits a prompt. */ | "userPromptSubmitted" + /** Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. */ + | "userPromptTransformed" /** Runs when a session starts. */ | "sessionStart" /** Runs when a session ends. */ diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 828eaa3ce..106292969 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -2466,6 +2466,7 @@ class _HookType(Enum): SUBAGENT_START = "subagentStart" SUBAGENT_STOP = "subagentStop" USER_PROMPT_SUBMITTED = "userPromptSubmitted" + USER_PROMPT_TRANSFORMED = "userPromptTransformed" # Internal: this type is an internal SDK API and is not part of the public surface. @dataclass diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index e243ec1dc..ea494d9ba 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -20866,6 +20866,9 @@ pub enum HookType { /// Runs after the user submits a prompt. #[serde(rename = "userPromptSubmitted")] UserPromptSubmitted, + /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. + #[serde(rename = "userPromptTransformed")] + UserPromptTransformed, /// Runs when a session starts. #[serde(rename = "sessionStart")] SessionStart, diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 1a8462d66..817b92379 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.71", + "@github/copilot": "^1.0.72-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -501,9 +501,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.71.tgz", - "integrity": "sha512-F3axBi+sXSLYDJbxCBW36bM6MYKNC2rlyAf3Ivo/MjiHKKJW7j5AmaR1IRYS9Gt8r9mxOwWFM1cJFA+CuLaR8g==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.72-0.tgz", + "integrity": "sha512-JS8LxGYvtPokXUHuqMbQhOqhMBPuOVMUIUy8BftlYxMJKN00oVp/Xe1aAqng6ULRdi35HnuyJ8nFfdBvE6UYsQ==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { @@ -513,20 +513,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.71", - "@github/copilot-darwin-x64": "1.0.71", - "@github/copilot-linux-arm64": "1.0.71", - "@github/copilot-linux-x64": "1.0.71", - "@github/copilot-linuxmusl-arm64": "1.0.71", - "@github/copilot-linuxmusl-x64": "1.0.71", - "@github/copilot-win32-arm64": "1.0.71", - "@github/copilot-win32-x64": "1.0.71" + "@github/copilot-darwin-arm64": "1.0.72-0", + "@github/copilot-darwin-x64": "1.0.72-0", + "@github/copilot-linux-arm64": "1.0.72-0", + "@github/copilot-linux-x64": "1.0.72-0", + "@github/copilot-linuxmusl-arm64": "1.0.72-0", + "@github/copilot-linuxmusl-x64": "1.0.72-0", + "@github/copilot-win32-arm64": "1.0.72-0", + "@github/copilot-win32-x64": "1.0.72-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.71.tgz", - "integrity": "sha512-mEWzyqbqRAWgyU7i2uuSRoVPx/TwaFQX0nZmw0bc30aJ0BnO7cy2kYQyCHw8ykmf/tfxT0xauZ6k0BOFmWizzQ==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.72-0.tgz", + "integrity": "sha512-/tWTIYNBXOQbJeniN9Sktsk0/Nnjnmr7jhaiY0ysumITK7wNQ3r8A2sZbv+WeODlfH/TfRvVS4e056U8NjKqnQ==", "cpu": [ "arm64" ], @@ -541,9 +541,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.71.tgz", - "integrity": "sha512-Md9yEg406OBVBx3w4PeEj62TubulVLBcHleqmCoOoUmPgUxPZotUbrqz3rtbzADbXfrrD7JWvVsbd2UiNL194w==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.72-0.tgz", + "integrity": "sha512-Q6jFeRx1ch5S42EeJR616fGAg8eJ+Y9QlRx3s1xWUHPVQEUBbap8aD5REl1GSDr4+TkkPO+WDW5Hf9xhO5SK5A==", "cpu": [ "x64" ], @@ -558,9 +558,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.71.tgz", - "integrity": "sha512-ykLJYOqBj3jRB5IJCDugLClAqbr7DmtTbUjlNY7+Jdq/n6i+d7xUQGclf1IWL5gnxbGQVAf+zkToD+sRM389Kg==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.72-0.tgz", + "integrity": "sha512-5A/DZUKSnXkP+nVIa1bPHslJlv6f+vagsoYbbDfsky8DrsjdR4KKyFXPsJkSD+o253f5zstl7CnpUejrZ1R/+Q==", "cpu": [ "arm64" ], @@ -575,9 +575,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.71.tgz", - "integrity": "sha512-pC0FNHG+BBwZd6yZlM85kkAGN+uJhM6o+THi76N2GnnSxmw7+remb1mvYxdgRVbdCm+LBUIbCKRWJLuMwrfb6A==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.72-0.tgz", + "integrity": "sha512-SyY9sHaH/YwiKipKGSxXaAU755VlVInpSWCvNPycF7Z5oAmWNHeF1AOa5dwCYIRYxm8EgFXSLj5wTgNxXCz9Iw==", "cpu": [ "x64" ], @@ -592,9 +592,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.71.tgz", - "integrity": "sha512-hBmDljFTjacxqZTasCEy43H8EIzuXB/hHEBBCMFjhB9J00nIxsO6Dh0woTifKpx7knTYZdpTjjca3D0pAoZlUA==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.72-0.tgz", + "integrity": "sha512-QI5+rWpixbgcxwcpYwmHcGi2L996/Wmmoto4eTR+axoAHBCny84dkWP7DPoFO40GLTNTBWxZATxOP/awa1o5ww==", "cpu": [ "arm64" ], @@ -609,9 +609,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.71.tgz", - "integrity": "sha512-CfTXU8pa5dxRz22xQzoi3TiG1PJo9+WR8PRDiPSdkIBSyPJ1NvX87DJmfXjTgeAfR+wkjt/p0keDCaBBVhNmUA==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.72-0.tgz", + "integrity": "sha512-msZCZZ4DovCKGJrUsvolozjOh9jq2u4ojAY70maB25MnnKm4uuvqUrkATVq9bDNU+nAcZuoiKKVRwDuByOlMww==", "cpu": [ "x64" ], @@ -626,9 +626,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.71.tgz", - "integrity": "sha512-+HI1DokixXhHUahj06Fw67ZAigBuXKC58BFma4UJOGrQsDgwOSbqeTQHCw6vuymzjKlg3sactfsCUTaefkjscQ==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.72-0.tgz", + "integrity": "sha512-OOOh39JxHrjIvzS9UT5c6H+V7IiRuLT96iDGpn5V1g8CnwSyNq/LbiRZxoYpqzqzmSAvqQOCQpZUwoe/JFIYNw==", "cpu": [ "arm64" ], @@ -643,9 +643,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.71", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.71.tgz", - "integrity": "sha512-02kXOBd9CwBbCaztuf71WYWn+uGapCuiaasomN4tcMH3HBVZ4gi3J0ZUoRcgcS80xh81uQyeBHbnUKzb/RE/9A==", + "version": "1.0.72-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.72-0.tgz", + "integrity": "sha512-Luix48UZ+dCs5xSCnc9i5/vJsIrvHhV62k6VyGUsbPVF5thjdXJU5T2Xi4Nsnt8ReLgQ5z/9YOBx9orUfDgGHg==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index 18b19e21a..79758d524 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -14,7 +14,7 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@github/copilot": "^1.0.71", + "@github/copilot": "^1.0.72-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index 009a1e40a..8308432b3 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -379,6 +379,38 @@ describe("ReplayingCapiProxy", () => { expect(result.conversations[0].messages[0].content).toBe(fullNotification); }); + test("normalizes the idle-phrasing task completion notification", async () => { + const idleNotification = [ + "", + '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.', + "", + ].join("\n"); + const fullNotification = [ + "", + 'Agent "sdk-background-agent" (general-purpose) has completed successfully. Use read_agent with agent_id "sdk-background-agent" to retrieve the full results.', + "", + ].join("\n"); + + const requestBody = JSON.stringify({ + messages: [ + { + role: "user", + content: idleNotification, + }, + ], + }); + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Done" } }], + }); + + const outputPath = await createProxy([ + { url: "/chat/completions", requestBody, responseBody }, + ]); + + const result = await readYamlOutput(outputPath); + expect(result.conversations[0].messages[0].content).toBe(fullNotification); + }); + test("strips agent_instructions from user messages", async () => { const requestBody = JSON.stringify({ messages: [ @@ -509,7 +541,7 @@ Always include PINEAPPLE_COCONUT_42. expect(toolMessages[1].content).toBe("[beta result]"); }); - test("collapses the available-tools list to a stable placeholder", async () => { + test("strips the available-tools list so its contents don't matter", async () => { const requestBody = JSON.stringify({ messages: [ { role: "user", content: "Help me" }, @@ -543,14 +575,56 @@ Always include PINEAPPLE_COCONUT_42. const toolMessage = result.conversations[0].messages.find( (m) => m.role === "tool", ); - // The whole enumeration collapses so snapshots stay stable as the built-in - // tool set evolves (e.g. write_agent being added). + // The whole "Available tools..." sentence is stripped so snapshots stay + // stable whether or not the runtime emits the list and as the built-in tool + // set evolves (e.g. write_agent being added). + expect(toolMessage?.content).toBe( + "Tool 'report_intent' does not exist.", + ); + }); + + test("collapses background-agent start advice to a stable placeholder", async () => { + const requestBody = JSON.stringify({ + messages: [ + { role: "user", content: "Help me" }, + { + role: "assistant", + tool_calls: [ + { + id: "tc1", + type: "function", + function: { name: "task", arguments: "{}" }, + }, + ], + }, + { + role: "tool", + tool_call_id: "tc1", + content: + "Agent started in background with agent_id: read-file. You'll be notified when it completes. Tell the user you're waiting and end your response, or continue unrelated work until notified. The agent supports multi-turn conversations — use write_agent to send follow-up messages.", + }, + ], + }); + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Done" } }], + }); + + const outputPath = await createProxy([ + { url: "/chat/completions", requestBody, responseBody }, + ]); + + const result = await readYamlOutput(outputPath); + const toolMessage = result.conversations[0].messages.find( + (m) => m.role === "tool", + ); + // The volatile advice tail collapses so snapshots stay stable as the wording + // evolves across runtime versions, while the agent_id is preserved. expect(toolMessage?.content).toBe( - "Tool 'report_intent' does not exist. Available tools that can be called are ${available_tools}.", + "Agent started in background with agent_id: read-file. ${background_agent_advice}", ); }); - test("normalizes read_agent timing metadata", async () => { + test("normalizes read_agent timing and lifecycle metadata", async () => { const requestBody = JSON.stringify({ messages: [ { role: "user", content: "Help me" }, @@ -588,7 +662,49 @@ Always include PINEAPPLE_COCONUT_42. (m) => m.role === "tool", ); expect(toolMessage?.content).toBe( - "Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s\n\nDone.", + "Agent ${agent_state}. agent_id: read-file, agent_type: explore, status: ${agent_status}, description: Reading subagent-test.txt, elapsed: 0s, total_turns: ${turns}\n\nDone.", + ); + }); + + test("normalizes the 1.0.72 idle-phrasing read_agent result to match the older completed form", async () => { + const requestBody = JSON.stringify({ + messages: [ + { role: "user", content: "Help me" }, + { + role: "assistant", + tool_calls: [ + { + id: "tc1", + type: "function", + function: { + name: "read_agent", + arguments: '{"agent_id":"read-file","wait":true}', + }, + }, + ], + }, + { + role: "tool", + tool_call_id: "tc1", + content: + "Agent is idle (waiting for messages). agent_id: read-file, agent_type: explore, status: idle, description: Reading subagent-test.txt, elapsed: 1.25s, total_turns: 1\n\n[Turn 0]\nDone.", + }, + ], + }); + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Done" } }], + }); + + const outputPath = await createProxy([ + { url: "/chat/completions", requestBody, responseBody }, + ]); + + const result = await readYamlOutput(outputPath); + const toolMessage = result.conversations[0].messages.find( + (m) => m.role === "tool", + ); + expect(toolMessage?.content).toBe( + "Agent ${agent_state}. agent_id: read-file, agent_type: explore, status: ${agent_status}, description: Reading subagent-test.txt, elapsed: 0s, total_turns: ${turns}\n\nDone.", ); }); @@ -947,6 +1063,163 @@ Always include PINEAPPLE_COCONUT_42. } }); + test("matches truncated report_intent results against a runtime that emits the tool list", async () => { + const cachePath = path.join(tempDir, "cache.yaml"); + // Older snapshots truncated the report_intent error to just the first + // sentence, with no "Available tools..." enumeration recorded on disk. + const cacheContent = yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { role: "system", content: "${system}" }, + { role: "user", content: "Report intent" }, + { + role: "assistant", + tool_calls: [ + { + id: "toolcall_0", + type: "function", + function: { name: "report_intent", arguments: "{}" }, + }, + ], + }, + { + role: "tool", + tool_call_id: "toolcall_0", + content: "Tool 'report_intent' does not exist.", + }, + { role: "assistant", content: "Done" }, + ], + }, + ], + } satisfies NormalizedData); + await writeFile(cachePath, cacheContent); + + const proxy = new ReplayingCapiProxy( + "http://localhost:9999", + cachePath, + workDir, + ); + const proxyUrl = await proxy.start(); + + try { + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: { + model: "test-model", + messages: [ + { role: "system", content: "System prompt" }, + { role: "user", content: "Report intent" }, + { + role: "assistant", + tool_calls: [ + { + id: "runtime-call-id", + type: "function", + function: { name: "report_intent", arguments: "{}" }, + }, + ], + }, + { + role: "tool", + tool_call_id: "runtime-call-id", + // Newer runtime appends the full built-in tool enumeration. + content: + "Tool 'report_intent' does not exist. Available tools that can be called are bash, read_bash, view, read_agent, list_agents, write_agent, grep, glob, task.", + }, + ], + }, + }); + + expect(response.status).toBe(200); + expect( + (JSON.parse(response.body) as ChatCompletion).choices[0].message + .content, + ).toBe("Done"); + } finally { + await proxy.stop(); + } + }); + + test("matches background-agent start results after the advice wording changes", async () => { + const cachePath = path.join(tempDir, "cache.yaml"); + // Legacy snapshot recorded before the runtime appended multi-turn advice + // to the background-agent start message. + const cacheContent = yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { role: "system", content: "${system}" }, + { role: "user", content: "Spawn an agent" }, + { + role: "assistant", + tool_calls: [ + { + id: "toolcall_0", + type: "function", + function: { name: "task", arguments: "{}" }, + }, + ], + }, + { + role: "tool", + tool_call_id: "toolcall_0", + content: + "Agent started in background with agent_id: read-file. You'll be notified when it completes. Tell the user you're waiting and end your response, or continue unrelated work until notified.", + }, + { role: "assistant", content: "Done" }, + ], + }, + ], + } satisfies NormalizedData); + await writeFile(cachePath, cacheContent); + + const proxy = new ReplayingCapiProxy( + "http://localhost:9999", + cachePath, + workDir, + ); + const proxyUrl = await proxy.start(); + + try { + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: { + model: "test-model", + messages: [ + { role: "system", content: "System prompt" }, + { role: "user", content: "Spawn an agent" }, + { + role: "assistant", + tool_calls: [ + { + id: "runtime-call-id", + type: "function", + function: { name: "task", arguments: "{}" }, + }, + ], + }, + { + role: "tool", + tool_call_id: "runtime-call-id", + // Newer runtime appends multi-turn advice referencing write_agent. + content: + "Agent started in background with agent_id: read-file. You'll be notified when it completes. Tell the user you're waiting and end your response, or continue unrelated work until notified. The agent supports multi-turn conversations — use write_agent to send follow-up messages.", + }, + ], + }, + }); + + expect(response.status).toBe(200); + expect( + (JSON.parse(response.body) as ChatCompletion).choices[0].message + .content, + ).toBe("Done"); + } finally { + await proxy.stop(); + } + }); + test("expands workdir placeholder in cached response", async () => { const cachePath = path.join(tempDir, "cache.yaml"); const cacheContent = yaml.stringify({ @@ -1122,6 +1395,157 @@ Always include PINEAPPLE_COCONUT_42. } }); + test("matches cached completion notification against the idle-phrasing runtime", async () => { + const cachePath = path.join(tempDir, "cache.yaml"); + // Snapshot recorded with the canonical completion wording. + const canonicalNotification = [ + "", + 'Agent "read-file" (explore) has completed successfully. Use read_agent with agent_id "read-file" to retrieve the full results.', + "", + ].join("\n"); + // Newer runtime emits the idle phrasing with write_agent advice. + const idleNotification = [ + "", + '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.', + "", + ].join("\n"); + + const cacheContent = yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { role: "system", content: "${system}" }, + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi!" }, + { role: "user", content: canonicalNotification }, + { role: "assistant", content: "Read agent completed." }, + ], + }, + ], + } satisfies NormalizedData); + await writeFile(cachePath, cacheContent); + + const proxy = new ReplayingCapiProxy( + "http://localhost:9999", + cachePath, + workDir, + ); + const proxyUrl = await proxy.start(); + + try { + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: { + model: "test-model", + messages: [ + { role: "system", content: "Be helpful" }, + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi!" }, + { role: "user", content: idleNotification }, + ], + }, + }); + + expect(response.status).toBe(200); + expect( + (JSON.parse(response.body) as ChatCompletion).choices[0].message + .content, + ).toBe("Read agent completed."); + } finally { + await proxy.stop(); + } + }); + + test("matches cached read_agent result against the 1.0.72 idle-phrasing runtime", async () => { + const cachePath = path.join(tempDir, "cache.yaml"); + // Snapshot recorded against the older runtime: an agent that finished its + // work reported "Agent completed." with a completed status and a trailing + // duration, and no per-turn markers. + const storedResult = + "Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s\n\nThe file says hello."; + // The 1.0.72 runtime reports the same finished agent as idle, with a + // total_turns count of at least 1 and a "[Turn N]" marker before the body. + const runtimeResult = + "Agent is idle (waiting for messages). agent_id: read-file, agent_type: explore, status: idle, description: Reading subagent-test.txt, elapsed: 0s, total_turns: 1\n\n[Turn 0]\nThe file says hello."; + + const cacheContent = yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { role: "system", content: "${system}" }, + { role: "user", content: "Read the file" }, + { + role: "assistant", + tool_calls: [ + { + id: "toolcall_0", + type: "function", + function: { + name: "read_agent", + arguments: '{"agent_id":"read-file","wait":true}', + }, + }, + ], + }, + { + role: "tool", + tool_call_id: "toolcall_0", + content: storedResult, + }, + { role: "assistant", content: "The file was read successfully." }, + ], + }, + ], + } satisfies NormalizedData); + await writeFile(cachePath, cacheContent); + + const proxy = new ReplayingCapiProxy( + "http://localhost:9999", + cachePath, + workDir, + ); + const proxyUrl = await proxy.start(); + + try { + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: { + model: "test-model", + messages: [ + { role: "system", content: "Be helpful" }, + { role: "user", content: "Read the file" }, + { + role: "assistant", + tool_calls: [ + { + id: "runtime-call-id", + type: "function", + function: { + name: "read_agent", + arguments: '{"agent_id":"read-file","wait":true}', + }, + }, + ], + }, + { + role: "tool", + tool_call_id: "runtime-call-id", + content: runtimeResult, + }, + ], + }, + }); + + expect(response.status).toBe(200); + expect( + (JSON.parse(response.body) as ChatCompletion).choices[0].message + .content, + ).toBe("The file was read successfully."); + } finally { + await proxy.stop(); + } + }); + test("matches parallel tool results regardless of arrival order", async () => { const cachePath = path.join(tempDir, "cache.yaml"); const cacheContent = yaml.stringify({ diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index 1c29fb755..5dbc5e007 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -127,7 +127,9 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { { toolName: "${shell}", normalizer: normalizeShellExitMarkers }, { toolName: "*", normalizer: normalizeGhAuthMessages }, { toolName: "*", normalizer: normalizeAvailableToolNames }, + { toolName: "task", normalizer: normalizeBackgroundAgentAdvice }, { toolName: "read_agent", normalizer: normalizeReadAgentTimings }, + { toolName: "read_agent", normalizer: normalizeReadAgentLifecycle }, ]; /** @@ -1222,8 +1224,9 @@ function transformOpenAIRequestMessage( } function normalizeUserMessage(content: string): string { - return normalizeSkillContextFrontmatter(content) - .replace(taskCompletionNotificationPattern, taskCompletionNotificationReplacement) + return normalizeAgentCompletionNotification( + normalizeSkillContextFrontmatter(content), + ) .replace(/.*?<\/current_datetime>/g, "") .replace(/[\s\S]*?<\/reminder>/g, "") .replace(/[\s\S]*?<\/system_reminder>/g, "") @@ -1236,19 +1239,29 @@ function normalizeUserMessage(content: string): string { .trim(); } -const taskCompletionNotificationPattern = - /Use read_agent with agent_id "([^"]+)" to retrieve unread results\./g; -const taskCompletionNotificationReplacement = - 'Use read_agent with agent_id "$1" to retrieve the full results.'; +// The runtime's background-agent completion system_notification has been reworded +// across CLI versions — both the status sentence ("has completed successfully." vs +// "has finished processing and is now idle.") and the follow-up advice ("to retrieve +// unread results." vs "to read the results, or write_agent to send follow-up +// messages."). Collapse every known phrasing to one canonical form so snapshots keep +// matching regardless of which runtime version recorded them or replays against them. +function normalizeAgentCompletionNotification(content: string): string { + return content + .replace( + /Agent ("[^"]*" \([^)]*\)) has finished processing and is now idle\./g, + "Agent $1 has completed successfully.", + ) + .replace( + /Use read_agent with agent_id "([^"]+)" to (?:retrieve unread results|read the results, or write_agent to send follow-up messages)\./g, + 'Use read_agent with agent_id "$1" to retrieve the full results.', + ); +} function normalizeStoredUserMessages(conversations: NormalizedConversation[]) { for (const conversation of conversations) { for (const message of conversation.messages) { if (message.role === "user" && typeof message.content === "string") { - message.content = message.content.replace( - taskCompletionNotificationPattern, - taskCompletionNotificationReplacement, - ); + message.content = normalizeAgentCompletionNotification(message.content); } } } @@ -1299,17 +1312,18 @@ function coalesceMessages( return result; } -// Re-normalizes the built-in tool enumeration in stored tool results at load -// time. Snapshots recorded before normalizeAvailableToolNames collapsed the -// whole list (or recorded against an older tool set) still contain the literal -// enumeration on disk; the result normalizers only run against live requests, -// so without this the stored side would keep the stale list and never match a -// request whose tool set has since changed. +// Re-normalizes stored tool results at load time. Snapshots recorded against an +// older runtime still contain the literal built-in tool enumeration (or the +// background-agent advice wording) on disk; the result normalizers only run +// against live requests, so without this the stored side would keep the stale +// text and never match a request whose runtime output has since changed. function normalizeStoredToolMessages(conversations: NormalizedConversation[]) { for (const conversation of conversations) { for (const message of conversation.messages) { if (message.role === "tool" && typeof message.content === "string") { message.content = normalizeAvailableToolNames(message.content); + message.content = normalizeBackgroundAgentAdvice(message.content); + message.content = normalizeReadAgentLifecycle(message.content); } } } @@ -1405,21 +1419,64 @@ function normalizeReadAgentTimings(result: string): string { .replace(/\bduration: \d+(?:\.\d+)?s\b/g, "duration: 0s"); } -// Stable placeholder for the built-in tool enumeration the runtime emits when a -// nonexistent tool is called (see normalizeAvailableToolNames). -export const availableToolsPlaceholder = "${available_tools}"; +// Background-agent lifecycle framing changed with 1.0.72's multi-turn agents. +// An agent that has finished its work now reports +// "Agent is idle (waiting for messages)." with status: idle, total_turns: >=1 +// whereas older runtimes reported +// "Agent completed." with status: completed, total_turns: 0, duration: . +// 1.0.72 also prefixes each turn's output with a "[Turn N]" marker. None of this +// changes what the read_agent-based tests assert (e.g. subagent_hooks checks that +// hooks fire, not the completion wording), and the agent's actual response body is +// itself replayed from the snapshot, so only this CLI-generated framing drifts. +// Collapse the lifecycle fields to stable placeholders, drop the trailing +// duration, and strip the turn markers so snapshots keep matching across runtime +// versions. Applied symmetrically to stored snapshots and incoming requests, and +// guarded on the leading status header so unrelated tool results are never +// touched. +function normalizeReadAgentLifecycle(result: string): string { + if (!/^Agent (?:completed|is idle \(waiting for messages\))\./.test(result)) { + return result; + } + return result + .replace( + /^Agent (?:completed|is idle \(waiting for messages\))\./, + "Agent ${agent_state}.", + ) + .replace(/\bstatus: \w+/, "status: ${agent_status}") + .replace(/\btotal_turns: \d+/, "total_turns: ${turns}") + .replace(/, duration: \d+(?:\.\d+)?s\b/g, "") + .replace(/\n\[Turn \d+\]\n/g, "\n"); +} // When a model calls a tool that doesn't exist (e.g., the removed report_intent -// tool), the runtime replies with "Available tools that can be called are ." -// That enumeration is both platform-specific (shell tool family names differ -// across OSes) and runtime-version-specific (built-in tools such as write_agent -// are added or removed over time), so any test that trips this path would break -// whenever the tool set changes. Collapse the whole list to a stable placeholder -// so snapshots keep matching as the built-in tool set evolves. +// tool), the runtime replies with "Tool '' does not exist. Available tools +// that can be called are ." Some runtime versions omit that second sentence +// entirely, and when present the enumeration is both platform-specific (shell tool +// family names differ across OSes) and runtime-version-specific (built-in tools +// such as write_agent are added or removed over time). Strip the whole "Available +// tools..." sentence so its presence, absence, and exact contents are all +// equivalent and snapshots keep matching as the built-in tool set evolves. function normalizeAvailableToolNames(result: string): string { return result.replace( - /(Available tools that can be called are )[^.]*/g, - (_full, prefix: string) => prefix + availableToolsPlaceholder, + /\s*Available tools that can be called are [^.]*\./g, + "", + ); +} + +// Stable placeholder for the advice text the runtime appends after starting a +// background agent (see normalizeBackgroundAgentAdvice). +export const backgroundAgentAdvicePlaceholder = "${background_agent_advice}"; + +// When the task tool starts an agent in the background, the runtime replies with +// "Agent started in background with agent_id: ." followed by advice text +// ("You'll be notified when it completes... The agent supports multi-turn +// conversations — use write_agent...") that changes across runtime versions. +// Collapse that trailing advice to a stable placeholder so snapshots keep +// matching as the wording evolves, while preserving the meaningful agent_id. +function normalizeBackgroundAgentAdvice(result: string): string { + return result.replace( + /(Agent started in background with agent_id: \S+\.)[\s\S]*/, + (_full, prefix: string) => `${prefix} ${backgroundAgentAdvicePlaceholder}`, ); }